Skip to content

Commit 825ec42

Browse files
authored
feat(widgets): redesign actor search and actor detail (#430)
- add Actor widgets for search and detail Search: <img width="890" height="848" alt="image" src="https://github.com/user-attachments/assets/dc6b7216-be7d-4a67-a8e5-88d8e76c36de" /> Mobile search: <img width="492" height="549" alt="Screenshot 2026-02-12 at 11 46 03" src="https://github.com/user-attachments/assets/c5bd5b84-3c1a-429e-b669-db76c44888cd" /> Detail: <img width="819" height="756" alt="image" src="https://github.com/user-attachments/assets/0dbfdfe5-617b-4f78-8f38-73642bb45e75" /> Fullscreen detail: <img width="1618" height="1005" alt="image" src="https://github.com/user-attachments/assets/ea06b8bc-7161-4e77-b73f-d9386826840d" /> Mobile detail: <img width="417" height="909" alt="image" src="https://github.com/user-attachments/assets/a7fe7f21-ecd3-42cf-b366-2d3d97e27850" />
2 parents 87b7c92 + cb38e9d commit 825ec42

27 files changed

Lines changed: 1507 additions & 727 deletions

package-lock.json

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/fetch-actor-details.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ EXAMPLES:
6868

6969
if (apifyMcpServer.options.uiMode === 'openai') {
7070
const { structuredContent: processedStructuredContent, actorUrl } = processActorDetailsForResponse(details);
71-
const widgetStructuredContent = {
71+
const structuredContent = {
7272
actorInfo: details.actorCardStructured,
7373
inputSchema: details.inputSchema,
7474
actorDetails: processedStructuredContent.actorDetails,
@@ -85,7 +85,7 @@ An interactive widget has been rendered with detailed Actor information.
8585
const widgetConfig = getWidgetConfig(WIDGET_URIS.SEARCH_ACTORS);
8686
return buildMCPResponse({
8787
texts,
88-
structuredContent: widgetStructuredContent,
88+
structuredContent,
8989
_meta: {
9090
...widgetConfig?.meta,
9191
'openai/widgetDescription': `Actor details for ${parsed.actor} from Apify Store`,

src/tools/store_collection.ts

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,15 @@
11
import type { ActorStoreList } from 'apify-client';
22
import { z } from 'zod';
33

4-
import { ApifyClient } from '../apify-client.js';
54
import { HelperTools } from '../const.js';
65
import { getWidgetConfig, WIDGET_URIS } from '../resources/widgets.js';
7-
import type { ActorPricingModel, ExtendedActorStoreList, InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
8-
import { formatActorForWidget, formatActorToActorCard, formatActorToStructuredCard } from '../utils/actor-card.js';
6+
import type { ActorPricingModel, InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
7+
import { formatActorForWidget, formatActorToActorCard, formatActorToStructuredCard, type WidgetActor } from '../utils/actor-card.js';
98
import { searchAndFilterActors } from '../utils/actor-search.js';
109
import { compileSchema } from '../utils/ajv.js';
1110
import { buildMCPResponse } from '../utils/mcp.js';
1211
import { actorSearchOutputSchema } from './structured-output-schemas.js';
1312

14-
export async function searchActorsByKeywords(
15-
search: string,
16-
apifyToken: string,
17-
limit: number | undefined = undefined,
18-
offset: number | undefined = undefined,
19-
allowsAgenticUsers: boolean | undefined = undefined,
20-
): Promise<ExtendedActorStoreList[]> {
21-
const client = new ApifyClient({ token: apifyToken });
22-
const storeClient = client.store();
23-
if (allowsAgenticUsers !== undefined) storeClient.params = { ...storeClient.params, allowsAgenticUsers };
24-
25-
const results = await storeClient.list({ search, limit, offset });
26-
return results.items;
27-
}
28-
2913
export const searchActorsArgsSchema = z.object({
3014
limit: z.number()
3115
.int()
@@ -172,7 +156,7 @@ You can also try using more specific or alternative keywords related to your sea
172156
count: number;
173157
instructions?: string;
174158
// Widget format actors (not validated by schema, but available for widget UI)
175-
widgetActors?: ReturnType<typeof formatActorForWidget>[];
159+
widgetActors?: WidgetActor[];
176160
} = {
177161
actors: structuredActorCards,
178162
query: parsed.keywords,
@@ -184,17 +168,19 @@ You can also try using more specific or alternative keywords related to your sea
184168
// Add widget format actors when widget mode is enabled
185169
if (apifyMcpServer.options.uiMode === 'openai') {
186170
structuredContent.widgetActors = actors.map(formatActorForWidget);
187-
}
188171

189-
// When widget mode is enabled, return minimal text with widget metadata
190-
// When widget mode is disabled, return full text response without widget metadata
191-
if (apifyMcpServer.options.uiMode === 'openai') {
172+
const actorCards = actors.map((actor) => formatActorToActorCard(actor));
173+
const actorsText = actorCards.join('\n\n');
192174
const texts = [`
193175
# Search results:
194176
- **Search query:** ${parsed.keywords}
195177
- **Number of Actors found:** ${actors.length}
196178
197-
An interactive widget has been rendered with the search results.
179+
An interactive widget has been rendered with the search results. The user can already see the list of Actors visually in the widget, so do NOT print or summarize the Actor list in your response.
180+
181+
# Actors:
182+
183+
${actorsText}
198184
`];
199185

200186
const widgetConfig = getWidgetConfig(WIDGET_URIS.SEARCH_ACTORS);

src/tools/structured-output-schemas.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ export const actorInfoSchema = {
111111
},
112112
pricing: pricingSchema,
113113
stats: statsSchema,
114-
rating: { type: 'number', description: 'Actor rating' },
114+
rating: {
115+
type: 'object' as const, // Literal type required for MCP SDK type compatibility
116+
properties: {
117+
average: { type: 'number', description: 'Average rating' },
118+
count: { type: 'number', description: 'Number of ratings' },
119+
},
120+
},
115121
modifiedAt: { type: 'string', description: 'Last modification date' },
116122
isDeprecated: { type: 'boolean', description: 'Whether the Actor is deprecated' },
117123
},

src/types.ts

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
33
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
44
import type { InitializeRequest, Notification, Prompt, Request, ToolSchema } from '@modelcontextprotocol/sdk/types.js';
55
import type { ValidateFunction } from 'ajv';
6-
import type { Actor, ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client';
6+
import type {
7+
Actor as ActorOutdated,
8+
ActorDefaultRunOptions,
9+
ActorDefinition,
10+
ActorRunPricingInfo,
11+
ActorStats,
12+
ActorStoreList as ActorStoreListOutdated,
13+
PricePerEventActorPricingInfo as PricePerEventActorPricingInfoOutdated,
14+
} from 'apify-client';
715
import type z from 'zod';
816

917
import type { ACTOR_PRICING_MODEL, TELEMETRY_ENV, TOOL_STATUS } from './const.js';
@@ -59,17 +67,17 @@ export type ActorDefinitionWithDesc = Omit<ActorDefinition, 'input'> & {
5967
*/
6068
export type ActorDefinitionPruned = Pick<ActorDefinitionWithDesc,
6169
'id' | 'actorFullName' | 'buildTag' | 'readme' | 'readmeSummary' | 'input' | 'description' | 'defaultRunOptions'> & {
62-
webServerMcpPath?: string; // Optional, used for Actorized MCP server tools
63-
pictureUrl?: string; // Optional, URL to the Actor's icon/picture
64-
};
70+
webServerMcpPath?: string; // Optional, used for Actorized MCP server tools
71+
pictureUrl?: string; // Optional, URL to the Actor's icon/picture
72+
};
6573

6674
/**
6775
* Actor definition combined with full actor metadata.
6876
* Contains both the pruned definition (for schemas) and complete actor info.
6977
*/
7078
export type ActorDefinitionWithInfo = {
7179
definition: ActorDefinitionPruned;
72-
info: Actor;
80+
info: ActorOutdated;
7381
};
7482

7583
/**
@@ -198,27 +206,28 @@ export type PricingTier = 'FREE' | 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM' | '
198206
*/
199207
export type ActorChargeEvent = {
200208
eventTitle: string;
201-
eventDescription: string;
209+
eventDescription?: string;
202210
/** Flat price per event in USD (if not tiered) */
203211
eventPriceUsd?: number;
204212
/** Tiered pricing per event, by tier name (FREE, BRONZE, etc.) */
205213
eventTieredPricingUsd?: Partial<Record<PricingTier, TieredEventPrice>>;
206214
};
207215

208-
/**
209-
* Pricing per event for an Actor, supporting both flat and tiered pricing.
210-
*/
211-
export type PricingPerEvent = {
212-
actorChargeEvents: Record<string, ActorChargeEvent>;
213-
};
216+
export type TieredPricing = {
217+
[tier: string]: {
218+
tieredPricePerUnitUsd: number;
219+
};
220+
}
214221

215-
export type ExtendedPricingInfo = PricingInfo & {
216-
pricePerUnitUsd?: number;
217-
trialMinutes?: number;
218-
unitName?: string; // Name of the unit for the pricing model
219-
pricingPerEvent: PricingPerEvent;
220-
tieredPricing?: Partial<Record<PricingTier, { tieredPricePerUnitUsd: number }>>;
221-
};
222+
type PricePerEventActorPricingInfo = PricePerEventActorPricingInfoOutdated & {
223+
pricingPerEvent: {
224+
actorChargeEvents: Record<string, ActorChargeEvent>;
225+
};
226+
}
227+
228+
export type PricingInfo = ActorRunPricingInfo & {
229+
tieredPricing?: TieredPricing;
230+
} | PricePerEventActorPricingInfo;
222231

223232
export type ToolCategory = keyof typeof toolCategories;
224233
/**
@@ -234,8 +243,8 @@ export type Input = {
234243
*/
235244
actors?: string[] | string;
236245
/**
237-
* @deprecated Use `enableAddingActors` instead.
238-
*/
246+
* @deprecated Use `enableAddingActors` instead.
247+
*/
239248
enableActorAutoLoading?: boolean | string;
240249
enableAddingActors?: boolean | string;
241250
maxActorMemoryBytes?: number;
@@ -265,13 +274,48 @@ export type TelemetryEnv = (typeof TELEMETRY_ENV)[keyof typeof TELEMETRY_ENV];
265274
export type ActorInfo = {
266275
webServerMcpPath: string | null; // To determined if the Actor is an MCP server
267276
definition: ActorDefinitionPruned;
268-
actor: Actor;
277+
actor: ActorOutdated;
269278
};
270279

271-
export type ExtendedActorStoreList = ActorStoreList & {
272-
categories?: string[];
273-
bookmarkCount?: number;
280+
export type ActorStoreList = ActorStoreListOutdated & {
281+
actorReviewCount?: number;
274282
actorReviewRating?: number;
283+
badge?: string | null;
284+
bookmarkCount?: number;
285+
categories?: string[];
286+
currentPricingInfo: ActorRunPricingInfo;
287+
isWhiteListedForAgenticPayments?: boolean;
288+
notice?: string | null;
289+
userFullName?: string;
290+
stats: ActorStats & {
291+
actorReviewCount?: number;
292+
actorReviewRating?: number;
293+
bookmarkCount?: number;
294+
publicActorRunStats30Days?: Partial<Record<string, number>> & {
295+
SUCCEEDED?: number;
296+
TOTAL?: number;
297+
};
298+
};
299+
};
300+
301+
export type Actor = ActorOutdated & {
302+
actorPermissionLevel?: string;
303+
hasNoDataset?: boolean;
304+
isCritical?: boolean;
305+
isGeneric?: boolean;
306+
isSourceCodeHidden?: boolean;
307+
pictureUrl?: string;
308+
standbyUrl?: string | null;
309+
stats: ActorStats & {
310+
publicActorRunStats30Days?: Partial<Record<string, number>> & {
311+
SUCCEEDED?: number;
312+
TOTAL?: number;
313+
};
314+
actorReviewCount?: number;
315+
actorReviewRating?: number;
316+
bookmarkCount?: number;
317+
lastRunStartedAt?: string | Date | null;
318+
};
275319
};
276320

277321
export type ActorDefinitionStorage = {
@@ -454,6 +498,7 @@ export type StructuredActorCard = {
454498
title?: string;
455499
url: string;
456500
fullName: string;
501+
pictureUrl?: string;
457502
developer: {
458503
username: string;
459504
isOfficialApify: boolean;
@@ -468,7 +513,10 @@ export type StructuredActorCard = {
468513
successRate?: number;
469514
bookmarks?: number;
470515
};
471-
rating?: number;
516+
rating?: {
517+
average: number;
518+
count: number;
519+
};
472520
modifiedAt?: string;
473521
isDeprecated: boolean;
474522
}

0 commit comments

Comments
 (0)