n8n-nodes-browser-smart-automation 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/McpClientTool/McpClientTool.node.js +2 -13
  2. package/dist/McpClientTool/McpClientTool.node.js.map +1 -1
  3. package/dist/McpClientTool/utils.js +1 -1
  4. package/dist/McpClientTool/utils.js.map +1 -1
  5. package/dist/McpTrigger/McpTrigger.node.js +1 -1
  6. package/dist/McpTrigger/McpTrigger.node.js.map +1 -1
  7. package/dist/shared/N8nBinaryLoader.js +203 -0
  8. package/dist/shared/N8nBinaryLoader.js.map +1 -0
  9. package/dist/shared/N8nJsonLoader.js +89 -0
  10. package/dist/shared/N8nJsonLoader.js.map +1 -0
  11. package/dist/shared/N8nTool.js +106 -0
  12. package/dist/shared/N8nTool.js.map +1 -0
  13. package/dist/shared/embeddingInputValidation.js +55 -0
  14. package/dist/shared/embeddingInputValidation.js.map +1 -0
  15. package/dist/shared/helpers.js +220 -13
  16. package/dist/shared/helpers.js.map +1 -1
  17. package/dist/shared/httpProxyAgent.js +40 -2
  18. package/dist/shared/httpProxyAgent.js.map +1 -1
  19. package/dist/shared/logWrapper.js +347 -2
  20. package/dist/shared/logWrapper.js.map +1 -1
  21. package/dist/shared/schemaParsing.js +47 -4
  22. package/dist/shared/schemaParsing.js.map +1 -1
  23. package/dist/shared/sharedFields.js +142 -7
  24. package/dist/shared/sharedFields.js.map +1 -1
  25. package/dist/shared/typesN8nTool.js +17 -0
  26. package/dist/shared/typesN8nTool.js.map +1 -0
  27. package/dist/shared/utils.js +1 -1
  28. package/dist/shared/utils.js.map +1 -1
  29. package/package.json +23 -7
  30. package/jest.config.js +0 -24
  31. package/nodes/McpClient/McpClient.node.ts +0 -327
  32. package/nodes/McpClient/__test__/McpClient.node.test.ts +0 -221
  33. package/nodes/McpClient/__test__/utils.test.ts +0 -302
  34. package/nodes/McpClient/listSearch.ts +0 -48
  35. package/nodes/McpClient/resourceMapping.ts +0 -48
  36. package/nodes/McpClient/utils.ts +0 -281
  37. package/nodes/McpClientTool/McpClientTool.node.ts +0 -468
  38. package/nodes/McpClientTool/__test__/McpClientTool.node.test.ts +0 -730
  39. package/nodes/McpClientTool/loadOptions.ts +0 -45
  40. package/nodes/McpClientTool/types.ts +0 -1
  41. package/nodes/McpClientTool/utils.ts +0 -116
  42. package/nodes/McpTrigger/FlushingTransport.ts +0 -61
  43. package/nodes/McpTrigger/McpServer.ts +0 -317
  44. package/nodes/McpTrigger/McpTrigger.node.ts +0 -204
  45. package/nodes/McpTrigger/__test__/FlushingTransport.test.ts +0 -102
  46. package/nodes/McpTrigger/__test__/McpServer.test.ts +0 -532
  47. package/nodes/McpTrigger/__test__/McpTrigger.node.test.ts +0 -171
  48. package/nodes/shared/__test__/utils.test.ts +0 -318
  49. package/nodes/shared/descriptions.ts +0 -65
  50. package/nodes/shared/helpers.ts +0 -31
  51. package/nodes/shared/httpProxyAgent.ts +0 -11
  52. package/nodes/shared/logWrapper.ts +0 -13
  53. package/nodes/shared/schemaParsing.ts +0 -9
  54. package/nodes/shared/sharedFields.ts +0 -20
  55. package/nodes/shared/types.ts +0 -12
  56. package/nodes/shared/utils.ts +0 -296
  57. package/officail/package.json +0 -255
  58. package/tsconfig.json +0 -32
  59. package/tsup.config.ts +0 -16
