n8n-nodes-browser-smart-automation 0.1.2 → 0.1.5
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.
- package/dist/McpClientTool/McpClientTool.node.js +2 -13
- package/dist/McpClientTool/McpClientTool.node.js.map +1 -1
- package/dist/McpClientTool/utils.js +1 -1
- package/dist/McpClientTool/utils.js.map +1 -1
- package/dist/McpTrigger/McpTrigger.node.js +1 -1
- package/dist/McpTrigger/McpTrigger.node.js.map +1 -1
- package/dist/shared/N8nBinaryLoader.js +203 -0
- package/dist/shared/N8nBinaryLoader.js.map +1 -0
- package/dist/shared/N8nJsonLoader.js +89 -0
- package/dist/shared/N8nJsonLoader.js.map +1 -0
- package/dist/shared/N8nTool.js +106 -0
- package/dist/shared/N8nTool.js.map +1 -0
- package/dist/shared/embeddingInputValidation.js +55 -0
- package/dist/shared/embeddingInputValidation.js.map +1 -0
- package/dist/shared/helpers.js +220 -13
- package/dist/shared/helpers.js.map +1 -1
- package/dist/shared/httpProxyAgent.js +40 -2
- package/dist/shared/httpProxyAgent.js.map +1 -1
- package/dist/shared/logWrapper.js +347 -2
- package/dist/shared/logWrapper.js.map +1 -1
- package/dist/shared/schemaParsing.js +47 -4
- package/dist/shared/schemaParsing.js.map +1 -1
- package/dist/shared/sharedFields.js +142 -7
- package/dist/shared/sharedFields.js.map +1 -1
- package/dist/shared/typesN8nTool.js +17 -0
- package/dist/shared/typesN8nTool.js.map +1 -0
- package/dist/shared/utils.js +1 -1
- package/dist/shared/utils.js.map +1 -1
- package/package.json +25 -7
- package/jest.config.js +0 -24
- package/nodes/McpClient/McpClient.node.ts +0 -327
- package/nodes/McpClient/__test__/McpClient.node.test.ts +0 -221
- package/nodes/McpClient/__test__/utils.test.ts +0 -302
- package/nodes/McpClient/listSearch.ts +0 -48
- package/nodes/McpClient/resourceMapping.ts +0 -48
- package/nodes/McpClient/utils.ts +0 -281
- package/nodes/McpClientTool/McpClientTool.node.ts +0 -468
- package/nodes/McpClientTool/__test__/McpClientTool.node.test.ts +0 -730
- package/nodes/McpClientTool/loadOptions.ts +0 -45
- package/nodes/McpClientTool/types.ts +0 -1
- package/nodes/McpClientTool/utils.ts +0 -116
- package/nodes/McpTrigger/FlushingTransport.ts +0 -61
- package/nodes/McpTrigger/McpServer.ts +0 -317
- package/nodes/McpTrigger/McpTrigger.node.ts +0 -204
- package/nodes/McpTrigger/__test__/FlushingTransport.test.ts +0 -102
- package/nodes/McpTrigger/__test__/McpServer.test.ts +0 -532
- package/nodes/McpTrigger/__test__/McpTrigger.node.test.ts +0 -171
- package/nodes/shared/__test__/utils.test.ts +0 -318
- package/nodes/shared/descriptions.ts +0 -65
- package/nodes/shared/helpers.ts +0 -31
- package/nodes/shared/httpProxyAgent.ts +0 -11
- package/nodes/shared/logWrapper.ts +0 -13
- package/nodes/shared/schemaParsing.ts +0 -9
- package/nodes/shared/sharedFields.ts +0 -20
- package/nodes/shared/types.ts +0 -12
- package/nodes/shared/utils.ts +0 -296
- package/officail/package.json +0 -255
- package/tsconfig.json +0 -32
- 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
|
-
});
|