@@ -18,10 +18,11 @@ import { connectMCPClient } from '../mcp/client.js';
1818import { getMCPServerTools } from '../mcp/proxy.js' ;
1919import { actorDefinitionPrunedCache } from '../state.js' ;
2020import type { ActorDefinitionStorage , ActorInfo , ApifyToken , DatasetItem , ToolEntry } from '../types.js' ;
21- import { ensureOutputWithinCharLimit , getActorDefinitionStorageFieldNames } from '../utils/actor.js' ;
21+ import { ensureOutputWithinCharLimit , getActorDefinitionStorageFieldNames , getActorMcpUrlCached } from '../utils/actor.js' ;
2222import { fetchActorDetails } from '../utils/actor-details.js' ;
2323import { buildActorResponseContent } from '../utils/actor-response.js' ;
2424import { ajv } from '../utils/ajv.js' ;
25+ import { buildMCPResponse } from '../utils/mcp.js' ;
2526import type { ProgressTracker } from '../utils/progress.js' ;
2627import type { JsonSchemaProperty } from '../utils/schema-generation.js' ;
2728import { generateSchemaFromItems } from '../utils/schema-generation.js' ;
@@ -329,12 +330,19 @@ MANDATORY TWO-STEP WORKFLOW:
329330
330331Step 1: Get Actor Info (step="info", default)
331332• First call this tool with step="info" to get Actor details and input schema
332- • This returns the Actor description, documentation, and required input schema
333+ • For regular Actors: returns the Actor input schema
334+ • For MCP server Actors: returns list of available tools with their schemas
333335• You MUST do this step first - it's required to understand how to call the Actor
334336
335- Step 2: Call Actor (step="call")
337+ Step 2: Call Actor (step="call")
336338• Only after step 1, call again with step="call" and proper input based on the schema
337- • This executes the Actor and returns the results
339+ • For regular Actors: executes the Actor and returns results
340+ • For MCP server Actors: use format "actor-name:tool-name" to call specific tools
341+
342+ MCP SERVER ACTORS:
343+ • For MCP server actors, step="info" lists available tools instead of input schema
344+ • To call an MCP tool, use actor name format: "actor-name:tool-name" with step="call"
345+ • Example: actor="apify/my-mcp-actor:search-tool", step="call", input={...}
338346
339347The step parameter enforces this workflow - you cannot call an Actor without first getting its info.` ,
340348 inputSchema : zodToJsonSchema ( callActorArgs ) ,
@@ -347,29 +355,66 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
347355 const { args, apifyToken, progressTracker, extra, apifyMcpServer } = toolArgs ;
348356 const { actor : actorName , step, input, callOptions } = callActorArgs . parse ( args ) ;
349357
358+ // Parse special format: actor:tool
359+ const mcpToolMatch = actorName . match ( / ^ ( .+ ) : ( .+ ) $ / ) ;
360+ let baseActorName = actorName ;
361+ let mcpToolName : string | undefined ;
362+
363+ if ( mcpToolMatch ) {
364+ baseActorName = mcpToolMatch [ 1 ] ;
365+ mcpToolName = mcpToolMatch [ 2 ] ;
366+ }
367+
368+ // For definition resolution we always use token-based client; Skyfire is only for actual Actor runs
369+ const apifyClientForDefinition = new ApifyClient ( { token : apifyToken } ) ;
370+ // Resolve MCP server URL
371+ const needsMcpUrl = mcpToolName !== undefined || step === 'info' ;
372+ const mcpServerUrlOrFalse = needsMcpUrl ? await getActorMcpUrlCached ( baseActorName , apifyClientForDefinition ) : false ;
373+ const isActorMcpServer = mcpServerUrlOrFalse && typeof mcpServerUrlOrFalse === 'string' ;
374+
375+ // Standby Actors, thus MCPs, are not supported in Skyfire mode
376+ if ( isActorMcpServer && apifyMcpServer . options . skyfireMode ) {
377+ return buildMCPResponse ( [ `MCP server Actors are not supported in Skyfire mode. Please use a regular Apify token without Skyfire.` ] ) ;
378+ }
379+
350380 try {
351381 if ( step === 'info' ) {
352- const apifyClient = new ApifyClient ( { token : apifyToken } ) ;
353- // Step 1: Return Actor card and schema directly
354- const details = await fetchActorDetails ( apifyClient , actorName ) ;
355- if ( ! details ) {
356- return {
357- content : [ { type : 'text' , text : `Actor information for '${ actorName } ' was not found. Please check the Actor ID or name and ensure the Actor exists.` } ] ,
358- } ;
382+ if ( isActorMcpServer ) {
383+ // MCP server: list tools
384+ const mcpServerUrl = mcpServerUrlOrFalse ;
385+ let client : Client | undefined ;
386+ // Nested try to ensure client is closed
387+ try {
388+ client = await connectMCPClient ( mcpServerUrl , apifyToken ) ;
389+ const toolsResponse = await client . listTools ( ) ;
390+
391+ const toolsInfo = toolsResponse . tools . map ( ( tool ) => `**${ tool . name } **\n${ tool . description || 'No description' } \nInput Schema: ${ JSON . stringify ( tool . inputSchema , null , 2 ) } ` ,
392+ ) . join ( '\n\n' ) ;
393+
394+ return buildMCPResponse ( [ `This is an MCP Server Actor with the following tools:\n\n${ toolsInfo } \n\nTo call a tool, use step="call" with actor name format: "${ baseActorName } :{toolName}"` ] ) ;
395+ } finally {
396+ if ( client ) await client . close ( ) ;
397+ }
398+ } else {
399+ // Regular actor: return schema
400+ const details = await fetchActorDetails ( apifyClientForDefinition , baseActorName ) ;
401+ if ( ! details ) {
402+ return buildMCPResponse ( [ `Actor information for '${ baseActorName } ' was not found. Please check the Actor ID or name and ensure the Actor exists.` ] ) ;
403+ }
404+ const content = [
405+ { type : 'text' , text : `**Input Schema:**\n${ JSON . stringify ( details . inputSchema , null , 0 ) } ` } ,
406+ ] ;
407+ /**
408+ * Add Skyfire instructions also in the info step since clients are most likely truncating the long tool description of the call-actor.
409+ */
410+ if ( apifyMcpServer . options . skyfireMode ) {
411+ content . push ( {
412+ type : 'text' ,
413+ text : SKYFIRE_TOOL_INSTRUCTIONS ,
414+ } ) ;
415+ }
416+ return { content } ;
359417 }
360- const content = [
361- { type : 'text' , text : `**Input Schema:**\n${ JSON . stringify ( details . inputSchema , null , 0 ) } ` } ,
362- ] ;
363- /**
364- * Add Skyfire instructions also in the info step since clients are most likely truncating the long tool description of the call-actor.
365- */
366- if ( apifyMcpServer . options . skyfireMode ) {
367- content . push ( {
368- type : 'text' ,
369- text : SKYFIRE_TOOL_INSTRUCTIONS ,
370- } ) ;
371- }
372- return { content } ;
373418 }
374419
375420 /**
@@ -396,32 +441,45 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
396441
397442 // Step 2: Call the Actor
398443 if ( ! input ) {
399- return {
400- content : [
401- { type : 'text' , text : `Input is required when step="call". Please provide the input parameter based on the Actor's input schema.` } ,
402- ] ,
403- } ;
444+ return buildMCPResponse ( [ `Input is required when step="call". Please provide the input parameter based on the Actor's input schema.` ] ) ;
404445 }
405446
447+ // Handle MCP tool calls
448+ if ( mcpToolName ) {
449+ if ( ! isActorMcpServer ) {
450+ return buildMCPResponse ( [ `Actor '${ baseActorName } ' is not an MCP server.` ] ) ;
451+ }
452+
453+ const mcpServerUrl = mcpServerUrlOrFalse ;
454+ let client : Client | undefined ;
455+ try {
456+ client = await connectMCPClient ( mcpServerUrl , apifyToken ) ;
457+
458+ const result = await client . callTool ( {
459+ name : mcpToolName ,
460+ arguments : input ,
461+ } ) ;
462+
463+ return { content : result . content } ;
464+ } finally {
465+ if ( client ) await client . close ( ) ;
466+ }
467+ }
468+
469+ // Handle regular Actor calls
406470 const [ actor ] = await getActorsAsTools ( [ actorName ] , apifyClient ) ;
407471
408472 if ( ! actor ) {
409- return {
410- content : [
411- { type : 'text' , text : `Actor '${ actorName } ' not found.` } ,
412- ] ,
413- } ;
473+ return buildMCPResponse ( [ `Actor '${ actorName } ' was not found.` ] ) ;
414474 }
415475
416476 if ( ! actor . tool . ajvValidate ( input ) ) {
417477 const { errors } = actor . tool . ajvValidate ;
418478 if ( errors && errors . length > 0 ) {
419- return {
420- content : [
421- { type : 'text' , text : `Input validation failed for Actor '${ actorName } ': ${ errors . map ( ( e ) => e . message ) . join ( ', ' ) } ` } ,
422- { type : 'text' , text : `Input Schema:\n${ JSON . stringify ( actor . tool . inputSchema ) } ` } ,
423- ] ,
424- } ;
479+ return buildMCPResponse ( [
480+ `Input validation failed for Actor '${ actorName } ': ${ errors . map ( ( e ) => e . message ) . join ( ', ' ) } ` ,
481+ `Input Schema:\n${ JSON . stringify ( actor . tool . inputSchema ) } ` ,
482+ ] ) ;
425483 }
426484 }
427485
@@ -444,12 +502,8 @@ The step parameter enforces this workflow - you cannot call an Actor without fir
444502
445503 return { content } ;
446504 } catch ( error ) {
447- log . error ( 'Error with Actor operation' , { error, actorName, step } ) ;
448- return {
449- content : [
450- { type : 'text' , text : `Error with Actor operation: ${ error instanceof Error ? error . message : String ( error ) } ` } ,
451- ] ,
452- } ;
505+ log . error ( 'Failed to call Actor' , { error, actorName, step } ) ;
506+ return buildMCPResponse ( [ `Failed to call Actor '${ actorName } ': ${ error instanceof Error ? error . message : String ( error ) } ` ] ) ;
453507 }
454508 } ,
455509 } ,
0 commit comments