Skip to content

Commit 005db2a

Browse files
authored
feat: Improve error handling (required for claude connector) (#331)
* feat: Improve error handling (required for claude connector) * feat: Add hint to use tool to get schema * fix: Add isError flag to tool execution errors per MCP spec
1 parent 149cbbc commit 005db2a

File tree

16 files changed

+120
-122
lines changed

16 files changed

+120
-122
lines changed

evals/evaluation-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function filterById(testCases: TestCase[], idPattern: string): TestCase[]
7373
export async function loadTools(): Promise<ToolBase[]> {
7474
const apifyClient = new ApifyClient({ token: process.env.APIFY_API_TOKEN || '' });
7575
const urlTools = await processParamsGetTools('', apifyClient);
76-
return urlTools.map((t: ToolEntry) => getToolPublicFieldOnly(t.tool)) as ToolBase[];
76+
return urlTools.map((t: ToolEntry) => getToolPublicFieldOnly(t)) as ToolBase[];
7777
}
7878

7979
export function transformToolsToOpenAIFormat(tools: ToolBase[]): OpenAI.Chat.Completions.ChatCompletionTool[] {

src/mcp/server.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export class ActorsMcpServer {
180180
* Loads missing toolNames from a provided list of tool names.
181181
* Skips toolNames that are already loaded and loads only the missing ones.
182182
* @param toolNames - Array of tool names to ensure are loaded
183-
* @param apifyToken - Apify API token for authentication
183+
* @param apifyClient
184184
*/
185185
public async loadToolsByName(toolNames: string[], apifyClient: ApifyClient) {
186186
const loadedTools = this.listAllToolNames();
@@ -215,7 +215,7 @@ export class ActorsMcpServer {
215215
* Load actors as tools, upsert them to the server, and return the tool entries.
216216
* This is a public method that wraps getActorsAsTools and handles the upsert operation.
217217
* @param actorIdsOrNames - Array of actor IDs or names to load as tools
218-
* @param apifyToken - Apify API token for authentication
218+
* @param apifyClient
219219
* @returns Promise<ToolEntry[]> - Array of loaded tool entries
220220
*/
221221
public async loadActorsAsTools(actorIdsOrNames: string[], apifyClient: ApifyClient): Promise<ToolEntry[]> {
@@ -483,7 +483,9 @@ export class ActorsMcpServer {
483483

484484
// Validate token
485485
if (!apifyToken && !this.options.skyfireMode) {
486-
const msg = 'APIFY_TOKEN is required. It must be set in the environment variables or passed as a parameter in the body.';
486+
const msg = `APIFY_TOKEN is required but was not provided.
487+
Please set the APIFY_TOKEN environment variable or pass it as a parameter in the request body.
488+
You can obtain your Apify token from https://console.apify.com/account/integrations.`;
487489
log.softFail(msg, { statusCode: 400 });
488490
await this.server.sendLoggingMessage({ level: 'error', data: msg });
489491
throw new McpError(
@@ -507,7 +509,10 @@ export class ActorsMcpServer {
507509
const tool = Array.from(this.tools.values())
508510
.find((t) => t.name === name || (t.type === 'actor' && t.actorFullName === name));
509511
if (!tool) {
510-
const msg = `Tool ${name} not found. Available tools: ${this.listToolNames().join(', ')}`;
512+
const availableTools = this.listToolNames();
513+
const msg = `Tool "${name}" was not found.
514+
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
515+
Please verify the tool name is correct. You can list all available tools using the tools/list request.`;
511516
log.softFail(msg, { statusCode: 404 });
512517
await this.server.sendLoggingMessage({ level: 'error', data: msg });
513518
throw new McpError(
@@ -516,7 +521,8 @@ export class ActorsMcpServer {
516521
);
517522
}
518523
if (!args) {
519-
const msg = `Missing arguments for tool ${name}`;
524+
const msg = `Missing arguments for tool "${name}".
525+
Please provide the required arguments for this tool. Check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool to see what parameters are required.`;
520526
log.softFail(msg, { statusCode: 400 });
521527
await this.server.sendLoggingMessage({ level: 'error', data: msg });
522528
throw new McpError(
@@ -529,7 +535,11 @@ export class ActorsMcpServer {
529535
args = decodeDotPropertyNames(args);
530536
log.debug('Validate arguments for tool', { toolName: tool.name, input: args });
531537
if (!tool.ajvValidate(args)) {
532-
const msg = `Invalid arguments for tool ${tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.ajvValidate.errors)}`;
538+
const errors = tool?.ajvValidate.errors || [];
539+
const errorMessages = errors.map((e: { message?: string; instancePath?: string }) => `${e.instancePath || 'root'}: ${e.message || 'validation error'}`).join('; ');
540+
const msg = `Invalid arguments for tool "${tool.name}".
541+
Validation errors: ${errorMessages}.
542+
Please check the tool's input schema using ${HelperTools.ACTOR_GET_DETAILS} tool and ensure all required parameters are provided with correct types and values.`;
533543
log.softFail(msg, { statusCode: 400 });
534544
await this.server.sendLoggingMessage({ level: 'error', data: msg });
535545
throw new McpError(
@@ -569,16 +579,11 @@ export class ActorsMcpServer {
569579
try {
570580
client = await connectMCPClient(tool.serverUrl, apifyToken);
571581
if (!client) {
572-
const msg = `Failed to connect to MCP server ${tool.serverUrl}`;
573-
// Note: Timeout errors are already logged as warning in connectMCPClient
574-
// This is a fallback log for when connection fails (client-side issue)
582+
const msg = `Failed to connect to MCP server at "${tool.serverUrl}".
583+
Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.`;
575584
log.softFail(msg, { statusCode: 408 }); // 408 Request Timeout
576585
await this.server.sendLoggingMessage({ level: 'error', data: msg });
577-
return {
578-
content: [
579-
{ type: 'text', text: msg },
580-
],
581-
};
586+
return buildMCPResponse([msg], true);
582587
}
583588

584589
// Only set up notification handlers if progressToken is provided by the client
@@ -619,12 +624,7 @@ export class ActorsMcpServer {
619624
if (this.options.skyfireMode
620625
&& args['skyfire-pay-id'] === undefined
621626
) {
622-
return {
623-
content: [{
624-
type: 'text',
625-
text: SKYFIRE_TOOL_INSTRUCTIONS,
626-
}],
627-
};
627+
return buildMCPResponse([SKYFIRE_TOOL_INSTRUCTIONS]);
628628
}
629629

630630
// Create progress tracker if progressToken is available
@@ -669,11 +669,15 @@ export class ActorsMcpServer {
669669
logHttpError(error, 'Error occurred while calling tool', { toolName: name });
670670
const errorMessage = (error instanceof Error) ? error.message : 'Unknown error';
671671
return buildMCPResponse([
672-
`Error calling tool ${name}: ${errorMessage}`,
673-
]);
672+
`Error calling tool "${name}": ${errorMessage}.
673+
Please verify the tool name, input parameters, and ensure all required resources are available.`,
674+
], true);
674675
}
675676

676-
const msg = `Unknown tool: ${name}`;
677+
const availableTools = this.listToolNames();
678+
const msg = `Unknown tool type for "${name}".
679+
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
680+
Please verify the tool name and ensure the tool is properly registered.`;
677681
log.softFail(msg, { statusCode: 404 });
678682
await this.server.sendLoggingMessage({
679683
level: 'error',

src/mcp/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
3939
/**
4040
* Process input parameters from URL and get tools
4141
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
42-
* @param url
43-
* @param apifyToken
42+
* @param url The URL to process
43+
* @param apifyClient The Apify client instance
44+
* @param initializeRequestData Optional initialize request data
4445
*/
4546
export async function processParamsGetTools(url: string, apifyClient: ApifyClient, initializeRequestData?: InitializeRequest) {
4647
const input = parseInputParamsFromUrl(url);

src/tools/actor.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ export async function getActorsAsTools(
287287
try {
288288
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
289289
if (!actorDefinitionPruned) {
290-
log.info('Actor not found or definition is not available', { actorName: actorIdOrName });
290+
log.softFail('Actor not found or definition is not available', { actorName: actorIdOrName, statusCode: 404 });
291291
return null;
292292
}
293293
// Cache the pruned Actor definition
@@ -408,7 +408,9 @@ EXAMPLES:
408408

409409
// Standby Actors, thus MCPs, are not supported in Skyfire mode
410410
if (isActorMcpServer && apifyMcpServer.options.skyfireMode) {
411-
return buildMCPResponse([`MCP server Actors are not supported in Skyfire mode. Please use a regular Apify token without Skyfire.`]);
411+
return buildMCPResponse([
412+
`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+
], true);
412414
}
413415

414416
try {
@@ -421,7 +423,7 @@ EXAMPLES:
421423
try {
422424
client = await connectMCPClient(mcpServerUrl, apifyToken);
423425
if (!client) {
424-
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]);
426+
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`], true);
425427
}
426428
const toolsResponse = await client.listTools();
427429

@@ -436,7 +438,9 @@ EXAMPLES:
436438
// Regular actor: return schema
437439
const details = await fetchActorDetails(apifyClientForDefinition, baseActorName);
438440
if (!details) {
439-
return buildMCPResponse([`Actor information for '${baseActorName}' was not found. Please check the Actor ID or name and ensure the Actor exists.`]);
441+
return buildMCPResponse([`Actor information for '${baseActorName}' was not found.
442+
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}.`], true);
440444
}
441445
const content = [
442446
`Actor name: ${actorName}`,
@@ -474,28 +478,28 @@ EXAMPLES:
474478

475479
// Step 2: Call the Actor
476480
if (!input) {
477-
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);
478482
}
479483

480484
// Handle the case where LLM does not respect instructions when calling MCP server Actors
481485
// and does not provide the tool name.
482486
const isMcpToolNameInvalid = mcpToolName === undefined || mcpToolName.trim().length === 0;
483487
if (isActorMcpServer && isMcpToolNameInvalid) {
484-
return buildMCPResponse([CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG]);
488+
return buildMCPResponse([CALL_ACTOR_MCP_MISSING_TOOL_NAME_MSG], true);
485489
}
486490

487491
// Handle MCP tool calls
488492
if (mcpToolName) {
489493
if (!isActorMcpServer) {
490-
return buildMCPResponse([`Actor '${baseActorName}' is not an MCP server.`]);
494+
return buildMCPResponse([`Actor '${baseActorName}' is not an MCP server.`], true);
491495
}
492496

493497
const mcpServerUrl = mcpServerUrlOrFalse;
494498
let client: Client | null = null;
495499
try {
496500
client = await connectMCPClient(mcpServerUrl, apifyToken);
497501
if (!client) {
498-
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`]);
502+
return buildMCPResponse([`Failed to connect to MCP server ${mcpServerUrl}`], true);
499503
}
500504

501505
const result = await client.callTool({
@@ -513,7 +517,9 @@ EXAMPLES:
513517
const [actor] = await getActorsAsTools([actorName], apifyClient);
514518

515519
if (!actor) {
516-
return buildMCPResponse([`Actor '${actorName}' was not found.`]);
520+
return buildMCPResponse([`Actor '${actorName}' was not found.
521+
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}.`], true);
517523
}
518524

519525
if (!actor.ajvValidate(input)) {
@@ -548,7 +554,9 @@ EXAMPLES:
548554
return { content };
549555
} catch (error) {
550556
logHttpError(error, 'Failed to call Actor', { actorName, performStep });
551-
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}`]);
557+
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}.
558+
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}.`], true);
552560
}
553561
},
554562
};

src/tools/build.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { filterSchemaProperties, shortenProperties } from './utils.js';
1919
* First, fetch the Actor details to get the default build tag and buildId.
2020
* Then, fetch the build details and return actorName, description, and input schema.
2121
* @param {string} actorIdOrName - Actor ID or Actor full name.
22+
* @param {ApifyClient} apifyClient - The Apify client instance.
2223
* @param {number} limit - Truncate the README to this limit.
23-
* @param {string} apifyToken
2424
* @returns {Promise<ActorDefinitionWithDesc | null>} - The actor definition with description or null if not found.
2525
*/
2626
export async function getActorDefinition(
@@ -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: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { HelperTools } from '../const.js';
66
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
77
import { fetchActorDetails } from '../utils/actor-details.js';
88
import { ajv } from '../utils/ajv.js';
9+
import { buildMCPResponse } from '../utils/mcp.js';
910

1011
const fetchActorDetailsToolArgsSchema = z.object({
1112
actor: z.string()
@@ -40,26 +41,28 @@ USAGE EXAMPLES:
4041
const apifyClient = new ApifyClient({ token: apifyToken });
4142
const details = await fetchActorDetails(apifyClient, parsed.actor);
4243
if (!details) {
43-
return {
44-
content: [{ type: 'text', text: `Actor information for '${parsed.actor}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
45-
};
44+
const texts = [`Actor information for '${parsed.actor}' was not found.
45+
Please verify Actor ID or name format and ensure that the Actor exists.
46+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`,
47+
];
48+
return buildMCPResponse(texts, true);
4649
}
4750

4851
const actorUrl = `https://apify.com/${details.actorInfo.username}/${details.actorInfo.name}`;
4952
// Add link to README title
5053
details.readme = details.readme.replace(/^# /, `# [README](${actorUrl}/readme): `);
5154

52-
const content = [
53-
{ type: 'text', text: `# Actor information\n${details.actorCard}` },
54-
{ type: 'text', text: `${details.readme}` },
55+
const texts = [
56+
`# Actor information\n${details.actorCard}`,
57+
`${details.readme}`,
5558
];
5659

5760
// Include input schema if it has properties
5861
if (details.inputSchema.properties || Object.keys(details.inputSchema.properties).length !== 0) {
59-
content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\`` });
62+
texts.push(`# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\``);
6063
}
6164
// Return the actor card, README, and input schema (if it has non-empty properties) as separate text blocks
6265
// This allows better formatting in the final output
63-
return { content };
66+
return buildMCPResponse(texts);
6467
},
6568
} as const;

0 commit comments

Comments
 (0)