Skip to content

Commit 95139e6

Browse files
committed
feat: add outputSchema option to fetch-actor-details tools
- Add outputSchema boolean option to actorDetailsOutputOptionsSchema - Pass actorOutputSchema through meta from internal server - Add typeObjectToString utility for human-readable type display - Include outputSchema in structured response content - Update fetch-actor-details and fetch-actor-details-internal tools
1 parent 37d5b0e commit 95139e6

File tree

7 files changed

+80
-5
lines changed

7 files changed

+80
-5
lines changed

src/mcp/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@ export class ActorsMcpServer {
617617
const metaApifyToken = meta?.apifyToken;
618618
const apifyToken = (metaApifyToken || this.options.token || process.env.APIFY_TOKEN) as string;
619619
const userRentedActorIds = meta?.userRentedActorIds;
620+
const actorOutputSchema = meta?.actorOutputSchema;
620621
// mcpSessionId was injected upstream it is important and required for long running tasks as the store uses it and there is not other way to pass it
621622
const mcpSessionId = meta?.mcpSessionId;
622623
if (!mcpSessionId) {
@@ -764,6 +765,7 @@ Please remove the "task" parameter from the tool call request or use a different
764765
mcpServer: this.server,
765766
apifyToken,
766767
userRentedActorIds,
768+
actorOutputSchema,
767769
progressTracker,
768770
}) as object;
769771

src/tools/fetch-actor-details-internal.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ but the user did NOT explicitly ask for Actor details presentation.`,
4949
openWorldHint: false,
5050
},
5151
call: async (toolArgs: InternalToolArgs) => {
52-
const { args, apifyToken, apifyMcpServer } = toolArgs;
52+
const { args, apifyToken, apifyMcpServer, actorOutputSchema } = toolArgs;
5353
const parsed = fetchActorDetailsInternalArgsSchema.parse(args);
5454
const apifyClient = new ApifyClient({ token: apifyToken });
5555

@@ -68,6 +68,7 @@ but the user did NOT explicitly ask for Actor details presentation.`,
6868
cardOptions,
6969
apifyClient,
7070
apifyToken,
71+
actorOutputSchema,
7172
skyfireMode: apifyMcpServer?.options.skyfireMode,
7273
});
7374

src/tools/fetch-actor-details.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ EXAMPLES:
5454
openWorldHint: false,
5555
},
5656
call: async (toolArgs: InternalToolArgs) => {
57-
const { args, apifyToken, apifyMcpServer } = toolArgs;
57+
const { args, apifyToken, apifyMcpServer, actorOutputSchema } = toolArgs;
5858
const parsed = fetchActorDetailsToolArgsSchema.parse(args);
5959
const apifyClient = new ApifyClient({ token: apifyToken });
6060

@@ -106,6 +106,7 @@ An interactive widget has been rendered with detailed Actor information.
106106
cardOptions,
107107
apifyClient,
108108
apifyToken,
109+
actorOutputSchema,
109110
skyfireMode: apifyMcpServer?.options.skyfireMode,
110111
});
111112

