Skip to content

Commit 878ada4

Browse files
committed
feat(gpt-apps): auto-include get-actor-run for call-actor and uiMode
1 parent a9241e2 commit 878ada4

10 files changed

Lines changed: 121 additions & 19 deletions

File tree

src/const.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ These tools are called **Actors**. They enable you to extract structured data fr
218218
- First call with \`step="info"\` or use \`${HelperTools.ACTOR_GET_DETAILS}\` to obtain the Actor's schema.
219219
- Then call with \`step="call"\` to execute the Actor.
220220
- When \`step="call"\`, supports async execution via the \`async\` parameter:
221-
- When \`async: false\` or not provided (default when UI mode is disabled): Waits for completion and returns results immediately.
222-
- When \`async: true\` (default when UI mode is enabled): Starts the run and returns immediately with runId. Use \`${HelperTools.ACTOR_RUNS_GET}\` to check status and retrieve results.
221+
- When \`async: false\` or not provided (default when UI mode is disabled): Waits for completion and returns results immediately.
222+
- When \`async: true\` (default when UI mode is enabled): Starts the run and returns immediately with runId. Use \`${HelperTools.ACTOR_RUNS_GET}\` to check status and retrieve results.
223223
224224
### Tool disambiguation
225225
- **${HelperTools.ACTOR_OUTPUT_GET} vs ${HelperTools.DATASET_GET_ITEMS}:**

src/mcp/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export class ActorsMcpServer {
309309
* Used primarily for SSE.
310310
*/
311311
public async loadToolsFromUrl(url: string, apifyClient: ApifyClient) {
312-
const tools = await processParamsGetTools(url, apifyClient);
312+
const tools = await processParamsGetTools(url, apifyClient, this.options.uiMode);
313313
if (tools.length > 0) {
314314
log.debug('Loading tools from query parameters');
315315
this.upsertTools(tools, false);

src/mcp/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { TaskStore } from '@modelcontextprotocol/sdk/experimental/tasks/int
55
import type { ApifyClient } from 'apify-client';
66

77
import { processInput } from '../input.js';
8-
import type { Input } from '../types.js';
8+
import type { Input, UiMode } from '../types.js';
99
import { loadToolsFromInput } from '../utils/tools-loader.js';
1010
import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from './const.js';
1111

@@ -41,10 +41,11 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
4141
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
4242
* @param url The URL to process
4343
* @param apifyClient The Apify client instance
44+
* @param uiMode UI mode from server options
4445
*/
45-
export async function processParamsGetTools(url: string, apifyClient: ApifyClient) {
46+
export async function processParamsGetTools(url: string, apifyClient: ApifyClient, uiMode?: UiMode) {
4647
const input = parseInputParamsFromUrl(url);
47-
return await loadToolsFromInput(input, apifyClient);
48+
return await loadToolsFromInput(input, apifyClient, uiMode);
4849
}
4950

5051
export function parseInputParamsFromUrl(url: string): Input {

src/stdio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async function main() {
188188

189189
const apifyClient = new ApifyClient({ token: apifyToken });
190190
// Use the shared tools loading logic
191-
const tools = await loadToolsFromInput(normalizedInput, apifyClient);
191+
const tools = await loadToolsFromInput(normalizedInput, apifyClient, argv.uiMode);
192192

193193
mcpServer.upsertTools(tools);
194194

src/tools/actor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ EXAMPLES:
402402
// Additional props true to allow skyfire-pay-id
403403
additionalProperties: true,
404404
}),
405+
_meta: {
406+
...getWidgetConfig(WIDGET_URIS.ACTOR_RUN)?.meta,
407+
},
405408
annotations: {
406409
title: 'Call Actor',
407410
readOnlyHint: false,

src/tools/fetch-actor-details.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ USAGE EXAMPLES:
3232
inputSchema: z.toJSONSchema(fetchActorDetailsToolArgsSchema) as ToolInputSchema,
3333
outputSchema: actorDetailsOutputSchema,
3434
ajvValidate: compileSchema(z.toJSONSchema(fetchActorDetailsToolArgsSchema)),
35+
_meta: {
36+
...getWidgetConfig(WIDGET_URIS.SEARCH_ACTORS)?.meta,
37+
},
3538
annotations: {
3639
title: 'Fetch Actor details',
3740
readOnlyHint: true,

src/tools/run.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ USAGE EXAMPLES:
4141
- user_input: What is the datasetId for run y2h7sK3Wc?`,
4242
inputSchema: z.toJSONSchema(getActorRunArgs) as ToolInputSchema,
4343
ajvValidate: compileSchema(z.toJSONSchema(getActorRunArgs)),
44+
_meta: {
45+
...getWidgetConfig(WIDGET_URIS.ACTOR_RUN)?.meta,
46+
},
4447
annotations: {
4548
title: 'Get Actor run',
4649
readOnlyHint: true,

src/utils/tools-loader.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import { callActor } from '../tools/actor.js';
1313
import { getActorOutput } from '../tools/get-actor-output.js';
1414
import { addTool } from '../tools/helpers.js';
1515
import { getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
16-
import type { Input, InternalToolArgs, ToolCategory, ToolEntry } from '../types.js';
16+
import { getActorRun } from '../tools/run.js';
17+
import type { Input, InternalToolArgs, ToolCategory, ToolEntry, UiMode } from '../types.js';
1718
import { getExpectedToolsByCategories } from './tools.js';
1819

1920
// Lazily-computed cache of internal tools by name to avoid circular init issues.
@@ -34,11 +35,13 @@ function getInternalToolByNameMap(): Map<string, ToolEntry> {
3435
*
3536
* @param input The processed Input object
3637
* @param apifyClient The Apify client instance
38+
* @param uiMode Optional UI mode.
3739
* @returns An array of tool entries
3840
*/
3941
export async function loadToolsFromInput(
4042
input: Input,
4143
apifyClient: ApifyClient,
44+
uiMode?: UiMode,
4245
): Promise<ToolEntry[]> {
4346
// Helpers for readability
4447
const normalizeSelectors = (value: Input['tools']): (string | ToolCategory)[] | undefined => {
@@ -139,6 +142,15 @@ export async function loadToolsFromInput(
139142
result.push(getActorOutput);
140143
}
141144

145+
/**
146+
* If call-actor tool is present or UI mode is enabled, automatically include get-actor-run
147+
* to allow checking run status and retrieving results.
148+
*/
149+
const hasGetActorRun = result.some((entry) => entry.name === HelperTools.ACTOR_RUNS_GET);
150+
if (!hasGetActorRun && (hasCallActor || uiMode === 'openai')) {
151+
result.push(getActorRun);
152+
}
153+
142154
// TEMP: for now we disable this swapping logic as the add-actor tool was misbehaving in some clients
143155
// Handle client capabilities logic for 'actors' category to swap call-actor for add-actor
144156
// if client supports dynamic tools.

tests/helpers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function checkApifyToken(): void {
2727
}
2828

2929
function appendSearchParams(url: URL, options?: McpClientOptions): void {
30-
const { actors, enableAddingActors, tools, telemetry } = options || {};
30+
const { actors, enableAddingActors, tools, telemetry, uiMode } = options || {};
3131
if (actors !== undefined) {
3232
url.searchParams.append('actors', actors.join(','));
3333
}
@@ -40,6 +40,9 @@ function appendSearchParams(url: URL, options?: McpClientOptions): void {
4040
// Append telemetry parameters (default to false for tests when not explicitly set)
4141
const telemetryEnabled = telemetry?.enabled !== undefined ? telemetry.enabled : false;
4242
url.searchParams.append('telemetry-enabled', telemetryEnabled.toString());
43+
if (uiMode !== undefined) {
44+
url.searchParams.append('ui', uiMode);
45+
}
4346
}
4447

4548
export async function createMcpSseClient(

tests/integration/suite.ts

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,14 @@ export function createIntegrationTestsSuite(
145145
it('should list all default tools and Actors', async () => {
146146
client = await createClientFn();
147147
const tools = await client.listTools();
148-
expect(tools.tools.length).toEqual(defaultTools.length + defaults.actors.length + 1);
148+
expect(tools.tools.length).toEqual(defaultTools.length + defaults.actors.length + 2);
149149

150150
const names = getToolNames(tools);
151151
expectToolNamesToContain(names, DEFAULT_TOOL_NAMES);
152152
expectToolNamesToContain(names, DEFAULT_ACTOR_NAMES);
153-
expect(names).toContain('get-actor-output');
153+
expect(names).toContain(HelperTools.ACTOR_OUTPUT_GET);
154+
// get-actor-run should be automatically included when call-actor is present
155+
expect(names).toContain(HelperTools.ACTOR_RUNS_GET);
154156
await client.close();
155157
});
156158

@@ -165,12 +167,14 @@ export function createIntegrationTestsSuite(
165167
const expectedActors = ['apify-slash-rag-web-browser'];
166168

167169
const expectedTotal = expectedActorsTools.concat(expectedDocsTools, expectedActors);
168-
expect(names).toHaveLength(expectedTotal.length + 1);
170+
expect(names).toHaveLength(expectedTotal.length + 2);
169171

170172
expectToolNamesToContain(names, expectedActorsTools);
171173
expectToolNamesToContain(names, expectedDocsTools);
172174
expectToolNamesToContain(names, expectedActors);
173-
expect(names).toContain('get-actor-output');
175+
expect(names).toContain(HelperTools.ACTOR_OUTPUT_GET);
176+
// get-actor-run should be automatically included when call-actor is present
177+
expect(names).toContain(HelperTools.ACTOR_RUNS_GET);
174178

175179
await client.close();
176180
});
@@ -204,11 +208,13 @@ export function createIntegrationTestsSuite(
204208
it('should list all default tools and Actors when enableAddingActors is false', async () => {
205209
client = await createClientFn({ enableAddingActors: false });
206210
const names = getToolNames(await client.listTools());
207-
expect(names.length).toEqual(defaultTools.length + defaults.actors.length + 1);
211+
expect(names.length).toEqual(defaultTools.length + defaults.actors.length + 2);
208212

209213
expectToolNamesToContain(names, DEFAULT_TOOL_NAMES);
210214
expectToolNamesToContain(names, DEFAULT_ACTOR_NAMES);
211-
expect(names).toContain('get-actor-output');
215+
expect(names).toContain(HelperTools.ACTOR_OUTPUT_GET);
216+
// get-actor-run should be automatically included when call-actor is present
217+
expect(names).toContain(HelperTools.ACTOR_RUNS_GET);
212218

213219
await client.close();
214220
});
@@ -412,9 +418,11 @@ export function createIntegrationTestsSuite(
412418
const selectedToolName = actorNameToToolName(ACTOR_PYTHON_EXAMPLE);
413419
client = await createClientFn({ enableAddingActors: true, tools: ['actors'] });
414420
const names = getToolNames(await client.listTools());
415-
// Only the actors category, get-actor-output and add-actor should be loaded
416-
const numberOfTools = toolCategories.actors.length + 2;
421+
// Only the actors category, get-actor-output, get-actor-run, and add-actor should be loaded
422+
const numberOfTools = toolCategories.actors.length + 3;
417423
expect(names).toHaveLength(numberOfTools);
424+
// get-actor-run should be automatically included when call-actor is present
425+
expect(names).toContain(HelperTools.ACTOR_RUNS_GET);
418426
// Check that the Actor is not in the tools list
419427
expect(names).not.toContain(selectedToolName);
420428

@@ -1038,11 +1046,13 @@ export function createIntegrationTestsSuite(
10381046
// Test with enableAddingActors = false via env var
10391047
client = await createClientFn({ enableAddingActors: false, useEnv: true });
10401048
const names = getToolNames(await client.listTools());
1041-
expect(names.length).toEqual(defaultTools.length + defaults.actors.length + 1);
1049+
expect(names.length).toEqual(defaultTools.length + defaults.actors.length + 2);
10421050

10431051
expectToolNamesToContain(names, DEFAULT_TOOL_NAMES);
10441052
expectToolNamesToContain(names, DEFAULT_ACTOR_NAMES);
1045-
expect(names).toContain('get-actor-output');
1053+
expect(names).toContain(HelperTools.ACTOR_OUTPUT_GET);
1054+
// get-actor-run should be automatically included when call-actor is present
1055+
expect(names).toContain(HelperTools.ACTOR_RUNS_GET);
10461056

10471057
await client.close();
10481058
});
@@ -1493,6 +1503,73 @@ export function createIntegrationTestsSuite(
14931503
expect(searchActorsTool?._meta?.['openai/outputTemplate']).toBeDefined();
14941504
expect(searchActorsTool?._meta?.['openai/widgetAccessible']).toBe(true);
14951505

1506+
const fetchActorDetailsToolFromList = tools.tools.find((tool) => tool.name === HelperTools.ACTOR_GET_DETAILS);
1507+
expect(fetchActorDetailsToolFromList).toBeDefined();
1508+
expect(fetchActorDetailsToolFromList?._meta).toBeDefined();
1509+
expect(fetchActorDetailsToolFromList?._meta?.['openai/outputTemplate']).toBeDefined();
1510+
expect(fetchActorDetailsToolFromList?._meta?.['openai/widgetAccessible']).toBe(true);
1511+
1512+
const callActorTool = tools.tools.find((tool) => tool.name === HelperTools.ACTOR_CALL);
1513+
expect(callActorTool).toBeDefined();
1514+
expect(callActorTool?._meta).toBeDefined();
1515+
expect(callActorTool?._meta?.['openai/outputTemplate']).toBeDefined();
1516+
expect(callActorTool?._meta?.['openai/widgetAccessible']).toBe(true);
1517+
1518+
await client.close();
1519+
});
1520+
1521+
it.runIf(options.transport === 'sse' || options.transport === 'streamable-http')('should use uiMode URL parameter when provided', async () => {
1522+
client = await createClientFn({ uiMode: 'openai' });
1523+
const tools = await client.listTools();
1524+
expect(tools.tools.length).toBeGreaterThan(0);
1525+
1526+
// Verify that tools have OpenAI metadata when UI mode is enabled via URL parameter
1527+
const searchActorsTool = tools.tools.find((tool) => tool.name === HelperTools.STORE_SEARCH);
1528+
expect(searchActorsTool).toBeDefined();
1529+
expect(searchActorsTool?._meta).toBeDefined();
1530+
expect(searchActorsTool?._meta?.['openai/outputTemplate']).toBeDefined();
1531+
expect(searchActorsTool?._meta?.['openai/widgetAccessible']).toBe(true);
1532+
1533+
const fetchActorDetailsToolFromList = tools.tools.find((tool) => tool.name === HelperTools.ACTOR_GET_DETAILS);
1534+
expect(fetchActorDetailsToolFromList).toBeDefined();
1535+
expect(fetchActorDetailsToolFromList?._meta).toBeDefined();
1536+
expect(fetchActorDetailsToolFromList?._meta?.['openai/outputTemplate']).toBeDefined();
1537+
expect(fetchActorDetailsToolFromList?._meta?.['openai/widgetAccessible']).toBe(true);
1538+
1539+
const callActorTool = tools.tools.find((tool) => tool.name === HelperTools.ACTOR_CALL);
1540+
expect(callActorTool).toBeDefined();
1541+
expect(callActorTool?._meta).toBeDefined();
1542+
expect(callActorTool?._meta?.['openai/outputTemplate']).toBeDefined();
1543+
expect(callActorTool?._meta?.['openai/widgetAccessible']).toBe(true);
1544+
1545+
await client.close();
1546+
});
1547+
1548+
it('should automatically include get-actor-run when uiMode is enabled', async () => {
1549+
client = await createClientFn({ uiMode: 'openai' });
1550+
const tools = await client.listTools();
1551+
const toolNames = getToolNames(tools);
1552+
1553+
// When uiMode is enabled, default tools include call-actor, so get-actor-run should be included
1554+
expect(toolNames).toContain(HelperTools.ACTOR_CALL);
1555+
expect(toolNames).toContain(HelperTools.ACTOR_RUNS_GET);
1556+
1557+
await client.close();
1558+
});
1559+
1560+
it.runIf(options.transport === 'sse' || options.transport === 'streamable-http')('should include get-actor-run without call-actor', async () => {
1561+
client = await createClientFn({ uiMode: 'openai', tools: ['docs'] });
1562+
const tools = await client.listTools();
1563+
const toolNames = getToolNames(tools);
1564+
1565+
// get-actor-run should be included when uiMode is enabled, even if call-actor is not present
1566+
expect(toolNames).toContain(HelperTools.ACTOR_RUNS_GET);
1567+
// Docs tools should be present
1568+
expect(toolNames).toContain(HelperTools.DOCS_SEARCH);
1569+
expect(toolNames).toContain(HelperTools.DOCS_FETCH);
1570+
// call-actor should NOT be present since only 'docs' was selected
1571+
expect(toolNames).not.toContain(HelperTools.ACTOR_CALL);
1572+
14961573
await client.close();
14971574
});
14981575
});

0 commit comments

Comments
 (0)