diff --git a/src/tools/actor.ts b/src/tools/actor.ts index b0439d13..56a42efb 100644 --- a/src/tools/actor.ts +++ b/src/tools/actor.ts @@ -244,6 +244,14 @@ async function getMCPServersAsTools( return []; } return await getMCPServerTools(actorId, client, mcpServerUrl); + } catch (error) { + // Server error - log and continue processing other actors + log.error('Failed to connect to MCP server', { + actorFullName: actorInfo.actorDefinitionPruned.actorFullName, + actorId, + error, + }); + return []; } finally { if (client) await client.close(); } @@ -273,17 +281,26 @@ export async function getActorsAsTools( } as ActorInfo; } - const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient); - if (!actorDefinitionPruned) { - log.error('Actor not found or definition is not available', { actorName: actorIdOrName }); + try { + const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient); + if (!actorDefinitionPruned) { + log.info('Actor not found or definition is not available', { actorName: actorIdOrName }); + return null; + } + // Cache the pruned Actor definition + actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionPruned); + return { + actorDefinitionPruned, + webServerMcpPath: getActorMCPServerPath(actorDefinitionPruned), + } as ActorInfo; + } catch (error) { + // Server error - log and continue processing other actors + log.error('Failed to fetch Actor definition', { + actorName: actorIdOrName, + error, + }); return null; } - // Cache the pruned Actor definition - actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionPruned); - return { - actorDefinitionPruned, - webServerMcpPath: getActorMCPServerPath(actorDefinitionPruned), - } as ActorInfo; }), ); @@ -431,12 +448,10 @@ EXAMPLES: } /** - * In Skyfire mode, we check for the presence of `skyfire-pay-id`. - * If it is missing, we return instructions to the LLM on how to create it and pass it to the tool. - */ - if (apifyMcpServer.options.skyfireMode - && args['skyfire-pay-id'] === undefined - ) { + * In Skyfire mode, we check for the presence of `skyfire-pay-id`. + * If it is missing, we return instructions to the LLM on how to create it and pass it to the tool. + */ + if (apifyMcpServer.options.skyfireMode && args['skyfire-pay-id'] === undefined) { return { content: [{ type: 'text', @@ -446,8 +461,8 @@ EXAMPLES: } /** - * Create Apify token, for Skyfire mode use `skyfire-pay-id` and for normal mode use `apifyToken`. - */ + * Create Apify token, for Skyfire mode use `skyfire-pay-id` and for normal mode use `apifyToken`. + */ const apifyClient = apifyMcpServer.options.skyfireMode && typeof args['skyfire-pay-id'] === 'string' ? new ApifyClient({ skyfirePayId: args['skyfire-pay-id'] }) : new ApifyClient({ token: apifyToken }); diff --git a/src/tools/build.ts b/src/tools/build.ts index 8b3b132c..7199a947 100644 --- a/src/tools/build.ts +++ b/src/tools/build.ts @@ -1,8 +1,6 @@ import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; -import log from '@apify/log'; - import { ApifyClient } from '../apify-client.js'; import { ACTOR_README_MAX_LENGTH, HelperTools } from '../const.js'; import type { @@ -32,7 +30,7 @@ export async function getActorDefinition( ): Promise { const actorClient = apifyClient.actor(actorIdOrName); try { - // Fetch actor details + // Fetch Actor details const actor = await actorClient.get(); if (!actor) { return null; @@ -53,9 +51,20 @@ export async function getActorDefinition( } return null; } catch (error) { - const errorMessage = `Failed to fetch input schema for Actor: ${actorIdOrName} with error ${error}.`; - log.error(errorMessage); - throw new Error(errorMessage); + // Check if it's a "not found" error (404 or 400 status codes) + const isNotFound = typeof error === 'object' + && error !== null + && 'statusCode' in error + && (error.statusCode === 404 || error.statusCode === 400); + + if (isNotFound) { + // Return null for not found - caller will log appropriately + return null; + } + + // For server errors, throw the original error (preserve error type) + // Caller should catch and log + throw error; } } function pruneActorDefinition(response: ActorDefinitionWithDesc): ActorDefinitionPruned { @@ -121,14 +130,23 @@ export const actorDefinitionTool: ToolEntry = { const parsed = getActorDefinitionArgsSchema.parse(args); const apifyClient = new ApifyClient({ token: apifyToken }); - const v = await getActorDefinition(parsed.actorName, apifyClient, parsed.limit); - if (!v) { - return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }] }; - } - if (v && v.input && 'properties' in v.input && v.input) { - const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties }); - v.input.properties = shortenProperties(properties); + try { + const v = await getActorDefinition(parsed.actorName, apifyClient, parsed.limit); + if (!v) { + return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }] }; + } + if (v && v.input && 'properties' in v.input && v.input) { + const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties }); + v.input.properties = shortenProperties(properties); + } + return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; + } catch (error) { + return { + content: [{ + type: 'text', + text: `Failed to fetch Actor definition: ${error instanceof Error ? error.message : String(error)}`, + }], + }; } - return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] }; }, } as const; diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 523ee962..2a7649e3 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -180,6 +180,16 @@ export function buildActorInputSchema(actorFullName: string, input: IActorInputS delete working.schemaVersion; } + // Remove $ref and $schema fields if present + // since AJV cannot resolve external schema references + // $ref and $schema are present in apify/website-content-crawler input schema + if ('$ref' in working) { + delete (working as { $ref?: string }).$ref; + } + if ('$schema' in working) { + delete (working as { $schema?: string }).$schema; + } + let finalSchema = working; if (isRag) { finalSchema = pruneSchemaPropertiesByWhitelist(finalSchema, RAG_WEB_BROWSER_WHITELISTED_FIELDS); diff --git a/src/utils/actor-details.ts b/src/utils/actor-details.ts index 3a8915bc..7b8ad916 100644 --- a/src/utils/actor-details.ts +++ b/src/utils/actor-details.ts @@ -1,5 +1,7 @@ import type { Actor, Build } from 'apify-client'; +import log from '@apify/log'; + import type { ApifyClient } from '../apify-client.js'; import { filterSchemaProperties, shortenProperties } from '../tools/utils.js'; import type { IActorInputSchema } from '../types.js'; @@ -15,23 +17,40 @@ export interface ActorDetailsResult { } export async function fetchActorDetails(apifyClient: ApifyClient, actorName: string): Promise { - const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([ - apifyClient.actor(actorName).get(), - apifyClient.actor(actorName).defaultBuild().then(async (build) => build.get()), - ]); - if (!actorInfo || !buildInfo || !buildInfo.actorDefinition) return null; - const inputSchema = (buildInfo.actorDefinition.input || { - type: 'object', - properties: {}, - }) as IActorInputSchema; - inputSchema.properties = filterSchemaProperties(inputSchema.properties); - inputSchema.properties = shortenProperties(inputSchema.properties); - const actorCard = formatActorToActorCard(actorInfo); - return { - actorInfo, - buildInfo, - actorCard, - inputSchema, - readme: buildInfo.actorDefinition.readme || 'No README provided.', - }; + try { + const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([ + apifyClient.actor(actorName).get(), + apifyClient.actor(actorName).defaultBuild().then(async (build) => build.get()), + ]); + if (!actorInfo || !buildInfo || !buildInfo.actorDefinition) return null; + const inputSchema = (buildInfo.actorDefinition.input || { + type: 'object', + properties: {}, + }) as IActorInputSchema; + inputSchema.properties = filterSchemaProperties(inputSchema.properties); + inputSchema.properties = shortenProperties(inputSchema.properties); + const actorCard = formatActorToActorCard(actorInfo); + return { + actorInfo, + buildInfo, + actorCard, + inputSchema, + readme: buildInfo.actorDefinition.readme || 'No README provided.', + }; + } catch (error) { + // Check if it's a 404 error (actor not found) - this is expected + const is404 = typeof error === 'object' + && error !== null + && 'statusCode' in error + && (error as { statusCode?: number }).statusCode === 404; + + if (is404) { + // Log 404 errors at info level since they're expected (user may query non-existent actors) + log.info(`Actor '${actorName}' not found`, { actorName }); + } else { + // Log other errors at error level + log.error(`Failed to fetch actor details for '${actorName}'`, { actorName, error }); + } + return null; + } } diff --git a/src/utils/actor.ts b/src/utils/actor.ts index e6eefd63..56b11e98 100644 --- a/src/utils/actor.ts +++ b/src/utils/actor.ts @@ -20,16 +20,32 @@ export async function getActorMcpUrlCached( return cached as string | false; } - const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient); - const mcpPath = actorDefinitionPruned && getActorMCPServerPath(actorDefinitionPruned); - if (actorDefinitionPruned && mcpPath) { - const url = await getActorMCPServerURL(actorDefinitionPruned.id, mcpPath); - mcpServerCache.set(actorIdOrName, url); - return url; - } + try { + const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient); + const mcpPath = actorDefinitionPruned && getActorMCPServerPath(actorDefinitionPruned); + if (actorDefinitionPruned && mcpPath) { + const url = await getActorMCPServerURL(actorDefinitionPruned.id, mcpPath); + mcpServerCache.set(actorIdOrName, url); + return url; + } + + mcpServerCache.set(actorIdOrName, false); + return false; + } catch (error) { + // Check if it's a "not found" error (404 or 400 status codes) + const isNotFound = typeof error === 'object' + && error !== null + && 'statusCode' in error + && (error.statusCode === 404 || error.statusCode === 400); - mcpServerCache.set(actorIdOrName, false); - return false; + if (isNotFound) { + // Actor doesn't exist - cache false and return false + mcpServerCache.set(actorIdOrName, false); + return false; + } + // Real server error - don't cache, let it propagate + throw error; + } } /**