@@ -2,6 +2,7 @@ import type { Server as HttpServer } from 'node:http';
22
33import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
44import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' ;
5+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' ;
56import type { Express } from 'express' ;
67import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
78
@@ -12,7 +13,7 @@ import { defaults, HelperTools } from '../../src/const.js';
1213import { ActorsMcpServer } from '../../src/mcp/server.js' ;
1314import { actorNameToToolName } from '../../src/tools/utils.js' ;
1415
15- async function createMCPClient (
16+ async function createMCPSSEClient (
1617 serverUrl : string ,
1718 options ?: {
1819 actors ?: string [ ] ;
@@ -39,17 +40,45 @@ async function createMCPClient(
3940 authorization : `Bearer ${ process . env . APIFY_TOKEN } ` ,
4041 } ,
4142 } ,
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 } ) ;
43+ } ,
44+ ) ;
45+
46+ const client = new Client ( {
47+ name : 'sse-client' ,
48+ version : '1.0.0' ,
49+ } ) ;
50+ await client . connect ( transport ) ;
51+
52+ return client ;
53+ }
54+
55+ async function createMCPStreamableClient (
56+ serverUrl : string ,
57+ options ?: {
58+ actors ?: string [ ] ;
59+ enableAddingActors ?: boolean ;
60+ } ,
61+ ) : Promise < Client > {
62+ if ( ! process . env . APIFY_TOKEN ) {
63+ throw new Error ( 'APIFY_TOKEN environment variable is not set.' ) ;
64+ }
65+ const url = new URL ( serverUrl ) ;
66+ const { actors, enableAddingActors } = options || { } ;
67+ if ( actors ) {
68+ url . searchParams . append ( 'actors' , actors . join ( ',' ) ) ;
69+ }
70+ if ( enableAddingActors ) {
71+ url . searchParams . append ( 'enableAddingActors' , 'true' ) ;
72+ }
73+
74+ const transport = new StreamableHTTPClientTransport (
75+ url ,
76+ {
77+ requestInit : {
78+ headers : {
79+ authorization : `Bearer ${ process . env . APIFY_TOKEN } ` ,
5080 } ,
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
81+ } ,
5382 } ,
5483 ) ;
5584
@@ -62,17 +91,22 @@ async function createMCPClient(
6291 return client ;
6392}
6493
65- describe ( 'Actors MCP Server' , {
94+ describe ( 'Actors MCP Server SSE ' , {
6695 concurrent : false , // Run test serially to prevent port already in use
6796} , ( ) => {
6897 let app : Express ;
6998 let server : ActorsMcpServer ;
7099 let httpServer : HttpServer ;
71- const testPort = 7357 ;
100+ const testPort = 50000 ;
72101 const testHost = `http://localhost:${ testPort } ` ;
73102
74103 beforeEach ( async ( ) => {
75- server = new ActorsMcpServer ( ) ;
104+ // same as in main.ts
105+ // TODO: unify
106+ server = new ActorsMcpServer ( {
107+ enableAddingActors : false ,
108+ enableDefaultActors : false ,
109+ } ) ;
76110 log . setLevel ( log . LEVELS . OFF ) ;
77111
78112 // Create express app using the proper server setup
@@ -82,9 +116,13 @@ describe('Actors MCP Server', {
82116 await new Promise < void > ( ( resolve ) => {
83117 httpServer = app . listen ( testPort , ( ) => resolve ( ) ) ;
84118 } ) ;
119+
120+ // TODO: figure out why this is needed
121+ await new Promise < void > ( ( resolve ) => { setTimeout ( resolve , 1000 ) ; } ) ;
85122 } ) ;
86123
87124 afterEach ( async ( ) => {
125+ await server . close ( ) ;
88126 await new Promise < void > ( ( resolve ) => {
89127 httpServer . close ( ( ) => resolve ( ) ) ;
90128 } ) ;
@@ -122,7 +160,203 @@ describe('Actors MCP Server', {
122160 } ) ;
123161
124162 it ( 'default tools list' , async ( ) => {
125- const client = await createMCPClient ( `${ testHost } /sse` ) ;
163+ const client = await createMCPSSEClient ( `${ testHost } /sse` ) ;
164+
165+ const tools = await client . listTools ( ) ;
166+ const names = tools . tools . map ( ( tool ) => tool . name ) ;
167+ expect ( names . length ) . toEqual ( defaults . helperTools . length + defaults . actors . length ) ;
168+ for ( const tool of defaults . helperTools ) {
169+ expect ( names ) . toContain ( tool ) ;
170+ }
171+ for ( const actorTool of defaults . actors ) {
172+ expect ( names ) . toContain ( actorNameToToolName ( actorTool ) ) ;
173+ }
174+
175+ await client . close ( ) ;
176+ } ) ;
177+
178+ it ( 'use only specific Actor and call it' , async ( ) => {
179+ const actorName = 'apify/python-example' ;
180+ const selectedToolName = actorNameToToolName ( actorName ) ;
181+ const client = await createMCPSSEClient ( `${ testHost } /sse` , {
182+ actors : [ actorName ] ,
183+ enableAddingActors : false ,
184+ } ) ;
185+
186+ const tools = await client . listTools ( ) ;
187+ const names = tools . tools . map ( ( tool ) => tool . name ) ;
188+ expect ( names . length ) . toEqual ( defaults . helperTools . length + 1 ) ;
189+ for ( const tool of defaults . helperTools ) {
190+ expect ( names ) . toContain ( tool ) ;
191+ }
192+ expect ( names ) . toContain ( selectedToolName ) ;
193+
194+ const result = await client . callTool ( {
195+ name : selectedToolName ,
196+ arguments : {
197+ first_number : 1 ,
198+ second_number : 2 ,
199+ } ,
200+ } ) ;
201+
202+ expect ( result ) . toEqual ( {
203+ content : [ {
204+ text : JSON . stringify ( {
205+ first_number : 1 ,
206+ second_number : 2 ,
207+ sum : 3 ,
208+ } ) ,
209+ type : 'text' ,
210+ } ] ,
211+ } ) ;
212+
213+ await client . close ( ) ;
214+ } ) ;
215+
216+ it ( 'load Actors from parameters' , async ( ) => {
217+ const actors = [ 'apify/rag-web-browser' , 'apify/instagram-scraper' ] ;
218+ const client = await createMCPSSEClient ( `${ testHost } /sse` , {
219+ actors,
220+ enableAddingActors : false ,
221+ } ) ;
222+
223+ const tools = await client . listTools ( ) ;
224+ const names = tools . tools . map ( ( tool ) => tool . name ) ;
225+ expect ( names . length ) . toEqual ( defaults . helperTools . length + actors . length ) ;
226+ for ( const tool of defaults . helperTools ) {
227+ expect ( names ) . toContain ( tool ) ;
228+ }
229+ for ( const actor of actors ) {
230+ expect ( names ) . toContain ( actorNameToToolName ( actor ) ) ;
231+ }
232+
233+ await client . close ( ) ;
234+ } ) ;
235+
236+ it ( 'load Actor dynamically and call it' , async ( ) => {
237+ const actor = 'apify/python-example' ;
238+ const selectedToolName = actorNameToToolName ( actor ) ;
239+ const client = await createMCPSSEClient ( `${ testHost } /sse` , {
240+ enableAddingActors : true ,
241+ } ) ;
242+
243+ const tools = await client . listTools ( ) ;
244+ const names = tools . tools . map ( ( tool ) => tool . name ) ;
245+ expect ( names . length ) . toEqual ( defaults . helperTools . length + defaults . actorAddingTools . length + defaults . actors . length ) ;
246+ for ( const tool of defaults . helperTools ) {
247+ expect ( names ) . toContain ( tool ) ;
248+ }
249+ for ( const tool of defaults . actorAddingTools ) {
250+ expect ( names ) . toContain ( tool ) ;
251+ }
252+ for ( const actorTool of defaults . actors ) {
253+ expect ( names ) . toContain ( actorNameToToolName ( actorTool ) ) ;
254+ }
255+
256+ // Add Actor dynamically
257+ await client . callTool ( {
258+ name : HelperTools . ADD_ACTOR ,
259+ arguments : {
260+ actorName : actor ,
261+ } ,
262+ } ) ;
263+
264+ // Check if tools was added
265+ const toolsAfterAdd = await client . listTools ( ) ;
266+ const namesAfterAdd = toolsAfterAdd . tools . map ( ( tool ) => tool . name ) ;
267+ expect ( namesAfterAdd . length ) . toEqual ( defaults . helperTools . length + defaults . actorAddingTools . length + defaults . actors . length + 1 ) ;
268+ expect ( namesAfterAdd ) . toContain ( selectedToolName ) ;
269+
270+ const result = await client . callTool ( {
271+ name : selectedToolName ,
272+ arguments : {
273+ first_number : 1 ,
274+ second_number : 2 ,
275+ } ,
276+ } ) ;
277+
278+ expect ( result ) . toEqual ( {
279+ content : [ {
280+ text : JSON . stringify ( {
281+ first_number : 1 ,
282+ second_number : 2 ,
283+ sum : 3 ,
284+ } ) ,
285+ type : 'text' ,
286+ } ] ,
287+ } ) ;
288+
289+ await client . close ( ) ;
290+ } ) ;
291+
292+ it ( 'should remove Actor from tools list' , async ( ) => {
293+ const actor = 'apify/python-example' ;
294+ const selectedToolName = actorNameToToolName ( actor ) ;
295+ const client = await createMCPSSEClient ( `${ testHost } /sse` , {
296+ actors : [ actor ] ,
297+ enableAddingActors : true ,
298+ } ) ;
299+
300+ // Verify actor is in the tools list
301+ const toolsBefore = await client . listTools ( ) ;
302+ const namesBefore = toolsBefore . tools . map ( ( tool ) => tool . name ) ;
303+ expect ( namesBefore ) . toContain ( selectedToolName ) ;
304+
305+ // Remove the actor
306+ await client . callTool ( {
307+ name : HelperTools . REMOVE_ACTOR ,
308+ arguments : {
309+ toolName : selectedToolName ,
310+ } ,
311+ } ) ;
312+
313+ // Verify actor is removed
314+ const toolsAfter = await client . listTools ( ) ;
315+ const namesAfter = toolsAfter . tools . map ( ( tool ) => tool . name ) ;
316+ expect ( namesAfter ) . not . toContain ( selectedToolName ) ;
317+
318+ await client . close ( ) ;
319+ } ) ;
320+ } ) ;
321+
322+ describe ( 'Actors MCP Server Streamable HTTP' , {
323+ concurrent : false , // Run test serially to prevent port already in use
324+ } , ( ) => {
325+ let app : Express ;
326+ let server : ActorsMcpServer ;
327+ let httpServer : HttpServer ;
328+ const testPort = 50001 ;
329+ const testHost = `http://localhost:${ testPort } ` ;
330+
331+ beforeEach ( async ( ) => {
332+ // same as in main.ts
333+ // TODO: unify
334+ server = new ActorsMcpServer ( {
335+ enableAddingActors : false ,
336+ enableDefaultActors : false ,
337+ } ) ;
338+ log . setLevel ( log . LEVELS . OFF ) ;
339+
340+ // Create express app using the proper server setup
341+ app = createExpressApp ( testHost , server ) ;
342+
343+ // Start test server
344+ await new Promise < void > ( ( resolve ) => {
345+ httpServer = app . listen ( testPort , ( ) => resolve ( ) ) ;
346+ } ) ;
347+
348+ // TODO: figure out why this is needed
349+ await new Promise < void > ( ( resolve ) => { setTimeout ( resolve , 1000 ) ; } ) ;
350+ } ) ;
351+
352+ afterEach ( async ( ) => {
353+ await new Promise < void > ( ( resolve ) => {
354+ httpServer . close ( ( ) => resolve ( ) ) ;
355+ } ) ;
356+ } ) ;
357+
358+ it ( 'default tools list' , async ( ) => {
359+ const client = await createMCPStreamableClient ( `${ testHost } /mcp` ) ;
126360
127361 const tools = await client . listTools ( ) ;
128362 const names = tools . tools . map ( ( tool ) => tool . name ) ;
@@ -140,7 +374,7 @@ describe('Actors MCP Server', {
140374 it ( 'use only specific Actor and call it' , async ( ) => {
141375 const actorName = 'apify/python-example' ;
142376 const selectedToolName = actorNameToToolName ( actorName ) ;
143- const client = await createMCPClient ( `${ testHost } /sse ` , {
377+ const client = await createMCPStreamableClient ( `${ testHost } /mcp ` , {
144378 actors : [ actorName ] ,
145379 enableAddingActors : false ,
146380 } ) ;
@@ -175,9 +409,9 @@ describe('Actors MCP Server', {
175409 await client . close ( ) ;
176410 } ) ;
177411
178- it ( 'load Actors from parameters via SSE client ' , async ( ) => {
412+ it ( 'load Actors from parameters' , async ( ) => {
179413 const actors = [ 'apify/rag-web-browser' , 'apify/instagram-scraper' ] ;
180- const client = await createMCPClient ( `${ testHost } /sse ` , {
414+ const client = await createMCPStreamableClient ( `${ testHost } /mcp ` , {
181415 actors,
182416 enableAddingActors : false ,
183417 } ) ;
@@ -198,7 +432,7 @@ describe('Actors MCP Server', {
198432 it ( 'load Actor dynamically and call it' , async ( ) => {
199433 const actor = 'apify/python-example' ;
200434 const selectedToolName = actorNameToToolName ( actor ) ;
201- const client = await createMCPClient ( `${ testHost } /sse ` , {
435+ const client = await createMCPStreamableClient ( `${ testHost } /mcp ` , {
202436 enableAddingActors : true ,
203437 } ) ;
204438
@@ -254,7 +488,7 @@ describe('Actors MCP Server', {
254488 it ( 'should remove Actor from tools list' , async ( ) => {
255489 const actor = 'apify/python-example' ;
256490 const selectedToolName = actorNameToToolName ( actor ) ;
257- const client = await createMCPClient ( `${ testHost } /sse ` , {
491+ const client = await createMCPStreamableClient ( `${ testHost } /mcp ` , {
258492 actors : [ actor ] ,
259493 enableAddingActors : true ,
260494 } ) ;
0 commit comments