Skip to content

Commit f3b2e4f

Browse files
authored
feat: add tool selection by feature param (#172)
* add tools param to select tools by feature * vibe reafactor tool loading logic from input (url and cli args) * rename to tool categories, include beta tools as separte preview category, remove beta flag param * simplify building the internal tool map
1 parent d993b10 commit f3b2e4f

File tree

12 files changed

+269
-135
lines changed

12 files changed

+269
-135
lines changed

src/actor/server.ts

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,9 @@ import log from '@apify/log';
1313

1414
import { ActorsMcpServer } from '../mcp/server.js';
1515
import { parseInputParamsFromUrl } from '../mcp/utils.js';
16-
import { getActorsAsTools } from '../tools/actor.js';
1716
import { getHelpMessage, HEADER_READINESS_PROBE, Routes } from './const.js';
1817
import { getActorRunData } from './utils.js';
1918

20-
/**
21-
* Helper function to load tools and actors based on input parameters
22-
* @param mcpServer The MCP server instance
23-
* @param url The request URL to parse parameters from
24-
* @param apifyToken The Apify token for authentication
25-
*/
26-
async function loadToolsAndActors(mcpServer: ActorsMcpServer, url: string, apifyToken: string): Promise<void> {
27-
const input = parseInputParamsFromUrl(url);
28-
if (input.actors || input.enableAddingActors) {
29-
await mcpServer.loadToolsFromUrl(url, apifyToken);
30-
}
31-
if (!input.actors) {
32-
await mcpServer.loadDefaultActors(apifyToken);
33-
}
34-
}
35-
3619
export function createExpressApp(
3720
host: string,
3821
mcpServerOptions: {
@@ -85,13 +68,21 @@ export function createExpressApp(
8568
try {
8669
log.info(`Received GET message at: ${Routes.SSE}`);
8770
const mcpServer = new ActorsMcpServer(mcpServerOptions, false);
88-
// Load tools from Actor input for backwards compatibility
89-
if (mcpServerOptions.actors && mcpServerOptions.actors.length > 0) {
90-
const tools = await getActorsAsTools(mcpServerOptions.actors, process.env.APIFY_TOKEN as string);
91-
mcpServer.upsertTools(tools);
92-
}
93-
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);
9471
const transport = new SSEServerTransport(Routes.MESSAGE, res);
72+
73+
// Load MCP server tools
74+
const apifyToken = process.env.APIFY_TOKEN as string;
75+
const input = parseInputParamsFromUrl(req.url);
76+
if (input.actors || input.enableAddingActors || input.tools) {
77+
log.debug('[SSE] Loading tools from URL', { sessionId: transport.sessionId });
78+
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
79+
}
80+
// Load default tools if no actors are specified
81+
if (!input.actors) {
82+
log.debug('[SSE] Loading default tools', { sessionId: transport.sessionId });
83+
await mcpServer.loadDefaultActors(apifyToken);
84+
}
85+
9586
transportsSSE[transport.sessionId] = transport;
9687
mcpServers[transport.sessionId] = mcpServer;
9788
await mcpServer.connect(transport);
@@ -164,13 +155,20 @@ export function createExpressApp(
164155
enableJsonResponse: false, // Use SSE response mode
165156
});
166157
const mcpServer = new ActorsMcpServer(mcpServerOptions, false);
167-
// Load tools from Actor input for backwards compatibility
168-
if (mcpServerOptions.actors && mcpServerOptions.actors.length > 0) {
169-
const tools = await getActorsAsTools(mcpServerOptions.actors, process.env.APIFY_TOKEN as string);
170-
mcpServer.upsertTools(tools);
171-
}
158+
172159
// Load MCP server tools
173-
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);
160+
const apifyToken = process.env.APIFY_TOKEN as string;
161+
const input = parseInputParamsFromUrl(req.url);
162+
if (input.actors || input.enableAddingActors || input.tools) {
163+
log.debug('[Streamable] Loading tools from URL', { sessionId: transport.sessionId });
164+
await mcpServer.loadToolsFromUrl(req.url, apifyToken);
165+
}
166+
// Load default tools if no actors are specified
167+
if (!input.actors) {
168+
log.debug('[Streamable] Loading default tools', { sessionId: transport.sessionId });
169+
await mcpServer.loadDefaultActors(apifyToken);
170+
}
171+
174172
// Connect the transport to the MCP server BEFORE handling the request
175173
await mcpServer.connect(transport);
176174

src/input.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import log from '@apify/log';
55

6-
import type { Input } from './types.js';
6+
import type { Input, ToolCategory } from './types.js';
77

88
/**
99
* Process input parameters, split Actors string into an array
@@ -30,7 +30,8 @@ export function processInput(originalInput: Partial<Input>): Input {
3030
input.enableAddingActors = input.enableAddingActors === true || input.enableAddingActors === 'true';
3131
}
3232

33-
// If beta present, set input.beta to true
34-
input.beta = input.beta !== undefined && (input.beta !== false && input.beta !== 'false');
33+
if (input.tools && typeof input.tools === 'string') {
34+
input.tools = input.tools.split(',').map((tool: string) => tool.trim()) as ToolCategory[];
35+
}
3536
return input;
3637
}

src/mcp/server.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
SERVER_NAME,
2424
SERVER_VERSION,
2525
} from '../const.js';
26-
import { addRemoveTools, betaTools, callActorGetDataset, defaultTools, getActorsAsTools } from '../tools/index.js';
26+
import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
2727
import { actorNameToToolName, decodeDotPropertyNames } from '../tools/utils.js';
2828
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
2929
import { connectMCPClient } from './client.js';
@@ -33,7 +33,6 @@ import { processParamsGetTools } from './utils.js';
3333
type ActorsMcpServerOptions = {
3434
enableAddingActors?: boolean;
3535
enableDefaultActors?: boolean;
36-
enableBeta?: boolean; // Enable beta features
3736
};
3837

3938
type ToolsChangedHandler = (toolNames: string[]) => void;
@@ -52,7 +51,6 @@ export class ActorsMcpServer {
5251
this.options = {
5352
enableAddingActors: options.enableAddingActors ?? true,
5453
enableDefaultActors: options.enableDefaultActors ?? true, // Default to true for backward compatibility
55-
enableBeta: options.enableBeta ?? false, // Disabled by default
5654
};
5755
this.server = new Server(
5856
{
@@ -78,10 +76,6 @@ export class ActorsMcpServer {
7876
this.enableDynamicActorTools();
7977
}
8078

81-
if (this.options.enableBeta) {
82-
this.upsertTools(betaTools, false);
83-
}
84-
8579
// Initialize automatically for backward compatibility
8680
this.initialize().catch((error) => {
8781
log.error('Failed to initialize server:', error);
@@ -172,7 +166,11 @@ export class ActorsMcpServer {
172166
const loadedTools = this.listAllToolNames();
173167
const actorsToLoad: string[] = [];
174168
const toolsToLoad: ToolEntry[] = [];
175-
const internalToolMap = new Map([...defaultTools, ...addRemoveTools, ...betaTools].map((tool) => [tool.tool.name, tool]));
169+
const internalToolMap = new Map([
170+
...defaultTools,
171+
...addRemoveTools,
172+
...Object.values(toolCategories).flat(),
173+
].map((tool) => [tool.tool.name, tool]));
176174

177175
for (const tool of toolNames) {
178176
// Skip if the tool is already loaded

src/mcp/utils.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { createHash } from 'node:crypto';
22
import { parse } from 'node:querystring';
33

44
import { processInput } from '../input.js';
5-
import { addRemoveTools, betaTools, getActorsAsTools } from '../tools/index.js';
6-
import type { Input, ToolEntry } from '../types.js';
5+
import type { Input } from '../types.js';
6+
import { loadToolsFromInput } from '../utils/tools-loader.js';
77
import { MAX_TOOL_NAME_LENGTH, SERVER_ID_LENGTH } from './const.js';
88

99
/**
@@ -34,26 +34,14 @@ export function getProxyMCPServerToolName(url: string, toolName: string): string
3434
}
3535

3636
/**
37-
* Process input parameters and get tools
37+
* Process input parameters from URL and get tools
3838
* If URL contains query parameter `actors`, return tools from Actors otherwise return null.
3939
* @param url
4040
* @param apifyToken
4141
*/
4242
export async function processParamsGetTools(url: string, apifyToken: string) {
4343
const input = parseInputParamsFromUrl(url);
44-
let tools: ToolEntry[] = [];
45-
if (input.actors) {
46-
const actors = input.actors as string[];
47-
// Normal Actors as a tool
48-
tools = await getActorsAsTools(actors, apifyToken);
49-
}
50-
if (input.enableAddingActors) {
51-
tools.push(...addRemoveTools);
52-
}
53-
if (input.beta) {
54-
tools.push(...betaTools);
55-
}
56-
return tools;
44+
return await loadToolsFromInput(input, apifyToken);
5745
}
5846

5947
export function parseInputParamsFromUrl(url: string): Input {

src/stdio.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import { hideBin } from 'yargs/helpers';
2222

2323
import log from '@apify/log';
2424

25-
import { defaults } from './const.js';
2625
import { ActorsMcpServer } from './mcp/server.js';
27-
import { getActorsAsTools } from './tools/index.js';
26+
import { toolCategories } from './tools/index.js';
27+
import type { Input, ToolCategory } from './types.js';
28+
import { loadToolsFromInput } from './utils/tools-loader.js';
2829

2930
// Keeping this interface here and not types.ts since
3031
// it is only relevant to the CLI/STDIO transport in this file
@@ -36,7 +37,8 @@ interface CliArgs {
3637
enableAddingActors: boolean;
3738
/** @deprecated */
3839
enableActorAutoLoading: boolean;
39-
beta: boolean;
40+
/** Tool categories to include */
41+
tools?: string;
4042
}
4143

4244
// Configure logging, set to ERROR
@@ -47,24 +49,35 @@ const argv = yargs(hideBin(process.argv))
4749
.usage('Usage: $0 [options]')
4850
.option('actors', {
4951
type: 'string',
50-
describe: 'Comma-separated list of Actor full names to add to the server',
52+
describe: 'Comma-separated list of Actor full names to add to the server.',
5153
example: 'apify/google-search-scraper,apify/instagram-scraper',
5254
})
5355
.option('enable-adding-actors', {
5456
type: 'boolean',
5557
default: true,
56-
describe: 'Enable dynamically adding Actors as tools based on user requests',
58+
describe: 'Enable dynamically adding Actors as tools based on user requests.',
5759
})
5860
.option('enableActorAutoLoading', {
5961
type: 'boolean',
6062
default: true,
6163
hidden: true,
62-
describe: 'Deprecated: use enable-adding-actors instead',
64+
describe: 'Deprecated: use enable-adding-actors instead.',
6365
})
64-
.option('beta', {
65-
type: 'boolean',
66-
default: false,
67-
describe: 'Enable beta features',
66+
.options('tools', {
67+
type: 'string',
68+
describe: `Comma-separated list of specific tool categories to enable.
69+
70+
Available choices: ${Object.keys(toolCategories).join(', ')}
71+
72+
Tool categories are as follows:
73+
- docs: Search and fetch Apify documentation tools.
74+
- runs: Get Actor runs list, run details, and logs from a specific Actor run.
75+
- storage: Access datasets, key-value stores, and their records.
76+
- preview: Experimental tools in preview mode.
77+
78+
Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default.
79+
`,
80+
example: 'docs,runs,storage',
6881
})
6982
.help('help')
7083
.alias('h', 'help')
@@ -73,13 +86,14 @@ const argv = yargs(hideBin(process.argv))
7386
'To connect, set your MCP client server command to `npx @apify/actors-mcp-server`'
7487
+ ' and set the environment variable `APIFY_TOKEN` to your Apify API token.\n',
7588
)
76-
.epilogue('For more information, visit https://github.com/apify/actors-mcp-server')
89+
.epilogue('For more information, visit https://mcp.apify.com or https://github.com/apify/actors-mcp-server')
7790
.parseSync() as CliArgs;
7891

7992
const enableAddingActors = argv.enableAddingActors && argv.enableActorAutoLoading;
8093
const actors = argv.actors as string || '';
8194
const actorList = actors ? actors.split(',').map((a: string) => a.trim()) : [];
82-
const enableBeta = argv.beta;
95+
// Keys of the tool categories to enable
96+
const toolCategoryKeys = argv.tools ? argv.tools.split(',').map((t: string) => t.trim()) : [];
8397

8498
// Validate environment
8599
if (!process.env.APIFY_TOKEN) {
@@ -88,8 +102,18 @@ if (!process.env.APIFY_TOKEN) {
88102
}
89103

90104
async function main() {
91-
const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false, enableBeta });
92-
const tools = await getActorsAsTools(actorList.length ? actorList : defaults.actors, process.env.APIFY_TOKEN as string);
105+
const mcpServer = new ActorsMcpServer({ enableAddingActors, enableDefaultActors: false });
106+
107+
// Create an Input object from CLI arguments
108+
const input: Input = {
109+
actors: actorList.length ? actorList : [],
110+
enableAddingActors,
111+
tools: toolCategoryKeys as ToolCategory[],
112+
};
113+
114+
// Use the shared tools loading logic
115+
const tools = await loadToolsFromInput(input, process.env.APIFY_TOKEN as string, actorList.length === 0);
116+
93117
mcpServer.upsertTools(tools);
94118

95119
// Start server

src/tools/index.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,58 @@
11
// Import specific tools that are being used
2+
import type { ToolCategory } from '../types.js';
23
import { callActor, callActorGetDataset, getActorsAsTools } from './actor.js';
4+
import { getDataset, getDatasetItems } from './dataset.js';
5+
import { getUserDatasetsList } from './dataset_collection.js';
36
import { fetchApifyDocsTool } from './fetch-apify-docs.js';
47
import { getActorDetailsTool } from './get-actor-details.js';
5-
import { addTool, helpTool } from './helpers.js';
8+
import { addTool } from './helpers.js';
9+
import { getKeyValueStore, getKeyValueStoreKeys, getKeyValueStoreRecord } from './key_value_store.js';
10+
import { getUserKeyValueStoresList } from './key_value_store_collection.js';
11+
import { getActorRun, getActorRunLog } from './run.js';
12+
import { getUserRunsList } from './run_collection.js';
613
import { searchApifyDocsTool } from './search-apify-docs.js';
714
import { searchActors } from './store_collection.js';
815

16+
export const toolCategories = {
17+
docs: [
18+
searchApifyDocsTool,
19+
fetchApifyDocsTool,
20+
],
21+
runs: [
22+
getActorRun,
23+
getUserRunsList,
24+
getActorRunLog,
25+
],
26+
storage: [
27+
getDataset,
28+
getDatasetItems,
29+
getKeyValueStore,
30+
getKeyValueStoreKeys,
31+
getKeyValueStoreRecord,
32+
getUserDatasetsList,
33+
getUserKeyValueStoresList,
34+
],
35+
preview: [
36+
callActor,
37+
],
38+
};
39+
export const toolCategoriesEnabledByDefault: ToolCategory[] = [
40+
'docs',
41+
];
42+
943
export const defaultTools = [
10-
// abortActorRun,
11-
// actorDetailsTool,
12-
// getActor,
13-
// getActorLog,
14-
// getActorRun,
15-
// getDataset,
16-
// getDatasetItems,
17-
// getKeyValueStore,
18-
// getKeyValueStoreKeys,
19-
// getKeyValueStoreRecord,
20-
// getUserRunsList,
21-
// getUserDatasetsList,
22-
// getUserKeyValueStoresList,
2344
getActorDetailsTool,
24-
helpTool,
2545
searchActors,
26-
searchApifyDocsTool,
27-
fetchApifyDocsTool,
28-
];
29-
30-
export const betaTools = [
31-
callActor,
46+
// Add the tools from the enabled categories
47+
...toolCategoriesEnabledByDefault.map((key) => toolCategories[key]).flat(),
3248
];
3349

3450
export const addRemoveTools = [
3551
addTool,
36-
// removeTool,
3752
];
3853

3954
// Export only the tools that are being used
4055
export {
41-
addTool,
42-
// removeTool,
4356
getActorsAsTools,
4457
callActorGetDataset,
4558
};

src/tools/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const GetRunLogArgs = z.object({
5959
* https://docs.apify.com/api/v2/actor-run-get
6060
* /v2/actor-runs/{runId}/log{?token}
6161
*/
62-
export const getActorLog: ToolEntry = {
62+
export const getActorRunLog: ToolEntry = {
6363
type: 'internal',
6464
tool: {
6565
name: HelperTools.ACTOR_RUNS_LOG,

0 commit comments

Comments
 (0)