Skip to content

Commit 0ebbf50

Browse files
authored
fix: deduplicate error logs, use info for 404/400 errors, fix ajv validate when it contains $ref (#335)
* fix: deduplicate error logs, use info for 404/400 errors, fix ajv validate when it contains $ref * fix: Do not log error when fetching non-existing Actor * fix: comments
1 parent 80aed10 commit 0ebbf50

File tree

5 files changed

+137
-59
lines changed

5 files changed

+137
-59
lines changed

src/tools/actor.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,14 @@ async function getMCPServersAsTools(
244244
return [];
245245
}
246246
return await getMCPServerTools(actorId, client, mcpServerUrl);
247+
} catch (error) {
248+
// Server error - log and continue processing other actors
249+
log.error('Failed to connect to MCP server', {
250+
actorFullName: actorInfo.actorDefinitionPruned.actorFullName,
251+
actorId,
252+
error,
253+
});
254+
return [];
247255
} finally {
248256
if (client) await client.close();
249257
}
@@ -273,17 +281,26 @@ export async function getActorsAsTools(
273281
} as ActorInfo;
274282
}
275283

276-
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
277-
if (!actorDefinitionPruned) {
278-
log.error('Actor not found or definition is not available', { actorName: actorIdOrName });
284+
try {
285+
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
286+
if (!actorDefinitionPruned) {
287+
log.info('Actor not found or definition is not available', { actorName: actorIdOrName });
288+
return null;
289+
}
290+
// Cache the pruned Actor definition
291+
actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionPruned);
292+
return {
293+
actorDefinitionPruned,
294+
webServerMcpPath: getActorMCPServerPath(actorDefinitionPruned),
295+
} as ActorInfo;
296+
} catch (error) {
297+
// Server error - log and continue processing other actors
298+
log.error('Failed to fetch Actor definition', {
299+
actorName: actorIdOrName,
300+
error,
301+
});
279302
return null;
280303
}
281-
// Cache the pruned Actor definition
282-
actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionPruned);
283-
return {
284-
actorDefinitionPruned,
285-
webServerMcpPath: getActorMCPServerPath(actorDefinitionPruned),
286-
} as ActorInfo;
287304
}),
288305
);
289306

@@ -431,12 +448,10 @@ EXAMPLES:
431448
}
432449

