Skip to content

Commit 193f098

Browse files
authored
fix: Add internal tools for Actor discovery (#28)
* fix: Update README.md with information about clientSse.ts * Working client with issues * Add internal tools * Move client to a separate repository. Update README.md * Require APIFY_TOKEN when server starts
1 parent 96ecc6d commit 193f098

File tree

14 files changed

+377
-76
lines changed

14 files changed

+377
-76
lines changed

.actor/input_schema.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
"lukaskrivka/google-maps-with-contact-details"
1515
]
1616
},
17+
"enableActorAutoLoading": {
18+
"title": "Enable automatic loading of Actors based on context and use-case (experimental, check if it supported by your client)",
19+
"type": "boolean",
20+
"description": "When enabled, the server can dynamically add Actors as tools based on user requests and context. \n\nNote: Not all MCP clients support this feature. To try it, you can use the [Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client). This is an experimental feature and may require client-specific support.",
21+
"default": false
22+
},
23+
"maxActorMemoryBytes": {
24+
"title": "Limit the maximum memory used by an Actor",
25+
"type": "integer",
26+
"description": "Limit the maximum memory used by an Actor in bytes. This is important setting for Free plan users to avoid exceeding the memory limit.",
27+
"prefill": 4096,
28+
"default": 4096
29+
},
1730
"debugActor": {
1831
"title": "Debug Actor",
1932
"type": "string",
@@ -28,7 +41,7 @@
2841
"description": "Specify the input for the Actor that will be used for debugging in normal mode",
2942
"editor": "json",
3043
"prefill": {
31-
"query": "hello world"
44+
"query": "hello world"
3245
}
3346
}
3447
}

