Skip to content

Commit abc22ca

Browse files
committed
feat: vibe new mcp input spec
1 parent 0bb8397 commit abc22ca

File tree

12 files changed

+262
-81
lines changed

12 files changed

+262
-81
lines changed

src/index-internals.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import { defaults, HelperTools } from './const.js';
66
import { parseInputParamsFromUrl, processParamsGetTools } from './mcp/utils.js';
7-
import { addRemoveTools, defaultTools, getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from './tools/index.js';
7+
import { addTool } from './tools/helpers.js';
8+
import { defaultTools, getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from './tools/index.js';
89
import { actorNameToToolName } from './tools/utils.js';
910
import type { ToolCategory } from './types.js';
1011
import { getToolPublicFieldOnly } from './utils/tools.js';
@@ -15,7 +16,7 @@ export {
1516
HelperTools,
1617
defaults,
1718
defaultTools,
18-
addRemoveTools,
19+
addTool,
1920
toolCategories,
2021
toolCategoriesEnabledByDefault,
2122
type ToolCategory,

src/input.ts

Lines changed: 16 additions & 2 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, ToolCategory } from './types.js';
6+
import type { Input, ToolSelector } from './types.js';
77

88
/**
99
* Process input parameters, split Actors string into an array
@@ -43,7 +43,21 @@ export function processInput(originalInput: Partial<Input>): Input {
4343
/**
4444
* Filter out empty strings just in case.
4545
*/
46-
input.tools = input.tools.split(',').map((tool: string) => tool.trim()).filter((tool) => tool !== '') as ToolCategory[];
46+
input.tools = input.tools.split(',').map((tool: string) => tool.trim()).filter((tool) => tool !== '') as ToolSelector[];
47+
}
48+
// Normalize explicit empty string to empty array (signals no internal tools)
49+
if (input.tools === '') {
50+
input.tools = [] as unknown as ToolSelector[];
51+
}
52+
53+
// Backward compatibility: if tools is explicitly specified, merge also actors into tools selectors
54+
// This keeps previous semantics when tools is undefined (defaults categories apply).
55+
if (input.tools !== undefined && Array.isArray(input.actors) && input.actors.length > 0) {
56+
let currentTools: ToolSelector[] = [];
57+
if (input.tools !== undefined) {
58+
currentTools = Array.isArray(input.tools) ? input.tools : [input.tools as ToolSelector];
59+
}
60+
input.tools = [...currentTools, ...input.actors] as ToolSelector[];
4761
}
4862
return input;
4963
}

