Skip to content

Commit bec6dfa

Browse files
committed
add actorized mcp servers integration test, internal mcp server mcp client add support for streamable - use it first then fallback to legacy sse
1 parent 4838e85 commit bec6dfa

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed

src/mcp/client.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4+
5+
import log from '@apify/log';
36

47
import { getMCPServerID } from './utils.js';
58

9+
/**
10+
* Creates and connects a ModelContextProtocol client.
11+
* First tries streamable HTTP transport, then falls back to SSE transport.
12+
*/
13+
export async function connectMCPClient(
14+
url: string, token: string,
15+
): Promise<Client> {
16+
try {
17+
return await createMCPStreamableClient(url, token);
18+
} catch {
19+
// If streamable HTTP transport fails, fall back to SSE transport
20+
log.info('Streamable HTTP transport failed, falling back to SSE transport', {
21+
url,
22+
});
23+
return await createMCPSSEClient(url, token);
24+
}
25+
}
26+
627
/**
728
* Creates and connects a ModelContextProtocol client.
829
*/
9-
export async function createMCPClient(
30+
async function createMCPSSEClient(
1031
url: string, token: string,
1132
): Promise<Client> {
1233
const transport = new SSEClientTransport(
@@ -39,3 +60,29 @@ export async function createMCPClient(
3960

4061
return client;
4162
}
63+
64+
/**
65+
* Creates and connects a ModelContextProtocol client using the streamable HTTP transport.
66+
*/
67+
async function createMCPStreamableClient(
68+
url: string, token: string,
69+
): Promise<Client> {
70+
const transport = new StreamableHTTPClientTransport(
71+
new URL(url),
72+
{
73+
requestInit: {
74+
headers: {
75+
authorization: `Bearer ${token}`,
76+
},
77+
},
78+
});
79+
80+
const client = new Client({
81+
name: getMCPServerID(url),
82+
version: '1.0.0',
83+
});
84+
85+
await client.connect(transport);
86+
87+
return client;
88+
}

src/mcp/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import { addRemoveTools, callActorGetDataset, defaultTools, getActorsAsTools } from '../tools/index.js';
2828
import { actorNameToToolName } from '../tools/utils.js';
2929
import type { ActorMcpTool, ActorTool, HelperTool, ToolEntry } from '../types.js';
30-
import { createMCPClient } from './client.js';
30+
import { connectMCPClient } from './client.js';
3131
import { EXTERNAL_TOOL_CALL_TIMEOUT_MSEC } from './const.js';
3232
import { processParamsGetTools } from './utils.js';
3333

@@ -432,7 +432,7 @@ export class ActorsMcpServer {
432432
const serverTool = tool.tool as ActorMcpTool;
433433
let client: Client | undefined;
434434
try {
435-
client = await createMCPClient(serverTool.serverUrl, apifyToken);
435+
client = await connectMCPClient(serverTool.serverUrl, apifyToken);
436436
const res = await client.callTool({
437437
name: serverTool.originToolName,
438438
arguments: args,

src/tools/actor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
HelperTools,
1515
} from '../const.js';
1616
import { getActorMCPServerPath, getActorMCPServerURL } from '../mcp/actors.js';
17-
import { createMCPClient } from '../mcp/client.js';
17+
import { connectMCPClient } from '../mcp/client.js';
1818
import { getMCPServerTools } from '../mcp/proxy.js';
1919
import { actorDefinitionPrunedCache } from '../state.js';
2020
import type { ActorInfo, InternalTool, ToolEntry } from '../types.js';
@@ -172,7 +172,7 @@ async function getMCPServersAsTools(
172172

173173
let client: Client | undefined;
174174
try {
175-
client = await createMCPClient(mcpServerUrl, apifyToken);
175+
client = await connectMCPClient(mcpServerUrl, apifyToken);
176176
const serverTools = await getMCPServerTools(actorId, client, mcpServerUrl);
177177
actorsMCPServerTools.push(...serverTools);
178178
} finally {

tests/const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { defaultTools } from '../src/tools/index.js';
33
import { actorNameToToolName } from '../src/tools/utils.js';
44

55
export const ACTOR_PYTHON_EXAMPLE = 'apify/python-example';
6+
export const ACTOR_MCP_SERVER_ACTOR_NAME = 'apify/actors-mcp-server';
67
export const DEFAULT_TOOL_NAMES = defaultTools.map((tool) => tool.tool.name);
78
export const DEFAULT_ACTOR_NAMES = defaults.actors.map((tool) => actorNameToToolName(tool));

tests/integration/suite.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from
66
import { defaults, HelperTools } from '../../src/const.js';
77
import { addRemoveTools, defaultTools } from '../../src/tools/index.js';
88
import { actorNameToToolName } from '../../src/tools/utils.js';
9-
import { ACTOR_PYTHON_EXAMPLE, DEFAULT_ACTOR_NAMES, DEFAULT_TOOL_NAMES } from '../const.js';
10-
import { type McpClientOptions } from '../helpers.js';
9+
import { ACTOR_MCP_SERVER_ACTOR_NAME, ACTOR_PYTHON_EXAMPLE, DEFAULT_ACTOR_NAMES, DEFAULT_TOOL_NAMES } from '../const.js';
10+
import { addActor, type McpClientOptions } from '../helpers.js';
1111

1212
interface IntegrationTestsSuiteOptions {
1313
suiteName: string;
@@ -227,6 +227,38 @@ export function createIntegrationTestsSuite(
227227
await client.close();
228228
});
229229

230+
it('should be able to add and call Actorized MCP server', async () => {
231+
const client = await createClientFn({ enableAddingActors: true });
232+
233+
const toolNamesBefore = getToolNames(await client.listTools());
234+
const searchToolCountBefore = toolNamesBefore.filter((name) => name.includes(HelperTools.STORE_SEARCH)).length;
235+
expect(searchToolCountBefore).toBe(1);
236+
237+
// Add self as an Actorized MCP server
238+
await addActor(client, ACTOR_MCP_SERVER_ACTOR_NAME);
239+
240+
const toolNamesAfter = getToolNames(await client.listTools());
241+
const searchToolCountAfter = toolNamesAfter.filter((name) => name.includes(HelperTools.STORE_SEARCH)).length;
242+
expect(searchToolCountAfter).toBe(2);
243+
244+
// Find the search tool from the Actorized MCP server
245+
const actorizedMCPSearchTool = toolNamesAfter.find(
246+
(name) => name.includes(HelperTools.STORE_SEARCH) && name !== HelperTools.STORE_SEARCH);
247+
expect(actorizedMCPSearchTool).toBeDefined();
248+
249+
const result = await client.callTool({
250+
name: actorizedMCPSearchTool as string,
251+
arguments: {
252+
search: ACTOR_MCP_SERVER_ACTOR_NAME,
253+
limit: 1,
254+
},
255+
});
256+
expect(result.content).toBeDefined();
257+
258+
await client.close();
259+
});
260+
261+
// Session termination is only possible for streamable HTTP transport.
230262
it.runIf(options.transport === 'streamable-http')('should successfully terminate streamable session', async () => {
231263
const client = await createClientFn();
232264
await client.listTools();

0 commit comments

Comments
 (0)