433450
/**
434-
* In Skyfire mode, we check for the presence of `skyfire-pay-id`.
435-
* If it is missing, we return instructions to the LLM on how to create it and pass it to the tool.
436-
*/
437-
if (apifyMcpServer.options.skyfireMode
438-
&& args['skyfire-pay-id'] === undefined
439-
) {
451+
* In Skyfire mode, we check for the presence of `skyfire-pay-id`.
452+
* If it is missing, we return instructions to the LLM on how to create it and pass it to the tool.
453+
*/
454+
if (apifyMcpServer.options.skyfireMode && args['skyfire-pay-id'] === undefined) {
440455
return {
441456
content: [{
442457
type: 'text',
@@ -446,8 +461,8 @@ EXAMPLES:
446461
}
447462

448463
/**
449-
* Create Apify token, for Skyfire mode use `skyfire-pay-id` and for normal mode use `apifyToken`.
450-
*/
464+
* Create Apify token, for Skyfire mode use `skyfire-pay-id` and for normal mode use `apifyToken`.
465+
*/
451466
const apifyClient = apifyMcpServer.options.skyfireMode && typeof args['skyfire-pay-id'] === 'string'
452467
? new ApifyClient({ skyfirePayId: args['skyfire-pay-id'] })
453468
: new ApifyClient({ token: apifyToken });

src/tools/build.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { z } from 'zod';
22
import zodToJsonSchema from 'zod-to-json-schema';
33

4-
import log from '@apify/log';
5-
64
import { ApifyClient } from '../apify-client.js';
75
import { ACTOR_README_MAX_LENGTH, HelperTools } from '../const.js';
86
import type {
@@ -32,7 +30,7 @@ export async function getActorDefinition(
3230
): Promise<ActorDefinitionPruned | null> {
3331
const actorClient = apifyClient.actor(actorIdOrName);
3432
try {
35-
// Fetch actor details
33+
// Fetch Actor details
3634
const actor = await actorClient.get();
3735
if (!actor) {
3836
return null;
@@ -53,9 +51,20 @@ export async function getActorDefinition(
5351
}
5452
return null;
5553
} catch (error) {
56-
const errorMessage = `Failed to fetch input schema for Actor: ${actorIdOrName} with error ${error}.`;
57-
log.error(errorMessage);
58-
throw new Error(errorMessage);
54+
// Check if it's a "not found" error (404 or 400 status codes)
55+
const isNotFound = typeof error === 'object'
56+
&& error !== null
57+
&& 'statusCode' in error
58+
&& (error.statusCode === 404 || error.statusCode === 400);
59+
60+
if (isNotFound) {
61+
// Return null for not found - caller will log appropriately
62+
return null;
63+
}
64+
65+
// For server errors, throw the original error (preserve error type)
66+
// Caller should catch and log
67+
throw error;
5968
}
6069
}
6170
function pruneActorDefinition(response: ActorDefinitionWithDesc): ActorDefinitionPruned {
@@ -121,14 +130,23 @@ export const actorDefinitionTool: ToolEntry = {
121130

122131
const parsed = getActorDefinitionArgsSchema.parse(args);
123132
const apifyClient = new ApifyClient({ token: apifyToken });
124-
const v = await getActorDefinition(parsed.actorName, apifyClient, parsed.limit);
125-
if (!v) {
126-
return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }] };
127-
}
128-
if (v && v.input && 'properties' in v.input && v.input) {
129-
const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties });
130-
v.input.properties = shortenProperties(properties);
133+
try {
134+
const v = await getActorDefinition(parsed.actorName, apifyClient, parsed.limit);
135+
if (!v) {
136+
return { content: [{ type: 'text', text: `Actor '${parsed.actorName}' not found.` }] };
137+
}
138+
if (v && v.input && 'properties' in v.input && v.input) {
139+
const properties = filterSchemaProperties(v.input.properties as { [key: string]: ISchemaProperties });
140+
v.input.properties = shortenProperties(properties);
141+
}
142+
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };
143+
} catch (error) {
144+
return {
145+
content: [{
146+
type: 'text',
147+
text: `Failed to fetch Actor definition: ${error instanceof Error ? error.message : String(error)}`,
148+
}],
149+
};
131150
}
132-
return { content: [{ type: 'text', text: `\`\`\`json\n${JSON.stringify(v)}\n\`\`\`` }] };
133151
},
134152
} as const;

src/tools/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ export function buildActorInputSchema(actorFullName: string, input: IActorInputS
180180
delete working.schemaVersion;
181181
}
182182

183+
// Remove $ref and $schema fields if present
184+
// since AJV cannot resolve external schema references
185+
// $ref and $schema are present in apify/website-content-crawler input schema
186+
if ('$ref' in working) {
187+
delete (working as { $ref?: string }).$ref;
188+
}
189+
if ('$schema' in working) {
190+
delete (working as { $schema?: string }).$schema;
191+
}
192+
183193
let finalSchema = working;
184194
if (isRag) {
185195
finalSchema = pruneSchemaPropertiesByWhitelist(finalSchema, RAG_WEB_BROWSER_WHITELISTED_FIELDS);

src/utils/actor-details.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Actor, Build } from 'apify-client';
22

3+
import log from '@apify/log';
4+
35
import type { ApifyClient } from '../apify-client.js';
46
import { filterSchemaProperties, shortenProperties } from '../tools/utils.js';
57
import type { IActorInputSchema } from '../types.js';
@@ -15,23 +17,40 @@ export interface ActorDetailsResult {
1517
}
1618

1719
export async function fetchActorDetails(apifyClient: ApifyClient, actorName: string): Promise<ActorDetailsResult | null> {
18-
const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([
19-
apifyClient.actor(actorName).get(),
20-
apifyClient.actor(actorName).defaultBuild().then(async (build) => build.get()),
21-
]);
22-
if (!actorInfo || !buildInfo || !buildInfo.actorDefinition) return null;
23-
const inputSchema = (buildInfo.actorDefinition.input || {
24-
type: 'object',
25-
properties: {},
26-
}) as IActorInputSchema;
27-
inputSchema.properties = filterSchemaProperties(inputSchema.properties);
28-
inputSchema.properties = shortenProperties(inputSchema.properties);
29-
const actorCard = formatActorToActorCard(actorInfo);
30-
return {
31-
actorInfo,
32-
buildInfo,
33-
actorCard,
34-
inputSchema,
35-
readme: buildInfo.actorDefinition.readme || 'No README provided.',
36-
};
20+
try {
21+
const [actorInfo, buildInfo]: [Actor | undefined, Build | undefined] = await Promise.all([
22+
apifyClient.actor(actorName).get(),
23+
apifyClient.actor(actorName).defaultBuild().then(async (build) => build.get()),
24+
]);
25+
if (!actorInfo || !buildInfo || !buildInfo.actorDefinition) return null;
26+
const inputSchema = (buildInfo.actorDefinition.input || {
27+
type: 'object',
28+
properties: {},
29+
}) as IActorInputSchema;
30+
inputSchema.properties = filterSchemaProperties(inputSchema.properties);
31+
inputSchema.properties = shortenProperties(inputSchema.properties);
32+
const actorCard = formatActorToActorCard(actorInfo);
33+
return {
34+
actorInfo,
35+
buildInfo,
36+
actorCard,
37+
inputSchema,
38+
readme: buildInfo.actorDefinition.readme || 'No README provided.',
39+
};
40+
} catch (error) {
41+
// Check if it's a 404 error (actor not found) - this is expected
42+
const is404 = typeof error === 'object'
43+
&& error !== null
44+
&& 'statusCode' in error
45+
&& (error as { statusCode?: number }).statusCode === 404;
46+
47+
if (is404) {
48+
// Log 404 errors at info level since they're expected (user may query non-existent actors)
49+
log.info(`Actor '${actorName}' not found`, { actorName });
50+
} else {
51+
// Log other errors at error level
52+
log.error(`Failed to fetch actor details for '${actorName}'`, { actorName, error });
53+
}
54+
return null;
55+
}
3756
}

src/utils/actor.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,32 @@ export async function getActorMcpUrlCached(
2020
return cached as string | false;
2121
}
2222

23-
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
24-
const mcpPath = actorDefinitionPruned && getActorMCPServerPath(actorDefinitionPruned);
25-
if (actorDefinitionPruned && mcpPath) {
26-
const url = await getActorMCPServerURL(actorDefinitionPruned.id, mcpPath);
27-
mcpServerCache.set(actorIdOrName, url);
28-
return url;
29-
}
23+
try {
24+
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
25+
const mcpPath = actorDefinitionPruned && getActorMCPServerPath(actorDefinitionPruned);
26+
if (actorDefinitionPruned && mcpPath) {
27+
const url = await getActorMCPServerURL(actorDefinitionPruned.id, mcpPath);
28+
mcpServerCache.set(actorIdOrName, url);
29+
return url;
30+
}
31+
32+
mcpServerCache.set(actorIdOrName, false);
33+
return false;
34+
} catch (error) {
35+
// Check if it's a "not found" error (404 or 400 status codes)
36+
const isNotFound = typeof error === 'object'
37+
&& error !== null
38+
&& 'statusCode' in error
39+
&& (error.statusCode === 404 || error.statusCode === 400);
3040

31-
mcpServerCache.set(actorIdOrName, false);
32-
return false;
41+
if (isNotFound) {
42+
// Actor doesn't exist - cache false and return false
43+
mcpServerCache.set(actorIdOrName, false);
44+
return false;
45+
}
46+
// Real server error - don't cache, let it propagate
47+
throw error;
48+
}
3349
}
3450

3551
/**

0 commit comments

Comments
 (0)