src/mcp/server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
SERVER_VERSION,
2626
} from '../const.js';
2727
import { prompts } from '../prompts/index.js';
28-
import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
28+
import { addTool } from '../tools/helpers.js';
29+
import { callActorGetDataset, defaultTools, getActorsAsTools, toolCategories } from '../tools/index.js';
2930
import { decodeDotPropertyNames } from '../tools/utils.js';
3031
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
3132
import { createProgressTracker } from '../utils/progress.js';
@@ -151,7 +152,7 @@ export class ActorsMcpServer {
151152
const toolsToLoad: ToolEntry[] = [];
152153
const internalToolMap = new Map([
153154
...defaultTools,
154-
...addRemoveTools,
155+
addTool,
155156
...Object.values(toolCategories).flat(),
156157
].map((tool) => [tool.tool.name, tool]));
157158

src/stdio.ts

Lines changed: 9 additions & 4 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 { processInput } from './input.js';
2526
import { ActorsMcpServer } from './mcp/server.js';
2627
import { toolCategories } from './tools/index.js';
27-
import type { Input, ToolCategory } from './types.js';
28+
import type { Input, ToolSelector } from './types.js';
2829
import { loadToolsFromInput } from './utils/tools-loader.js';
2930

3031
// Keeping this interface here and not types.ts since
@@ -70,10 +71,11 @@ const argv = yargs(hideBin(process.argv))
7071
Available choices: ${Object.keys(toolCategories).join(', ')}
7172
7273
Tool categories are as follows:
74+
- actors: Actor discovery and calling utilities.
7375
- docs: Search and fetch Apify documentation tools.
7476
- runs: Get Actor runs list, run details, and logs from a specific Actor run.
7577
- storage: Access datasets, key-value stores, and their records.
76-
- preview: Experimental tools in preview mode.
78+
- experimental: Experimental tools in preview mode.
7779
7880
Note: Tools that enable you to search Actors from the Apify Store and get their details are always enabled by default.
7981
`,
@@ -120,11 +122,14 @@ async function main() {
120122
const input: Input = {
121123
actors: actorList,
122124
enableAddingActors,
123-
tools: toolCategoryKeys as ToolCategory[],
125+
tools: toolCategoryKeys as ToolSelector[],
124126
};
125127

128+
// Normalize (merges actors into tools for backward compatibility)
129+
const normalized = processInput(input);
130+
126131
// Use the shared tools loading logic
127-
const tools = await loadToolsFromInput(input, process.env.APIFY_TOKEN as string);
132+
const tools = await loadToolsFromInput(normalized, process.env.APIFY_TOKEN as string);
128133

129134
mcpServer.upsertTools(tools);
130135

src/tools/index.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import { searchApifyDocsTool } from './search-apify-docs.js';
1515
import { searchActors } from './store_collection.js';
1616

1717
export const toolCategories = {
18-
'actor-discovery': [
18+
experimental: [
19+
addTool,
20+
],
21+
actors: [
1922
getActorDetailsTool,
2023
searchActors,
21-
/**
22-
* TODO: we should add the add-actor tool here but we would need to change the configuraton
23-
* interface around the ?enableAddingActors
24-
*/
24+
callActor,
2525
],
2626
docs: [
2727
searchApifyDocsTool,
@@ -42,24 +42,14 @@ export const toolCategories = {
4242
getUserDatasetsList,
4343
getUserKeyValueStoresList,
4444
],
45-
preview: [
46-
callActor,
47-
],
4845
};
4946
export const toolCategoriesEnabledByDefault: ToolCategory[] = [
50-
'actor-discovery',
47+
'actors',
5148
'docs',
5249
];
5350

5451
export const defaultTools = getExpectedToolsByCategories(toolCategoriesEnabledByDefault);
5552

56-
/**
57-
* Tools related to `enableAddingActors` param for dynamic Actor adding.
58-
*/
59-
export const addRemoveTools = [
60-
addTool,
61-
];
62-
6353
// Export only the tools that are being used
6454
export {
6555
getActorsAsTools,

src/types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ export interface InternalTool extends ToolBase {
211211
}
212212

213213
export type ToolCategory = keyof typeof toolCategories;
214+
/**
215+
* Selector for tools input - can be a category key or a specific tool name.
216+
*/
217+
export type ToolSelector = ToolCategory | string;
214218

215219
export type Input = {
216220
/**
@@ -228,12 +232,12 @@ export type Input = {
228232
debugActor?: string;
229233
debugActorInput?: unknown;
230234
/**
231-
* Tool categories to include
232-
* When `tools` is undefined that means the default tools categories should be loaded.
233-
* If it as empty string or empty array then no tools should be loaded.
234-
* Otherwise the specified tools categories should be loaded.
235+
* Tool selectors to include (category keys or concrete tool names).
236+
* When `tools` is undefined that means the default tool categories should be loaded.
237+
* If it is an empty string or empty array then no internal tools should be loaded.
238+
* Otherwise the specified categories and/or concrete tool names should be loaded.
235239
*/
236-
tools?: ToolCategory[] | string;
240+
tools?: ToolSelector[] | string;
237241
};
238242

239243
// Utility type to get a union of values from an object type

src/utils/tools-loader.ts

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*/
55

66
import { defaults } from '../const.js';
7-
import { addRemoveTools, getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
7+
import { addTool } from '../tools/helpers.js';
8+
import { getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
89
import type { Input, ToolCategory, ToolEntry } from '../types.js';
910
import { getExpectedToolsByCategories } from './tools.js';
1011

@@ -22,30 +23,100 @@ export async function loadToolsFromInput(
2223
): Promise<ToolEntry[]> {
2324
let tools: ToolEntry[] = [];
2425

25-
// Load actors as tools
26-
if (input.actors !== undefined) {
27-
const actors = Array.isArray(input.actors) ? input.actors : [input.actors];
28-
tools = await getActorsAsTools(actors, apifyToken);
26+
// Prepare lists for actor and internal tool/category selectors from `tools`
27+
let toolSelectors: (string | ToolCategory)[] | undefined;
28+
if (input.tools === undefined) {
29+
toolSelectors = undefined;
30+
} else if (Array.isArray(input.tools)) {
31+
toolSelectors = input.tools.filter((s) => String(s).trim() !== '');
2932
} else {
30-
// Use default actors if no actors are specified
31-
tools = await getActorsAsTools(defaults.actors, apifyToken);
33+
toolSelectors = [input.tools].filter((s) => String(s).trim() !== '');
3234
}
3335

34-
// Add tools for adding/removing actors if enabled
35-
if (input.enableAddingActors) {
36-
tools.push(...addRemoveTools);
36+
// Build a name -> tool entry map for all known internal (category) tools
37+
const allCategoryTools: ToolEntry[] = getExpectedToolsByCategories(Object.keys(toolCategories) as ToolCategory[]);
38+
const toolNameMap = new Map<string, ToolEntry>();
39+
for (const entry of allCategoryTools) {
40+
toolNameMap.set(entry.tool.name, entry);
3741
}
3842

39-
// Add tools from enabled categories
40-
if (input.tools !== undefined) {
41-
const toolKeys = Array.isArray(input.tools) ? input.tools : [input.tools];
42-
for (const toolKey of toolKeys) {
43-
const keyTools = toolCategories[toolKey as ToolCategory] || [];
44-
tools.push(...keyTools);
43+
// Classify selectors from `tools` into categories/internal tools and actor names
44+
const internalCategoryEntries: ToolEntry[] = [];
45+
const actorSelectorsFromTools: string[] = [];
46+
if (toolSelectors !== undefined) {
47+
for (const selector of toolSelectors) {
48+
const categoryTools = toolCategories[selector as ToolCategory];
49+
if (categoryTools && Array.isArray(categoryTools)) {
50+
internalCategoryEntries.push(...categoryTools);
51+
continue;
52+
}
53+
const internalByName = toolNameMap.get(String(selector));
54+
if (internalByName) {
55+
internalCategoryEntries.push(internalByName);
56+
continue;
57+
}
58+
// Treat unknown selectors as Actor IDs/full names
59+
actorSelectorsFromTools.push(String(selector));
4560
}
61+
}
62+
63+
// Resolve actor list to load
64+
let actorsFromInputField: string[] | undefined;
65+
if (input.actors === undefined) {
66+
actorsFromInputField = undefined; // use defaults later unless overridden by tools
67+
} else if (Array.isArray(input.actors)) {
68+
actorsFromInputField = input.actors;
69+
} else {
70+
actorsFromInputField = [input.actors];
71+
}
72+
73+
let actorNamesToLoad: string[] = [];
74+
if (actorsFromInputField !== undefined) {
75+
actorNamesToLoad = actorsFromInputField;
76+
} else if (actorSelectorsFromTools.length > 0) {
77+
// If no explicit `actors` were provided, but `tools` includes actor names,
78+
// load exactly those instead of defaults
79+
actorNamesToLoad = actorSelectorsFromTools;
80+
} else {
81+
// Use default actors if nothing specified anywhere
82+
actorNamesToLoad = defaults.actors;
83+
}
84+
85+
// If both fields specify actors, merge them
86+
if (actorsFromInputField !== undefined && actorSelectorsFromTools.length > 0) {
87+
const merged = new Set<string>([...actorNamesToLoad, ...actorSelectorsFromTools]);
88+
actorNamesToLoad = Array.from(merged);
89+
}
90+
91+
// Load actor tools (if any)
92+
if (actorNamesToLoad.length > 0) {
93+
tools = await getActorsAsTools(actorNamesToLoad, apifyToken);
94+
}
95+
96+
// Add tool for dynamically adding actors if enabled
97+
if (input.enableAddingActors) {
98+
tools.push(addTool);
99+
}
100+
101+
// Add internal tools from categories/tool names or defaults when `tools` unspecified
102+
if (toolSelectors !== undefined) {
103+
// Respect disable flag: do not include add-actor even if explicitly requested
104+
const filteredInternal = input.enableAddingActors
105+
? internalCategoryEntries
106+
: internalCategoryEntries.filter((entry) => entry.tool.name !== addTool.tool.name);
107+
tools.push(...filteredInternal);
46108
} else {
47109
tools.push(...getExpectedToolsByCategories(toolCategoriesEnabledByDefault));
48110
}
49111

112+
// De-duplicate by tool name
113+
const seen = new Set<string>();
114+
tools = tools.filter((entry) => {
115+
const { name } = entry.tool;
116+
if (seen.has(name)) return false;
117+
seen.add(name);
118+
return true;
119+
});
120+
50121
return tools;
51122
}

tests/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { toolCategoriesEnabledByDefault } from '../dist/index-internals.js';
21
import { defaults } from '../src/const.js';
2+
import { toolCategoriesEnabledByDefault } from '../src/tools/index.js';
33
import { actorNameToToolName } from '../src/tools/utils.js';
44
import { getExpectedToolNamesByCategories } from '../src/utils/tools.js';
55

tests/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ToolCategory } from '../src/types.js';
1010
export interface McpClientOptions {
1111
actors?: string[];
1212
enableAddingActors?: boolean;
13-
tools?: ToolCategory[]; // Tool categories to include
13+
tools?: (ToolCategory | string)[]; // Tool categories or specific tool names to include
1414
}
1515

1616
export async function createMcpSseClient(

tests/integration/internals.test.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import log from '@apify/log';
55
import { actorNameToToolName } from '../../dist/tools/utils.js';
66
import { defaults } from '../../src/const.js';
77
import { ActorsMcpServer } from '../../src/index.js';
8-
import { addRemoveTools, defaultTools, getActorsAsTools } from '../../src/tools/index.js';
8+
import { addTool } from '../../src/tools/helpers.js';
9+
import { defaultTools, getActorsAsTools } from '../../src/tools/index.js';
910
import type { Input } from '../../src/types.js';
1011
import { loadToolsFromInput } from '../../src/utils/tools-loader.js';
1112
import { ACTOR_PYTHON_EXAMPLE } from '../const.js';
@@ -31,7 +32,7 @@ describe('MCP server internals integration tests', () => {
3132
const names = actorsMcpServer.listAllToolNames();
3233
const expectedToolNames = [
3334
...defaults.actors,
34-
...addRemoveTools.map((tool) => tool.tool.name),
35+
addTool.tool.name,
3536
...defaultTools.map((tool) => tool.tool.name),
3637
ACTOR_PYTHON_EXAMPLE,
3738
];
@@ -50,7 +51,7 @@ describe('MCP server internals integration tests', () => {
5051

5152
it('should notify tools changed handler on tool modifications', async () => {
5253
let latestTools: string[] = [];
53-
const numberOfTools = addRemoveTools.length + defaults.actors.length + defaultTools.length;
54+
const numberOfTools = 1 + defaults.actors.length + defaultTools.length;
5455

5556
let toolNotificationCount = 0;
5657
const onToolsChanged = (tools: string[]) => {
@@ -72,9 +73,7 @@ describe('MCP server internals integration tests', () => {
7273
expect(toolNotificationCount).toBe(1);
7374
expect(latestTools.length).toBe(numberOfTools + 1);
7475
expect(latestTools).toContain(actor);
75-
for (const tool of [...addRemoveTools]) {
76-
expect(latestTools).toContain(tool.tool.name);
77-
}
76+
expect(latestTools).toContain(addTool.tool.name);
7877
for (const tool of defaults.actors) {
7978
expect(latestTools).toContain(tool);
8079
}
@@ -86,9 +85,7 @@ describe('MCP server internals integration tests', () => {
8685
expect(toolNotificationCount).toBe(2);
8786
expect(latestTools.length).toBe(numberOfTools);
8887
expect(latestTools).not.toContain(actor);
89-
for (const tool of [...addRemoveTools]) {
90-
expect(latestTools).toContain(tool.tool.name);
91-
}
88+
expect(latestTools).toContain(addTool.tool.name);
9289
for (const tool of defaults.actors) {
9390
expect(latestTools).toContain(tool);
9491
}
@@ -97,7 +94,7 @@ describe('MCP server internals integration tests', () => {
9794
it('should stop notifying after unregistering tools changed handler', async () => {
9895
let latestTools: string[] = [];
9996
let notificationCount = 0;
100-
const numberOfTools = addRemoveTools.length + defaults.actors.length + defaultTools.length;
97+
const numberOfTools = 1 + defaults.actors.length + defaultTools.length;
10198
const onToolsChanged = (tools: string[]) => {
10299
latestTools = tools;
103100
notificationCount++;

0 commit comments

Comments
 (0)