Skip to content

Commit 725f9ee

Browse files
committed
Update docs and update apify-client-js with fixes for Actor definition
1 parent 1109776 commit 725f9ee

File tree

11 files changed

+106
-62
lines changed

11 files changed

+106
-62
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Alternatively, you can use simple python [client_see.py](https://github.com/apif
154154
data: {"result":{"content":[{"type":"text","text":"{\"searchString\":\"restaurants in San Francisco\",\"rank\":1,\"title\":\"Gary Danko\",\"description\":\"Renowned chef Gary Danko's fixed-price menus of American cuisine ... \",\"price\":\"$100+\"...}}]}}
155155
```
156156
157-
## MCP Server at a local host
157+
## MCP Server at a local host
158158
159159
### Prerequisites
160160
@@ -248,15 +248,15 @@ ANTHROPIC_API_KEY=your-anthropic-api-token
248248
```
249249
In the `examples` directory, you can find two clients that interact with the server via
250250
standard input/output (stdio):
251-
1. [`clientStdio.ts`](https://github.com/apify/actor-mcp-server/tree/main/src/examples/clientStdio.ts):
251+
1. [`clientStdio.ts`](https://github.com/apify/actor-mcp-server/tree/main/src/examples/clientStdio.ts)
252252
This client script starts the MCP server with two specified Actors.
253253
It then calls the `apify/rag-web-browser` tool with a query and prints the result.
254254
It demonstrates how to connect to the MCP server, list available tools, and call a specific tool using stdio transport.
255255
```bash
256256
node dist/examples/clientStdio.js
257257
```
258258

259-
2. [`clientStdioChat.ts`](https://github.com/apify/actor-mcp-server/tree/main/src/examples/clientStdioChat.ts):
259+
2. [`clientStdioChat.ts`](https://github.com/apify/actor-mcp-server/tree/main/src/examples/clientStdioChat.ts)
260260
This client script also starts the MCP server but provides an interactive command-line chat interface.
261261
It prompts the user to interact with the server, allowing for dynamic tool calls and responses.
262262
This example is useful for testing and debugging interactions with the MCP server in conversational manner.

package-lock.json

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

package.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,27 @@
99
"main": "dist/index.js",
1010
"bin": {
1111
"apify-mcp-server": "dist/index.js"
12-
},
13-
"repository": {
14-
"type": "git",
15-
"url": "https://github.com/apify/actor-mcp-server.git"
16-
},
17-
"bugs": {
18-
"url": "https://github.com/apify/actor-mcp-server/issues"
19-
},
20-
"homepage": "https://apify.com/apify/mcp-server",
21-
"keywords": [
22-
"apify",
23-
"mcp",
24-
"server",
25-
"actors",
26-
"model context protocol"
27-
],
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/apify/actor-mcp-server.git"
16+
},
17+
"bugs": {
18+
"url": "https://github.com/apify/actor-mcp-server/issues"
19+
},
20+
"homepage": "https://apify.com/apify/mcp-server",
21+
"keywords": [
22+
"apify",
23+
"mcp",
24+
"server",
25+
"actors",
26+
"model context protocol"
27+
],
2828
"dependencies": {
2929
"@modelcontextprotocol/sdk": "^1.1.0",
3030
"ajv": "^8.17.1",
3131
"apify": "^3.2.6",
32-
"apify-client": "^2.11.0",
32+
"apify-client": "^2.11.1",
3333
"express": "^4.21.2",
3434
"minimist": "^1.2.8"
3535
},

src/actorDefinition.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { Ajv } from 'ajv';
2-
import type { ActorDefinition } from 'apify-client';
32
import { ApifyClient } from 'apify-client';
43

54
import { log } from './logger.js';
6-
7-
interface ActorDefinitionWithDesc extends ActorDefinition {
8-
description: string;
9-
}
5+
import type { ActorDefinitionWithDesc, Tool } from './types';
106

117
/**
128
* Get actor input schema by actor name.
139
* First, fetch the actor details to get the default build tag and buildId.
1410
* Then, fetch the build details and return actorName, description, and input schema.
15-
* @param actorFullName
11+
* @param {string} actorFullName - The full name of the actor.
12+
* @returns {Promise<ActorDefinitionWithDesc | null>} - The actor definition with description or null if not found.
1613
*/
1714
async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinitionWithDesc | null> {
1815
if (!process.env.APIFY_API_TOKEN) {
@@ -33,19 +30,16 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
3330
// Extract default build label
3431
const tag = actor.defaultRunOptions?.build || '';
3532
const buildId = actor.taggedBuilds?.[tag]?.buildId || '';
36-
const description = actor.description || '';
3733

3834
if (!buildId) {
3935
log.error(`Failed to fetch input schema for actor: ${actorFullName}. Build ID not found.`);
4036
return null;
4137
}
4238
// Fetch build details and return the input schema
4339
const buildDetails = await client.build(buildId).get();
44-
if (buildDetails && 'actorDefinition' in buildDetails) {
45-
// The buildDetails schema contains actorDefinitions but return type is actorDefinition
40+
if (buildDetails?.actorDefinition) {
4641
const actorDefinitions = buildDetails?.actorDefinition as ActorDefinitionWithDesc;
47-
actorDefinitions.description = description;
48-
// Change the name to the actorFullName (we need to have tools with a full name to call the actor)
42+
actorDefinitions.description = actor.description || '';
4943
actorDefinitions.name = actorFullName;
5044
return actorDefinitions;
5145
}
@@ -57,10 +51,15 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
5751
}
5852

5953
/**
60-
* Get actor input schemas by actor full names and create MCP tools.
61-
* @param actors - Array of actor full names
54+
* Fetches actor input schemas by actor full names and creates MCP tools.
55+
*
56+
* This function retrieves the input schemas for the specified actors and compiles them into MCP tools.
57+
* It uses the AJV library to validate the input schemas.
58+
*
59+
* @param {string[]} actors - An array of actor full names.
60+
* @returns {Promise<Tool[]>} - A promise that resolves to an array of MCP tools.
6261
*/
63-
export async function getActorsAsTools(actors: string[]) {
62+
export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
6463
// Fetch input schemas in parallel
6564
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
6665
const results = await Promise.all(actors.map(fetchActorDefinition));

src/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const defaults = {
1111
'clockworks/free-tiktok-scraper',
1212
'compass/crawler-google-places',
1313
'lukaskrivka/google-maps-with-contact-details',
14-
'voyager/booking-scraper'
14+
'voyager/booking-scraper',
1515
],
1616
};
1717

src/examples/clientStdio.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
/**
33
* Connect to the MCP server using stdio transport and call a tool.
44
* You need provide a path to MCP server and APIFY_API_TOKEN in .env file.
5-
*
6-
* Also, you need to choose ACTORS to run in the server, for example: apify/rag-web-browser
5+
* You can choose ACTORS to run in the server, for example: apify/rag-web-browser
76
*/
87

98
import { execSync } from 'child_process';

src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
/**
2+
* This script initializes and starts the Apify MCP server using the Stdio transport.
3+
*
4+
* Usage:
5+
* node <script_name> --actors=<actor1,actor2,...>
6+
*
7+
* Command-line arguments:
8+
* --actors - A comma-separated list of actor full names to add to the server.
9+
*
10+
* Example:
11+
* node index.js --actors=apify/google-search-scraper,apify/instagram-scraper
12+
*/
13+
114
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
215
import minimist from 'minimist';
316

src/input.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Input } from './types.js';
22

3-
export async function processInput(originalInput: Partial<Input>) {
3+
/**
4+
* Process input parameters, split actors string into an array
5+
* @param originalInput
6+
* @returns input
7+
*/
8+
export async function processInput(originalInput: Partial<Input>): Promise<Input> {
49
const input = originalInput as Input;
510

611
// actors can be a string or an array of strings
@@ -10,5 +15,5 @@ export async function processInput(originalInput: Partial<Input>) {
1015
if (!input.actors || input.actors.length === 0) {
1116
throw new Error('The `actors` parameter must be a non-empty array.');
1217
}
13-
return { input };
18+
return input;
1419
}

src/main.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ const HELP_MESSAGE = `Connect to the server with GET request to ${HOST}/sse?toke
2828

2929
/**
3030
* Process input parameters and update tools
31+
* If URL contains query parameter actors, add tools from actors, otherwise add tools from default actors
32+
* @param url
3133
*/
3234
async function processParamsAndUpdateTools(url: string) {
3335
const params = parse(url.split('?')[1] || '') as ParsedUrlQuery;
3436
delete params.token;
3537
log.debug(`Received input parameters: ${JSON.stringify(params)}`);
36-
const { input } = await processInput(params as Input);
38+
const input = await processInput(params as Input);
3739
await (input.actors ? mcpServer.addToolsFromActors(input.actors as string[]) : mcpServer.addToolsFromDefaultActors());
3840
}
3941

@@ -55,6 +57,7 @@ app.head(Routes.ROOT, (_req: Request, res: Response) => {
5557
app.get(Routes.SSE, async (req: Request, res: Response) => {
5658
try {
5759
log.info(`Received GET message at: ${req.url}`);
60+
await processParamsAndUpdateTools(req.url);
5861
transport = new SSEServerTransport(Routes.MESSAGE, res);
5962
await mcpServer.connect(transport);
6063
} catch (error) {
@@ -86,7 +89,7 @@ if (STANDBY_MODE) {
8689
});
8790
} else {
8891
log.info('Actor is not designed to run in the NORMAL model (use this mode only for debugging purposes)');
89-
const { input } = await processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
92+
const input = await processInput((await Actor.getInput<Partial<Input>>()) ?? ({} as Input));
9093
log.info(`Loaded input: ${JSON.stringify(input)} `);
9194

9295
if (input && !input.debugActor && !input.debugActorInput) {

src/server.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@
55
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
66
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
77
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8-
import type { ValidateFunction } from 'ajv';
98
import { Actor } from 'apify';
109
import { ApifyClient } from 'apify-client';
1110

1211
import { getActorsAsTools } from './actorDefinition.js';
1312
import { defaults, SERVER_NAME, SERVER_VERSION } from './const.js';
1413
import { log } from './logger.js';
14+
import type { Tool } from './types';
1515

1616
/**
1717
* Create Apify MCP server
1818
*/
1919
export class ApifyMcpServer {
2020
private server: Server;
21-
private tools: { name: string; description: string; inputSchema: object, ajvValidate: ValidateFunction}[];
21+
private tools: Map<string, Tool>;
2222

2323
constructor() {
2424
this.server = new Server(
@@ -32,11 +32,22 @@ export class ApifyMcpServer {
3232
},
3333
},
3434
);
35-
this.tools = [];
35+
this.tools = new Map();
3636
this.setupErrorHandling();
3737
this.setupToolHandlers();
3838
}
3939

40+
/**
41+
* Calls an Apify actor and retrieves the dataset items.
42+
*
43+
* It requires the `APIFY_API_TOKEN` environment variable to be set.
44+
* If the `APIFY_IS_AT_HOME` the dataset items are pushed to the Apify dataset.
45+
*
46+
* @param {string} actorName - The name of the actor to call.
47+
* @param {unknown} input - The input to pass to the actor.
48+
* @returns {Promise<object[]>} - A promise that resolves to an array of dataset items.
49+
* @throws {Error} - Throws an error if the `APIFY_API_TOKEN` is not set
50+
*/
4051
public async callActorGetDataset(actorName: string, input: unknown): Promise<object[]> {
4152
if (!process.env.APIFY_API_TOKEN) {
4253
throw new Error('APIFY_API_TOKEN is required but not set. Please set it as an environment variable');
@@ -71,18 +82,10 @@ export class ApifyMcpServer {
7182
await this.addToolsFromActors(defaults.actors);
7283
}
7384

74-
public addToolIfNotExist(name: string, description: string, inputSchema: object, ajvValidate: ValidateFunction): void {
75-
if (!this.tools.find((x) => x.name === name)) {
76-
this.tools.push({ name, description, inputSchema, ajvValidate });
77-
log.info(`Added tool: ${name}`);
78-
} else {
79-
log.info(`Tool already exists: ${name}`);
80-
}
81-
}
82-
83-
public updateTools(tools: { name: string; description: string; inputSchema: object, ajvValidate: ValidateFunction}[]): void {
85+
public updateTools(tools: Tool[]): void {
8486
for (const tool of tools) {
85-
this.addToolIfNotExist(tool.name, tool.description, tool.inputSchema, tool.ajvValidate);
87+
this.tools.set(tool.name, tool);
88+
log.info(`Added/Updated tool: ${tool.name}`);
8689
}
8790
}
8891

@@ -98,13 +101,18 @@ export class ApifyMcpServer {
98101

99102
private setupToolHandlers(): void {
100103
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
101-
return { tools: this.tools };
104+
return { tools: this.tools.values() };
102105
});
103106

107+
/**
108+
* Handles the request to call a tool.
109+
* @param {object} request - The request object containing tool name and arguments.
110+
* @throws {Error} - Throws an error if the tool is unknown or arguments are invalid.
111+
*/
104112
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
105113
const { name, arguments: args } = request.params;
106114

107-
const tool = this.tools.find((t) => t.name === name);
115+
const tool = this.tools.get(name);
108116
if (!tool) {
109117
throw new Error(`Unknown tool: ${name}`);
110118
}

0 commit comments

Comments
 (0)