src/tools/structured-output-schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export const actorDetailsOutputSchema = {
128128
actorInfo: actorInfoSchema,
129129
readme: { type: 'string', description: 'Actor README documentation.' },
130130
inputSchema: { type: 'object' as const, description: 'Actor input schema.' }, // Literal type required for MCP SDK type compatibility
131+
outputSchema: { type: ['string', 'null'], description: 'TypeScript type skeleton inferred from recent successful runs.' },
131132
},
132133
};
133134

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export type InternalToolArgs = {
128128
apifyToken: string;
129129
/** List of Actor IDs that the user has rented */
130130
userRentedActorIds?: string[];
131+
/** Actor output schema as type object (injected by internal server) */
132+
actorOutputSchema?: Record<string, unknown> | null;
131133
/** Optional progress tracker for long running internal tools, like call-actor */
132134
progressTracker?: ProgressTracker | null;
133135
};
@@ -459,6 +461,8 @@ export type ApifyRequestParams = {
459461
apifyToken?: string;
460462
/** List of Actor IDs that the user has rented */
461463
userRentedActorIds?: string[];
464+
/** Actor output schema as type object (injected by internal server) */
465+
actorOutputSchema?: Record<string, unknown> | null;
462466
/** Progress token for out-of-band progress notifications (standard MCP) */
463467
progressToken?: string | number;
464468
/** Allow other metadata fields */

src/utils/actor-details.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,50 @@ import { formatActorToActorCard, formatActorToStructuredCard } from './actor-car
1111
import { logHttpError } from './logging.js';
1212
import { buildMCPResponse } from './mcp.js';
1313

14+
/**
15+
* Convert a type object to TypeScript-like string representation.
16+
* Used for human-readable text output.
17+
*
18+
* Example:
19+
* Input: { first_number: "number", tags: ["string"], user: { name: "string" } }
20+
* Output: "{ first_number: number, tags: string[], user: { name: string } }"
21+
*/
22+
function typeObjectToString(obj: Record<string, unknown>): string {
23+
const pairs: string[] = [];
24+
25+
for (const [key, value] of Object.entries(obj)) {
26+
if (Array.isArray(value)) {
27+
// Array type
28+
const itemType = typeValueToString(value[0]);
29+
pairs.push(`${key}: ${itemType}[]`);
30+
} else if (typeof value === 'object' && value !== null) {
31+
// Nested object type
32+
const nestedStr = typeObjectToString(value as Record<string, unknown>);
33+
pairs.push(`${key}: ${nestedStr}`);
34+
} else if (typeof value === 'string') {
35+
// Primitive type
36+
pairs.push(`${key}: ${value}`);
37+
}
38+
}
39+
40+
return `{ ${pairs.join(', ')} }`;
41+
}
42+
43+
/**
44+
* Convert a single type value to string.
45+
*/
46+
function typeValueToString(value: unknown): string {
47+
if (Array.isArray(value)) {
48+
const itemType = typeValueToString(value[0]);
49+
return `${itemType}[]`;
50+
} if (typeof value === 'object' && value !== null) {
51+
return typeObjectToString(value as Record<string, unknown>);
52+
} if (typeof value === 'string') {
53+
return value;
54+
}
55+
return 'unknown';
56+
}
57+
1458
// Keep the type here since it is a self-contained module
1559
export type ActorDetailsResult = {
1660
actorInfo: Actor;
@@ -98,7 +142,7 @@ export function processActorDetailsForResponse(details: ActorDetailsResult) {
98142
* Used by both public and internal fetch-actor-details tools.
99143
*
100144
* Behavior:
101-
* - If output is undefined or empty object: use defaults (all true except mcpTools)
145+
* - If output is undefined or empty object: use defaults (all true except mcpTools and outputSchema)
102146
* - If any property is explicitly set: only include sections with explicit true values
103147
*/
104148
export const actorDetailsOutputOptionsSchema = z.object({
@@ -109,6 +153,7 @@ export const actorDetailsOutputOptionsSchema = z.object({
109153
metadata: z.boolean().optional().describe('Include developer, categories, last modified date, and deprecation status.'),
110154
inputSchema: z.boolean().optional().describe('Include required input parameters schema.'),
111155
readme: z.boolean().optional().describe('Include full README documentation.'),
156+
outputSchema: z.boolean().optional().describe('Include inferred output schema from recent successful runs (TypeScript type).'),
112157
mcpTools: z.boolean().optional().describe('List available tools (only for MCP server Actors).'),
113158
});
114159

@@ -120,6 +165,7 @@ export const actorDetailsOutputDefaults = {
120165
metadata: true,
121166
inputSchema: true,
122167
readme: true,
168+
outputSchema: false,
123169
mcpTools: false,
124170
};
125171

@@ -145,6 +191,7 @@ export function resolveOutputOptions(output?: z.infer<typeof actorDetailsOutputO
145191
metadata: output?.metadata === true,
146192
inputSchema: output?.inputSchema === true,
147193
readme: output?.readme === true,
194+
outputSchema: output?.outputSchema === true,
148195
mcpTools: output?.mcpTools === true,
149196
};
150197
}
@@ -224,7 +271,7 @@ You can search for available Actors using the tool: ${HelperTools.STORE_SEARCH}.
224271

225272
/**
226273
* Build text and structured response for actor details.
227-
* Handles all resolved output options: description, stats, readme, inputSchema, mcpTools.
274+
* Handles all resolved output options: description, stats, readme, inputSchema, outputSchema, mcpTools.
228275
* All output properties should be boolean (resolved via resolveOutputOptions).
229276
*/
230277
export async function buildActorDetailsTextResponse(options: {
@@ -238,17 +285,19 @@ export async function buildActorDetailsTextResponse(options: {
238285
metadata: boolean;
239286
readme: boolean;
240287
inputSchema: boolean;
288+
outputSchema: boolean;
241289
mcpTools: boolean;
242290
};
243291
cardOptions: ActorCardOptions;
244292
apifyClient: ApifyClient;
245293
apifyToken: string;
294+
actorOutputSchema?: Record<string, unknown> | null;
246295
skyfireMode?: boolean;
247296
}): Promise<{
248297
texts: string[];
249298
structuredContent: Record<string, unknown>;
250299
}> {
251-
const { actorName, details, output, cardOptions, apifyClient, apifyToken, skyfireMode } = options;
300+
const { actorName, details, output, cardOptions, apifyClient, apifyToken, actorOutputSchema, skyfireMode } = options;
252301

253302
const actorUrl = `https://apify.com/${details.actorInfo.username}/${details.actorInfo.name}`;
254303
const formattedReadme = details.readme.replace(/^# /, `# [README](${actorUrl}/readme): `);
@@ -276,6 +325,16 @@ export async function buildActorDetailsTextResponse(options: {
276325
texts.push(`# [Input schema](${actorUrl}/input)\n\`\`\`json\n${JSON.stringify(details.inputSchema)}\n\`\`\``);
277326
}
278327

328+
// Add output schema if requested
329+
if (output.outputSchema) {
330+
if (actorOutputSchema && Object.keys(actorOutputSchema).length > 0) {
331+
const typeString = typeObjectToString(actorOutputSchema);
332+
texts.push(`# Output Schema (TypeScript)\nInferred from recent successful runs:\n\`\`\`typescript\ntype ActorOutput = ${typeString}\n\`\`\``);
333+
} else {
334+
texts.push(`# Output Schema\nNo output schema available. The Actor may not have recent successful runs, or the output structure could not be determined.`);
335+
}
336+
}
337+
279338
// Handle MCP tools
280339
if (output.mcpTools) {
281340
const message = await getMcpToolsMessage(actorName, apifyClient, apifyToken, skyfireMode);
@@ -287,6 +346,7 @@ export async function buildActorDetailsTextResponse(options: {
287346
actorInfo: needsCard ? details.actorCardStructured : undefined,
288347
readme: output.readme ? formattedReadme : undefined,
289348
inputSchema: output.inputSchema ? details.inputSchema : undefined,
349+
outputSchema: output.outputSchema ? actorOutputSchema : undefined,
290350
};
291351

292352
return { texts, structuredContent };

src/web/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)