@@ -1,532 +0,0 @@
1
- import type { Tool } from '@langchain/core/tools';
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
- import type { Request } from 'express';
5
- import { captor, mock } from 'jest-mock-extended';
6
-
7
- import type { CompressionResponse } from '../FlushingTransport';
8
- import { FlushingSSEServerTransport, FlushingStreamableHTTPTransport } from '../FlushingTransport';
9
- import { McpServerManager } from '../McpServer';
10
-
11
- const sessionId = 'mock-session-id';
12
- const mockServer = mock<Server>();
13
- jest.mock('@modelcontextprotocol/sdk/server/index.js', () => {
14
- return {
15
- Server: jest.fn().mockImplementation(() => mockServer),
16
- };
17
- });
18
-
19
- const mockTransport = mock<FlushingSSEServerTransport>({ sessionId });
20
- mockTransport.handleRequest.mockImplementation(jest.fn());
21
- const mockStreamableTransport = mock<FlushingStreamableHTTPTransport>();
22
- mockStreamableTransport.onclose = jest.fn();
23
-
24
- jest.mock('../FlushingTransport', () => {
25
- return {
26
- FlushingSSEServerTransport: jest.fn().mockImplementation(() => mockTransport),
27
- FlushingStreamableHTTPTransport: jest.fn().mockImplementation(() => mockStreamableTransport),
28
- };
29
- });
30
-
31
- describe('McpServer', () => {
32
- const mockRequest = mock<Request>({ query: { sessionId }, path: '/sse' });
33
- const mockResponse = mock<CompressionResponse>();
34
- const mockTool = mock<Tool>({ name: 'mockTool' });
35
-
36
- const mcpServerManager = McpServerManager.instance(mock());
37
-
38
- beforeEach(() => {
39
- jest.clearAllMocks();
40
- mockResponse.status.mockReturnThis();
41
- });
42
-
43
- describe('connectTransport', () => {
44
- const postUrl = '/post-url';
45
-
46
- it('should set up a transport and server', async () => {
47
- await mcpServerManager.createServerWithSSETransport('mcpServer', postUrl, mockResponse);
48
-
49
- // Check that FlushingSSEServerTransport was initialized with correct params
50
- expect(FlushingSSEServerTransport).toHaveBeenCalledWith(postUrl, mockResponse);
51
-
52
- // Check that Server was initialized
53
- expect(Server).toHaveBeenCalled();
54
-
55
- // Check that transport and server are stored
56
- expect(mcpServerManager.transports[sessionId]).toBeDefined();
57
- expect(mcpServerManager.servers[sessionId]).toBeDefined();
58
-
59
- // Check that connect was called on the server
60
- expect(mcpServerManager.servers[sessionId].connect).toHaveBeenCalled();
61
-
62
- // Check that flush was called if available
63
- expect(mockResponse.flush).toHaveBeenCalled();
64
- });
65
-
66
- it('should set up close handler that cleans up resources', async () => {
67
- await mcpServerManager.createServerWithSSETransport('mcpServer', postUrl, mockResponse);
68
-
69
- // Get the close callback and execute it
70
- const closeCallbackCaptor = captor<() => Promise<void>>();
71
- expect(mockResponse.on).toHaveBeenCalledWith('close', closeCallbackCaptor);
72
- await closeCallbackCaptor.value();
73
-
74
- // Check that resources were cleaned up
75
- expect(mcpServerManager.transports[sessionId]).toBeUndefined();
76
- expect(mcpServerManager.servers[sessionId]).toBeUndefined();
77
- });
78
- });
79
-
80
- describe('handlePostMessage', () => {
81
- it('should call transport.handleRequest when transport exists', async () => {
82
- mockTransport.handleRequest.mockImplementation(async () => {
83
- // @ts-expect-error private property `resolveFunctions`
84
- mcpServerManager.resolveFunctions[`${sessionId}_123`]();
85
- });
86
-
87
- // Add the transport directly
88
- mcpServerManager.transports[sessionId] = mockTransport;
89
-
90
- mockRequest.rawBody = Buffer.from(
91
- JSON.stringify({
92
- jsonrpc: '2.0',
93
- method: 'tools/call',
94
- id: 123,
95
- params: { name: 'mockTool' },
96
- }),
97
- );
98
-
99
- // Call the method
100
- const result = await mcpServerManager.handlePostMessage(mockRequest, mockResponse, [
101
- mockTool,
102
- ]);
103
-
104
- // Verify that transport's handleRequest was called
105
- expect(mockTransport.handleRequest).toHaveBeenCalledWith(
106
- mockRequest,
107
- mockResponse,
108
- expect.any(Object),
109
- );
110
-
111
- // Verify that we check if it was a tool call
112
- expect(result).toBe(true);
113
-
114
- // Verify flush was called
115
- expect(mockResponse.flush).toHaveBeenCalled();
116
- });
117
-
118
- it('should handle multiple tool calls with different ids', async () => {
119
- const firstId = 123;
120
- const secondId = 456;
121
-
122
- mockTransport.handleRequest.mockImplementation(async () => {
123
- const requestKey = mockRequest.rawBody?.toString().includes(`"id":${firstId}`)
124
- ? `${sessionId}_${firstId}`
125
- : `${sessionId}_${secondId}`;
126
- // @ts-expect-error private property `resolveFunctions`
127
- mcpServerManager.resolveFunctions[requestKey]();
128
- });
129
-
130
- // Add the transport directly
131
- mcpServerManager.transports[sessionId] = mockTransport;
132
-
133
- // First tool call
134
- mockRequest.rawBody = Buffer.from(
135
- JSON.stringify({
136
- jsonrpc: '2.0',
137
- method: 'tools/call',
138
- id: firstId,
139
- params: { name: 'mockTool', arguments: { param: 'first call' } },
140
- }),
141
- );
142
-
143
- // Handle first tool call
144
- const firstResult = await mcpServerManager.handlePostMessage(mockRequest, mockResponse, [
145
- mockTool,
146
- ]);
147
- expect(firstResult).toBe(true);
148
- expect(mockTransport.handleRequest).toHaveBeenCalledWith(
149
- mockRequest,
150
- mockResponse,
151
- expect.any(Object),
152
- );
153
-
154
- // Second tool call with different id
155
- mockRequest.rawBody = Buffer.from(
156
- JSON.stringify({
157
- jsonrpc: '2.0',
158
- method: 'tools/call',
159
- id: secondId,
160
- params: { name: 'mockTool', arguments: { param: 'second call' } },
161
- }),
162
- );
163
-
164
- // Handle second tool call
165
- const secondResult = await mcpServerManager.handlePostMessage(mockRequest, mockResponse, [
166
- mockTool,
167
- ]);
168
- expect(secondResult).toBe(true);
169
-
170
- // Verify transport's handleRequest was called twice
171
- expect(mockTransport.handleRequest).toHaveBeenCalledTimes(2);
172
-
173
- // Verify flush was called for both requests
174
- expect(mockResponse.flush).toHaveBeenCalledTimes(2);
175
- });
176
-
177
- it('should return 401 when transport does not exist', async () => {
178
- // Set up request with rawBody and ensure sessionId is properly set
179
- const testRequest = mock<Request>({
180
- query: { sessionId: 'non-existent-session' },
181
- path: '/sse',
182
- });
183
- testRequest.rawBody = Buffer.from(
184
- JSON.stringify({
185
- jsonrpc: '2.0',
186
- method: 'tools/call',
187
- id: 123,
188
- params: { name: 'mockTool' },
189
- }),
190
- );
191
-
192
- // Call without setting up transport for this sessionId
193
- await mcpServerManager.handlePostMessage(testRequest, mockResponse, [mockTool]);
194
-
195
- // Verify error status was set
196
- expect(mockResponse.status).toHaveBeenCalledWith(401);
197
- expect(mockResponse.send).toHaveBeenCalledWith(expect.stringContaining('No transport found'));
198
- });
199
- });
200
-
201
- describe('createServerWithStreamableHTTPTransport', () => {
202
- it('should set up a transport and server with StreamableHTTPServerTransport', async () => {
203
- const mockStreamableRequest = mock<Request>({
204
- headers: { 'mcp-session-id': sessionId },
205
- path: '/mcp',
206
- body: {},
207
- });
208
-
209
- mockStreamableTransport.handleRequest.mockResolvedValue(undefined);
210
-
211
- await mcpServerManager.createServerWithStreamableHTTPTransport(
212
- 'mcpServer',
213
- mockResponse,
214
- mockStreamableRequest,
215
- );
216
-
217
- // Check that FlushingStreamableHTTPTransport was initialized with correct params
218
- expect(FlushingStreamableHTTPTransport).toHaveBeenCalledWith(
219
- {
220
- sessionIdGenerator: expect.any(Function),
221
- onsessioninitialized: expect.any(Function),
222
- },
223
- mockResponse,
224
- );
225
-
226
- // Check that Server was initialized
227
- expect(Server).toHaveBeenCalled();
228
-
229
- // Check that handleRequest was called
230
- expect(mockStreamableTransport.handleRequest).toHaveBeenCalled();
231
- });
232
-
233
- it('should handle session initialization callback', async () => {
234
- const mockStreamableRequest = mock<Request>({
235
- headers: { 'mcp-session-id': sessionId },
236
- path: '/mcp',
237
- body: {},
238
- });
239
-
240
- // Set up the mock to simulate session initialization
241
- mockStreamableTransport.onclose = jest.fn();
242
- mockStreamableTransport.handleRequest.mockResolvedValue(undefined);
243
-
244
- jest
245
- .mocked(FlushingStreamableHTTPTransport)
246
- .mockImplementationOnce((options: StreamableHTTPServerTransportOptions) => {
247
- // Simulate session initialization asynchronously using queueMicrotask instead of setTimeout
248
- queueMicrotask(() => {
249
- if (options.onsessioninitialized) {
250
- void options.onsessioninitialized(sessionId);
251
- }
252
- });
253
- return mockStreamableTransport;
254
- });
255
-
256
- await mcpServerManager.createServerWithStreamableHTTPTransport(
257
- 'mcpServer',
258
- mockResponse,
259
- mockStreamableRequest,
260
- );
261
-
262
- // Wait for microtask to complete
263
- await Promise.resolve();
264
-
265
- // Check that transport and server are stored after session init
266
- expect(mcpServerManager.transports[sessionId]).toBeDefined();
267
- expect(mcpServerManager.servers[sessionId]).toBeDefined();
268
- });
269
-
270
- it('should handle transport close callback for StreamableHTTPServerTransport', async () => {
271
- const mockStreamableRequest = mock<Request>({
272
- headers: { 'mcp-session-id': sessionId },
273
- path: '/mcp',
274
- body: {},
275
- });
276
-
277
- let onCloseCallback: (() => void) | undefined;
278
- mockStreamableTransport.handleRequest.mockResolvedValue(undefined);
279
-
280
- jest
281
- .mocked(FlushingStreamableHTTPTransport)
282
- .mockImplementationOnce((options: StreamableHTTPServerTransportOptions) => {
283
- // Simulate session initialization and capture onclose callback asynchronously using queueMicrotask
284
- queueMicrotask(() => {
285
- if (options.onsessioninitialized) {
286
- void options.onsessioninitialized(sessionId);
287
- onCloseCallback = mockStreamableTransport.onclose;
288
- }
289
- });
290
- return mockStreamableTransport;
291
- });
292
-
293
- await mcpServerManager.createServerWithStreamableHTTPTransport(
294
- 'mcpServer',
295
- mockResponse,
296
- mockStreamableRequest,
297
- );
298
-
299
- // Wait for microtask to complete
300
- await Promise.resolve();
301
-
302
- // Simulate transport close
303
- if (onCloseCallback) {
304
- onCloseCallback();
305
- }
306
-
307
- // Check that resources were cleaned up
308
- expect(mcpServerManager.transports[sessionId]).toBeUndefined();
309
- expect(mcpServerManager.servers[sessionId]).toBeUndefined();
310
- });
311
- });
312
-
313
- describe('handlePostMessage with StreamableHTTPServerTransport', () => {
314
- it('should handle StreamableHTTPServerTransport with session ID in header', async () => {
315
- const mockStreamableRequest = mock<Request>({
316
- headers: { 'mcp-session-id': sessionId },
317
- path: '/mcp',
318
- });
319
-
320
- mockStreamableTransport.handleRequest.mockImplementation(async () => {
321
- // @ts-expect-error private property `resolveFunctions`
322
- mcpServerManager.resolveFunctions[`${sessionId}_123`]();
323
- });
324
-
325
- // Add the transport directly
326
- mcpServerManager.transports[sessionId] = mockStreamableTransport;
327
-
328
- mockStreamableRequest.rawBody = Buffer.from(
329
- JSON.stringify({
330
- jsonrpc: '2.0',
331
- method: 'tools/call',
332
- id: 123,
333
- params: { name: 'mockTool' },
334
- }),
335
- );
336
-
337
- // Call the method
338
- const result = await mcpServerManager.handlePostMessage(mockStreamableRequest, mockResponse, [
339
- mockTool,
340
- ]);
341
-
342
- // Verify that transport's handleRequest was called
343
- expect(mockStreamableTransport.handleRequest).toHaveBeenCalledWith(
344
- mockStreamableRequest,
345
- mockResponse,
346
- expect.any(Object),
347
- );
348
-
349
- // Verify that we check if it was a tool call
350
- expect(result).toBe(true);
351
-
352
- // Verify flush was called
353
- expect(mockResponse.flush).toHaveBeenCalled();
354
- });
355
-
356
- it('should return 401 when StreamableHTTPServerTransport does not exist', async () => {
357
- const testRequest = mock<Request>({
358
- headers: { 'mcp-session-id': 'non-existent-session' },
359
- path: '/mcp',
360
- });
361
- testRequest.rawBody = Buffer.from(
362
- JSON.stringify({
363
- jsonrpc: '2.0',
364
- method: 'tools/call',
365
- id: 123,
366
- params: { name: 'mockTool' },
367
- }),
368
- );
369
-
370
- // Call without setting up transport for this sessionId
371
- await mcpServerManager.handlePostMessage(testRequest, mockResponse, [mockTool]);
372
-
373
- // Verify error status was set
374
- expect(mockResponse.status).toHaveBeenCalledWith(401);
375
- expect(mockResponse.send).toHaveBeenCalledWith(expect.stringContaining('No transport found'));
376
- });
377
- });
378
-
379
- describe('getSessionId', () => {
380
- it('should return session ID from query parameter', () => {
381
- const request = mock<Request>();
382
- request.query = { sessionId: 'test-session-query' };
383
- request.headers = {};
384
-
385
- const result = mcpServerManager.getSessionId(request);
386
-
387
- expect(result).toBe('test-session-query');
388
- });
389
-
390
- it('should return session ID from header when query is not present', () => {
391
- const request = mock<Request>();
392
- request.query = {};
393
- request.headers = { 'mcp-session-id': 'test-session-header' };
394
-
395
- const result = mcpServerManager.getSessionId(request);
396
-
397
- expect(result).toBe('test-session-header');
398
- });
399
-
400
- it('should return undefined when neither query parameter nor header is present', () => {
401
- const request = mock<Request>();
402
- request.query = {};
403
- request.headers = {};
404
-
405
- const result = mcpServerManager.getSessionId(request);
406
-
407
- expect(result).toBeUndefined();
408
- });
409
- });
410
-
411
- describe('getTransport', () => {
412
- const testSessionId = 'test-session-transport';
413
-
414
- beforeEach(() => {
415
- // Clear transports before each test
416
- mcpServerManager.transports = {};
417
- });
418
-
419
- it('should return transport when it exists for the session', () => {
420
- const mockTransportInstance = mock<FlushingSSEServerTransport>();
421
- mcpServerManager.transports[testSessionId] = mockTransportInstance;
422
-
423
- const result = mcpServerManager.getTransport(testSessionId);
424
-
425
- expect(result).toBe(mockTransportInstance);
426
- });
427
-
428
- it('should return undefined when transport does not exist for the session', () => {
429
- const result = mcpServerManager.getTransport('non-existent-session');
430
-
431
- expect(result).toBeUndefined();
432
- });
433
-
434
- it('should return correct transport when multiple transports exist', () => {
435
- const mockTransport1 = mock<FlushingSSEServerTransport>();
436
- const mockTransport2 = mock<FlushingStreamableHTTPTransport>();
437
-
438
- mcpServerManager.transports['session-1'] = mockTransport1;
439
- mcpServerManager.transports['session-2'] = mockTransport2;
440
-
441
- const result1 = mcpServerManager.getTransport('session-1');
442
- const result2 = mcpServerManager.getTransport('session-2');
443
-
444
- expect(result1).toBe(mockTransport1);
445
- expect(result2).toBe(mockTransport2);
446
- });
447
- });
448
-
449
- describe('handleDeleteRequest', () => {
450
- beforeEach(() => {
451
- // Clear transports and servers before each test
452
- mcpServerManager.transports = {};
453
- mcpServerManager.servers = {};
454
- });
455
-
456
- it('should handle DELETE request for StreamableHTTP transport', async () => {
457
- const deleteSessionId = 'delete-session-id';
458
- const mockDeleteRequest = mock<Request>({
459
- headers: { 'mcp-session-id': deleteSessionId },
460
- });
461
- const mockDeleteResponse = mock<CompressionResponse>();
462
- mockDeleteResponse.status.mockReturnThis();
463
-
464
- // Create a mock transport that passes instanceof check
465
- const mockHttpTransport = Object.create(FlushingStreamableHTTPTransport.prototype);
466
- mockHttpTransport.handleRequest = jest.fn();
467
-
468
- // Set up the transport
469
- mcpServerManager.transports[deleteSessionId] = mockHttpTransport;
470
-
471
- // Call handleDeleteRequest
472
- await mcpServerManager.handleDeleteRequest(mockDeleteRequest, mockDeleteResponse);
473
-
474
- // Verify transport.handleRequest was called
475
- expect(mockHttpTransport.handleRequest).toHaveBeenCalledWith(
476
- mockDeleteRequest,
477
- mockDeleteResponse,
478
- );
479
- });
480
-
481
- it('should return 400 when no sessionId provided', async () => {
482
- const mockDeleteRequest = mock<Request>({
483
- query: {},
484
- headers: {},
485
- });
486
- const mockDeleteResponse = mock<CompressionResponse>();
487
- mockDeleteResponse.status.mockReturnThis();
488
-
489
- // Mock getSessionId to return undefined
490
- jest.spyOn(mcpServerManager, 'getSessionId').mockReturnValueOnce(undefined);
491
-
492
- // Call handleDeleteRequest without sessionId
493
- await mcpServerManager.handleDeleteRequest(mockDeleteRequest, mockDeleteResponse);
494
-
495
- // Verify 400 response
496
- expect(mockDeleteResponse.status).toHaveBeenCalledWith(400);
497
- });
498
-
499
- it('should return 404 for non-existent session', async () => {
500
- const mockDeleteRequest = mock<Request>({
501
- headers: { 'mcp-session-id': 'non-existent-session' },
502
- });
503
- const mockDeleteResponse = mock<CompressionResponse>();
504
- mockDeleteResponse.status.mockReturnThis();
505
-
506
- // Call handleDeleteRequest with non-existent sessionId
507
- await mcpServerManager.handleDeleteRequest(mockDeleteRequest, mockDeleteResponse);
508
-
509
- // Verify 404 response (session not found)
510
- expect(mockDeleteResponse.status).toHaveBeenCalledWith(404);
511
- });
512
-
513
- it('should return 405 for SSE transport session', async () => {
514
- const sseSessionId = 'sse-session-id';
515
- const mockDeleteRequest = mock<Request>({
516
- query: { sessionId: sseSessionId },
517
- });
518
- const mockDeleteResponse = mock<CompressionResponse>();
519
- mockDeleteResponse.status.mockReturnThis();
520
- const mockSSETransport = mock<FlushingSSEServerTransport>();
521
-
522
- // Set up SSE transport
523
- mcpServerManager.transports[sseSessionId] = mockSSETransport;
524
-
525
- // Call handleDeleteRequest
526
- await mcpServerManager.handleDeleteRequest(mockDeleteRequest, mockDeleteResponse);
527
-
528
- // Verify 405 response (DELETE not supported for SSE)
529
- expect(mockDeleteResponse.status).toHaveBeenCalledWith(405);
530
- });
531
- });
532
- });
@@ -1,171 +0,0 @@
1
- import type { Tool } from '@langchain/core/tools';
2
- import type { Request, Response } from 'express';
3
- import { mock } from 'jest-mock-extended';
4
- import type { INode, IWebhookFunctions } from 'n8n-workflow';
5
-
6
- import * as helpers from '@utils/helpers';
7
-
8
- import type {
9
- FlushingSSEServerTransport,
10
- FlushingStreamableHTTPTransport,
11
- } from '../FlushingTransport';
12
- import type { McpServerManager } from '../McpServer';
13
- import { McpTrigger } from '../McpTrigger.node';
14
-
15
- const mockTool = mock<Tool>({ name: 'mockTool' });
16
- jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue([mockTool]);
17
-
18
- const mockServerManager = mock<McpServerManager>();
19
- jest.mock('../McpServer', () => ({
20
- McpServerManager: {
21
- instance: jest.fn().mockImplementation(() => mockServerManager),
22
- },
23
- }));
24
-
25
- describe('McpTrigger Node', () => {
26
- const sessionId = 'mock-session-id';
27
- const mockContext = mock<IWebhookFunctions>();
28
- const mockRequest = mock<Request>({ query: { sessionId }, path: '/custom-path' });
29
- const mockResponse = mock<Response>();
30
- let mcpTrigger: McpTrigger;
31
-
32
- beforeEach(() => {
33
- jest.clearAllMocks();
34
-
35
- mcpTrigger = new McpTrigger();
36
- mockContext.getRequestObject.mockReturnValue(mockRequest);
37
- mockContext.getResponseObject.mockReturnValue(mockResponse);
38
- mockContext.getNode.mockReturnValue({
39
- name: 'McpTrigger',
40
- typeVersion: 2,
41
- } as INode);
42
- mockServerManager.transports = {};
43
- });
44
-
45
- describe('webhook method', () => {
46
- it('should handle setup webhook', async () => {
47
- // Configure the context for setup webhook
48
- mockContext.getWebhookName.mockReturnValue('setup');
49
-
50
- // Call the webhook method
51
- const result = await mcpTrigger.webhook(mockContext);
52
-
53
- // Verify that the connectTransport method was called with correct URL
54
- expect(mockServerManager.createServerWithSSETransport).toHaveBeenCalledWith(
55
- 'McpTrigger',
56
- '/custom-path',
57
- mockResponse,
58
- );
59
-
60
- // Verify the returned result has noWebhookResponse: true
61
- expect(result).toEqual({ noWebhookResponse: true });
62
- });
63
-
64
- it('should handle default webhook for tool execution', async () => {
65
- // Configure the context for default webhook (tool execution)
66
- mockContext.getWebhookName.mockReturnValue('default');
67
-
68
- // Mock the session ID retrieval and transport existence
69
- mockServerManager.getSessionId.mockReturnValue(sessionId);
70
- mockServerManager.getTransport.mockReturnValue(mock<FlushingSSEServerTransport>({}));
71
-
72
- // Mock that the server executes a tool and returns true
73
- mockServerManager.handlePostMessage.mockResolvedValueOnce(true);
74
-
75
- // Call the webhook method
76
- const result = await mcpTrigger.webhook(mockContext);
77
-
78
- // Verify that handlePostMessage was called with request, response and tools
79
- expect(mockServerManager.handlePostMessage).toHaveBeenCalledWith(mockRequest, mockResponse, [
80
- mockTool,
81
- ]);
82
-
83
- // Verify the returned result when a tool was called
84
- expect(result).toEqual({
85
- noWebhookResponse: true,
86
- workflowData: [[{ json: {} }]],
87
- });
88
- });
89
-
90
- it('should handle default webhook when no tool was executed', async () => {
91
- // Configure the context for default webhook
92
- mockContext.getWebhookName.mockReturnValue('default');
93
-
94
- // Mock the session ID retrieval and transport existence
95
- mockServerManager.getSessionId.mockReturnValue(sessionId);
96
- mockServerManager.getTransport.mockReturnValue(mock<FlushingSSEServerTransport>({}));
97
-
98
- // Mock that the server doesn't execute a tool and returns false
99
- mockServerManager.handlePostMessage.mockResolvedValueOnce(false);
100
-
101
- // Call the webhook method
102
- const result = await mcpTrigger.webhook(mockContext);
103
-
104
- // Verify the returned result when no tool was called
105
- expect(result).toEqual({ noWebhookResponse: true });
106
- });
107
-
108
- it('should pass the correct server name to McpServerSingleton.instance for version > 1', async () => {
109
- // Configure node with version > 1 and custom name
110
- mockContext.getNode.mockReturnValue({
111
- name: 'My custom MCP server!',
112
- typeVersion: 1.1,
113
- } as INode);
114
- mockContext.getWebhookName.mockReturnValue('setup');
115
- // Call the webhook method
116
- await mcpTrigger.webhook(mockContext);
117
-
118
- // Verify that connectTransport was called with the sanitized server name
119
- expect(mockServerManager.createServerWithSSETransport).toHaveBeenCalledWith(
120
- 'My_custom_MCP_server_',
121
- '/custom-path',
122
- mockResponse,
123
- );
124
- });
125
-
126
- it('should use default server name for version 1', async () => {
127
- // Configure node with version 1
128
- mockContext.getNode.mockReturnValue({
129
- typeVersion: 1,
130
- } as INode);
131
- mockContext.getWebhookName.mockReturnValue('setup');
132
-
133
- // Call the webhook method
134
- await mcpTrigger.webhook(mockContext);
135
-
136
- // Verify that connectTransport was called with the default server name
137
- expect(mockServerManager.createServerWithSSETransport).toHaveBeenCalledWith(
138
- 'n8n-mcp-server',
139
- '/custom-path',
140
- mockResponse,
141
- );
142
- });
143
-
144
- it('should handle DELETE webhook for StreamableHTTP session termination', async () => {
145
- // Configure the context for DELETE webhook
146
- mockContext.getWebhookName.mockReturnValue('default');
147
- const mockDeleteRequest = mock<Request>({
148
- method: 'DELETE',
149
- headers: { 'mcp-session-id': sessionId },
150
- path: '/custom-path',
151
- });
152
- mockContext.getRequestObject.mockReturnValueOnce(mockDeleteRequest);
153
-
154
- // Mock existing StreamableHTTP transport
155
- mockServerManager.getSessionId.mockReturnValue(sessionId);
156
- mockServerManager.getTransport.mockReturnValue(mock<FlushingStreamableHTTPTransport>({}));
157
-
158
- // Call the webhook method
159
- const result = await mcpTrigger.webhook(mockContext);
160
-
161
- // Verify that handleDeleteRequest was called
162
- expect(mockServerManager.handleDeleteRequest).toHaveBeenCalledWith(
163
- mockDeleteRequest,
164
- mockResponse,
165
- );
166
-
167
- // Verify the returned result
168
- expect(result).toEqual({ noWebhookResponse: true });
169
- });
170
- });
171
- });