Skip to content

Commit c8b2da4

Browse files
committed
Start Standby server with Actors provided at input
1 parent 0aae5c6 commit c8b2da4

File tree

7 files changed

+47
-16
lines changed

7 files changed

+47
-16
lines changed

.actor/input_schema.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
"type": "object",
44
"schemaVersion": 1,
55
"properties": {
6+
"actors": {
7+
"title": "Actors names to be exposed for an AI application (AI agent)",
8+
"type": "array",
9+
"description": "Specify the names of actors that will be exposed to an AI application (AI agent) and communicate with Apify Actors via the MCP protocol",
10+
"editor": "stringList",
11+
"prefill": [
12+
"apidojo/tweet-scraper",
13+
"apify/facebook-posts-scraper",
14+
"apify/google-search-scraper",
15+
"apify/instagram-scraper",
16+
"apify/rag-web-browser",
17+
"clockworks/free-tiktok-scraper",
18+
"lukaskrivka/google-maps-with-contact-details",
19+
"voyager/booking-scraper"
20+
]
21+
},
622
"debugActor": {
723
"title": "Debug actor",
824
"type": "string",

src/actorDefinition.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
5656
* This function retrieves the input schemas for the specified actors and compiles them into MCP tools.
5757
* It uses the AJV library to validate the input schemas.
5858
*
59+
* Tool name can't contain /, so it is replaced with _
60+
*
5961
* @param {string[]} actors - An array of actor full names.
6062
* @returns {Promise<Tool[]>} - A promise that resolves to an array of MCP tools.
6163
*/
@@ -69,6 +71,7 @@ export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
6971
try {
7072
tools.push({
7173
name: result.name.replace('/', '_'),
74+
actorName: result.name,
7275
description: result.description,
7376
inputSchema: result.input || {},
7477
ajvValidate: ajv.compile(result.input || {}),

src/examples/clientStdioChat.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,20 @@
1919

2020
import { execSync } from 'child_process';
2121
import * as readline from 'readline';
22+
import { fileURLToPath } from "url";
2223

2324
import { Anthropic } from '@anthropic-ai/sdk';
2425
import type { Message, ToolUseBlock, MessageParam } from '@anthropic-ai/sdk/resources/messages';
2526
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2627
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2728
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
2829
import dotenv from 'dotenv';
30+
import path from "path";
2931

30-
dotenv.config({ path: '../../.env' });
32+
const filename = fileURLToPath(import.meta.url);
33+
const dirname = path.dirname(filename);
34+
35+
dotenv.config({ path: path.resolve(dirname, '../../.env') });
3136

3237
const REQUEST_TIMEOUT = 120_000; // 2 minutes
3338
const MAX_TOKENS = 2048; // Maximum tokens for Claude response
@@ -36,7 +41,7 @@ const MAX_TOKENS = 2048; // Maximum tokens for Claude response
3641
// const CLAUDE_MODEL = 'claude-3-5-haiku-20241022'; // a fastest model
3742
const CLAUDE_MODEL = 'claude-3-haiku-20240307'; // a fastest and most compact model for near-instant responsiveness
3843
const DEBUG = true;
39-
const DEBUG_SERVER_PATH = '../../dist/index.js';
44+
const DEBUG_SERVER_PATH = path.resolve(dirname, '../../dist/index.js');
4045

4146
const NODE_PATH = execSync('which node').toString().trim();
4247

src/input.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { defaults } from './const.js';
12
import type { Input } from './types.js';
23

34
/**
@@ -6,7 +7,7 @@ import type { Input } from './types.js';
67
* @returns input
78
*/
89
export async function processInput(originalInput: Partial<Input>): Promise<Input> {
9-
const input = originalInput as Input;
10+
const input = { ...defaults, ...originalInput } as Input;
1011

1112
// actors can be a string or an array of strings
1213
if (input.actors && typeof input.actors === 'string') {

src/main.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ async function processParamsAndUpdateTools(url: string) {
3636
delete params.token;
3737
log.debug(`Received input parameters: ${JSON.stringify(params)}`);
3838
const input = await processInput(params as Input);
39-
await (input.actors ? mcpServer.addToolsFromActors(input.actors as string[]) : mcpServer.addToolsFromDefaultActors());
39+
if (input.actors) {
40+
await mcpServer.addToolsFromActors(input.actors as string[]);
41+
}
4042
}
4143

4244
app.get(Routes.ROOT, async (req: Request, res: Response) => {
@@ -81,16 +83,20 @@ app.use((req: Request, res: Response) => {
8183
res.status(404).json({ message: `There is nothing at route ${req.method} ${req.originalUrl}. ${HELP_MESSAGE}` }).end();
8284
});
8385

86+
const input = await processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
87+
log.info(`Loaded input: ${JSON.stringify(input)} `);
88+
8489
if (STANDBY_MODE) {
8590
log.info('Actor is running in the STANDBY mode.');
91+
if (input.actors && input.actors.length > 0) {
92+
await mcpServer.addToolsFromActors(input.actors as string[]);
93+
}
8694

8795
app.listen(PORT, () => {
8896
log.info(`The Actor web server is listening for user requests at ${HOST}.`);
8997
});
9098
} else {
9199
log.info('Actor is not designed to run in the NORMAL model (use this mode only for debugging purposes)');
92-
const input = await processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
93-
log.info(`Loaded input: ${JSON.stringify(input)} `);
94100

95101
if (input && !input.debugActor && !input.debugActorInput) {
96102
await Actor.fail('If you need to debug a specific actor, please provide the debugActor and debugActorInput fields in the input.');

src/server.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,22 @@ export class ApifyMcpServer {
5252
if (!process.env.APIFY_API_TOKEN) {
5353
throw new Error('APIFY_API_TOKEN is required but not set. Please set it as an environment variable');
5454
}
55-
const name = actorName.replace('_', '/');
5655
try {
57-
log.info(`Calling actor ${name} with input: ${JSON.stringify(input)}`);
56+
log.info(`Calling actor ${actorName} with input: ${JSON.stringify(input)}`);
5857
const client = new ApifyClient({ token: process.env.APIFY_API_TOKEN });
59-
const actorClient = client.actor(name);
58+
const actorClient = client.actor(actorName);
6059

6160
const results = await actorClient.call(input);
6261
const dataset = await client.dataset(results.defaultDatasetId).listItems();
63-
log.info(`Actor ${name} finished with ${dataset.items.length} items`);
62+
log.info(`Actor ${actorName} finished with ${dataset.items.length} items`);
6463

6564
if (process.env.APIFY_IS_AT_HOME) {
6665
await Actor.pushData(dataset.items);
6766
log.info(`Pushed ${dataset.items.length} items to the dataset`);
6867
}
6968
return dataset.items;
7069
} catch (error) {
71-
log.error(`Error calling actor: ${error}. Actor: ${name}, input: ${JSON.stringify(input)}`);
70+
log.error(`Error calling actor: ${error}. Actor: ${actorName}, input: ${JSON.stringify(input)}`);
7271
throw new Error(`Error calling actor: ${error}`);
7372
}
7473
}
@@ -112,18 +111,18 @@ export class ApifyMcpServer {
112111
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
113112
const { name, arguments: args } = request.params;
114113

115-
const tool = this.tools.get(name);
114+
// Anthropic can't handle '/' in tool names. The replace is only necessary when calling the tool from stdio clients.
115+
const tool = this.tools.get(name) || this.tools.get(name.replace('/', '_'));
116116
if (!tool) {
117117
throw new Error(`Unknown tool: ${name}`);
118118
}
119-
120-
log.info(`Validate arguments for tool: ${name} with arguments: ${JSON.stringify(args)}`);
119+
log.info(`Validate arguments for tool: ${tool.name} with arguments: ${JSON.stringify(args)}`);
121120
if (!tool.ajvValidate(args)) {
122-
throw new Error(`Invalid arguments for tool ${name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.ajvValidate.errors)}`);
121+
throw new Error(`Invalid arguments for tool ${tool.name}: args: ${JSON.stringify(args)} error: ${JSON.stringify(tool?.ajvValidate.errors)}`);
123122
}
124123

125124
try {
126-
const items = await this.callActorGetDataset(name, args);
125+
const items = await this.callActorGetDataset(tool.actorName, args);
127126
return { content: items.map((item) => ({ type: 'text', text: JSON.stringify(item) })) };
128127
} catch (error) {
129128
log.error(`Error calling tool: ${error}`);

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ActorDefinitionWithDesc extends ActorDefinition {
1313

1414
export interface Tool {
1515
name: string;
16+
actorName: string;
1617
description: string;
1718
inputSchema: object;
1819
ajvValidate: ValidateFunction;

0 commit comments

Comments
 (0)