From babe4c62bc7d632c283aea78c0351e3c3e4e7422 Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 16 Jul 2025 16:12:57 +0200 Subject: [PATCH 1/4] add tools param to select tools by feature --- src/actor/server.ts | 56 ++++++++++++++++++-------------------- src/input.ts | 6 +++- src/mcp/server.ts | 11 ++++++-- src/mcp/utils.ts | 12 ++++++-- src/stdio.ts | 38 ++++++++++++++++++++++---- src/tools/index.ts | 48 ++++++++++++++++++-------------- src/tools/run.ts | 2 +- src/types.ts | 4 +++ tests/helpers.ts | 18 ++++++++++-- tests/integration/suite.ts | 55 +++++++++++++++++++++++++++++++++++-- tests/unit/input.test.ts | 36 ++++++++++++++++++++++++ 11 files changed, 219 insertions(+), 67 deletions(-) diff --git a/src/actor/server.ts b/src/actor/server.ts index de68e4f9..0b4d5d77 100644 --- a/src/actor/server.ts +++ b/src/actor/server.ts @@ -13,26 +13,9 @@ import log from '@apify/log'; import { ActorsMcpServer } from '../mcp/server.js'; import { parseInputParamsFromUrl } from '../mcp/utils.js'; -import { getActorsAsTools } from '../tools/actor.js'; import { getHelpMessage, HEADER_READINESS_PROBE, Routes } from './const.js'; import { getActorRunData } from './utils.js'; -/** - * Helper function to load tools and actors based on input parameters - * @param mcpServer The MCP server instance - * @param url The request URL to parse parameters from - * @param apifyToken The Apify token for authentication - */ -async function loadToolsAndActors(mcpServer: ActorsMcpServer, url: string, apifyToken: string): Promise { - const input = parseInputParamsFromUrl(url); - if (input.actors || input.enableAddingActors) { - await mcpServer.loadToolsFromUrl(url, apifyToken); - } - if (!input.actors) { - await mcpServer.loadDefaultActors(apifyToken); - } -} - export function createExpressApp( host: string, mcpServerOptions: { @@ -85,13 +68,21 @@ export function createExpressApp( try { log.info(`Received GET message at: ${Routes.SSE}`); const mcpServer = new ActorsMcpServer(mcpServerOptions, false); - // Load tools from Actor input for backwards compatibility - if (mcpServerOptions.actors && mcpServerOptions.actors.length > 0) { - const tools = await getActorsAsTools(mcpServerOptions.actors, process.env.APIFY_TOKEN as string); - mcpServer.upsertTools(tools); - } - await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string); const transport = new SSEServerTransport(Routes.MESSAGE, res); + + // Load MCP server tools + const apifyToken = process.env.APIFY_TOKEN as string; + const input = parseInputParamsFromUrl(req.url); + if (input.actors || input.enableAddingActors || input.beta || input.tools) { + log.debug('[SSE] Loading tools from URL', { sessionId: transport.sessionId }); + await mcpServer.loadToolsFromUrl(req.url, apifyToken); + } + // Load default tools if no actors are specified + if (!input.actors) { + log.debug('[SSE] Loading default tools', { sessionId: transport.sessionId }); + await mcpServer.loadDefaultActors(apifyToken); + } + transportsSSE[transport.sessionId] = transport; mcpServers[transport.sessionId] = mcpServer; await mcpServer.connect(transport); @@ -164,13 +155,20 @@ export function createExpressApp( enableJsonResponse: false, // Use SSE response mode }); const mcpServer = new ActorsMcpServer(mcpServerOptions, false); - // Load tools from Actor input for backwards compatibility - if (mcpServerOptions.actors && mcpServerOptions.actors.length > 0) { - const tools = await getActorsAsTools(mcpServerOptions.actors, process.env.APIFY_TOKEN as string); - mcpServer.upsertTools(tools); - } + // Load MCP server tools - await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string); + const apifyToken = process.env.APIFY_TOKEN as string; + const input = parseInputParamsFromUrl(req.url); + if (input.actors || input.enableAddingActors || input.beta || input.tools) { + log.debug('[Streamable] Loading tools from URL', { sessionId: transport.sessionId }); + await mcpServer.loadToolsFromUrl(req.url, apifyToken); + } + // Load default tools if no actors are specified + if (!input.actors) { + log.debug('[Streamable] Loading default tools', { sessionId: transport.sessionId }); + await mcpServer.loadDefaultActors(apifyToken); + } + // Connect the transport to the MCP server BEFORE handling the request await mcpServer.connect(transport); diff --git a/src/input.ts b/src/input.ts index 0825dcad..c52986cc 100644 --- a/src/input.ts +++ b/src/input.ts @@ -3,7 +3,7 @@ */ import log from '@apify/log'; -import type { Input } from './types.js'; +import type { FeatureToolKey, Input } from './types.js'; /** * Process input parameters, split Actors string into an array @@ -32,5 +32,9 @@ export function processInput(originalInput: Partial): Input { // If beta present, set input.beta to true input.beta = input.beta !== undefined && (input.beta !== false && input.beta !== 'false'); + + if (input.tools && typeof input.tools === 'string') { + input.tools = input.tools.split(',').map((tool: string) => tool.trim()) as FeatureToolKey[]; + } return input; } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 75f00a4e..6d57e208 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -23,9 +23,9 @@ import { SERVER_NAME, SERVER_VERSION, } from '../const.js'; -import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, getActorsAsTools } from '../tools/index.js'; +import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, featureTools, getActorsAsTools } from '../tools/index.js'; import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js'; -import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; +import type { ActorMcpTool, ActorTool, FeatureToolKey, HelperTool, ToolEntry } from '../types.js'; import { connectMCPClient } from './client.js'; import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js'; import { processParamsGetTools } from './utils.js'; @@ -173,6 +173,13 @@ export class ActorsMcpServer { const actorsToLoad: string[] = []; const toolsToLoad: ToolEntry[] = []; const internalToolMap = new Map([...defaultTools, ...addRemoveTools, ...betaTools].map((tool) => [tool.tool.name, tool])); + // Add all feature tools + for (const key of Object.keys(featureTools)) { + const tools = featureTools[key as FeatureToolKey]; + for (const tool of tools) { + internalToolMap.set(tool.tool.name, tool); + } + } for (const tool of toolNames) { // Skip if the tool is already loaded diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index a49be51d..6f1f42e8 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -2,8 +2,8 @@ import { createHash } from 'node:crypto'; import { parse } from 'node:querystring'; import { processInput } from '../input.js'; -import { addRemoveTools, betaTools, getActorsAsTools } from '../tools/index.js'; -import type { Input, ToolEntry } from '../types.js'; +import { addRemoveTools, betaTools, featureTools, getActorsAsTools } from '../tools/index.js'; +import type { FeatureToolKey, Input, ToolEntry } from '../types.js'; import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from './const.js'; /** @@ -53,6 +53,14 @@ export async function processParamsGetTools(url: string, apifyToken: string) { if (input.beta) { tools.push(...betaTools); } + if (input.tools) { + for (const toolKey of input.tools) { + // Get tools by feature key + const keyTools = featureTools[toolKey as FeatureToolKey] || []; + // Push them into the tools array + tools.push(...keyTools); + } + } return tools; } diff --git a/src/stdio.ts b/src/stdio.ts index 0038226c..245f6403 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -24,7 +24,8 @@ import log from '@apify/log'; import { defaults } from './const.js'; import { ActorsMcpServer } from './mcp/server.js'; -import { getActorsAsTools } from './tools/index.js'; +import { featureTools, getActorsAsTools } from './tools/index.js'; +import type { FeatureToolKey } from './types.js'; // Keeping this interface here and not types.ts since // it is only relevant to the CLI/STDIO transport in this file @@ -37,6 +38,7 @@ interface CliArgs { /** @deprecated */ enableActorAutoLoading: boolean; beta: boolean; + tools?: string; } // Configure logging, set to ERROR @@ -47,24 +49,39 @@ const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') .option('actors', { type: 'string', - describe: 'Comma-separated list of Actor full names to add to the server', + describe: 'Comma-separated list of Actor full names to add to the server.', example: 'apify/google-search-scraper,apify/instagram-scraper', }) .option('enable-adding-actors', { type: 'boolean', default: true, - describe: 'Enable dynamically adding Actors as tools based on user requests', + describe: 'Enable dynamically adding Actors as tools based on user requests.', }) .option('enableActorAutoLoading', { type: 'boolean', default: true, hidden: true, - describe: 'Deprecated: use enable-adding-actors instead', + describe: 'Deprecated: use enable-adding-actors instead.', }) .option('beta', { type: 'boolean', default: false, - describe: 'Enable beta features', + describe: 'Enable beta features.', + }) + .options('tools', { + type: 'string', + describe: `Comma-separated list of specific feature tools to enable. + +Available choices: ${Object.keys(featureTools).join(', ')} + +Feature tools are categorized as follows: +- docs: Search and fetch Apify documentation tools. +- runs: Get Actor runs list, run details, and logs from a specific Actor run. +- storage: Access datasets, key-value stores, and their records. + +Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. +`, + example: 'docs,runs,storage', }) .help('help') .alias('h', 'help') @@ -73,13 +90,15 @@ const argv = yargs(hideBin(process.argv)) 'To connect, set your MCP client server command to `npx @apify/actors-mcp-server`' + ' and set the environment variable `APIFY_TOKEN` to your Apify API token.\n', ) - .epilogue('For more information, visit https://github.com/apify/actors-mcp-server') + .epilogue('For more information, visit https://mcp.apify.com or https://github.com/apify/actors-mcp-server') .parseSync() as CliArgs; const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading; const actors = argv.actors as string || ''; const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; const enableBeta = argv.beta; +// Keys of the feature tools to enable +const featureToolKeys = argv.tools ? argv.tools.split(',').map((t: string) => t.trim()) : []; // Validate environment if (!process.env.APIFY_TOKEN) { @@ -90,6 +109,13 @@ if (!process.env.APIFY_TOKEN) { async function main() { const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false, enableBeta }); const tools = await getActorsAsTools(actorList.length ? actorList : defaults.actors, process.env.APIFY_TOKEN as string); + // Add feature tools based on the command line arguments + if (featureToolKeys.length > 0) { + for (const key of featureToolKeys) { + const featureToolsList = featureTools[key as FeatureToolKey] || []; + tools.push(...featureToolsList); + } + } mcpServer.upsertTools(tools); // Start server diff --git a/src/tools/index.ts b/src/tools/index.ts index 69ad1feb..8ee8292b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,45 +1,53 @@ // Import specific tools that are being used import { callActor, callActorGetDataset, getActorsAsTools } from './actor.js'; +import { getDataset, getDatasetItems } from './dataset.js'; +import { getUserDatasetsList } from './dataset_collection.js'; import { fetchApifyDocsTool } from './fetch-apify-docs.js'; import { getActorDetailsTool } from './get-actor-details.js'; -import { addTool, helpTool } from './helpers.js'; +import { addTool } from './helpers.js'; +import { getKeyValueStore, getKeyValueStoreKeys, getKeyValueStoreRecord } from './key_value_store.js'; +import { getUserKeyValueStoresList } from './key_value_store_collection.js'; +import { getActorRun, getActorRunLog } from './run.js'; +import { getUserRunsList } from './run_collection.js'; import { searchApifyDocsTool } from './search-apify-docs.js'; import { searchActors } from './store_collection.js'; export const defaultTools = [ - // abortActorRun, - // actorDetailsTool, - // getActor, - // getActorLog, - // getActorRun, - // getDataset, - // getDatasetItems, - // getKeyValueStore, - // getKeyValueStoreKeys, - // getKeyValueStoreRecord, - // getUserRunsList, - // getUserDatasetsList, - // getUserKeyValueStoresList, getActorDetailsTool, - helpTool, searchActors, - searchApifyDocsTool, - fetchApifyDocsTool, ]; +export const featureTools = { + docs: [ + searchApifyDocsTool, + fetchApifyDocsTool, + ], + runs: [ + getActorRun, + getUserRunsList, + getActorRunLog, + ], + storage: [ + getDataset, + getDatasetItems, + getKeyValueStore, + getKeyValueStoreKeys, + getKeyValueStoreRecord, + getUserDatasetsList, + getUserKeyValueStoresList, + ], +}; + export const betaTools = [ callActor, ]; export const addRemoveTools = [ addTool, - // removeTool, ]; // Export only the tools that are being used export { - addTool, - // removeTool, getActorsAsTools, callActorGetDataset, }; diff --git a/src/tools/run.ts b/src/tools/run.ts index 6f3d5801..5800500b 100644 --- a/src/tools/run.ts +++ b/src/tools/run.ts @@ -59,7 +59,7 @@ const GetRunLogArgs = z.object({ * https://docs.apify.com/api/v2/actor-run-get * /v2/actor-runs/{runId}/log{?token} */ -export const getActorLog: ToolEntry = { +export const getActorRunLog: ToolEntry = { type: 'internal', tool: { name: HelperTools.ACTOR_RUNS_LOG, diff --git a/src/types.ts b/src/types.ts index d24518cb..22f1e624 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,7 @@ import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingIn import type { ACTOR_PRICING_MODEL } from './const.js'; import type { ActorsMcpServer } from './mcp/server.js'; +import type { featureTools } from './tools/index.js'; export interface ISchemaProperties { type: string; @@ -213,6 +214,8 @@ export interface InternalTool extends ToolBase { call: (toolArgs: InternalToolArgs) => Promise; } +export type FeatureToolKey = keyof typeof featureTools; + export type Input = { actors: string[] | string; /** @@ -225,6 +228,7 @@ export type Input = { debugActorInput?: unknown; /** Enable beta features flag */ beta?: boolean | string; + tools?: FeatureToolKey[] | string; }; // Utility type to get a union of values from an object type diff --git a/tests/helpers.ts b/tests/helpers.ts index 2600c7c1..e115709c 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -5,11 +5,13 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ import { expect } from 'vitest'; import { HelperTools } from '../src/const.js'; +import type { FeatureToolKey } from '../src/types.js'; export interface McpClientOptions { actors?: string[]; enableAddingActors?: boolean; enableBeta?: boolean; // Optional, used for beta features + tools?: FeatureToolKey[]; // Optional, used for feature tools } export async function createMcpSseClient( @@ -20,7 +22,7 @@ export async function createMcpSseClient( throw new Error('APIFY_TOKEN environment variable is not set.'); } const url = new URL(serverUrl); - const { actors, enableAddingActors, enableBeta } = options || {}; + const { actors, enableAddingActors, enableBeta, tools } = options || {}; if (actors) { url.searchParams.append('actors', actors.join(',')); } @@ -30,6 +32,9 @@ export async function createMcpSseClient( if (enableBeta !== undefined) { url.searchParams.append('beta', enableBeta.toString()); } + if (tools && tools.length > 0) { + url.searchParams.append('tools', tools.join(',')); + } const transport = new SSEClientTransport( url, @@ -59,7 +64,7 @@ export async function createMcpStreamableClient( throw new Error('APIFY_TOKEN environment variable is not set.'); } const url = new URL(serverUrl); - const { actors, enableAddingActors, enableBeta } = options || {}; + const { actors, enableAddingActors, enableBeta, tools } = options || {}; if (actors) { url.searchParams.append('actors', actors.join(',')); } @@ -69,6 +74,9 @@ export async function createMcpStreamableClient( if (enableBeta !== undefined) { url.searchParams.append('beta', enableBeta.toString()); } + if (tools && tools.length > 0) { + url.searchParams.append('tools', tools.join(',')); + } const transport = new StreamableHTTPClientTransport( url, @@ -96,7 +104,7 @@ export async function createMcpStdioClient( if (!process.env.APIFY_TOKEN) { throw new Error('APIFY_TOKEN environment variable is not set.'); } - const { actors, enableAddingActors, enableBeta } = options || {}; + const { actors, enableAddingActors, enableBeta, tools } = options || {}; const args = ['dist/stdio.js']; if (actors) { args.push('--actors', actors.join(',')); @@ -107,6 +115,10 @@ export async function createMcpStdioClient( if (enableBeta !== undefined) { args.push('--beta', enableBeta.toString()); } + if (tools && tools.length > 0) { + args.push('--tools', tools.join(',')); + } + const transport = new StdioClientTransport({ command: 'node', args, diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index fcf44eca..c9462c01 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -3,11 +3,12 @@ import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/cl import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { betaTools } from '../../dist/tools/index.js'; +import { betaTools, featureTools } from '../../dist/tools/index.js'; import { defaults, HelperTools } from '../../src/const.js'; import { addRemoveTools, defaultTools } from '../../src/tools/index.js'; import type { ISearchActorsResult } from '../../src/tools/store_collection.js'; import { actorNameToToolName } from '../../src/tools/utils.js'; +import type { FeatureToolKey } from '../../src/types.js'; import { ACTOR_MCP_SERVER_ACTOR_NAME, ACTOR_PYTHON_EXAMPLE, DEFAULT_ACTOR_NAMES, DEFAULT_TOOL_NAMES } from '../const.js'; import { addActor, type McpClientOptions } from '../helpers.js'; @@ -346,7 +347,9 @@ export function createIntegrationTestsSuite( }); it('should search Apify documentation', async () => { - const client = await createClientFn(); + const client = await createClientFn({ + tools: ['docs'], + }); const toolName = HelperTools.DOCS_SEARCH; const query = 'standby actor'; @@ -370,7 +373,9 @@ export function createIntegrationTestsSuite( }); it('should fetch Apify documentation page', async () => { - const client = await createClientFn(); + const client = await createClientFn({ + tools: ['docs'], + }); const toolName = HelperTools.DOCS_FETCH; const documentUrl = 'https://docs.apify.com/academy/getting-started/creating-actors'; @@ -389,6 +394,50 @@ export function createIntegrationTestsSuite( await client.close(); }); + it('should load correct tools for each feature tools key', async () => { + for (const key of Object.keys(featureTools)) { + const client = await createClientFn({ + tools: [key as FeatureToolKey], + }); + + const loadedTools = await client.listTools(); + const toolNames = getToolNames(loadedTools); + + const expectedTools = featureTools[key as FeatureToolKey]; + const expectedToolNames = expectedTools.map((tool) => tool.tool.name); + + expect(toolNames.length).toEqual(expectedTools.length + defaultTools.length + defaults.actors.length + addRemoveTools.length); + for (const expectedToolName of expectedToolNames) { + expect(toolNames).toContain(expectedToolName); + } + + await client.close(); + } + }); + + it('should handle multiple feature keys input correctly', async () => { + const client = await createClientFn({ + tools: ['docs', 'runs', 'storage'], + }); + + const loadedTools = await client.listTools(); + const toolNames = getToolNames(loadedTools); + + const expectedTools = [ + ...featureTools.docs, + ...featureTools.runs, + ...featureTools.storage, + ]; + const expectedToolNames = expectedTools.map((tool) => tool.tool.name); + + expect(toolNames.length).toEqual(expectedTools.length + defaultTools.length + defaults.actors.length + addRemoveTools.length); + for (const expectedToolName of expectedToolNames) { + expect(toolNames).toContain(expectedToolName); + } + + await client.close(); + }); + // Session termination is only possible for streamable HTTP transport. it.runIf(options.transport === 'streamable-http')('should successfully terminate streamable session', async () => { const client = await createClientFn(); diff --git a/tests/unit/input.test.ts b/tests/unit/input.test.ts index e63e7ae3..21eec3d8 100644 --- a/tests/unit/input.test.ts +++ b/tests/unit/input.test.ts @@ -86,4 +86,40 @@ describe('processInput', () => { const processed = processInput(input); expect(processed.beta).toBe(true); }); + + it('should keep tools as array of valid featureTools keys', async () => { + const input: Partial = { + actors: ['actor1'], + tools: ['docs', 'runs'], + }; + const processed = processInput(input); + expect(processed.tools).toEqual(['docs', 'runs']); + }); + + it('should handle empty tools array', async () => { + const input: Partial = { + actors: ['actor1'], + tools: [], + }; + const processed = processInput(input); + expect(processed.tools).toEqual([]); + }); + + it('should handle missing tools field (undefined)', async () => { + const input: Partial = { + actors: ['actor1'], + }; + const processed = processInput(input); + expect(processed.tools).toBeUndefined(); + }); + + it('should include all keys, even invalid ones', async () => { + const input: Partial = { + actors: ['actor1'], + // @ts-expect-error: purposely invalid key for test + tools: ['docs', 'invalidKey', 'storage'], + }; + const processed = processInput(input); + expect(processed.tools).toEqual(['docs', 'invalidKey', 'storage']); + }); }); From c319b9671b723f39ae33cd8d0e73564687553e3f Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 16 Jul 2025 16:26:14 +0200 Subject: [PATCH 2/4] vibe reafactor tool loading logic from input (url and cli args) --- src/mcp/utils.ts | 28 +++----------------- src/stdio.ts | 26 ++++++++++-------- src/utils/tools-loader.ts | 55 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 src/utils/tools-loader.ts diff --git a/src/mcp/utils.ts b/src/mcp/utils.ts index 6f1f42e8..8c682c06 100644 --- a/src/mcp/utils.ts +++ b/src/mcp/utils.ts @@ -2,8 +2,8 @@ import { createHash } from 'node:crypto'; import { parse } from 'node:querystring'; import { processInput } from '../input.js'; -import { addRemoveTools, betaTools, featureTools, getActorsAsTools } from '../tools/index.js'; -import type { FeatureToolKey, Input, ToolEntry } from '../types.js'; +import type { Input } from '../types.js'; +import { loadToolsFromInput } from '../utils/tools-loader.js'; import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from './const.js'; /** @@ -34,34 +34,14 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string } /** - * Process input parameters and get tools + * Process input parameters from URL and get tools * If URL contains query parameter `actors`, return tools from Actors otherwise return null. * @param url * @param apifyToken */ export async function processParamsGetTools(url: string, apifyToken: string) { const input = parseInputParamsFromUrl(url); - let tools: ToolEntry[] = []; - if (input.actors) { - const actors = input.actors as string[]; - // Normal Actors as a tool - tools = await getActorsAsTools(actors, apifyToken); - } - if (input.enableAddingActors) { - tools.push(...addRemoveTools); - } - if (input.beta) { - tools.push(...betaTools); - } - if (input.tools) { - for (const toolKey of input.tools) { - // Get tools by feature key - const keyTools = featureTools[toolKey as FeatureToolKey] || []; - // Push them into the tools array - tools.push(...keyTools); - } - } - return tools; + return await loadToolsFromInput(input, apifyToken); } export function parseInputParamsFromUrl(url: string): Input { diff --git a/src/stdio.ts b/src/stdio.ts index 245f6403..5cba7c95 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -22,10 +22,10 @@ import { hideBin } from 'yargs/helpers'; import log from '@apify/log'; -import { defaults } from './const.js'; import { ActorsMcpServer } from './mcp/server.js'; -import { featureTools, getActorsAsTools } from './tools/index.js'; -import type { FeatureToolKey } from './types.js'; +import { featureTools } from './tools/index.js'; +import type { FeatureToolKey, Input } from './types.js'; +import { loadToolsFromInput } from './utils/tools-loader.js'; // Keeping this interface here and not types.ts since // it is only relevant to the CLI/STDIO transport in this file @@ -108,14 +108,18 @@ if (!process.env.APIFY_TOKEN) { async function main() { const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false, enableBeta }); - const tools = await getActorsAsTools(actorList.length ? actorList : defaults.actors, process.env.APIFY_TOKEN as string); - // Add feature tools based on the command line arguments - if (featureToolKeys.length > 0) { - for (const key of featureToolKeys) { - const featureToolsList = featureTools[key as FeatureToolKey] || []; - tools.push(...featureToolsList); - } - } + + // Create an Input object from CLI arguments + const input: Input = { + actors: actorList.length ? actorList : [], + enableAddingActors, + beta: enableBeta, + tools: featureToolKeys as FeatureToolKey[], + }; + + // Use the shared tools loading logic + const tools = await loadToolsFromInput(input, process.env.APIFY_TOKEN as string, actorList.length === 0); + mcpServer.upsertTools(tools); // Start server diff --git a/src/utils/tools-loader.ts b/src/utils/tools-loader.ts new file mode 100644 index 00000000..4a3ffa60 --- /dev/null +++ b/src/utils/tools-loader.ts @@ -0,0 +1,55 @@ +/** + * Shared logic for loading tools based on Input type. + * This eliminates duplication between stdio.ts and processParamsGetTools. + */ + +import { defaults } from '../const.js'; +import { addRemoveTools, betaTools, featureTools, getActorsAsTools } from '../tools/index.js'; +import type { FeatureToolKey, Input, ToolEntry } from '../types.js'; + +/** + * Load tools based on the provided Input object. + * This function is used by both the stdio.ts and the processParamsGetTools function. + * + * @param input The processed Input object + * @param apifyToken The Apify API token + * @param useDefaultActors Whether to use default actors if no actors are specified + * @returns An array of tool entries + */ +export async function loadToolsFromInput( + input: Input, + apifyToken: string, + useDefaultActors = false, +): Promise { + let tools: ToolEntry[] = []; + + // Load actors as tools + if (input.actors && (Array.isArray(input.actors) ? input.actors.length > 0 : input.actors)) { + const actors = Array.isArray(input.actors) ? input.actors : [input.actors]; + tools = await getActorsAsTools(actors, apifyToken); + } else if (useDefaultActors) { + // Use default actors if no actors are specified and useDefaultActors is true + tools = await getActorsAsTools(defaults.actors, apifyToken); + } + + // Add tools for adding/removing actors if enabled + if (input.enableAddingActors) { + tools.push(...addRemoveTools); + } + + // Add beta tools if enabled + if (input.beta) { + tools.push(...betaTools); + } + + // Add feature tools based on the input + if (input.tools) { + const toolKeys = Array.isArray(input.tools) ? input.tools : [input.tools]; + for (const toolKey of toolKeys) { + const keyTools = featureTools[toolKey as FeatureToolKey] || []; + tools.push(...keyTools); + } + } + + return tools; +} From 1170dfc6cf619a8e72d4bd7b02a81f00e8f6e182 Mon Sep 17 00:00:00 2001 From: MQ Date: Thu, 17 Jul 2025 11:42:14 +0200 Subject: [PATCH 3/4] rename to tool categories, include beta tools as separte preview category, remove beta flag param --- src/actor/server.ts | 4 ++-- src/input.ts | 7 ++---- src/mcp/server.ts | 18 +++++---------- src/stdio.ts | 28 +++++++++-------------- src/tools/index.ts | 21 ++++++++++------- src/types.ts | 9 ++++---- src/utils/tools-loader.ts | 13 ++++------- tests/helpers.ts | 20 +++++------------ tests/integration/suite.ts | 46 ++++++++++++++++++++++++-------------- tests/unit/input.test.ts | 40 --------------------------------- 10 files changed, 76 insertions(+), 130 deletions(-) diff --git a/src/actor/server.ts b/src/actor/server.ts index 0b4d5d77..f3894008 100644 --- a/src/actor/server.ts +++ b/src/actor/server.ts @@ -73,7 +73,7 @@ export function createExpressApp( // Load MCP server tools const apifyToken = process.env.APIFY_TOKEN as string; const input = parseInputParamsFromUrl(req.url); - if (input.actors || input.enableAddingActors || input.beta || input.tools) { + if (input.actors || input.enableAddingActors || input.tools) { log.debug('[SSE] Loading tools from URL', { sessionId: transport.sessionId }); await mcpServer.loadToolsFromUrl(req.url, apifyToken); } @@ -159,7 +159,7 @@ export function createExpressApp( // Load MCP server tools const apifyToken = process.env.APIFY_TOKEN as string; const input = parseInputParamsFromUrl(req.url); - if (input.actors || input.enableAddingActors || input.beta || input.tools) { + if (input.actors || input.enableAddingActors || input.tools) { log.debug('[Streamable] Loading tools from URL', { sessionId: transport.sessionId }); await mcpServer.loadToolsFromUrl(req.url, apifyToken); } diff --git a/src/input.ts b/src/input.ts index c52986cc..a6a63812 100644 --- a/src/input.ts +++ b/src/input.ts @@ -3,7 +3,7 @@ */ import log from '@apify/log'; -import type { FeatureToolKey, Input } from './types.js'; +import type { Input, ToolCategory } from './types.js'; /** * Process input parameters, split Actors string into an array @@ -30,11 +30,8 @@ export function processInput(originalInput: Partial): Input { input.enableAddingActors = input.enableAddingActors === true || input.enableAddingActors === 'true'; } - // If beta present, set input.beta to true - input.beta = input.beta !== undefined && (input.beta !== false && input.beta !== 'false'); - if (input.tools && typeof input.tools === 'string') { - input.tools = input.tools.split(',').map((tool: string) => tool.trim()) as FeatureToolKey[]; + input.tools = input.tools.split(',').map((tool: string) => tool.trim()) as ToolCategory[]; } return input; } diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 6d57e208..8e00aafd 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -23,9 +23,9 @@ import { SERVER_NAME, SERVER_VERSION, } from '../const.js'; -import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, featureTools, getActorsAsTools } from '../tools/index.js'; +import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js'; import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js'; -import type { ActorMcpTool, ActorTool, FeatureToolKey, HelperTool, ToolEntry } from '../types.js'; +import type { ActorMcpTool, ActorTool, HelperTool, ToolCategory, ToolEntry } from '../types.js'; import { connectMCPClient } from './client.js'; import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js'; import { processParamsGetTools } from './utils.js'; @@ -33,7 +33,6 @@ import { processParamsGetTools } from './utils.js'; type ActorsMcpServerOptions = { enableAddingActors?: boolean; enableDefaultActors?: boolean; - enableBeta?: boolean; // Enable beta features }; type ToolsChangedHandler = (toolNames: string[]) => void; @@ -52,7 +51,6 @@ export class ActorsMcpServer { this.options = { enableAddingActors: options.enableAddingActors ?? true, enableDefaultActors: options.enableDefaultActors ?? true, // Default to true for backward compatibility - enableBeta: options.enableBeta ?? false, // Disabled by default }; this.server = new Server( { @@ -78,10 +76,6 @@ export class ActorsMcpServer { this.enableDynamicActorTools(); } - if (this.options.enableBeta) { - this.upsertTools(betaTools, false); - } - // Initialize automatically for backward compatibility this.initialize().catch((error) => { log.error('Failed to initialize server:', error); @@ -172,10 +166,10 @@ export class ActorsMcpServer { const loadedTools = this.listAllToolNames(); const actorsToLoad: string[] = []; const toolsToLoad: ToolEntry[] = []; - const internalToolMap = new Map([...defaultTools, ...addRemoveTools, ...betaTools].map((tool) => [tool.tool.name, tool])); - // Add all feature tools - for (const key of Object.keys(featureTools)) { - const tools = featureTools[key as FeatureToolKey]; + const internalToolMap = new Map([...defaultTools, ...addRemoveTools].map((tool) => [tool.tool.name, tool])); + // Add all category tools + for (const key of Object.keys(toolCategories)) { + const tools = toolCategories[key as ToolCategory]; for (const tool of tools) { internalToolMap.set(tool.tool.name, tool); } diff --git a/src/stdio.ts b/src/stdio.ts index 5cba7c95..a33ffe58 100644 --- a/src/stdio.ts +++ b/src/stdio.ts @@ -23,8 +23,8 @@ import { hideBin } from 'yargs/helpers'; import log from '@apify/log'; import { ActorsMcpServer } from './mcp/server.js'; -import { featureTools } from './tools/index.js'; -import type { FeatureToolKey, Input } from './types.js'; +import { toolCategories } from './tools/index.js'; +import type { Input, ToolCategory } from './types.js'; import { loadToolsFromInput } from './utils/tools-loader.js'; // Keeping this interface here and not types.ts since @@ -37,7 +37,7 @@ interface CliArgs { enableAddingActors: boolean; /** @deprecated */ enableActorAutoLoading: boolean; - beta: boolean; + /** Tool categories to include */ tools?: string; } @@ -63,21 +63,17 @@ const argv = yargs(hideBin(process.argv)) hidden: true, describe: 'Deprecated: use enable-adding-actors instead.', }) - .option('beta', { - type: 'boolean', - default: false, - describe: 'Enable beta features.', - }) .options('tools', { type: 'string', - describe: `Comma-separated list of specific feature tools to enable. + describe: `Comma-separated list of specific tool categories to enable. -Available choices: ${Object.keys(featureTools).join(', ')} +Available choices: ${Object.keys(toolCategories).join(', ')} -Feature tools are categorized as follows: +Tool categories are as follows: - docs: Search and fetch Apify documentation tools. - runs: Get Actor runs list, run details, and logs from a specific Actor run. - storage: Access datasets, key-value stores, and their records. +- preview: Experimental tools in preview mode. Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default. `, @@ -96,9 +92,8 @@ Note: Tools that enable you to search Actors from the Apify Store and get their const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading; const actors = argv.actors as string || ''; const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : []; -const enableBeta = argv.beta; -// Keys of the feature tools to enable -const featureToolKeys = argv.tools ? argv.tools.split(',').map((t: string) => t.trim()) : []; +// Keys of the tool categories to enable +const toolCategoryKeys = argv.tools ? argv.tools.split(',').map((t: string) => t.trim()) : []; // Validate environment if (!process.env.APIFY_TOKEN) { @@ -107,14 +102,13 @@ if (!process.env.APIFY_TOKEN) { } async function main() { - const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false, enableBeta }); + const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false }); // Create an Input object from CLI arguments const input: Input = { actors: actorList.length ? actorList : [], enableAddingActors, - beta: enableBeta, - tools: featureToolKeys as FeatureToolKey[], + tools: toolCategoryKeys as ToolCategory[], }; // Use the shared tools loading logic diff --git a/src/tools/index.ts b/src/tools/index.ts index 8ee8292b..16441f1b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,4 +1,5 @@ // Import specific tools that are being used +import type { ToolCategory } from '../types.js'; import { callActor, callActorGetDataset, getActorsAsTools } from './actor.js'; import { getDataset, getDatasetItems } from './dataset.js'; import { getUserDatasetsList } from './dataset_collection.js'; @@ -12,12 +13,7 @@ import { getUserRunsList } from './run_collection.js'; import { searchApifyDocsTool } from './search-apify-docs.js'; import { searchActors } from './store_collection.js'; -export const defaultTools = [ - getActorDetailsTool, - searchActors, -]; - -export const featureTools = { +export const toolCategories = { docs: [ searchApifyDocsTool, fetchApifyDocsTool, @@ -36,10 +32,19 @@ export const featureTools = { getUserDatasetsList, getUserKeyValueStoresList, ], + preview: [ + callActor, + ], }; +export const toolCategoriesEnabledByDefault: ToolCategory[] = [ + 'docs', +]; -export const betaTools = [ - callActor, +export const defaultTools = [ + getActorDetailsTool, + searchActors, + // Add the tools from the enabled categories + ...toolCategoriesEnabledByDefault.map((key) => toolCategories[key]).flat(), ]; export const addRemoveTools = [ diff --git a/src/types.ts b/src/types.ts index 22f1e624..99402292 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import type { ActorDefaultRunOptions, ActorDefinition, ActorStoreList, PricingIn import type { ACTOR_PRICING_MODEL } from './const.js'; import type { ActorsMcpServer } from './mcp/server.js'; -import type { featureTools } from './tools/index.js'; +import type { toolCategories } from './tools/index.js'; export interface ISchemaProperties { type: string; @@ -214,7 +214,7 @@ export interface InternalTool extends ToolBase { call: (toolArgs: InternalToolArgs) => Promise; } -export type FeatureToolKey = keyof typeof featureTools; +export type ToolCategory = keyof typeof toolCategories; export type Input = { actors: string[] | string; @@ -226,9 +226,8 @@ export type Input = { maxActorMemoryBytes?: number; debugActor?: string; debugActorInput?: unknown; - /** Enable beta features flag */ - beta?: boolean | string; - tools?: FeatureToolKey[] | string; + /** Tool categories to include */ + tools?: ToolCategory[] | string; }; // Utility type to get a union of values from an object type diff --git a/src/utils/tools-loader.ts b/src/utils/tools-loader.ts index 4a3ffa60..897a206e 100644 --- a/src/utils/tools-loader.ts +++ b/src/utils/tools-loader.ts @@ -4,8 +4,8 @@ */ import { defaults } from '../const.js'; -import { addRemoveTools, betaTools, featureTools, getActorsAsTools } from '../tools/index.js'; -import type { FeatureToolKey, Input, ToolEntry } from '../types.js'; +import { addRemoveTools, getActorsAsTools, toolCategories } from '../tools/index.js'; +import type { Input, ToolCategory, ToolEntry } from '../types.js'; /** * Load tools based on the provided Input object. @@ -37,16 +37,11 @@ export async function loadToolsFromInput( tools.push(...addRemoveTools); } - // Add beta tools if enabled - if (input.beta) { - tools.push(...betaTools); - } - - // Add feature tools based on the input + // Add tools from enabled categories if (input.tools) { const toolKeys = Array.isArray(input.tools) ? input.tools : [input.tools]; for (const toolKey of toolKeys) { - const keyTools = featureTools[toolKey as FeatureToolKey] || []; + const keyTools = toolCategories[toolKey as ToolCategory] || []; tools.push(...keyTools); } } diff --git a/tests/helpers.ts b/tests/helpers.ts index e115709c..fbf450b6 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -5,13 +5,12 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ import { expect } from 'vitest'; import { HelperTools } from '../src/const.js'; -import type { FeatureToolKey } from '../src/types.js'; +import type { ToolCategory } from '../src/types.js'; export interface McpClientOptions { actors?: string[]; enableAddingActors?: boolean; - enableBeta?: boolean; // Optional, used for beta features - tools?: FeatureToolKey[]; // Optional, used for feature tools + tools?: ToolCategory[]; // Tool categories to include } export async function createMcpSseClient( @@ -22,16 +21,13 @@ export async function createMcpSseClient( throw new Error('APIFY_TOKEN environment variable is not set.'); } const url = new URL(serverUrl); - const { actors, enableAddingActors, enableBeta, tools } = options || {}; + const { actors, enableAddingActors, tools } = options || {}; if (actors) { url.searchParams.append('actors', actors.join(',')); } if (enableAddingActors !== undefined) { url.searchParams.append('enableAddingActors', enableAddingActors.toString()); } - if (enableBeta !== undefined) { - url.searchParams.append('beta', enableBeta.toString()); - } if (tools && tools.length > 0) { url.searchParams.append('tools', tools.join(',')); } @@ -64,16 +60,13 @@ export async function createMcpStreamableClient( throw new Error('APIFY_TOKEN environment variable is not set.'); } const url = new URL(serverUrl); - const { actors, enableAddingActors, enableBeta, tools } = options || {}; + const { actors, enableAddingActors, tools } = options || {}; if (actors) { url.searchParams.append('actors', actors.join(',')); } if (enableAddingActors !== undefined) { url.searchParams.append('enableAddingActors', enableAddingActors.toString()); } - if (enableBeta !== undefined) { - url.searchParams.append('beta', enableBeta.toString()); - } if (tools && tools.length > 0) { url.searchParams.append('tools', tools.join(',')); } @@ -104,7 +97,7 @@ export async function createMcpStdioClient( if (!process.env.APIFY_TOKEN) { throw new Error('APIFY_TOKEN environment variable is not set.'); } - const { actors, enableAddingActors, enableBeta, tools } = options || {}; + const { actors, enableAddingActors, tools } = options || {}; const args = ['dist/stdio.js']; if (actors) { args.push('--actors', actors.join(',')); @@ -112,9 +105,6 @@ export async function createMcpStdioClient( if (enableAddingActors !== undefined) { args.push('--enable-adding-actors', enableAddingActors.toString()); } - if (enableBeta !== undefined) { - args.push('--beta', enableBeta.toString()); - } if (tools && tools.length > 0) { args.push('--tools', tools.join(',')); } diff --git a/tests/integration/suite.ts b/tests/integration/suite.ts index c9462c01..b5844875 100644 --- a/tests/integration/suite.ts +++ b/tests/integration/suite.ts @@ -3,12 +3,11 @@ import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/cl import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { betaTools, featureTools } from '../../dist/tools/index.js'; import { defaults, HelperTools } from '../../src/const.js'; -import { addRemoveTools, defaultTools } from '../../src/tools/index.js'; +import { addRemoveTools, defaultTools, toolCategories, toolCategoriesEnabledByDefault } from '../../src/tools/index.js'; import type { ISearchActorsResult } from '../../src/tools/store_collection.js'; import { actorNameToToolName } from '../../src/tools/utils.js'; -import type { FeatureToolKey } from '../../src/types.js'; +import type { ToolCategory } from '../../src/types.js'; import { ACTOR_MCP_SERVER_ACTOR_NAME, ACTOR_PYTHON_EXAMPLE, DEFAULT_ACTOR_NAMES, DEFAULT_TOOL_NAMES } from '../const.js'; import { addActor, type McpClientOptions } from '../helpers.js'; @@ -151,9 +150,9 @@ export function createIntegrationTestsSuite( it('should add Actor dynamically and call it via generic call-actor tool', async () => { const selectedToolName = actorNameToToolName(ACTOR_PYTHON_EXAMPLE); - const client = await createClientFn({ enableAddingActors: true, enableBeta: true }); + const client = await createClientFn({ enableAddingActors: true, tools: ['preview'] }); const names = getToolNames(await client.listTools()); - const numberOfTools = defaultTools.length + addRemoveTools.length + defaults.actors.length + betaTools.length; + const numberOfTools = defaultTools.length + addRemoveTools.length + defaults.actors.length + toolCategories.preview.length; expect(names.length).toEqual(numberOfTools); // Check that the Actor is not in the tools list expect(names).not.toContain(selectedToolName); @@ -192,9 +191,9 @@ export function createIntegrationTestsSuite( it('should not call Actor via call-actor tool if it is not added', async () => { const selectedToolName = actorNameToToolName(ACTOR_PYTHON_EXAMPLE); - const client = await createClientFn({ enableAddingActors: true, enableBeta: true }); + const client = await createClientFn({ enableAddingActors: true, tools: ['preview'] }); const names = getToolNames(await client.listTools()); - const numberOfTools = defaultTools.length + addRemoveTools.length + defaults.actors.length + betaTools.length; + const numberOfTools = defaultTools.length + addRemoveTools.length + defaults.actors.length + toolCategories.preview.length; expect(names.length).toEqual(numberOfTools); // Check that the Actor is not in the tools list expect(names).not.toContain(selectedToolName); @@ -394,16 +393,19 @@ export function createIntegrationTestsSuite( await client.close(); }); - it('should load correct tools for each feature tools key', async () => { - for (const key of Object.keys(featureTools)) { + it('should load correct tools for each category tools key', async () => { + for (const category of Object.keys(toolCategories)) { const client = await createClientFn({ - tools: [key as FeatureToolKey], + tools: [category as ToolCategory], }); const loadedTools = await client.listTools(); const toolNames = getToolNames(loadedTools); - const expectedTools = featureTools[key as FeatureToolKey]; + // If the category is enabled by default, it should not be loaded again, and its tools + // are accounted for in the default tools. + const isCategoryInDefault = toolCategoriesEnabledByDefault.includes(category as ToolCategory); + const expectedTools = isCategoryInDefault ? [] : toolCategories[category as ToolCategory]; const expectedToolNames = expectedTools.map((tool) => tool.tool.name); expect(toolNames.length).toEqual(expectedTools.length + defaultTools.length + defaults.actors.length + addRemoveTools.length); @@ -415,22 +417,32 @@ export function createIntegrationTestsSuite( } }); - it('should handle multiple feature keys input correctly', async () => { + it('should handle multiple tool category keys input correctly', async () => { + const categories = ['docs', 'runs', 'storage'] as ToolCategory[]; const client = await createClientFn({ - tools: ['docs', 'runs', 'storage'], + tools: categories, }); const loadedTools = await client.listTools(); const toolNames = getToolNames(loadedTools); const expectedTools = [ - ...featureTools.docs, - ...featureTools.runs, - ...featureTools.storage, + ...toolCategories.docs, + ...toolCategories.runs, + ...toolCategories.storage, ]; const expectedToolNames = expectedTools.map((tool) => tool.tool.name); - expect(toolNames.length).toEqual(expectedTools.length + defaultTools.length + defaults.actors.length + addRemoveTools.length); + // Handle case where tools are enabled by default + const selectedCategoriesInDefault = categories.filter((key) => toolCategoriesEnabledByDefault.includes(key)); + const numberOfToolsFromCategoriesInDefault = selectedCategoriesInDefault + .map((key) => toolCategories[key]) + .flat().length; + + const numberOfToolsExpected = defaultTools.length + defaults.actors.length + addRemoveTools.length + // Tools from tool categories minus the ones already in default tools + + (expectedTools.length - numberOfToolsFromCategoriesInDefault); + expect(toolNames.length).toEqual(numberOfToolsExpected); for (const expectedToolName of expectedToolNames) { expect(toolNames).toContain(expectedToolName); } diff --git a/tests/unit/input.test.ts b/tests/unit/input.test.ts index 21eec3d8..61ce0068 100644 --- a/tests/unit/input.test.ts +++ b/tests/unit/input.test.ts @@ -47,46 +47,6 @@ describe('processInput', () => { expect(processed.enableAddingActors).toBe(true); }); - it('should disable beta features by default', async () => { - const input: Partial = { - beta: undefined, - }; - const processed = processInput(input); - expect(processed.beta).toBe(false); - }); - - it('should disable beta features when beta is false', async () => { - const input: Partial = { - beta: false, - }; - const processed = processInput(input); - expect(processed.beta).toBe(false); - }); - - it('should disable beta when beta is "false"', async () => { - const input: Partial = { - beta: 'false', - }; - const processed = processInput(input); - expect(processed.beta).toBe(false); - }); - - it('should enable beta features when beta non empty string', async () => { - const input: Partial = { - beta: '1', - }; - const processed = processInput(input); - expect(processed.beta).toBe(true); - }); - - it('should enable beta features when beta is true', async () => { - const input: Partial = { - beta: true, - }; - const processed = processInput(input); - expect(processed.beta).toBe(true); - }); - it('should keep tools as array of valid featureTools keys', async () => { const input: Partial = { actors: ['actor1'], From f3edb872342ed296e2750bb5e0ddfb2d38767045 Mon Sep 17 00:00:00 2001 From: MQ Date: Thu, 17 Jul 2025 13:15:14 +0200 Subject: [PATCH 4/4] simplify building the internal tool map --- src/mcp/server.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 8e00aafd..73565dc6 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -25,7 +25,7 @@ import { } from '../const.js'; import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js'; import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js'; -import type { ActorMcpTool, ActorTool, HelperTool, ToolCategory, ToolEntry } from '../types.js'; +import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js'; import { connectMCPClient } from './client.js'; import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js'; import { processParamsGetTools } from './utils.js'; @@ -166,14 +166,11 @@ export class ActorsMcpServer { const loadedTools = this.listAllToolNames(); const actorsToLoad: string[] = []; const toolsToLoad: ToolEntry[] = []; - const internalToolMap = new Map([...defaultTools, ...addRemoveTools].map((tool) => [tool.tool.name, tool])); - // Add all category tools - for (const key of Object.keys(toolCategories)) { - const tools = toolCategories[key as ToolCategory]; - for (const tool of tools) { - internalToolMap.set(tool.tool.name, tool); - } - } + const internalToolMap = new Map([ + ...defaultTools, + ...addRemoveTools, + ...Object.values(toolCategories).flat(), + ].map((tool) => [tool.tool.name, tool])); for (const tool of toolNames) { // Skip if the tool is already loaded