Skip to content

Commit 1d69ad2

Browse files
committed
fix default tool loading for Actors MCP server, more integration tests
1 parent bcab5fb commit 1d69ad2

File tree

3 files changed

+249
-9
lines changed

3 files changed

+249
-9
lines changed

src/actor/server.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import express from 'express';
1212
import log from '@apify/log';
1313

1414
import { type ActorsMcpServer } from '../mcp/server.js';
15-
import { processParamsGetTools } from '../mcp/utils.js';
16-
import { addTool, removeTool } from '../tools/helpers.js';
15+
import { parseInputParamsFromUrl, processParamsGetTools } from '../mcp/utils.js';
1716
import { getHelpMessage, HEADER_READINESS_PROBE, Routes } from './const.js';
1817
import { getActorRunData } from './utils.js';
1918

@@ -47,6 +46,7 @@ export function createExpressApp(
4746
}
4847
try {
4948
log.info(`Received GET message at: ${Routes.ROOT}`);
49+
// TODO: I think we should remove this logic, root should return only help message
5050
const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string);
5151
if (tools) {
5252
mcpServer.updateTools(tools);
@@ -67,13 +67,12 @@ export function createExpressApp(
6767
app.get(Routes.SSE, async (req: Request, res: Response) => {
6868
try {
6969
log.info(`Received GET message at: ${Routes.SSE}`);
70-
const tools = await processParamsGetTools(req.url, process.env.APIFY_TOKEN as string);
71-
if (tools.length > 0) {
72-
mcpServer.updateTools(tools);
70+
const input = parseInputParamsFromUrl(req.url);
71+
if (input.actors || input.enableAddingActors) {
72+
await mcpServer.loadToolsFromUrl(req.url, process.env.APIFY_TOKEN as string);
7373
}
74-
// TODO fix this - we should not be loading default tools here or provide more generic way
75-
if (tools.length === 2 && tools.includes(addTool) && tools.includes(removeTool)) {
76-
// We are loading default Actors (if not specified otherwise), so that we don't have "empty" tools
74+
// Load default tools if no actors are specified
75+
if (!input.actors) {
7776
await mcpServer.loadDefaultTools(process.env.APIFY_TOKEN as string);
7877
}
7978
transportSSE = new SSEServerTransport(Routes.MESSAGE, res);

tests/integration/actor-server-test.ts

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,66 @@
11
import type { Server as HttpServer } from 'node:http';
22

3+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
35
import type { Express } from 'express';
46
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
57

68
import log from '@apify/log';
79

810
import { createExpressApp } from '../../src/actor/server.js';
9-
import { HelperTools } from '../../src/const.js';
11+
import { defaults, HelperTools } from '../../src/const.js';
1012
import { ActorsMcpServer } from '../../src/mcp/server.js';
13+
import { actorNameToToolName } from '../../src/tools/utils.js';
14+
15+
async function createMCPClient(
16+
serverUrl: string,
17+
options?: {
18+
actors?: string[];
19+
enableAddingActors?: boolean;
20+
},
21+
): Promise<Client> {
22+
if (!process.env.APIFY_TOKEN) {
23+
throw new Error('APIFY_TOKEN environment variable is not set.');
24+
}
25+
const url = new URL(serverUrl);
26+
const { actors, enableAddingActors } = options || {};
27+
if (actors) {
28+
url.searchParams.append('actors', actors.join(','));
29+
}
30+
if (enableAddingActors) {
31+
url.searchParams.append('enableAddingActors', 'true');
32+
}
33+
34+
const transport = new SSEClientTransport(
35+
url,
36+
{
37+
requestInit: {
38+
headers: {
39+
authorization: `Bearer ${process.env.APIFY_TOKEN}`,
40+
},
41+
},
42+
eventSourceInit: {
43+
// The EventSource package augments EventSourceInit with a "fetch" parameter.
44+
// You can use this to set additional headers on the outgoing request.
45+
// Based on this example: https://github.com/modelcontextprotocol/typescript-sdk/issues/118
46+
async fetch(input: Request | URL | string, init?: RequestInit) {
47+
const headers = new Headers(init?.headers || {});
48+
headers.set('authorization', `Bearer ${process.env.APIFY_TOKEN}`);
49+
return fetch(input, { ...init, headers });
50+
},
51+
// We have to cast to "any" to use it, since it's non-standard
52+
} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
53+
},
54+
);
55+
56+
const client = new Client({
57+
name: 'sse-client',
58+
version: '1.0.0',
59+
});
60+
await client.connect(transport);
61+
62+
return client;
63+
}
1164

1265
describe('Actors MCP Server', {
1366
concurrent: false, // Run test serially to prevent port already in use
@@ -67,4 +120,163 @@ describe('Actors MCP Server', {
67120
HelperTools.REMOVE_ACTOR,
68121
]);
69122
});
123+
124+
it('default tools list', async () => {
125+
const client = await createMCPClient(`${testHost}/sse`);
126+
127+
const tools = await client.listTools();
128+
const names = tools.tools.map((tool) => tool.name);
129+
expect(names.length).toEqual(defaults.helperTools.length + defaults.actors.length);
130+
for (const tool of defaults.helperTools) {
131+
expect(names).toContain(tool);
132+
}
133+
for (const actorTool of defaults.actors) {
134+
expect(names).toContain(actorNameToToolName(actorTool));
135+
}
136+
137+
await client.close();
138+
});
139+
140+
it('use only specific Actor and call it', async () => {
141+
const actorName = 'apify/python-example';
142+
const selectedToolName = actorNameToToolName(actorName);
143+
const client = await createMCPClient(`${testHost}/sse`, {
144+
actors: [actorName],
145+
enableAddingActors: false,
146+
});
147+
148+
const tools = await client.listTools();
149+
const names = tools.tools.map((tool) => tool.name);
150+
expect(names.length).toEqual(defaults.helperTools.length + 1);
151+
for (const tool of defaults.helperTools) {
152+
expect(names).toContain(tool);
153+
}
154+
expect(names).toContain(selectedToolName);
155+
156+
const result = await client.callTool({
157+
name: selectedToolName,
158+
arguments: {
159+
first_number: 1,
160+
second_number: 2,
161+
},
162+
});
163+
164+
expect(result).toEqual({
165+
content: [{
166+
text: JSON.stringify({
167+
first_number: 1,
168+
second_number: 2,
169+
sum: 3,
170+
}),
171+
type: 'text',
172+
}],
173+
});
174+
175+
await client.close();
176+
});
177+
178+
it('load Actors from parameters via SSE client', async () => {
179+
const actors = ['apify/rag-web-browser', 'apify/instagram-scraper'];
180+
const client = await createMCPClient(`${testHost}/sse`, {
181+
actors,
182+
enableAddingActors: false,
183+
});
184+
185+
const tools = await client.listTools();
186+
const names = tools.tools.map((tool) => tool.name);
187+
expect(names.length).toEqual(defaults.helperTools.length + actors.length);
188+
for (const tool of defaults.helperTools) {
189+
expect(names).toContain(tool);
190+
}
191+
for (const actor of actors) {
192+
expect(names).toContain(actorNameToToolName(actor));
193+
}
194+
195+
await client.close();
196+
});
197+
198+
it('load Actor dynamically and call it', async () => {
199+
const actor = 'apify/python-example';
200+
const selectedToolName = actorNameToToolName(actor);
201+
const client = await createMCPClient(`${testHost}/sse`, {
202+
enableAddingActors: true,
203+
});
204+
205+
const tools = await client.listTools();
206+
const names = tools.tools.map((tool) => tool.name);
207+
expect(names.length).toEqual(defaults.helperTools.length + defaults.actorAddingTools.length + defaults.actors.length);
208+
for (const tool of defaults.helperTools) {
209+
expect(names).toContain(tool);
210+
}
211+
for (const tool of defaults.actorAddingTools) {
212+
expect(names).toContain(tool);
213+
}
214+
for (const actorTool of defaults.actors) {
215+
expect(names).toContain(actorNameToToolName(actorTool));
216+
}
217+
218+
// Add Actor dynamically
219+
await client.callTool({
220+
name: HelperTools.ADD_ACTOR,
221+
arguments: {
222+
actorName: actor,
223+
},
224+
});
225+
226+
// Check if tools was added
227+
const toolsAfterAdd = await client.listTools();
228+
const namesAfterAdd = toolsAfterAdd.tools.map((tool) => tool.name);
229+
expect(namesAfterAdd.length).toEqual(defaults.helperTools.length + defaults.actorAddingTools.length + defaults.actors.length + 1);
230+
expect(namesAfterAdd).toContain(selectedToolName);
231+
232+
const result = await client.callTool({
233+
name: selectedToolName,
234+
arguments: {
235+
first_number: 1,
236+
second_number: 2,
237+
},
238+
});
239+
240+
expect(result).toEqual({
241+
content: [{
242+
text: JSON.stringify({
243+
first_number: 1,
244+
second_number: 2,
245+
sum: 3,
246+
}),
247+
type: 'text',
248+
}],
249+
});
250+
251+
await client.close();
252+
});
253+
254+
it('should remove Actor from tools list', async () => {
255+
const actor = 'apify/python-example';
256+
const selectedToolName = actorNameToToolName(actor);
257+
const client = await createMCPClient(`${testHost}/sse`, {
258+
actors: [actor],
259+
enableAddingActors: true,
260+
});
261+
262+
// Verify actor is in the tools list
263+
const toolsBefore = await client.listTools();
264+
const namesBefore = toolsBefore.tools.map((tool) => tool.name);
265+
expect(namesBefore).toContain(selectedToolName);
266+
267+
// Remove the actor
268+
await client.callTool({
269+
name: HelperTools.REMOVE_ACTOR,
270+
arguments: {
271+
toolName: selectedToolName,
272+
},
273+
});
274+
275+
// Verify actor is removed
276+
const toolsAfter = await client.listTools();
277+
const namesAfter = toolsAfter.tools.map((tool) => tool.name);
278+
expect(namesAfter).not.toContain(selectedToolName);
279+
280+
await client.close();
281+
});
70282
});

tests/integration/stdio.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,33 @@ describe('MCP STDIO', () => {
164164

165165
await client.close();
166166
});
167+
168+
it('should remove Actor from tools list', async () => {
169+
const actor = 'apify/python-example';
170+
const selectedToolName = actorNameToToolName(actor);
171+
const client = await createMCPClient({
172+
actors: [actor],
173+
enableAddingActors: true,
174+
});
175+
176+
// Verify actor is in the tools list
177+
const toolsBefore = await client.listTools();
178+
const namesBefore = toolsBefore.tools.map((tool) => tool.name);
179+
expect(namesBefore).toContain(selectedToolName);
180+
181+
// Remove the actor
182+
await client.callTool({
183+
name: HelperTools.REMOVE_ACTOR,
184+
arguments: {
185+
toolName: selectedToolName,
186+
},
187+
});
188+
189+
// Verify actor is removed
190+
const toolsAfter = await client.listTools();
191+
const namesAfter = toolsAfter.tools.map((tool) => tool.name);
192+
expect(namesAfter).not.toContain(selectedToolName);
193+
194+
await client.close();
195+
});
167196
});

0 commit comments

Comments
 (0)