Skip to content

Commit 37ab8cb

Browse files
MQ37jirispilka
andauthored
fix: update actor definition handling to include full metadata and improve MCP server identification (#372)
* fix: update actor definition handling to include full metadata and improve MCP server identification * Update src/tools/actor.ts Co-authored-by: Jiří Spilka <jiri.spilka@apify.com> * Update src/tools/build.ts Co-authored-by: Jiří Spilka <jiri.spilka@apify.com> * Update src/utils/actor.ts Co-authored-by: Jiří Spilka <jiri.spilka@apify.com> * fix: improve comment clarity for null filtering in getActorsAsTools function --------- Co-authored-by: Jiří Spilka <jiri.spilka@apify.com>
1 parent a3b34c0 commit 37ab8cb

6 files changed

Lines changed: 74 additions & 45 deletions

File tree

src/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
MCP_SERVER_CACHE_MAX_SIZE,
99
MCP_SERVER_CACHE_TTL_SECS,
1010
} from './const.js';
11-
import type { ActorDefinitionPruned, ApifyDocsSearchResult } from './types.js';
11+
import type { ActorDefinitionWithInfo, ApifyDocsSearchResult } from './types.js';
1212
import { TTLLRUCache } from './utils/ttl-lru.js';
1313

14-
export const actorDefinitionPrunedCache = new TTLLRUCache<ActorDefinitionPruned>(ACTOR_CACHE_MAX_SIZE, ACTOR_CACHE_TTL_SECS);
14+
export const actorDefinitionPrunedCache = new TTLLRUCache<ActorDefinitionWithInfo>(ACTOR_CACHE_MAX_SIZE, ACTOR_CACHE_TTL_SECS);
1515
export const searchApifyDocsCache = new TTLLRUCache<ApifyDocsSearchResult[]>(APIFY_DOCS_CACHE_MAX_SIZE, APIFY_DOCS_CACHE_TTL_SECS);
1616
/** Stores processed Markdown content */
1717
export const fetchApifyDocsCache = new TTLLRUCache<string>(APIFY_DOCS_CACHE_MAX_SIZE, APIFY_DOCS_CACHE_TTL_SECS);

src/tools/actor.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type { ProgressTracker } from '../utils/progress.js';
2828
import type { JsonSchemaProperty } from '../utils/schema-generation.js';
2929
import { generateSchemaFromItems } from '../utils/schema-generation.js';
3030
import { getActorDefinition } from './build.js';
31-
import { actorNameToToolName, buildActorInputSchema, fixedAjvCompile } from './utils.js';
31+
import { actorNameToToolName, buildActorInputSchema, fixedAjvCompile, isActorInfoMcpServer } from './utils.js';
3232

