Skip to content

Commit 6e1bf5e

Browse files
committed
fix: Add isError flag to tool execution errors per MCP spec
1 parent 99b8e73 commit 6e1bf5e

File tree

11 files changed

+33
-28
lines changed

11 files changed

+33
-28
lines changed

src/mcp/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ Please check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool
583583
Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.`;
584584
log.softFail(msg, { statusCode: 408 }); // 408 Request Timeout
585585
await this.server.sendLoggingMessage({ level: 'error', data: msg });
586-
return buildMCPResponse([msg]);
586+
return buildMCPResponse([msg], true);
587587
}
588588

589589
// Only set up notification handlers if progressToken is provided by the client
@@ -671,7 +671,7 @@ Please verify the server URL is correct and accessible, and ensure you have a va
671671
return buildMCPResponse([
672672
`Error calling tool "${name}": ${errorMessage}.
673673
Please verify the tool name, input parameters, and ensure all required resources are available.`,
674-
]);
674+
], true);
675675
}
676676

677677
const availableTools = this.listToolNames();

src/tools/actor.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ EXAMPLES:
410410
if (isActorMcpServer && apifyMcpServer.options.skyfireMode) {
411411
return buildMCPResponse([
412412
`This Actor (${actorName}) is an MCP server and cannot be accessed using a Skyfire token. To use this Actor, please provide a valid Apify token instead of a Skyfire token.`,
413-
]);
413+
], true);
414414
}
415415

416416
try {
@@ -423,7 +423,7 @@ EXAMPLES:
423423
try {
424424
client = await connectMCPClient(mcpServerUrl, apifyToken);
425425
if (!client) {
426-
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]);
426+
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`], true);
427427
}
428428
const toolsResponse = await client.listTools();
429429

@@ -440,7 +440,7 @@ EXAMPLES:
440440
if (!details) {
441441
return buildMCPResponse([`Actor information for '${baseActorName}' was not found.
442442
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
443-
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
443+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`], true);
444444
}
445445
const content = [
446446
`Actor name: ${actorName}`,
@@ -478,28 +478,28 @@ You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.
478478

479479
// Step 2: Call the Actor
480480
if (!input) {
481-
return buildMCPResponse([`Input is required when step="call". Please provide the input parameter based on the Actor's input schema.`]);
481+
return buildMCPResponse([`Input is required when step="call". Please provide the input parameter based on the Actor's input schema.`], true);
482482
}
483483

484484
// Handle the case where LLM does not respect instructions when calling MCP server Actors
485485
// and does not provide the tool name.
486486
const isMcpToolNameInvalid = mcpToolName === undefined || mcpToolName.trim().length === 0;
487487
if (isActorMcpServer && isMcpToolNameInvalid) {
488-
return buildMCPResponse([CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG]);
488+
return buildMCPResponse([CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG], true);
489489
}
490490

491491
// Handle MCP tool calls
492492
if (mcpToolName) {
493493
if (!isActorMcpServer) {
494-
return buildMCPResponse([`Actor '${baseActorName}' is not an MCP server.`]);
494+
return buildMCPResponse([`Actor '${baseActorName}' is not an MCP server.`], true);
495495
}
496496

497497
const mcpServerUrl = mcpServerUrlOrFalse;
498498
let client: Client | null = null;
499499
try {
500500
client = await connectMCPClient(mcpServerUrl, apifyToken);
501501
if (!client) {
502-
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]);
502+
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`], true);
503503
}
504504

505505
const result = await client.callTool({
@@ -519,7 +519,7 @@ You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.
519519
if (!actor) {
520520
return buildMCPResponse([`Actor '${actorName}' was not found.
521521
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
522-
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
522+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`], true);
523523
}
524524

525525
if (!actor.ajvValidate(input)) {
@@ -556,7 +556,7 @@ You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.
556556
logHttpError(error, 'Failed to call Actor', { actorName, performStep });
557557
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}.
558558
Please verify the Actor name, input parameters, and ensure the Actor exists.
559-
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}, or get Actor details using: ${HelperTools.ACTOR_GET_DETAILS}.`]);
559+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}, or get Actor details using: ${HelperTools.ACTOR_GET_DETAILS}.`], true);
560560
}
561561
},
562562
};

src/tools/build.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const actorDefinitionTool: ToolEntry = {
138138
try {
139139
const v = await getActorDefinition(parsed.actorName, apifyClient, parsed.limit);
140140
if (!v) {
141-
return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }] };
141+
return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }], isError: true };
142142
}
143143
if (v && v.input && 'properties' in v.input && v.input) {
144144
const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties });
@@ -151,6 +151,7 @@ export const actorDefinitionTool: ToolEntry = {
151151
type: 'text',
152152
text: `Failed to fetch Actor definition: ${error instanceof Error ? error.message : String(error)}`,
153153
}],
154+
isError: true,
154155
};
155156
}
156157
},

src/tools/dataset.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ USAGE EXAMPLES:
6868
const client = new ApifyClient({ token: apifyToken });
6969
const v = await client.dataset(parsed.datasetId).get();
7070
if (!v) {
71-
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] };
71+
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }], isError: true };
7272
}
7373
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };
7474
},
@@ -119,7 +119,7 @@ USAGE EXAMPLES:
119119
flatten,
120120
});
121121
if (!v) {
122-
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] };
122+
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }], isError: true };
123123
}
124124
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };
125125
},
@@ -175,7 +175,7 @@ USAGE EXAMPLES:
175175
});
176176

177177
if (!datasetResponse) {
178-
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] };
178+
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }], isError: true };
179179
}
180180

181181
const datasetItems = datasetResponse.items;
@@ -192,7 +192,7 @@ USAGE EXAMPLES:
192192
});
193193

194194
if (!schema) {
195-
return { content: [{ type: 'text', text: `Failed to generate schema for dataset '${parsed.datasetId}'.` }] };
195+
return { content: [{ type: 'text', text: `Failed to generate schema for dataset '${parsed.datasetId}'.` }], isError: true };
196196
}
197197

198198
return {

src/tools/fetch-actor-details.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ USAGE EXAMPLES:
4545
Please verify Actor ID or name format and ensure that the Actor exists.
4646
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`,
4747
];
48-
return buildMCPResponse(texts);
48+
return buildMCPResponse(texts, true);
4949
}
5050

