diff --git a/README.md b/README.md index d0cd32e7..e66bb683 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. @@ -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? @@ -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 @@ -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 diff --git a/package-lock.json b/package-lock.json index b8833ce0..9c8ea5dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,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", @@ -6077,6 +6078,12 @@ "node": ">= 0.4" } }, + "node_modules/mcp-client-capabilities": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mcp-client-capabilities/-/mcp-client-capabilities-0.0.5.tgz", + "integrity": "sha512-p/UlvvS9X6ZpgnnMbZXOIqu4i9zHDHQAVXKTvkznOl02J/zqqe4nBEk6ZcXTO2BpLe2PoB6zBD4onQbY5ZWE9Q==", + "license": "Apache-2.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index f4d8f44f..0a2a3b2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actor/server.ts b/src/actor/server.ts index 83d1beae..5d14ac45 100644 --- a/src/actor/server.ts +++ b/src/actor/server.ts @@ -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'; @@ -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; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 152d761e..af0c032f 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -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, @@ -52,6 +53,7 @@ interface ActorsMcpServerOptions { * Switch to enable Skyfire agentic payment mode. */ skyfireMode?: boolean; + initializeRequestData?: InitializeRequest; } /** @@ -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); diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 9963646b..98a7710c 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -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'; @@ -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 { diff --git a/src/stdio.ts b/src/stdio.ts index a0b96b24..d2e2621a 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -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); diff --git a/src/utils/mcp-clients.ts b/src/utils/mcp-clients.ts new file mode 100644 index 00000000..d183c9df --- /dev/null +++ b/src/utils/mcp-clients.ts @@ -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; +} diff --git a/src/utils/tools-loader.ts b/src/utils/tools-loader.ts index 5e7fb1a8..ab61d055 100644 --- a/src/utils/tools-loader.ts +++ b/src/utils/tools-loader.ts @@ -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. @@ -39,6 +41,7 @@ function getInternalToolByNameMap(): Map { export async function loadToolsFromInput( input: Input, apifyClient: ApifyClient, + initializeRequestData?: InitializeRequest, ): Promise { // Helpers for readability const normalizeSelectors = (value: Input['tools']): (string | ToolCategory)[] | undefined => { @@ -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; diff --git a/tests/helpers.ts b/tests/helpers.ts index fe11b796..1fc5d2ac 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -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( @@ -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); @@ -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); @@ -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); diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index 76fddb90..8a2a658d 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -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(); + }); }); }