Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ You can find detailed instructions for setting up the MCP server in the [Apify d
To interact with the Apify MCP server, you can use various MCP clients, such as:
- [Claude Desktop](https://claude.ai/download)
- [Visual Studio Code](https://code.visualstudio.com/)
- [LibreChat](https://www.librechat.ai/)
- [Apify Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client)
- Other clients at [https://modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients)
- More clients at [https://glama.ai/mcp/clients](https://glama.ai/mcp/clients)
Expand All @@ -83,7 +82,7 @@ With MCP server integrated, you can ask your AI assistant things like:
- "Provide a step-by-step guide on using the Model Context Protocol, including source URLs."
- "What Apify Actors can I use?"

### Supported Clients Matrix
### Supported clients matrix

The following table outlines the tested MCP clients and their level of support for key features.

Expand All @@ -92,12 +91,19 @@ The following table outlines the tested MCP clients and their level of support f
| **Claude.ai (web)** | ✅ Full | |
| **Claude Desktop** | 🟡 Partial | Tools may need to be reloaded manually in the client. |
| **VS Code (Genie)** | ✅ Full | |
| **LibreChat** | ❓ Untested | |
| **Apify Tester MCP Client** | ✅ Full | Designed for testing Apify MCP servers. |

*This matrix is a work in progress. If you have tested other clients, please consider contributing to this documentation.*
Apify MCP Server is compatible with any MCP client that adheres to the [Model Context Protocol](https://modelcontextprotocol.org/), but the level of support for dynamic tool discovery and other features may vary between clients. Therefore, the server uses [mcp-client-capabilities](https://github.com/apify/mcp-client-capabilities) to detect client capabilities and adjust its behavior accordingly.

# 🪄 Try Apify MCP Instantly
**Smart tool selection based on client capabilities:**

When the `actors` tool category is requested, the server intelligently selects the most appropriate Actor-related tools based on the client's capabilities:

- **Clients with dynamic tool support** (e.g., Claude.ai web, VS Code Genie): The server provides the `add-actor` tool instead of `call-actor`. This allows for a better user experience where users can dynamically discover and add new Actors as tools during their conversation.

- **Clients with limited dynamic tool support** (e.g., Claude Desktop): The server provides the standard `call-actor` tool along with other Actor category tools, ensuring compatibility while maintaining functionality.

# 🪄 Try Apify MCP instantly

Want to try Apify MCP without any setup?

Expand All @@ -106,7 +112,7 @@ Check out [Apify Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-cli
This interactive, chat-like interface provides an easy way to explore the capabilities of Apify MCP without any local setup.
Just sign in with your Apify account and start experimenting with web scraping, data extraction, and automation tools!

Or use the Anthropic Desktop extension file (dxt) for one-click installation: [Apify MCP server dxt file](https://github.com/apify/apify-mcp-server/releases/latest/download/apify-mcp-server.dxt)
Or use the MCP bundle file (formerly known as Anthropic Desktop extension file, or DXT) for one-click installation: [Apify MCP server MCPB file](https://github.com/apify/apify-mcp-server/releases/latest/download/apify-mcp-server.mcpb)

# 🛠️ Tools, resources, and prompts

Expand Down Expand Up @@ -172,6 +178,8 @@ Here is an overview list of all the tools provided by the Apify MCP Server.

> **Note:**
>
> When using the `actors` tool category, clients that support dynamic tool discovery (like Claude.ai web and VS Code) automatically receive the `add-actor` tool instead of `call-actor` for enhanced Actor discovery capabilities.

> The `get-actor-output` tool is automatically included with any Actor-related tool, such as `call-actor`, `add-actor`, or any specific Actor tool like `apify-slash-rag-web-browser`. When you call an Actor - either through the `call-actor` tool or directly via an Actor tool (e.g., `apify-slash-rag-web-browser`) - you receive a preview of the output. The preview depends on the Actor's output format and length; for some Actors and runs, it may include the entire output, while for others, only a limited version is returned to avoid overwhelming the LLM. To retrieve the full output of an Actor run, use the `get-actor-output` tool (supports limit, offset, and field filtering) with the `datasetId` provided by the Actor call.

### Tools configuration
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"apify-client": "^2.12.6",
"cheerio": "^1.1.2",
"express": "^4.21.2",
"mcp-client-capabilities": "^0.0.5",
"to-json-schema": "^0.2.5",
"turndown": "^7.2.0",
"yargs": "^17.7.2",
Expand Down
3 changes: 2 additions & 1 deletion src/actor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { randomUUID } from 'node:crypto';

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import type { Request, Response } from 'express';
import express from 'express';

Expand Down Expand Up @@ -154,7 +155,7 @@ export function createExpressApp(
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: false, // Use SSE response mode
});
const mcpServer = new ActorsMcpServer({ setupSigintHandler: false });
const mcpServer = new ActorsMcpServer({ setupSigintHandler: false, initializeRequestData: req.body as InitializeRequest });

// Load MCP server tools
const apifyToken = process.env.APIFY_TOKEN as string;
Expand Down
4 changes: 3 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import {
CallToolRequestSchema,
CallToolResultSchema,
Expand Down Expand Up @@ -52,6 +53,7 @@ interface ActorsMcpServerOptions {
* Switch to enable Skyfire agentic payment mode.
*/
skyfireMode?: boolean;
initializeRequestData?: InitializeRequest;
}

/**
Expand Down Expand Up @@ -230,7 +232,7 @@ export class ActorsMcpServer {
* Used primarily for SSE.
*/
public async loadToolsFromUrl(url: string, apifyClient: ApifyClient) {
const tools = await processParamsGetTools(url, apifyClient);
const tools = await processParamsGetTools(url, apifyClient, this.options.initializeRequestData);
if (tools.length > 0) {
log.debug('Loading tools from query parameters');
this.upsertTools(tools, false);
Expand Down
5 changes: 3 additions & 2 deletions src/mcp/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto';
import { parse } from 'node:querystring';

import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import type { ApifyClient } from 'apify-client';

import { processInput } from '../input.js';
Expand Down Expand Up @@ -41,9 +42,9 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
* @param url
* @param apifyToken
*/
export async function processParamsGetTools(url: string, apifyClient: ApifyClient) {
export async function processParamsGetTools(url: string, apifyClient: ApifyClient, initializeRequestData?: InitializeRequest) {
const input = parseInputParamsFromUrl(url);
return await loadToolsFromInput(input, apifyClient);
return await loadToolsFromInput(input, apifyClient, initializeRequestData);
}

export function parseInputParamsFromUrl(url: string): Input {
Expand Down
4 changes: 2 additions & 2 deletions src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,11 @@ async function main() {
};

// Normalize (merges actors into tools for backward compatibility)
const normalized = processInput(input);
const normalizedInput = processInput(input);

const apifyClient = new ApifyClient({ token: process.env.APIFY_TOKEN });
// Use the shared tools loading logic
const tools = await loadToolsFromInput(normalized, apifyClient);
const tools = await loadToolsFromInput(normalizedInput, apifyClient);

mcpServer.upsertTools(tools);

Expand Down
22 changes: 22 additions & 0 deletions src/utils/mcp-clients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { InitializeRequest } from '@modelcontextprotocol/sdk/types';
import mcpClients from 'mcp-client-capabilities';

/**
* Determines if the MCP client supports dynamic tools based on the InitializeRequest data.
*/
export function doesMcpClientSupportDynamicTools(initializeRequestData?: InitializeRequest): boolean {
const clientCapabilities = mcpClients[initializeRequestData?.params?.clientInfo?.name || ''];
if (!clientCapabilities) return false;

const clientProtocolVersion = clientCapabilities.protocolVersion;
const knownProtocolVersion = initializeRequestData?.params?.protocolVersion;

// Compare the protocolVersion to check if the client is up to date
// We check for strict equality because if the versions differ, we cannot be sure about the capabilities
if (clientProtocolVersion !== knownProtocolVersion) {
// Client version is different from the known version, we cannot be sure about its capabilities
return false;
}

return clientCapabilities.tools?.listChanged === true;
}
13 changes: 12 additions & 1 deletion src/utils/tools-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
* This eliminates duplication between stdio.ts and processParamsGetTools.
*/

import type { InitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import type { ValidateFunction } from 'ajv';
import type { ApifyClient } from 'apify';

import log from '@apify/log';

import { defaults } from '../const.js';
import { defaults, HelperTools } from '../const.js';
import { callActor } from '../tools/actor.js';
import { getActorOutput } from '../tools/get-actor-output.js';
import { addTool } from '../tools/helpers.js';
import { getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
import type { Input, InternalTool, InternalToolArgs, ToolCategory, ToolEntry } from '../types.js';
import { doesMcpClientSupportDynamicTools } from './mcp-clients.js';
import { getExpectedToolsByCategories } from './tools.js';

// Lazily-computed cache of internal tools by name to avoid circular init issues.
Expand All @@ -39,6 +41,7 @@ function getInternalToolByNameMap(): Map<string, ToolEntry> {
export async function loadToolsFromInput(
input: Input,
apifyClient: ApifyClient,
initializeRequestData?: InitializeRequest,
): Promise<ToolEntry[]> {
// Helpers for readability
const normalizeSelectors = (value: Input['tools']): (string | ToolCategory)[] | undefined => {
Expand Down Expand Up @@ -68,6 +71,14 @@ export async function loadToolsFromInput(
}

const categoryTools = toolCategories[selector as ToolCategory];

// Handler client capabilities logic for 'actors' category to swap call-actor for add-actor
// if client supports dynamic tools.
if (selector === 'actors' && doesMcpClientSupportDynamicTools(initializeRequestData)) {
internalSelections.push(...categoryTools.filter((t) => t.tool.name !== HelperTools.ACTOR_CALL));
internalSelections.push(addTool);
continue;
}
if (categoryTools) {
internalSelections.push(...categoryTools);
continue;
Expand Down
7 changes: 4 additions & 3 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface McpClientOptions {
enableAddingActors?: boolean;
tools?: (ToolCategory | string)[]; // Tool categories, specific tool or Actor names to include
useEnv?: boolean; // Use environment variables instead of command line arguments (stdio only)
clientName?: string; // Client name for identification
}

export async function createMcpSseClient(
Expand Down Expand Up @@ -45,7 +46,7 @@ export async function createMcpSseClient(
);

const client = new Client({
name: 'sse-client',
name: options?.clientName || 'sse-client',
version: '1.0.0',
});
await client.connect(transport);
Expand Down Expand Up @@ -84,7 +85,7 @@ export async function createMcpStreamableClient(
);

const client = new Client({
name: 'streamable-http-client',
name: options?.clientName || 'streamable-http-client',
version: '1.0.0',
});
await client.connect(transport);
Expand Down Expand Up @@ -134,7 +135,7 @@ export async function createMcpStdioClient(
env,
});
const client = new Client({
name: 'stdio-client',
name: options?.clientName || 'stdio-client',
version: '1.0.0',
});
await client.connect(transport);
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,5 +978,16 @@ export function createIntegrationTestsSuite(
const tools = await client.listTools();
expect(tools.tools.length).toBeGreaterThan(0);
});

it.runIf(options.transport === 'streamable-http')('should swap call-actor for add-actor when client supports dynamic tools', async () => {
client = await createClientFn({ clientName: 'Visual Studio Code', tools: ['actors'] });
const names = getToolNames(await client.listTools());

// should not contain call-actor but should contain add-actor
expect(names).not.toContain('call-actor');
expect(names).toContain('add-actor');

await client.close();
});
});
}