3333
// Define a named return type for callActorGetDataset
3434
export type CallActorGetDatasetResult = {
@@ -150,7 +150,7 @@ export async function callActorGetDataset(
150150
* 4. Properties are shortened using shortenProperties()
151151
* 5. Enums are added to descriptions with examples using addEnumsToDescriptionsWithExamples()
152152
*
153-
* @param {ActorInfo[]} actorsInfo - An array of ActorInfo objects with webServerMcpPath and actorDefinitionPruned.
153+
* @param {ActorInfo[]} actorsInfo - An array of ActorInfo objects with webServerMcpPath, definition, and Actor.
154154
* @returns {Promise<ToolEntry[]>} - A promise that resolves to an array of MCP tools.
155155
*/
156156
export async function getNormalActorsAsTools(
@@ -159,22 +159,22 @@ export async function getNormalActorsAsTools(
159159
const tools: ToolEntry[] = [];
160160

161161
for (const actorInfo of actorsInfo) {
162-
const { actorDefinitionPruned } = actorInfo;
162+
const { definition } = actorInfo;
163163

164-
if (!actorDefinitionPruned) continue;
164+
if (!definition) continue;
165165

166-
const isRag = actorDefinitionPruned.actorFullName === RAG_WEB_BROWSER;
167-
const { inputSchema } = buildActorInputSchema(actorDefinitionPruned.actorFullName, actorDefinitionPruned.input, isRag);
166+
const isRag = definition.actorFullName === RAG_WEB_BROWSER;
167+
const { inputSchema } = buildActorInputSchema(definition.actorFullName, definition.input, isRag);
168168

169-
let description = `This tool calls the Actor "${actorDefinitionPruned.actorFullName}" and retrieves its output results.
169+
let description = `This tool calls the Actor "${definition.actorFullName}" and retrieves its output results.
170170
Use this tool instead of the "${HelperTools.ACTOR_CALL}" if user requests this specific Actor.
171-
Actor description: ${actorDefinitionPruned.description}`;
171+
Actor description: ${definition.description}`;
172172
if (isRag) {
173173
description += RAG_WEB_BROWSER_ADDITIONAL_DESC;
174174
}
175175

176176
const memoryMbytes = Math.min(
177-
actorDefinitionPruned.defaultRunOptions?.memoryMbytes || ACTOR_MAX_MEMORY_MBYTES,
177+
definition.defaultRunOptions?.memoryMbytes || ACTOR_MAX_MEMORY_MBYTES,
178178
ACTOR_MAX_MEMORY_MBYTES,
179179
);
180180

@@ -183,25 +183,25 @@ Actor description: ${actorDefinitionPruned.description}`;
183183
ajvValidate = fixedAjvCompile(ajv, { ...inputSchema, additionalProperties: true });
184184
} catch (e) {
185185
log.error('Failed to compile schema', {
186-
actorName: actorDefinitionPruned.actorFullName,
186+
actorName: definition.actorFullName,
187187
error: e,
188188
});
189189
continue;
190190
}
191191

192192
tools.push({
193193
type: 'actor',
194-
name: actorNameToToolName(actorDefinitionPruned.actorFullName),
195-
actorFullName: actorDefinitionPruned.actorFullName,
194+
name: actorNameToToolName(definition.actorFullName),
195+
actorFullName: definition.actorFullName,
196196
description,
197197
inputSchema: inputSchema as ToolInputSchema,
198198
ajvValidate,
199199
memoryMbytes,
200-
icons: actorDefinitionPruned.pictureUrl
201-
? [{ src: actorDefinitionPruned.pictureUrl, mimeType: 'image/png' }]
200+
icons: definition.pictureUrl
201+
? [{ src: definition.pictureUrl, mimeType: 'image/png' }]
202202
: undefined,
203203
annotations: {
204-
title: actorDefinitionPruned.actorFullName,
204+
title: definition.actorFullName,
205205
openWorldHint: true,
206206
},
207207
// Allow long running tasks for Actor tools, make it optional for now
@@ -227,21 +227,21 @@ async function getMCPServersAsTools(
227227

228228
// Process all actors in parallel
229229
const actorToolPromises = actorsInfo.map(async (actorInfo) => {
230-
const actorId = actorInfo.actorDefinitionPruned.id;
230+
const actorId = actorInfo.definition.id;
231231
if (!actorInfo.webServerMcpPath) {
232232
log.warning('Actor does not have a web server MCP path, skipping', {
233-
actorFullName: actorInfo.actorDefinitionPruned.actorFullName,
233+
actorFullName: actorInfo.definition.actorFullName,
234234
actorId,
235235
});
236236
return [];
237237
}
238238

239239
const mcpServerUrl = await getActorMCPServerURL(
240-
actorInfo.actorDefinitionPruned.id, // Real ID of the Actor
240+
actorInfo.definition.id, // Real ID of the Actor
241241
actorInfo.webServerMcpPath,
242242
);
243243
log.debug('Retrieved MCP server URL for Actor', {
244-
actorFullName: actorInfo.actorDefinitionPruned.actorFullName,
244+
actorFullName: actorInfo.definition.actorFullName,
245245
actorId,
246246
mcpServerUrl,
247247
});
@@ -256,7 +256,7 @@ async function getMCPServersAsTools(
256256
return await getMCPServerTools(actorId, client, mcpServerUrl);
257257
} catch (error) {
258258
logHttpError(error, 'Failed to connect to MCP server', {
259-
actorFullName: actorInfo.actorDefinitionPruned.actorFullName,
259+
actorFullName: actorInfo.definition.actorFullName,
260260
actorId,
261261
});
262262
return [];
@@ -280,26 +280,28 @@ export async function getActorsAsTools(
280280

281281
const actorsInfo: (ActorInfo | null)[] = await Promise.all(
282282
actorIdsOrNames.map(async (actorIdOrName) => {
283-
const actorDefinitionPrunedCached = actorDefinitionPrunedCache.get(actorIdOrName);
284-
if (actorDefinitionPrunedCached) {
283+
const actorDefinitionWithInfoCached = actorDefinitionPrunedCache.get(actorIdOrName);
284+
if (actorDefinitionWithInfoCached) {
285285
return {
286-
actorDefinitionPruned: actorDefinitionPrunedCached,
287-
webServerMcpPath: getActorMCPServerPath(actorDefinitionPrunedCached),
286+
definition: actorDefinitionWithInfoCached.definition,
287+
actor: actorDefinitionWithInfoCached.info,
288+
webServerMcpPath: getActorMCPServerPath(actorDefinitionWithInfoCached.definition),
288289

289290
} as ActorInfo;
290291
}
291292

292293
try {
293-
const actorDefinitionPruned = await getActorDefinition(actorIdOrName, apifyClient);
294-
if (!actorDefinitionPruned) {
294+
const actorDefinitionWithInfo = await getActorDefinition(actorIdOrName, apifyClient);
295+
if (!actorDefinitionWithInfo) {
295296
log.softFail('Actor not found or definition is not available', { actorName: actorIdOrName, statusCode: 404 });
296297
return null;
297298
}
298-
// Cache the pruned Actor definition
299-
actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionPruned);
299+
// Cache the Actor definition with info
300+
actorDefinitionPrunedCache.set(actorIdOrName, actorDefinitionWithInfo);
300301
return {
301-
actorDefinitionPruned,
302-
webServerMcpPath: getActorMCPServerPath(actorDefinitionPruned),
302+
definition: actorDefinitionWithInfo.definition,
303+
actor: actorDefinitionWithInfo.info,
304+
webServerMcpPath: getActorMCPServerPath(actorDefinitionWithInfo.definition),
303305
} as ActorInfo;
304306
} catch (error) {
305307
logHttpError(error, 'Failed to fetch Actor definition', {
@@ -312,9 +314,14 @@ export async function getActorsAsTools(
312314

313315
const clonedActors = structuredClone(actorsInfo);
314316

315-
// Filter out nulls and separate Actors with MCP servers and normal Actors
316-
const actorMCPServersInfo = clonedActors.filter((actorInfo) => actorInfo && actorInfo.webServerMcpPath) as ActorInfo[];
317-
const normalActorsInfo = clonedActors.filter((actorInfo) => actorInfo && !actorInfo.webServerMcpPath) as ActorInfo[];
317+
// Filter out nulls - actorInfo can be null if the Actor was not found or an error occurred
318+
const nonNullActors = clonedActors.filter((actorInfo): actorInfo is ActorInfo => Boolean(actorInfo));
319+
320+
// Separate Actors with MCP servers and normal Actors
321+
// for MCP servers if mcp path is configured and also if the Actor standby mode is enabled
322+
const actorMCPServersInfo = nonNullActors.filter((actorInfo) => isActorInfoMcpServer(actorInfo));
323+
// all others
324+
const normalActorsInfo = nonNullActors.filter((actorInfo) => !isActorInfoMcpServer(actorInfo));
318325

319326
const [normalTools, mcpServerTools] = await Promise.all([
320327
getNormalActorsAsTools(normalActorsInfo),

src/tools/build.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ACTOR_README_MAX_LENGTH } from '../const.js';
33
import type {
44
ActorDefinitionPruned,
55
ActorDefinitionWithDesc,
6+
ActorDefinitionWithInfo,
67
SchemaProperties,
78
} from '../types.js';
89

@@ -13,13 +14,13 @@ import type {
1314
* @param {string} actorIdOrName - Actor ID or Actor full name.
1415
* @param {ApifyClient} apifyClient - The Apify client instance.
1516
* @param {number} limit - Truncate the README to this limit.
16-
* @returns {Promise<ActorDefinitionWithDesc | null>} - The actor definition with description or null if not found.
17+
* @returns {Promise<ActorDefinitionWithInfo | null>} - The Actor definition with info or null if not found.
1718
*/
1819
export async function getActorDefinition(
1920
actorIdOrName: string,
2021
apifyClient: ApifyClient,
2122
limit: number = ACTOR_README_MAX_LENGTH,
22-
): Promise<ActorDefinitionPruned | null> {
23+
): Promise<ActorDefinitionWithInfo | null> {
2324
const actorClient = apifyClient.actor(actorIdOrName);
2425
try {
2526
// Fetch Actor details
@@ -41,7 +42,10 @@ export async function getActorDefinition(
4142
actorDefinitions.defaultRunOptions = actor.defaultRunOptions;
4243
// Pass pictureUrl from actor object (untyped property but present in API response)
4344
(actorDefinitions as Record<string, unknown>).pictureUrl = (actor as unknown as Record<string, unknown>).pictureUrl;
44-
return pruneActorDefinition(actorDefinitions);
45+
return {
46+
definition: pruneActorDefinition(actorDefinitions),
47+
info: actor,
48+
};
4549
}
4650
return null;
4751
} catch (error) {

src/tools/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ValidateFunction } from 'ajv';
22
import type Ajv from 'ajv';
33

44
import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH, RAG_WEB_BROWSER_WHITELISTED_FIELDS } from '../const.js';
5-
import type { ActorInputSchema, ActorInputSchemaProperties, SchemaProperties } from '../types.js';
5+
import type { ActorInfo, ActorInputSchema, ActorInputSchemaProperties, SchemaProperties } from '../types.js';
66
import {
77
addGlobsProperties,
88
addKeyValueProperties,
@@ -12,6 +12,13 @@ import {
1212
addResourcePickerProperties as addArrayResourcePickerProperties,
1313
} from '../utils/apify-properties.js';
1414

15+
/*
16+
* Checks if the given ActorInfo represents an MCP server Actor.
17+
*/
18+
export function isActorInfoMcpServer(actorInfo: ActorInfo): boolean {
19+
return !!((actorInfo.webServerMcpPath && actorInfo.actor.actorStandby?.isEnabled));
20+
}
21+
1522
export function actorNameToToolName(actorName: string): string {
1623
return actorName
1724
.replace(/\//g, '-slash-')

src/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
33
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
44
import type { InitializeRequest, Notification, Prompt, Request, ToolSchema } from '@modelcontextprotocol/sdk/types.js';
55
import type { ValidateFunction } from 'ajv';
6-
import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client';
6+
import type { Actor, ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingInfo } from 'apify-client';
77
import type z from 'zod';
88

99
import type { ACTOR_PRICING_MODEL, TELEMETRY_ENV, TOOL_STATUS } from './const.js';
@@ -62,6 +62,15 @@ export type ActorDefinitionPruned = Pick<ActorDefinitionWithDesc,
6262
pictureUrl?: string; // Optional, URL to the Actor's icon/picture
6363
};
6464

65+
/**
66+
* Actor definition combined with full actor metadata.
67+
* Contains both the pruned definition (for schemas) and complete actor info.
68+
*/
69+
export type ActorDefinitionWithInfo = {
70+
definition: ActorDefinitionPruned;
71+
info: Actor;
72+
};
73+
6574
/**
6675
* Base type for all tools in the MCP server.
6776
* Extends the MCP SDK's Tool schema, which requires inputSchema to have type: "object".
@@ -248,7 +257,8 @@ export type TelemetryEnv = (typeof TELEMETRY_ENV)[keyof typeof TELEMETRY_ENV];
248257
*/
249258
export type ActorInfo = {
250259
webServerMcpPath: string | null; // To determined if the Actor is an MCP server
251-
actorDefinitionPruned: ActorDefinitionPruned;
260+
definition: ActorDefinitionPruned;
261+
actor: Actor;
252262
};
253263

254264
export type ExtendedActorStoreList = ActorStoreList & {

src/utils/actor.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export async function getActorMcpUrlCached(
2121
}
2222

2323
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);
24+
const actorDefinitionWithInfo = await getActorDefinition(actorIdOrName, apifyClient);
25+
const definition = actorDefinitionWithInfo?.definition;
26+
const mcpPath = definition && getActorMCPServerPath(definition);
27+
if (mcpPath) {
28+
const url = await getActorMCPServerURL(definition.id, mcpPath);
2829
mcpServerCache.set(actorIdOrName, url);
2930
return url;
3031
}

0 commit comments

Comments
 (0)