Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,15 +301,21 @@ npm run build
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:

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

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

## ⓘ Limitations and feedback

To limit the context size the properties in the `input schema` are pruned and description is truncated to 200 characters.
Enum fields and titles are truncated to max 50 options.

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.

# 🚀 Roadmap (January 2025)

- Document examples for [Superinference.ai](https://superinterface.ai/) and [LibreChat](https://www.librechat.ai/).
- Provide tools to search for Actors and load them as needed.
- Add Apify's dataset and key-value store as resources.
- Add tools such as Actor logs and Actor runs for debugging.
- Prune Actors input schema to reduce context size.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apify/actors-mcp-server",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"description": "Model Context Protocol Server for Apify Actors",
"engines": {
Expand Down
39 changes: 38 additions & 1 deletion src/actorDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Ajv } from 'ajv';
import { ApifyClient } from 'apify-client';

import { MAX_DESCRIPTION_LENGTH, MAX_ENUM_LENGTH } from './const.js';
import { log } from './logger.js';
import type { ActorDefinitionWithDesc, Tool } from './types';
import type { ActorDefinitionWithDesc, Tool, SchemaProperties } from './types.js';

/**
* Get actor input schema by actor name.
Expand Down Expand Up @@ -52,6 +53,38 @@ async function fetchActorDefinition(actorFullName: string): Promise<ActorDefinit
}
}

/**
* Shortens the description and enum values of schema properties.
* @param properties
*/
function shortenProperties(properties: { [key: string]: SchemaProperties}): { [key: string]: SchemaProperties } {
for (const property of Object.values(properties)) {
if (property.description.length > MAX_DESCRIPTION_LENGTH) {
property.description = `${property.description.slice(0, MAX_DESCRIPTION_LENGTH)}...`;
}
if (property.enum) {
property.enum = property.enum.slice(0, MAX_ENUM_LENGTH);
}
if (property.enumTitles) {
property.enumTitles = property.enumTitles.slice(0, MAX_ENUM_LENGTH);
}
}
return properties;
}

/**
* Filters schema properties to include only the necessary fields.
* @param properties
*/
function filterSchemaProperties(properties: { [key: string]: SchemaProperties }): { [key: string]: SchemaProperties } {
const filteredProperties: { [key: string]: SchemaProperties } = {};
for (const [key, property] of Object.entries(properties)) {
const { title, description, enum: enumValues, enumTitles, type, default: defaultValue, prefill } = property;
filteredProperties[key] = { title, description, enum: enumValues, enumTitles, type, default: defaultValue, prefill };
}
return filteredProperties;
}

/**
* Fetches actor input schemas by actor full names and creates MCP tools.
*
Expand All @@ -70,6 +103,10 @@ export async function getActorsAsTools(actors: string[]): Promise<Tool[]> {
const tools = [];
for (const result of results) {
if (result) {
if (result.input && 'properties' in result.input && result.input) {
const properties = filterSchemaProperties(result.input.properties as { [key: string]: SchemaProperties });
result.input.properties = shortenProperties(properties);
}
try {
tools.push({
name: result.name.replace('/', '_'),
Expand Down
7 changes: 7 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export const SERVER_NAME = 'apify-mcp-server';
export const SERVER_VERSION = '0.1.0';

export const HEADER_READINESS_PROBE = 'x-apify-container-server-readiness-probe';

export const MAX_ENUM_LENGTH = 50;
export const MAX_DESCRIPTION_LENGTH = 200;

export const USER_AGENT_ORIGIN = 'Origin/mcp-server';

export const defaults = {
actors: [
'apify/instagram-scraper',
Expand Down
4 changes: 4 additions & 0 deletions src/examples/client_sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ async def run() -> None:

tools = await session.list_tools()
print("Available Tools:", tools, end="\n\n")
for tool in tools.tools:
print(f"\n### Tool name ###: {tool.name}")
print(f"\tdescription: {tool.description}")
print(f"\tinputSchema: {tool.inputSchema}")

if hasattr(tools, "tools") and not tools.tools:
print("No tools available!")
Expand Down
7 changes: 6 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Actor } from 'apify';
import type { Request, Response } from 'express';
import express from 'express';

import { Routes } from './const.js';
import { HEADER_READINESS_PROBE, Routes } from './const.js';
import { processInput } from './input.js';
import { log } from './logger.js';
import { ApifyMcpServer } from './server.js';
Expand Down Expand Up @@ -46,6 +46,11 @@ async function processParamsAndUpdateTools(url: string) {

app.route(Routes.ROOT)
.get(async (req: Request, res: Response) => {
if (req.headers && req.get(HEADER_READINESS_PROBE) !== undefined) {
log.debug('Received readiness probe');
res.status(200).json({ message: 'Server is ready' }).end();
return;
}
try {
log.info(`Received GET message at: ${req.url}`);
await processParamsAndUpdateTools(req.url);
Expand Down
19 changes: 18 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import type { ApifyClientOptions } from 'apify';
import { Actor } from 'apify';
import { ApifyClient } from 'apify-client';
import type { AxiosRequestConfig } from 'axios';

import { getActorsAsTools } from './actorDefinition.js';
import {
Expand All @@ -15,6 +17,7 @@ import {
defaults,
SERVER_NAME,
SERVER_VERSION,
USER_AGENT_ORIGIN,
} from './const.js';
import { log } from './logger.js';
import type { Tool } from './types';
Expand Down Expand Up @@ -43,6 +46,18 @@ export class ApifyMcpServer {
this.setupToolHandlers();
}

/**
* Adds a User-Agent header to the request config.
* @param config
* @private
*/
private addUserAgent(config: AxiosRequestConfig): AxiosRequestConfig {
const updatedConfig = { ...config };
updatedConfig.headers = updatedConfig.headers ?? {};
updatedConfig.headers['User-Agent'] = `${updatedConfig.headers['User-Agent'] ?? ''}; ${USER_AGENT_ORIGIN}`;
return updatedConfig;
}

/**
* Calls an Apify actor and retrieves the dataset items.
*
Expand All @@ -60,7 +75,9 @@ export class ApifyMcpServer {
}
try {
log.info(`Calling actor ${actorName} with input: ${JSON.stringify(input)}`);
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const options: ApifyClientOptions = { requestInterceptors: [this.addUserAgent] };
const client = new ApifyClient({ ...options, token: process.env.APIFY_TOKEN });
const actorClient = client.actor(actorName);

const results = await actorClient.call(input);
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ export interface Tool {
inputSchema: object;
ajvValidate: ValidateFunction;
}

export interface SchemaProperties {
title: string;
description: string;
enum: string[]; // Array of string options for the enum
enumTitles: string[]; // Array of string titles for the enum
type: string; // Data type (e.g., "string")
default: string;
prefill: string;
}