README.md

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ Implementation of an MCP server for all [Apify Actors](https://apify.com/store).
66
This server enables interaction with one or more Apify Actors that can be defined in the MCP Server configuration.
77

88
The server can be used in two ways:
9-
- 🇦 **Apify MCP Server Actor**: runs an HTTP server with MCP protocol via Server-Sent Events.
10-
-**Apify MCP Server Stdio**: provides support for the MCP protocol via standard input/output stdio.
9+
- 🇦 **Apify MCP Server Actor**: runs an HTTP server with MCP and can be accessed via Server-Sent Events (SSE).
10+
-**Apify MCP Server Stdio**: runs the server locally with MCP via standard input/output (stdio).
11+
12+
You can test the MCP server using [Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client)
1113

1214
# 🎯 What does Apify MCP server do?
1315

@@ -19,8 +21,15 @@ For example it can:
1921
- use [Instagram Scraper](https://apify.com/apify/instagram-scraper) to scrape Instagram posts, profiles, places, photos, and comments
2022
- use [RAG Web Browser](https://apify.com/apify/web-scraper) to search the web, scrape the top N URLs, and return their content
2123

24+
# MCP Clients
25+
26+
To interact with the Apify MCP server, you can use MCP clients such as:
27+
- [Claude Desktop](https://claude.ai/download) (only Stdio support)
28+
- [LibreChat](https://www.librechat.ai/) (stdio and SSE support (yeah without Authorization header))
29+
- [Apify Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client) (SSE support with Authorization headers)
30+
- other clients at [https://modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients)
31+
- more clients at [https://glama.ai/mcp/clients](https://glama.ai/mcp/clients)
2232

23-
To interact with the Apify MCP server, you can use MCP clients such as [Claude Desktop](https://claude.ai/download), [LibreChat](https://www.librechat.ai/), or other [MCP clients](https://glama.ai/mcp/clients).
2433
Additionally, you can use simple example clients found in the [examples](https://github.com/apify/actor-mcp-server/tree/main/src/examples) directory.
2534

2635
When you have Actors integrated with the MCP server, you can ask:
@@ -54,6 +63,8 @@ To learn more, check out the blog post: [What are AI Agents?](https://blog.apify
5463

5564
## Tools
5665

66+
### Actors
67+
5768
Any [Apify Actor](https://apify.com/store) can be used as a tool.
5869
By default, the server is pre-configured with the Actors specified below, but it can be overridden by providing Actor input.
5970

@@ -79,6 +90,19 @@ You don't need to specify the input parameters or which Actor to call, everythin
7990
When a tool is called, the arguments are automatically passed to the Actor by the LLM.
8091
You can refer to the specific Actor's documentation for a list of available arguments.
8192

93+
### Helper tools
94+
95+
The server provides a set of helper tools to discover available Actors and retrieve their details:
96+
- `get-actor-details`: Retrieves documentation, input schema, and other details about a specific Actor.
97+
- `discover-actors`: Searches for relevant Actors using keywords and returns their details.
98+
99+
There are also tools to manage the available tools list. However, dynamically adding and removing tools requires the MCP client to have the capability to manage the tools list, which is typically not supported.
100+
101+
You can try this functionality using the [Apify Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client) Actor. To enable it, set the `enableActorAutoLoading` parameter.
102+
103+
- `add-actor-as-tool`: Adds an Actor by name to the available tools list without executing it, requiring user consent to run later.
104+
- `remove-actor-from-tool`: Removes an Actor by name from the available tools list when it's no longer needed.
105+
82106
## Prompt & Resources
83107

84108
The server does not provide any resources and prompts.
@@ -110,10 +134,13 @@ https://actors-mcp-server-task.apify.actor?token=<APIFY_TOKEN>
110134

111135
You can find a list of all available Actors in the [Apify Store](https://apify.com/store).
112136

113-
#### 💬 Interact with the MCP Server
137+
#### 💬 Interact with the MCP Server over SSE
114138

115139
Once the server is running, you can interact with Server-Sent Events (SSE) to send messages to the server and receive responses.
116-
You can use MCP clients such as [Superinference.ai](https://superinterface.ai/) or [LibreChat](https://www.librechat.ai/).
140+
The easiest way is to use [Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client) on Apify.
141+
142+
Other clients do not support SSE yet, but this will likely change.
143+
Please verify if MCP clients such ass [Superinference.ai](https://superinterface.ai/) or [LibreChat](https://www.librechat.ai/) support SSE with custom headers.
117144
([Claude Desktop](https://claude.ai/download) does not support SSE transport yet, see [Claude Desktop Configuration](#claude-desktop) section for more details).
118145

119146
In the client settings you need to provide server configuration:
@@ -273,6 +300,7 @@ ANTHROPIC_API_KEY=your-anthropic-api-token
273300
```
274301
In the `examples` directory, you can find two clients that interact with the server via
275302
standard input/output (stdio):
303+
276304
1. [`clientStdio.ts`](https://github.com/apify/actor-mcp-server/tree/main/src/examples/clientStdio.ts)
277305
This client script starts the MCP server with two specified Actors.
278306
It then calls the `apify/rag-web-browser` tool with a query and prints the result.
@@ -305,12 +333,12 @@ ANTHROPIC_API_KEY=your-anthropic-api-key
305333
```
306334
## Local client (SSE)
307335

308-
To test the server with the SSE transport, you can use python script `examples/client_sse.py`:
336+
To test the server with the SSE transport, you can use python script `examples/clientSse.ts`:
309337
Currently, the node.js client does not support to establish a connection to remote server witch custom headers.
310338
You need to change URL to your local server URL in the script.
311339

312340
```bash
313-
python src/examples/client_sse.py
341+
node dist/examples/clientSse.js
314342
```
315343

316344
## Debugging
@@ -334,17 +362,15 @@ Upon launching, the Inspector will display a URL that you can access in your bro
334362

335363
## ⓘ Limitations and feedback
336364

337-
To limit the context size the properties in the `input schema` are pruned and description is truncated to 200 characters.
365+
To limit the context size the properties in the `input schema` are pruned and description is truncated to 500 characters.
338366
Enum fields and titles are truncated to max 50 options.
339367

340368
Memory for each Actor is limited to 4GB.
341369
Free users have an 8GB limit, 128MB needs to be allocated for running `Actors-MCP-Server`.
342370

343-
If you need other features or have any feedback, please [submit an issue](https://console.apify.com/actors/3ox4R101TgZz67sLr/issues) in Apify Console to let us know.
371+
If you need other features or have any feedback, please [submit an issue](https://console.apify.com/actors/1lSvMAaRcadrM1Vgv/issues) in Apify Console to let us know.
344372

345373
# 🚀 Roadmap (January 2025)
346374

347-
- Document examples for [LibreChat](https://www.librechat.ai/).
348-
- Provide tools to search for Actors and load them as needed.
349375
- Add Apify's dataset and key-value store as resources.
350376
- Add tools such as Actor logs and Actor runs for debugging.

docs/actors-mcp-server.png

79.9 KB
Loading

package-lock.json

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

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
"apify": "^3.2.6",
3636
"apify-client": "^2.11.1",
3737
"express": "^4.21.2",
38-
"minimist": "^1.2.8"
38+
"minimist": "^1.2.8",
39+
"zod": "^3.24.1",
40+
"zod-to-json-schema": "^3.24.1"
3941
},
4042
"devDependencies": {
4143
"@anthropic-ai/sdk": "^0.33.1",

src/actorDefinition.ts renamed to src/actors.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { Ajv } from 'ajv';
22
import { ApifyClient } from 'apify-client';
33

4-
import { MAX_DESCRIPTION_LENGTH, MAX_ENUM_LENGTH, MAX_MEMORY_MBYTES } from './const.js';
4+
import { ACTOR_ADDITIONAL_INSTRUCTIONS, defaults, MAX_DESCRIPTION_LENGTH } from './const.js';
55
import { log } from './logger.js';
6-
import type { ActorDefinitionWithDesc, SchemaProperties, Tool } from './types.js';
6+
import type {
7+
ActorDefinitionPruned,
8+
ActorDefinitionWithDesc,
9+
SchemaProperties,
10+
Tool,
11+
} from './types.js';
12+
13+
export function actorNameToToolName(actorName: string): string {
14+
return actorName.replace('/', '--');
15+
}
16+
17+
export function toolNameToActorName(toolName: string): string {
18+
return toolName.replace('--', '/');
19+
}
720

821
/**
922
* Get actor input schema by actor name.
@@ -12,11 +25,7 @@ import type { ActorDefinitionWithDesc, SchemaProperties, Tool } from './types.js
1225
* @param {string} actorFullName - The full name of the actor.
1326
* @returns {Promise<ActorDefinitionWithDesc | null>} - The actor definition with description or null if not found.
1427
*/
15-
async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinitionWithDesc | null> {
16-
if (!process.env.APIFY_TOKEN) {
17-
log.error('APIFY_TOKEN is required but not set. Please set it as an environment variable');
18-
return null;
19-
}
28+
export async function getActorDefinition(actorFullName: string): Promise<ActorDefinitionPruned | null> {
2029
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
2130
const actorClient = client.actor(actorFullName);
2231

@@ -43,9 +52,9 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
4352
if (buildDetails?.actorDefinition) {
4453
const actorDefinitions = buildDetails?.actorDefinition as ActorDefinitionWithDesc;
4554
actorDefinitions.description = actor.description || '';
46-
actorDefinitions.name = actorFullName;
55+
actorDefinitions.actorFullName = actorFullName;
4756
actorDefinitions.defaultRunOptions = actor.defaultRunOptions;
48-
return actorDefinitions;
57+
return pruneActorDefinition(actorDefinitions);
4958
}
5059
return null;
5160
} catch (error) {
@@ -54,21 +63,26 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
5463
}
5564
}
5665

66+
function pruneActorDefinition(response: ActorDefinitionWithDesc): ActorDefinitionPruned {
67+
return {
68+
actorFullName: response.actorFullName || '',
69+
buildTag: response?.buildTag || '',
70+
readme: response?.readme || '',
71+
input: response?.input || null,
72+
description: response.description,
73+
defaultRunOptions: response.defaultRunOptions,
74+
};
75+
}
76+
5777
/**
5878
* Shortens the description and enum values of schema properties.
5979
* @param properties
6080
*/
61-
function shortenProperties(properties: { [key: string]: SchemaProperties}): { [key: string]: SchemaProperties } {
81+
export function shortenProperties(properties: { [key: string]: SchemaProperties}): { [key: string]: SchemaProperties } {
6282
for (const property of Object.values(properties)) {
6383
if (property.description.length > MAX_DESCRIPTION_LENGTH) {
6484
property.description = `${property.description.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
6585
}
66-
if (property.enum) {
67-
property.enum = property.enum.slice(0, MAX_ENUM_LENGTH);
68-
}
69-
if (property.enumTitles) {
70-
property.enumTitles = property.enumTitles.slice(0, MAX_ENUM_LENGTH);
71-
}
7286
}
7387
return properties;
7488
}
@@ -77,11 +91,11 @@ function shortenProperties(properties: { [key: string]: SchemaProperties}): { [k
7791
* Filters schema properties to include only the necessary fields.
7892
* @param properties
7993
*/
80-
function filterSchemaProperties(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
94+
export function filterSchemaProperties(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
8195
const filteredProperties: { [key: string]: SchemaProperties } = {};
8296
for (const [key, property] of Object.entries(properties)) {
83-
const { title, description, enum: enumValues, enumTitles, type, default: defaultValue, prefill } = property;
84-
filteredProperties[key] = { title, description, enum: enumValues, enumTitles, type, default: defaultValue, prefill };
97+
const { title, description, enum: enumValues, type, default: defaultValue, prefill } = property;
98+
filteredProperties[key] = { title, description, enum: enumValues, type, default: defaultValue, prefill };
8599
}
86100
return filteredProperties;
87101
}
@@ -98,9 +112,8 @@ function filterSchemaProperties(properties: { [key: string]: SchemaProperties })
98112
* @returns {Promise<Tool[]>} - A promise that resolves to an array of MCP tools.
99113
*/
100114
export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
101-
// Fetch input schemas in parallel
102115
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
103-
const results = await Promise.all(actors.map(fetchActorDefinition));
116+
const results = await Promise.all(actors.map(getActorDefinition));
104117
const tools = [];
105118
for (const result of results) {
106119
if (result) {
@@ -109,17 +122,17 @@ export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
109122
result.input.properties = shortenProperties(properties);
110123
}
111124
try {
112-
const memoryMbytes = result.defaultRunOptions?.memoryMbytes || MAX_MEMORY_MBYTES;
125+
const memoryMbytes = result.defaultRunOptions?.memoryMbytes || defaults.maxMemoryMbytes;
113126
tools.push({
114-
name: result.name.replace('/', '_'),
115-
actorName: result.name,
116-
description: result.description,
127+
name: actorNameToToolName(result.actorFullName),
128+
actorFullName: result.actorFullName,
129+
description: `${result.description} Instructions: ${ACTOR_ADDITIONAL_INSTRUCTIONS}`,
117130
inputSchema: result.input || {},
118131
ajvValidate: ajv.compile(result.input || {}),
119-
memoryMbytes: memoryMbytes > MAX_MEMORY_MBYTES ? MAX_MEMORY_MBYTES : memoryMbytes,
132+
memoryMbytes: memoryMbytes > defaults.maxMemoryMbytes ? defaults.maxMemoryMbytes : memoryMbytes,
120133
});
121134
} catch (validationError) {
122-
log.error(`Failed to compile AJV schema for actor: ${result.name}. Error: ${validationError}`);
135+
log.error(`Failed to compile AJV schema for actor: ${result.actorFullName}. Error: ${validationError}`);
123136
}
124137
}
125138
}

src/const.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ export const SERVER_NAME = 'apify-mcp-server';
22
export const SERVER_VERSION = '0.1.0';
33

44
export const HEADER_READINESS_PROBE = 'x-apify-container-server-readiness-probe';
5-
6-
export const MAX_ENUM_LENGTH = 50;
7-
export const MAX_DESCRIPTION_LENGTH = 200;
8-
// Limit memory to 4GB for Actors. Free users have 8 GB limit, but we need to reserve some memory for Actors-MCP-Server too
9-
export const MAX_MEMORY_MBYTES = 4096;
10-
5+
export const MAX_DESCRIPTION_LENGTH = 500;
116
export const USER_AGENT_ORIGIN = 'Origin/mcp-server';
127

138
export const defaults = {
@@ -16,11 +11,22 @@ export const defaults = {
1611
'apify/rag-web-browser',
1712
'lukaskrivka/google-maps-with-contact-details',
1813
],
14+
enableActorAutoLoading: false,
15+
maxMemoryMbytes: 4096,
1916
};
2017

21-
export const ACTOR_OUTPUT_MAX_CHARS_PER_ITEM = 2_000;
18+
export const ACTOR_OUTPUT_MAX_CHARS_PER_ITEM = 5_000;
2219
export const ACTOR_OUTPUT_TRUNCATED_MESSAGE = `Output was truncated because it will not fit into context.`
23-
+ ` There is no reason to call this tool again!`;
20+
+ `There is no reason to call this tool again!`;
21+
export const ACTOR_ADDITIONAL_INSTRUCTIONS = 'Never call/execute tool/Actor unless confirmed by the user. '
22+
+ 'Always limit the number of results in the call arguments.';
23+
24+
export enum InternalTools {
25+
DISCOVER_ACTORS = 'discover-actors',
26+
ADD_ACTOR_TO_TOOLS = 'add-actor-to-tools',
27+
REMOVE_ACTOR_FROM_TOOLS = 'remove-actor-from-tools',
28+
GET_ACTOR_DETAILS = 'get-actor-details',
29+
}
2430

2531
export enum Routes {
2632
ROOT = '/',

src/examples/clientSse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Connect to the MCP server using SSE transport and call a tool.
44
* The Actors MCP Server will load default Actors.
55
*
6+
* It requires the `APIFY_TOKEN` in the `.env` file.
67
*/
78

89
import path from 'path';
@@ -15,7 +16,6 @@ import dotenv from 'dotenv';
1516
import { EventSource } from 'eventsource';
1617

1718
const REQUEST_TIMEOUT = 120_000; // 2 minutes
18-
// Resolve dirname equivalent in ES module
1919
const filename = fileURLToPath(import.meta.url);
2020
const dirname = path.dirname(filename);
2121

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ log.setLevel(log.LEVELS.ERROR);
2323
const argv = minimist(process.argv.slice(2));
2424
const argActors = argv.actors?.split(',').map((actor: string) => actor.trim()) || [];
2525

26+
if (!process.env.APIFY_TOKEN) {
27+
log.error('APIFY_TOKEN is required but not set in the environment variables.');
28+
process.exit(1);
29+
}
30+
2631
async function main() {
2732
const server = new ApifyMcpServer();
2833
await (argActors.length !== 0

src/input.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ export async function processInput(originalInput: Partial<Input>): Promise<Input
1212
if (input.actors && typeof input.actors === 'string') {
1313
input.actors = input.actors.split(',').map((format: string) => format.trim()) as string[];
1414
}
15+
if (!input.enableActorAutoLoading) {
16+
input.enableActorAutoLoading = false;
17+
}
1518
return input;
1619
}

0 commit comments

Comments
 (0)