Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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.4",
"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;
}
11 changes: 11 additions & 0 deletions src/utils/tools-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* 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';

Expand All @@ -14,6 +15,7 @@ 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 !== 'call-actor'));
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();
});
});
}