Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const USER_AGENT_ORIGIN = 'Origin/mcp-server';

export enum HelperTools {
ACTOR_ADD = 'add-actor',
ACTOR_CALL = 'call-actor',
ACTOR_GET = 'get-actor',
ACTOR_GET_DETAILS = 'get-actor-details',
ACTOR_REMOVE = 'remove-actor',
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class ActorsMcpServer {
* Returns the list of all currently loaded Actor tool IDs.
* @returns {string[]} - Array of loaded Actor tool IDs (e.g., 'apify/rag-web-browser')
*/
private listActorToolNames(): string[] {
public listActorToolNames(): string[] {
return Array.from(this.tools.values())
.filter((tool) => tool.type === 'actor')
.map((tool) => (tool.tool as ActorTool).actorFullName);
Expand Down
97 changes: 95 additions & 2 deletions src/tools/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,11 @@
}),
);

const clonedActors = structuredClone(actorsInfo);

// Filter out nulls and separate Actors with MCP servers and normal Actors
const actorMCPServersInfo = actorsInfo.filter((actorInfo) => actorInfo && actorInfo.webServerMcpPath) as ActorInfo[];
const normalActorsInfo = actorsInfo.filter((actorInfo) => actorInfo && !actorInfo.webServerMcpPath) as ActorInfo[];
const actorMCPServersInfo = clonedActors.filter((actorInfo) => actorInfo && actorInfo.webServerMcpPath) as ActorInfo[];
const normalActorsInfo = clonedActors.filter((actorInfo) => actorInfo && !actorInfo.webServerMcpPath) as ActorInfo[];

const [normalTools, mcpServerTools] = await Promise.all([
getNormalActorsAsTools(normalActorsInfo),
Expand Down Expand Up @@ -287,3 +289,94 @@
},
} as InternalTool,
};

const callActorArgs = z.object({
actorName: z.string()
.describe('The name of the Actor to call.'),
input: z.object({}).passthrough()
.describe('The input JSON to pass to the Actor.'),
callOptions: z.object({
memory: z.number().optional(),
timeout: z.number().optional(),
}).optional()
.describe('Optional call options for the Actor.'),
});

export const callActor: ToolEntry = {
type: 'internal',
tool: {
name: HelperTools.ACTOR_CALL,
actorFullName: HelperTools.ACTOR_CALL,
description: `Call Actor and get dataset results. Call without input and result response with requred input properties. Actor MUST be added before calling, use ${HelperTools.ACTOR_ADD} tool before.`,
inputSchema: zodToJsonSchema(callActorArgs),
ajvValidate: ajv.compile(zodToJsonSchema(callActorArgs)),
call: async (toolArgs) => {
const { apifyMcpServer, args, apifyToken } = toolArgs;
const { actorName, input, callOptions } = callActorArgs.parse(args);

const actors = apifyMcpServer.listActorToolNames();
if (!actors.includes(actorName)) {
const toolsText = actors.length > 0 ? `Added Actors are: ${actors.join(', ')}` : 'Not added Actors yet.';
if (apifyMcpServer.tools.has(HelperTools.ACTOR_ADD)) {
return {
content: [{
type: 'text',
text: `Actor '${actorName}' is not added. Add it with tool '${HelperTools.ACTOR_ADD}'. ${toolsText}`,
}],
};
}
return {
content: [{
type: 'text',
text: `Actor '${actorName}' is not added. ${toolsText}`,
}],
};
}

try {
const [actor] = await getActorsAsTools([actorName], apifyToken);

if (!actor) {
return {
content: [
{ type: 'text', text: `Actor '${actorName}' not found.` },
],
};
}

if (!actor.tool.ajvValidate(input)) {
const { errors } = actor.tool.ajvValidate;
if (errors && errors.length > 0) {
return {
content: [
{ type: 'text', text: `Input validation failed for Actor '${actorName}': ${errors.map((e) => e.message).join(', ')}` },
{ type: 'json', json: actor.tool.inputSchema },
],
};
}
}

const { items } = await callActorGetDataset(
actorName,
input,
apifyToken,
callOptions,
);

return {
content: items.items.map((item: Record<string, unknown>) => ({
type: 'text',
text: JSON.stringify(item),
})),
};
} catch (error) {
console.error(`Error calling Actor: ${error}`);

Check warning on line 373 in src/tools/actor.ts

View workflow job for this annotation

GitHub Actions / Code checks

Unexpected console statement
return {
content: [
{ type: 'text', text: `Error calling Actor: ${error instanceof Error ? error.message : String(error)}` },
],
};
}
},
},
};
9 changes: 8 additions & 1 deletion src/tools/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ export const addTool: ToolEntry = {
type: 'internal',
tool: {
name: HelperTools.ACTOR_ADD,
description: `Add an Actor or MCP server to the available tools of the Apify MCP server. A tool is an Actor or MCP server that can be called by the user. Do not execute the tool, only add it and list it in the available tools. For example, when a user wants to scrape a website, first search for relevant Actors using ${HelperTools.STORE_SEARCH} tool, and once the user selects one they want to use, add it as a tool to the Apify MCP server.`,
description:
`Add an Actor or MCP server to the available tools of the Apify MCP server.
A tool is an Actor or MCP server that can be called by the user.
Do not execute the tool, only add it and list it in the available tools.
For example, when a user wants to scrape a website, first search for relevant Actors
using ${HelperTools.STORE_SEARCH} tool, and once the user selects one they want to use,
add it as a tool to the Apify MCP server.
If added tools is not available, use generic tool ${HelperTools.ACTOR_CALL} to call added Actor directly.`,
inputSchema: zodToJsonSchema(addToolArgsSchema),
ajvValidate: ajv.compile(zodToJsonSchema(addToolArgsSchema)),
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
Expand Down
3 changes: 2 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Import specific tools that are being used
import { callActorGetDataset, getActorsAsTools } from './actor.js';
import { callActor, callActorGetDataset, getActorsAsTools } from './actor.js';
import { getActorDetailsTool } from './get-actor-details.js';
import { addTool, helpTool } from './helpers.js';
import { searchActors } from './store_collection.js';
Expand All @@ -18,6 +18,7 @@ export const defaultTools = [
// getUserRunsList,
// getUserDatasetsList,
// getUserKeyValueStoresList,
callActor,
getActorDetailsTool,
helpTool,
searchActors,
Expand Down