Skip to content

Commit a194765

Browse files
authored
feat: Truncate input schema, limit description to 200 characters (#10)
* Handle server readiness probe * Add Origin/mcp-server to user-agent. Truncate inputSchema description to 200 characters. Limit enums to 50 values. * Memory for each Actor is limited to 4GB. Free users have an 8GB limit, 128MB needs to be allocated for running `Actors-MCP-Server`.
1 parent a1b0c11 commit a194765

File tree

8 files changed

+127
-14
lines changed

8 files changed

+127
-14
lines changed

README.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ For example it can:
1919
- use [Instagram Scraper](https://apify.com/apify/instagram-scraper) to scrape Instagram posts, profiles, places, photos, and comments
2020
- use [RAG Web Browser](https://apify.com/apify/web-scraper) to search the web, scrape the top N URLs, and return their content
2121

22-
To interact with the Apify MCP server, you can use MCP clients such as [Claude Desktop](https://claude.ai/download), [Superinference.ai](https://superinterface.ai/), or [LibreChat](https://www.librechat.ai/).
22+
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).
2324
Additionally, you can use simple example clients found in the [examples](https://github.com/apify/actor-mcp-server/tree/main/src/examples) directory.
2425

2526
When you have Actors integrated with the MCP server, you can ask:
@@ -29,6 +30,10 @@ When you have Actors integrated with the MCP server, you can ask:
2930
- "Provide a step-by-step guide on using the Model Context Protocol with source URLs."
3031
- "What Apify Actors I can use?"
3132

33+
The following image shows how the Apify MCP server interacts with the Apify platform and AI clients:
34+
35+
![Actors-MCP-server](https://raw.githubusercontent.com/apify/actors-mcp-server/refs/heads/master/docs/actors-mcp-server.png)
36+
3237
In the future, we plan to load Actors dynamically and provide Apify's dataset and key-value store as resources.
3338
See the [Roadmap](#-roadmap-january-2025) for more details.
3439

@@ -301,15 +306,27 @@ npm run build
301306
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
302307

303308
```bash
304-
npx @modelcontextprotocol/inspector node /path/to/actor-mcp-server/dist/index.js --env APIFY_TOKEN=your-apify-token
309+
npx @modelcontextprotocol/inspector node @apify/actors-mcp-server --env APIFY_TOKEN=your-apify-token
305310
```
306311

307312
Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
308313

314+
## ⓘ Limitations and feedback
315+
316+
To limit the context size the properties in the `input schema` are pruned and description is truncated to 200 characters.
317+
Enum fields and titles are truncated to max 50 options.
318+
319+
Memory for each Actor is limited to 4GB.
320+
Free users have an 8GB limit, 128MB needs to be allocated for running `Actors-MCP-Server`.
321+
322+
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.
323+
324+
Are you interested in AI Agents and AI applications?
325+
Visit the [Model Context Protocol](https://modelcontextprotocol.org/) website and read blog post [What are AI agents?](https://blog.apify.com/what-are-ai-agents/).
326+
309327
# 🚀 Roadmap (January 2025)
310328

311-
- Document examples for [Superinference.ai](https://superinterface.ai/) and [LibreChat](https://www.librechat.ai/).
329+
- Document examples for [LibreChat](https://www.librechat.ai/).
312330
- Provide tools to search for Actors and load them as needed.
313331
- Add Apify's dataset and key-value store as resources.
314332
- Add tools such as Actor logs and Actor runs for debugging.
315-
- Prune Actors input schema to reduce context size.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@apify/actors-mcp-server",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"type": "module",
55
"description": "Model Context Protocol Server for Apify Actors",
66
"engines": {

src/actorDefinition.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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';
45
import { log } from './logger.js';
5-
import type { ActorDefinitionWithDesc, Tool } from './types';
6+
import type { ActorDefinitionWithDesc, SchemaProperties, Tool } from './types.js';
67

78
/**
89
* Get actor input schema by actor name.
@@ -43,6 +44,7 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
4344
const actorDefinitions = buildDetails?.actorDefinition as ActorDefinitionWithDesc;
4445
actorDefinitions.description = actor.description || '';
4546
actorDefinitions.name = actorFullName;
47+
actorDefinitions.defaultRunOptions = actor.defaultRunOptions;
4648
return actorDefinitions;
4749
}
4850
return null;
@@ -52,6 +54,38 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
5254
}
5355
}
5456

57+
/**
58+
* Shortens the description and enum values of schema properties.
59+
* @param properties
60+
*/
61+
function shortenProperties(properties: { [key: string]: SchemaProperties}): { [key: string]: SchemaProperties } {
62+
for (const property of Object.values(properties)) {
63+
if (property.description.length > MAX_DESCRIPTION_LENGTH) {
64+
property.description = `${property.description.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
65+
}
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+
}
72+
}
73+
return properties;
74+
}
75+
76+
/**
77+
* Filters schema properties to include only the necessary fields.
78+
* @param properties
79+
*/
80+
function filterSchemaProperties(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
81+
const filteredProperties: { [key: string]: SchemaProperties } = {};
82+
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 };
85+
}
86+
return filteredProperties;
87+
}
88+
5589
/**
5690
* Fetches actor input schemas by actor full names and creates MCP tools.
5791
*
@@ -70,13 +104,19 @@ export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
70104
const tools = [];
71105
for (const result of results) {
72106
if (result) {
107+
if (result.input && 'properties' in result.input && result.input) {
108+
const properties = filterSchemaProperties(result.input.properties as { [key: string]: SchemaProperties });
109+
result.input.properties = shortenProperties(properties);
110+
}
73111
try {
112+
const memoryMbytes = result.defaultRunOptions?.memoryMbytes || MAX_MEMORY_MBYTES;
74113
tools.push({
75114
name: result.name.replace('/', '_'),
76115
actorName: result.name,
77116
description: result.description,
78117
inputSchema: result.input || {},
79118
ajvValidate: ajv.compile(result.input || {}),
119+
memoryMbytes: memoryMbytes > MAX_MEMORY_MBYTES ? MAX_MEMORY_MBYTES : memoryMbytes,
80120
});
81121
} catch (validationError) {
82122
log.error(`Failed to compile AJV schema for actor: ${result.name}. Error: ${validationError}`);

src/const.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
export const SERVER_NAME = 'apify-mcp-server';
22
export const SERVER_VERSION = '0.1.0';
33

4+
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+
11+
export const USER_AGENT_ORIGIN = 'Origin/mcp-server';
12+
413
export const defaults = {
514
actors: [
615
'apify/instagram-scraper',

src/examples/client_sse.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,18 @@ async def run() -> None:
3434

3535
tools = await session.list_tools()
3636
print("Available Tools:", tools, end="\n\n")
37+
for tool in tools.tools:
38+
print(f"\n### Tool name ###: {tool.name}")
39+
print(f"\tdescription: {tool.description}")
40+
print(f"\tinputSchema: {tool.inputSchema}")
3741

3842
if hasattr(tools, "tools") and not tools.tools:
3943
print("No tools available!")
4044
return
4145

46+
print("\n\nCall tool")
4247
result = await session.call_tool("apify/rag-web-browser", { "query": "example.com", "maxResults": 3 })
43-
print("Tools Call Result:")
48+
print("Tools call result:")
4449

4550
for content in result.content:
4651
print(content)

src/main.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { parse } from 'querystring';
33

44
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
55
import { Actor } from 'apify';
6+
import type { ActorCallOptions } from 'apify-client';
67
import type { Request, Response } from 'express';
78
import express from 'express';
89

9-
import { Routes } from './const.js';
10+
import { HEADER_READINESS_PROBE, MAX_MEMORY_MBYTES, Routes } from './const.js';
1011
import { processInput } from './input.js';
1112
import { log } from './logger.js';
1213
import { ApifyMcpServer } from './server.js';
@@ -46,6 +47,11 @@ async function processParamsAndUpdateTools(url: string) {
4647

4748
app.route(Routes.ROOT)
4849
.get(async (req: Request, res: Response) => {
50+
if (req.headers && req.get(HEADER_READINESS_PROBE) !== undefined) {
51+
log.debug('Received readiness probe');
52+
res.status(200).json({ message: 'Server is ready' }).end();
53+
return;
54+
}
4955
try {
5056
log.info(`Received GET message at: ${req.url}`);
5157
await processParamsAndUpdateTools(req.url);
@@ -110,6 +116,7 @@ if (STANDBY_MODE) {
110116
if (input && !input.debugActor && !input.debugActorInput) {
111117
await Actor.fail('If you need to debug a specific actor, please provide the debugActor and debugActorInput fields in the input');
112118
}
113-
await mcpServer.callActorGetDataset(input.debugActor!, input.debugActorInput!);
119+
const options = { memory: MAX_MEMORY_MBYTES } as ActorCallOptions;
120+
await mcpServer.callActorGetDataset(input.debugActor!, input.debugActorInput!, options);
114121
await Actor.exit();
115122
}

src/server.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
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 { ApifyClientOptions } from 'apify';
89
import { Actor } from 'apify';
10+
import type { ActorCallOptions } from 'apify-client';
911
import { ApifyClient } from 'apify-client';
12+
import type { AxiosRequestConfig } from 'axios';
1013

1114
import { getActorsAsTools } from './actorDefinition.js';
1215
import {
@@ -15,6 +18,7 @@ import {
1518
defaults,
1619
SERVER_NAME,
1720
SERVER_VERSION,
21+
USER_AGENT_ORIGIN,
1822
} from './const.js';
1923
import { log } from './logger.js';
2024
import type { Tool } from './types';
@@ -43,27 +47,46 @@ export class ApifyMcpServer {
4347
this.setupToolHandlers();
4448
}
4549

50+
/**
51+
* Adds a User-Agent header to the request config.
52+
* @param config
53+
* @private
54+
*/
55+
private addUserAgent(config: AxiosRequestConfig): AxiosRequestConfig {
56+
const updatedConfig = { ...config };
57+
updatedConfig.headers = updatedConfig.headers ?? {};
58+
updatedConfig.headers['User-Agent'] = `${updatedConfig.headers['User-Agent'] ?? ''}; ${USER_AGENT_ORIGIN}`;
59+
return updatedConfig;
60+
}
61+
4662
/**
4763
* Calls an Apify actor and retrieves the dataset items.
4864
*
4965
* It requires the `APIFY_TOKEN` environment variable to be set.
5066
* If the `APIFY_IS_AT_HOME` the dataset items are pushed to the Apify dataset.
5167
*
5268
* @param {string} actorName - The name of the actor to call.
69+
* @param {ActorCallOptions} callOptions - The options to pass to the actor.
5370
* @param {unknown} input - The input to pass to the actor.
5471
* @returns {Promise<object[]>} - A promise that resolves to an array of dataset items.
5572
* @throws {Error} - Throws an error if the `APIFY_TOKEN` is not set
5673
*/
57-
public async callActorGetDataset(actorName: string, input: unknown): Promise<object[]> {
74+
public async callActorGetDataset(
75+
actorName: string,
76+
input: unknown,
77+
callOptions: ActorCallOptions | undefined = undefined,
78+
): Promise<object[]> {
5879
if (!process.env.APIFY_TOKEN) {
5980
throw new Error('APIFY_TOKEN is required but not set. Please set it as an environment variable');
6081
}
6182
try {
6283
log.info(`Calling actor ${actorName} with input: ${JSON.stringify(input)}`);
63-
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
84+
85+
const options: ApifyClientOptions = { requestInterceptors: [this.addUserAgent] };
86+
const client = new ApifyClient({ ...options, token: process.env.APIFY_TOKEN });
6487
const actorClient = client.actor(actorName);
6588

66-
const results = await actorClient.call(input);
89+
const results = await actorClient.call(input, callOptions);
6790
const dataset = await client.dataset(results.defaultDatasetId).listItems();
6891
log.info(`Actor ${actorName} finished with ${dataset.items.length} items`);
6992

@@ -132,7 +155,7 @@ export class ApifyMcpServer {
132155
}
133156

134157
try {
135-
const items = await this.callActorGetDataset(tool.actorName, args);
158+
const items = await this.callActorGetDataset(tool.actorName, args, { memory: tool.memoryMbytes } as ActorCallOptions);
136159
const content = items.map((item) => {
137160
const text = JSON.stringify(item).slice(0, ACTOR_OUTPUT_MAX_CHARS_PER_ITEM);
138161
return text.length === ACTOR_OUTPUT_MAX_CHARS_PER_ITEM

src/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ValidateFunction } from 'ajv';
2-
import type { ActorDefinition } from 'apify-client';
2+
import type { ActorDefaultRunOptions, ActorDefinition } from 'apify-client';
33

44
export type Input = {
55
actors: string[] | string;
@@ -9,6 +9,7 @@ export type Input = {
99

1010
export interface ActorDefinitionWithDesc extends ActorDefinition {
1111
description: string;
12+
defaultRunOptions: ActorDefaultRunOptions
1213
}
1314

1415
export interface Tool {
@@ -17,4 +18,15 @@ export interface Tool {
1718
description: string;
1819
inputSchema: object;
1920
ajvValidate: ValidateFunction;
21+
memoryMbytes: number;
22+
}
23+
24+
export interface SchemaProperties {
25+
title: string;
26+
description: string;
27+
enum: string[]; // Array of string options for the enum
28+
enumTitles: string[]; // Array of string titles for the enum
29+
type: string; // Data type (e.g., "string")
30+
default: string;
31+
prefill: string;
2032
}

0 commit comments

Comments
 (0)