Skip to content

Commit 602cc28

Browse files
committed
feat: Improve error handling (required for claude connector)
1 parent 80aed10 commit 602cc28

File tree

6 files changed

+88
-94
lines changed

6 files changed

+88
-94
lines changed

src/mcp/server.ts

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

483483
// Validate token
484484
if (!apifyToken && !this.options.skyfireMode) {
485-
const msg = 'APIFY_TOKEN is required. It must be set in the environment variables or passed as a parameter in the body.';
485+
const msg = `APIFY_TOKEN is required but was not provided.
486+
Please set the APIFY_TOKEN environment variable or pass it as a parameter in the request body.
487+
You can obtain your Apify token from https://console.apify.com/account/integrations.`;
486488
log.error(msg);
487489
await this.server.sendLoggingMessage({ level: 'error', data: msg });
488490
throw new McpError(
@@ -506,7 +508,10 @@ export class ActorsMcpServer {
506508
const tool = Array.from(this.tools.values())
507509
.find((t) => t.name === name || (t.type === 'actor' && t.actorFullName === name));
508510
if (!tool) {
509-
const msg = `Tool ${name} not found. Available tools: ${this.listToolNames().join(', ')}`;
511+
const availableTools = this.listToolNames();
512+
const msg = `Tool "${name}" was not found.
513+
Available tools: ${availableTools.length > 0 ? availableTools.join(', ') : 'none'}.
514+
Please verify the tool name is correct. You can list all available tools using the tools/list request.`;
510515
log.error(msg);
511516
await this.server.sendLoggingMessage({ level: 'error', data: msg });
512517
throw new McpError(
@@ -515,7 +520,8 @@ export class ActorsMcpServer {
515520
);
516521
}
517522
if (!args) {
518-
const msg = `Missing arguments for tool ${name}`;
523+
const msg = `Missing arguments for tool "${name}".
524+
Please provide the required arguments for this tool. Check the tool's input schema to see what parameters are required.`;
519525
log.error(msg);
520526
await this.server.sendLoggingMessage({ level: 'error', data: msg });
521527
throw new McpError(
@@ -528,7 +534,11 @@ export class ActorsMcpServer {
528534
args = decodeDotPropertyNames(args);
529535
log.debug('Validate arguments for tool', { toolName: tool.name, input: args });
530536
if (!tool.ajvValidate(args)) {
531-
const msg = `Invalid arguments for tool ${tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.ajvValidate.errors)}`;
537+
const errors = tool?.ajvValidate.errors || [];
538+
const errorMessages = errors.map((e: { message?: string; instancePath?: string }) => `${e.instancePath || 'root'}: ${e.message || 'validation error'}`).join('; ');
539+
const msg = `Invalid arguments for tool "${tool.name}".
540+
Validation errors: ${errorMessages}.
541+
Please check the tool's input schema and ensure all required parameters are provided with correct types and values.`;
532542
log.error(msg);
533543
await this.server.sendLoggingMessage({ level: 'error', data: msg });
534544
throw new McpError(
@@ -568,14 +578,11 @@ export class ActorsMcpServer {
568578
try {
569579
client = await connectMCPClient(tool.serverUrl, apifyToken);
570580
if (!client) {
571-
const msg = `Failed to connect to MCP server ${tool.serverUrl}`;
581+
const msg = `Failed to connect to MCP server at "${tool.serverUrl}".
582+
Please verify the server URL is correct and accessible, and ensure you have a valid Apify token with appropriate permissions.`;
572583
log.error(msg);
573584
await this.server.sendLoggingMessage({ level: 'error', data: msg });
574-
return {
575-
content: [
576-
{ type: 'text', text: msg },
577-
],
578-
};
585+
return buildMCPResponse([msg]);
579586
}
580587

581588
// Only set up notification handlers if progressToken is provided by the client
@@ -616,12 +623,7 @@ export class ActorsMcpServer {
616623
if (this.options.skyfireMode
617624
&& args['skyfire-pay-id'] === undefined
618625
) {
619-
return {
620-
content: [{
621-
type: 'text',
622-
text: SKYFIRE_TOOL_INSTRUCTIONS,
623-
}],
624-
};
626+
return buildMCPResponse([SKYFIRE_TOOL_INSTRUCTIONS]);
625627
}
626628

627629
// Create progress tracker if progressToken is available
@@ -666,11 +668,15 @@ export class ActorsMcpServer {
666668
log.error('Error occurred while calling tool', { toolName: name, error });
667669
const errorMessage = (error instanceof Error) ? error.message : 'Unknown error';
668670
return buildMCPResponse([
669-
`Error calling tool ${name}: ${errorMessage}`,
671+
`Error calling tool "${name}": ${errorMessage}.
672+
Please verify the tool name, input parameters, and ensure all required resources are available.`,
670673
]);
671674
}
672675

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

src/tools/actor.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ EXAMPLES:
386386

387387
// Standby Actors, thus MCPs, are not supported in Skyfire mode
388388
if (isActorMcpServer && apifyMcpServer.options.skyfireMode) {
389-
return buildMCPResponse([`MCP server Actors are not supported in Skyfire mode. Please use a regular Apify token without Skyfire.`]);
389+
return buildMCPResponse([
390+
`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.`,
391+
]);
390392
}
391393

392394
try {
@@ -414,7 +416,9 @@ EXAMPLES:
414416
// Regular actor: return schema
415417
const details = await fetchActorDetails(apifyClientForDefinition, baseActorName);
416418
if (!details) {
417-
return buildMCPResponse([`Actor information for '${baseActorName}' was not found. Please check the Actor ID or name and ensure the Actor exists.`]);
419+
return buildMCPResponse([`Actor information for '${baseActorName}' was not found.
420+
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
421+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
418422
}
419423
const content = [
420424
`Actor name: ${actorName}`,
@@ -493,7 +497,9 @@ EXAMPLES:
493497
const [actor] = await getActorsAsTools([actorName], apifyClient);
494498

495499
if (!actor) {
496-
return buildMCPResponse([`Actor '${actorName}' was not found.`]);
500+
return buildMCPResponse([`Actor '${actorName}' was not found.
501+
Please verify Actor ID or name format (e.g., "username/name" like "apify/rag-web-browser") and ensure that the Actor exists.
502+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`]);
497503
}
498504

499505
if (!actor.ajvValidate(input)) {
@@ -528,7 +534,9 @@ EXAMPLES:
528534
return { content };
529535
} catch (error) {
530536
log.error('Failed to call Actor', { error, actorName, performStep });
531-
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}`]);
537+
return buildMCPResponse([`Failed to call Actor '${actorName}': ${error instanceof Error ? error.message : String(error)}.
538+
Please verify the Actor name, input parameters, and ensure the Actor exists.
539+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}, or get Actor details using: ${HelperTools.ACTOR_GET_DETAILS}.`]);
532540
}
533541
},
534542
};

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()
@@ -35,26 +36,28 @@ USAGE EXAMPLES:
3536
const apifyClient = new ApifyClient({ token: apifyToken });
3637
const details = await fetchActorDetails(apifyClient, parsed.actor);
3738
if (!details) {
38-
return {
39-
content: [{ type: 'text', text: `Actor information for '${parsed.actor}' was not found. Please check the Actor ID or name and ensure the Actor exists.` }],
40-
};
39+
const texts = [`Actor information for '${parsed.actor}' was not found.
40+
Please verify Actor ID or name format and ensure that the Actor exists.
41+
You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.`,
42+
];
43+
return buildMCPResponse(texts);
4144
}
4245

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

47-
const content = [
48-
{ type: 'text', text: `# Actor information\n${details.actorCard}` },
49-
{ type: 'text', text: `${details.readme}` },
50+
const texts = [
51+
`# Actor information\n${details.actorCard}`,
52+
`${details.readme}`,
5053
];
5154

5255
// Include input schema if it has properties
5356
if (details.inputSchema.properties || Object.keys(details.inputSchema.properties).length !== 0) {
54-
content.push({ type: 'text', text: `# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\`` });
57+
texts.push(`# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\``);
5558
}
5659
// Return the actor card, README, and input schema (if it has non-empty properties) as separate text blocks
5760
// This allows better formatting in the final output
58-
return { content };
61+
return buildMCPResponse(texts);
5962
},
6063
} as const;

src/tools/fetch-apify-docs.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { fetchApifyDocsCache } from '../state.js';
88
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
99
import { ajv } from '../utils/ajv.js';
1010
import { htmlToMarkdown } from '../utils/html-to-md.js';
11+
import { buildMCPResponse } from '../utils/mcp.js';
1112

1213
const fetchApifyDocsToolArgsSchema = z.object({
1314
url: z.string()
@@ -38,12 +39,9 @@ USAGE EXAMPLES:
3839

3940
// Only allow URLs starting with https://docs.apify.com
4041
if (!url.startsWith('https://docs.apify.com')) {
41-
return {
42-
content: [{
43-
type: 'text',
44-
text: `Only URLs starting with https://docs.apify.com are allowed.`,
45-
}],
46-
};
42+
return buildMCPResponse([`Invalid URL: "${url}".
43+
Only URLs starting with "https://docs.apify.com" are allowed.
44+
Please provide a valid Apify documentation URL. You can find documentation URLs using the ${HelperTools.DOCS_SEARCH} tool.`]);
4745
}
4846

4947
// Cache URL without fragment to avoid fetching the same page multiple times
@@ -53,12 +51,9 @@ USAGE EXAMPLES:
5351
try {
5452
const response = await fetch(url);
5553
if (!response.ok) {
56-
return {
57-
content: [{
58-
type: 'text',
59-
text: `Failed to fetch the documentation page at ${url}. Status: ${response.status} ${response.statusText}`,
60-
}],
61-
};
54+
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
55+
HTTP Status: ${response.status} ${response.statusText}.
56+
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
6257
}
6358
const html = await response.text();
6459
markdown = htmlToMarkdown(html);
@@ -67,20 +62,12 @@ USAGE EXAMPLES:
6762
fetchApifyDocsCache.set(urlWithoutFragment, markdown);
6863
} catch (error) {
6964
log.error('Failed to fetch the documentation page', { url, error });
70-
return {
71-
content: [{
72-
type: 'text',
73-
text: `Failed to fetch the documentation page at ${url}. Please check the URL and try again.`,
74-
}],
75-
};
65+
return buildMCPResponse([`Failed to fetch the documentation page at "${url}".
66+
Error: ${error instanceof Error ? error.message : String(error)}.
67+
Please verify the URL is correct and accessible. You can search for available documentation pages using the ${HelperTools.DOCS_SEARCH} tool.`]);
7668
}
7769
}
7870

79-
return {
80-
content: [{
81-
type: 'text',
82-
text: `Fetched content from ${url}:\n\n${markdown}`,
83-
}],
84-
};
71+
return buildMCPResponse([`Fetched content from ${url}:\n\n${markdown}`]);
8572
},
8673
} as const;

src/tools/search-apify-docs.ts

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { HelperTools } from '../const.js';
55
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../types.js';
66
import { ajv } from '../utils/ajv.js';
77
import { searchApifyDocsCached } from '../utils/apify-docs.js';
8+
import { buildMCPResponse } from '../utils/mcp.js';
89

910
const searchApifyDocsToolArgsSchema = z.object({
1011
query: z.string()
@@ -30,24 +31,24 @@ export const searchApifyDocsTool: ToolEntry = {
3031
type: 'internal',
3132
name: HelperTools.DOCS_SEARCH,
3233
description: `Search Apify documentation using full-text search.
33-
You can use it to find relevant documentation based on keywords.
34-
Apify documentation has information about Apify console, Actors (development
35-
(actor.json, input schema, dataset schema, dockerfile), deployment, builds, runs),
36-
schedules, storages (datasets, key-value store), Proxy, Integrations,
37-
Apify Academy (crawling and webscraping with Crawlee),
34+
You can use it to find relevant documentation based on keywords.
35+
Apify documentation has information about Apify console, Actors (development
36+
(actor.json, input schema, dataset schema, dockerfile), deployment, builds, runs),
37+
schedules, storages (datasets, key-value store), Proxy, Integrations,
38+
Apify Academy (crawling and webscraping with Crawlee),
3839
39-
The results will include the URL of the documentation page, a fragment identifier (if available),
40-
and a limited piece of content that matches the search query.
40+
The results will include the URL of the documentation page, a fragment identifier (if available),
41+
and a limited piece of content that matches the search query.
4142
42-
Fetch the full content of the document using the ${HelperTools.DOCS_FETCH} tool by providing the URL.
43+
Fetch the full content of the document using the ${HelperTools.DOCS_FETCH} tool by providing the URL.
4344
44-
USAGE:
45-
- Use when user asks about Apify documentation, Actor development, Crawlee, or Apify platform.
45+
USAGE:
46+
- Use when user asks about Apify documentation, Actor development, Crawlee, or Apify platform.
4647
47-
USAGE EXAMPLES:
48-
- query: How to use create Apify Actor?
49-
- query: How to define Actor input schema?
50-
- query: How scrape with Crawlee?`,
48+
USAGE EXAMPLES:
49+
- query: How to use create Apify Actor?
50+
- query: How to define Actor input schema?
51+
- query: How scrape with Crawlee?`,
5152
inputSchema: zodToJsonSchema(searchApifyDocsToolArgsSchema) as ToolInputSchema,
5253
ajvValidate: ajv.compile(zodToJsonSchema(searchApifyDocsToolArgsSchema)),
5354
call: async (toolArgs: InternalToolArgs) => {
@@ -60,24 +61,16 @@ export const searchApifyDocsTool: ToolEntry = {
6061
const results = resultsRaw.slice(parsed.offset, parsed.offset + parsed.limit);
6162

6263
if (results.length === 0) {
63-
return {
64-
content: [{
65-
type: 'text',
66-
text: `No results found for the query "${query}" with limit ${parsed.limit} and offset ${parsed.offset}. Try a different query or adjust the limit and offset.`,
67-
}],
68-
};
64+
return buildMCPResponse([`No results found for the query "${query}" with limit ${parsed.limit} and offset ${parsed.offset}.
65+
Please try a different query with different keywords, or adjust the limit and offset parameters.
66+
You can also try using more specific or alternative keywords related to your search topic.`]);
6967
}
7068

7169
const textContent = `You can use the Apify docs fetch tool to retrieve the full content of a document by its URL. The document fragment refers to the section of the content containing the relevant part for the search result item.
7270
Search results for "${query}":
7371
7472
${results.map((result) => `- Document URL: ${result.url}${result.fragment ? `\n Document fragment: ${result.fragment}` : ''}
7573
Content: ${result.content}`).join('\n\n')}`;
76-
return {
77-
content: [{
78-
type: 'text',
79-
text: textContent,
80-
}],
81-
};
74+
return buildMCPResponse([textContent]);
8275
},
8376
} as const;

0 commit comments

Comments
 (0)