5151
const actorUrl = `https://apify.com/${details.actorInfo.username}/${details.actorInfo.name}`;

src/tools/fetch-apify-docs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ USAGE EXAMPLES:
4545
if (!url.startsWith('https://docs.apify.com')) {
4646
return buildMCPResponse([`Invalid URL: "${url}".
4747
Only URLs starting with "https://docs.apify.com" are allowed.
48-
Please provide a valid Apify documentation URL. You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`]);
48+
Please provide a valid Apify documentation URL. You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`], true);
4949
}
5050

5151
// Cache URL without fragment to avoid fetching the same page multiple times
@@ -61,7 +61,7 @@ Please provide a valid Apify documentation URL. You can find documentation URLs
6161
logHttpError(error, 'Failed to fetch the documentation page', { url, statusText: response.statusText });
6262
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
6363
HTTP Status: ${response.status} ${response.statusText}.
64-
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
64+
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`], true);
6565
}
6666
const html = await response.text();
6767
markdown = htmlToMarkdown(html);
@@ -72,7 +72,7 @@ Please verify the URL is correct and accessible. You can search for available do
7272
logHttpError(error, 'Failed to fetch the documentation page', { url });
7373
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
7474
Error: ${error instanceof Error ? error.message : String(error)}.
75-
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
75+
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`], true);
7676
}
7777
}
7878

src/tools/get-actor-output.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Note: This tool is automatically included if the Apify MCP Server is configured
136136
});
137137

138138
if (!response) {
139-
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }] };
139+
return { content: [{ type: 'text', text: `Dataset '${parsed.datasetId}' not found.` }], isError: true };
140140
}
141141

142142
let { items } = response;

src/tools/get-html-skeleton.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ USAGE EXAMPLES:
6262
const parsed = getHtmlSkeletonArgs.parse(args);
6363

6464
if (!isValidHttpUrl(parsed.url)) {
65-
return buildMCPResponse([`The provided URL is not a valid HTTP or HTTPS URL: ${parsed.url}`]);
65+
return buildMCPResponse([`The provided URL is not a valid HTTP or HTTPS URL: ${parsed.url}`], true);
6666
}
6767

6868
// Try to get from cache first
@@ -81,16 +81,16 @@ USAGE EXAMPLES:
8181

8282
const datasetItems = await client.dataset(run.defaultDatasetId).listItems();
8383
if (datasetItems.items.length === 0) {
84-
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) did not return any output for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`]);
84+
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) did not return any output for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`], true);
8585
}
8686

8787
const firstItem = datasetItems.items[0] as unknown as ScrapedPageItem;
8888
if (firstItem.crawl.httpStatusMessage.toLocaleLowerCase() !== 'ok') {
89-
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) returned an HTTP status ${firstItem.crawl.httpStatusCode} (${firstItem.crawl.httpStatusMessage}) for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`]);
89+
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) returned an HTTP status ${firstItem.crawl.httpStatusCode} (${firstItem.crawl.httpStatusMessage}) for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`], true);
9090
}
9191

9292
if (!firstItem.html) {
93-
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) did not return any HTML content for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`]);
93+
return buildMCPResponse([`The scraping Actor (${RAG_WEB_BROWSER}) did not return any HTML content for the URL: ${parsed.url}. Please check the Actor run for more details: ${run.id}`], true);
9494
}
9595

9696
strippedHtml = stripHtml(firstItem.html);

src/tools/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ USAGE EXAMPLES:
4747
const client = new ApifyClient({ token: apifyToken });
4848
const v = await client.run(parsed.runId).get();
4949
if (!v) {
50-
return { content: [{ type: 'text', text: `Run with ID '${parsed.runId}' not found.` }] };
50+
return { content: [{ type: 'text', text: `Run with ID '${parsed.runId}' not found.` }], isError: true };
5151
}
5252
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };
5353
},

src/utils/mcp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
22
* Helper to build a response for MCP from an array of text strings.
3+
* @param texts - Array of text strings to include in the response
4+
* @param isError - Optional flag to mark the response as an error (default: false)
35
*/
4-
export function buildMCPResponse(texts: string[]) {
6+
export function buildMCPResponse(texts: string[], isError = false) {
57
return {
68
content: texts.map((text) => ({ type: 'text', text })),
9+
isError,
710
};
811
}

0 commit comments

Comments
 (0)