@mastra/mcp 0.11.3-alpha.0 → 0.11.3-alpha.2
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/CHANGELOG.md +18 -0
- package/dist/index.cjs +7 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/package.json +19 -6
- package/.turbo/turbo-build.log +0 -4
- package/eslint.config.js +0 -11
- package/integration-tests/node_modules/.bin/tsc +0 -21
- package/integration-tests/node_modules/.bin/tsserver +0 -21
- package/integration-tests/node_modules/.bin/vitest +0 -21
- package/integration-tests/package.json +0 -29
- package/integration-tests/src/mastra/agents/weather.ts +0 -34
- package/integration-tests/src/mastra/index.ts +0 -15
- package/integration-tests/src/mastra/mcp/index.ts +0 -46
- package/integration-tests/src/mastra/tools/weather.ts +0 -13
- package/integration-tests/src/server.test.ts +0 -238
- package/integration-tests/tsconfig.json +0 -13
- package/integration-tests/vitest.config.ts +0 -14
- package/src/__fixtures__/fire-crawl-complex-schema.ts +0 -1013
- package/src/__fixtures__/server-weather.ts +0 -16
- package/src/__fixtures__/stock-price.ts +0 -128
- package/src/__fixtures__/tools.ts +0 -94
- package/src/__fixtures__/weather.ts +0 -269
- package/src/client/client.test.ts +0 -585
- package/src/client/client.ts +0 -628
- package/src/client/configuration.test.ts +0 -856
- package/src/client/configuration.ts +0 -468
- package/src/client/elicitationActions.ts +0 -26
- package/src/client/index.ts +0 -3
- package/src/client/promptActions.ts +0 -70
- package/src/client/resourceActions.ts +0 -119
- package/src/index.ts +0 -2
- package/src/server/index.ts +0 -2
- package/src/server/promptActions.ts +0 -48
- package/src/server/resourceActions.ts +0 -90
- package/src/server/server-logging.test.ts +0 -181
- package/src/server/server.test.ts +0 -2142
- package/src/server/server.ts +0 -1442
- package/src/server/types.ts +0 -59
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -17
- package/vitest.config.ts +0 -8
|
@@ -1,856 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { openai } from '@ai-sdk/openai';
|
|
4
|
-
import { Agent } from '@mastra/core/agent';
|
|
5
|
-
import { RuntimeContext } from '@mastra/core/di';
|
|
6
|
-
import type { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from 'vitest';
|
|
8
|
-
import { allTools, mcpServerName } from '../__fixtures__/fire-crawl-complex-schema';
|
|
9
|
-
import type { LogHandler, LogMessage } from './client';
|
|
10
|
-
import { MCPClient } from './configuration';
|
|
11
|
-
|
|
12
|
-
vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
13
|
-
|
|
14
|
-
describe('MCPClient', () => {
|
|
15
|
-
let mcp: MCPClient;
|
|
16
|
-
let weatherProcess: ReturnType<typeof spawn>;
|
|
17
|
-
let clients: MCPClient[] = [];
|
|
18
|
-
let weatherServerPort: number;
|
|
19
|
-
|
|
20
|
-
beforeAll(async () => {
|
|
21
|
-
weatherServerPort = 60000 + Math.floor(Math.random() * 1000); // Generate a random port
|
|
22
|
-
// Start the weather SSE server
|
|
23
|
-
weatherProcess = spawn('npx', ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/weather.ts')], {
|
|
24
|
-
env: { ...process.env, WEATHER_SERVER_PORT: String(weatherServerPort) }, // Pass port as env var
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Wait for SSE server to be ready
|
|
28
|
-
let resolved = false;
|
|
29
|
-
await new Promise<void>((resolve, reject) => {
|
|
30
|
-
weatherProcess.on(`exit`, () => {
|
|
31
|
-
if (!resolved) reject();
|
|
32
|
-
});
|
|
33
|
-
if (weatherProcess.stderr) {
|
|
34
|
-
weatherProcess.stderr.on(`data`, chunk => {
|
|
35
|
-
console.error(chunk.toString());
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
if (weatherProcess.stdout) {
|
|
39
|
-
weatherProcess.stdout.on('data', chunk => {
|
|
40
|
-
if (chunk.toString().includes('server is running on SSE')) {
|
|
41
|
-
resolve();
|
|
42
|
-
resolved = true;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
beforeEach(async () => {
|
|
50
|
-
// Give each MCPClient a unique ID to prevent re-initialization errors across tests
|
|
51
|
-
const testId = "testId"
|
|
52
|
-
mcp = new MCPClient({
|
|
53
|
-
id: testId,
|
|
54
|
-
servers: {
|
|
55
|
-
stockPrice: {
|
|
56
|
-
command: 'npx',
|
|
57
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
58
|
-
env: {
|
|
59
|
-
FAKE_CREDS: 'test',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
weather: {
|
|
63
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`), // Use the dynamic port
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
clients.push(mcp);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
afterEach(async () => {
|
|
71
|
-
// Clean up any connected clients
|
|
72
|
-
await mcp.disconnect();
|
|
73
|
-
const index = clients.indexOf(mcp);
|
|
74
|
-
if (index > -1) {
|
|
75
|
-
clients.splice(index, 1);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterAll(async () => {
|
|
80
|
-
// Kill the weather SSE server
|
|
81
|
-
weatherProcess.kill('SIGINT');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('Instance Management', () => {
|
|
85
|
-
it('should initialize with server configurations', () => {
|
|
86
|
-
expect(mcp['serverConfigs']).toEqual({
|
|
87
|
-
stockPrice: {
|
|
88
|
-
command: 'npx',
|
|
89
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
90
|
-
env: {
|
|
91
|
-
FAKE_CREDS: 'test',
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
weather: {
|
|
95
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should get connected tools with namespaced tool names', async () => {
|
|
101
|
-
const connectedTools = await mcp.getTools();
|
|
102
|
-
|
|
103
|
-
// Each tool should be namespaced with its server name
|
|
104
|
-
expect(connectedTools).toHaveProperty('stockPrice_getStockPrice');
|
|
105
|
-
expect(connectedTools).toHaveProperty('weather_getWeather');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should get connected toolsets grouped by server', async () => {
|
|
109
|
-
const connectedToolsets = await mcp.getToolsets();
|
|
110
|
-
|
|
111
|
-
expect(connectedToolsets).toHaveProperty('stockPrice');
|
|
112
|
-
expect(connectedToolsets).toHaveProperty('weather');
|
|
113
|
-
expect(connectedToolsets.stockPrice).toHaveProperty('getStockPrice');
|
|
114
|
-
expect(connectedToolsets.weather).toHaveProperty('getWeather');
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('Resources', () => {
|
|
119
|
-
it('should get resources from connected MCP servers', async () => {
|
|
120
|
-
const resources = await mcp.resources.list();
|
|
121
|
-
|
|
122
|
-
expect(resources).toHaveProperty('weather');
|
|
123
|
-
expect(resources.weather).toBeDefined();
|
|
124
|
-
expect(resources.weather).toHaveLength(3);
|
|
125
|
-
|
|
126
|
-
// Verify that each expected resource exists with the correct structure
|
|
127
|
-
const weatherResources = resources.weather;
|
|
128
|
-
const currentWeather = weatherResources.find(r => r.uri === 'weather://current');
|
|
129
|
-
expect(currentWeather).toBeDefined();
|
|
130
|
-
expect(currentWeather).toMatchObject({
|
|
131
|
-
uri: 'weather://current',
|
|
132
|
-
name: 'Current Weather Data',
|
|
133
|
-
description: expect.any(String),
|
|
134
|
-
mimeType: 'application/json',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const forecast = weatherResources.find(r => r.uri === 'weather://forecast');
|
|
138
|
-
expect(forecast).toBeDefined();
|
|
139
|
-
expect(forecast).toMatchObject({
|
|
140
|
-
uri: 'weather://forecast',
|
|
141
|
-
name: 'Weather Forecast',
|
|
142
|
-
description: expect.any(String),
|
|
143
|
-
mimeType: 'application/json',
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const historical = weatherResources.find(r => r.uri === 'weather://historical');
|
|
147
|
-
expect(historical).toBeDefined();
|
|
148
|
-
expect(historical).toMatchObject({
|
|
149
|
-
uri: 'weather://historical',
|
|
150
|
-
name: 'Historical Weather Data',
|
|
151
|
-
description: expect.any(String),
|
|
152
|
-
mimeType: 'application/json',
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should list resource templates from connected MCP servers', async () => {
|
|
157
|
-
const templates = await mcp.resources.templates();
|
|
158
|
-
expect(templates).toHaveProperty('weather');
|
|
159
|
-
expect(templates.weather).toBeDefined();
|
|
160
|
-
expect(templates.weather.length).toBeGreaterThan(0);
|
|
161
|
-
const customForecastTemplate = templates.weather.find(
|
|
162
|
-
(t: ResourceTemplate) => t.uriTemplate === 'weather://custom/{city}/{days}',
|
|
163
|
-
);
|
|
164
|
-
expect(customForecastTemplate).toBeDefined();
|
|
165
|
-
expect(customForecastTemplate).toMatchObject({
|
|
166
|
-
uriTemplate: 'weather://custom/{city}/{days}',
|
|
167
|
-
name: 'Custom Weather Forecast',
|
|
168
|
-
description: expect.any(String),
|
|
169
|
-
mimeType: 'application/json',
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should read a specific resource from a server', async () => {
|
|
174
|
-
const resourceContent = await mcp.resources.read('weather', 'weather://current');
|
|
175
|
-
expect(resourceContent).toBeDefined();
|
|
176
|
-
expect(resourceContent.contents).toBeInstanceOf(Array);
|
|
177
|
-
expect(resourceContent.contents.length).toBe(1);
|
|
178
|
-
const contentItem = resourceContent.contents[0];
|
|
179
|
-
expect(contentItem.uri).toBe('weather://current');
|
|
180
|
-
expect(contentItem.mimeType).toBe('application/json');
|
|
181
|
-
expect(contentItem.text).toBeDefined();
|
|
182
|
-
let parsedText: any = {};
|
|
183
|
-
if (contentItem.text && typeof contentItem.text === 'string') {
|
|
184
|
-
try {
|
|
185
|
-
parsedText = JSON.parse(contentItem.text);
|
|
186
|
-
} catch {
|
|
187
|
-
// If parsing fails, parsedText remains an empty object
|
|
188
|
-
// console.error("Failed to parse resource content text:", _e);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
expect(parsedText).toHaveProperty('location');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should subscribe and unsubscribe from a resource on a specific server', async () => {
|
|
195
|
-
const serverName = 'weather';
|
|
196
|
-
const resourceUri = 'weather://current';
|
|
197
|
-
|
|
198
|
-
const subResult = await mcp.resources.subscribe(serverName, resourceUri);
|
|
199
|
-
expect(subResult).toEqual({});
|
|
200
|
-
|
|
201
|
-
const unsubResult = await mcp.resources.unsubscribe(serverName, resourceUri);
|
|
202
|
-
expect(unsubResult).toEqual({});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should receive resource updated notification from a specific server', async () => {
|
|
206
|
-
const serverName = 'weather';
|
|
207
|
-
const resourceUri = 'weather://current';
|
|
208
|
-
let notificationReceived = false;
|
|
209
|
-
let receivedUri = '';
|
|
210
|
-
|
|
211
|
-
await mcp.resources.list(); // Initial call to establish connection if needed
|
|
212
|
-
// Create the promise for the notification BEFORE subscribing
|
|
213
|
-
const resourceUpdatedPromise = new Promise<void>((resolve, reject) => {
|
|
214
|
-
mcp.resources.onUpdated(serverName, (params: { uri: string }) => {
|
|
215
|
-
if (params.uri === resourceUri) {
|
|
216
|
-
notificationReceived = true;
|
|
217
|
-
receivedUri = params.uri;
|
|
218
|
-
resolve();
|
|
219
|
-
} else {
|
|
220
|
-
console.log(`[Test LOG] Received update for ${params.uri}, waiting for ${resourceUri}`);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
setTimeout(() => reject(new Error(`Timeout waiting for resourceUpdated notification for ${resourceUri}`)), 4500);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await mcp.resources.subscribe(serverName, resourceUri); // Ensure subscription is active
|
|
227
|
-
|
|
228
|
-
await expect(resourceUpdatedPromise).resolves.toBeUndefined(); // Wait for the notification
|
|
229
|
-
|
|
230
|
-
expect(notificationReceived).toBe(true);
|
|
231
|
-
expect(receivedUri).toBe(resourceUri);
|
|
232
|
-
|
|
233
|
-
await mcp.resources.unsubscribe(serverName, resourceUri); // Cleanup
|
|
234
|
-
}, 5000);
|
|
235
|
-
|
|
236
|
-
it('should receive resource list changed notification from a specific server', async () => {
|
|
237
|
-
const serverName = 'weather';
|
|
238
|
-
let notificationReceived = false;
|
|
239
|
-
|
|
240
|
-
await mcp.resources.list(); // Initial call to establish connection
|
|
241
|
-
|
|
242
|
-
const resourceListChangedPromise = new Promise<void>((resolve, reject) => {
|
|
243
|
-
mcp.resources.onListChanged(serverName, () => {
|
|
244
|
-
notificationReceived = true;
|
|
245
|
-
resolve();
|
|
246
|
-
});
|
|
247
|
-
setTimeout(() => reject(new Error('Timeout waiting for resourceListChanged notification')), 4500);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// In a real scenario, something would trigger the server to send this.
|
|
251
|
-
// For the test, we rely on the interval in weather.ts or a direct call if available.
|
|
252
|
-
// Adding a small delay or an explicit trigger if the fixture supported it would be more robust.
|
|
253
|
-
// For now, we assume the interval in weather.ts will eventually fire it.
|
|
254
|
-
|
|
255
|
-
await expect(resourceListChangedPromise).resolves.toBeUndefined(); // Wait for the notification
|
|
256
|
-
|
|
257
|
-
expect(notificationReceived).toBe(true);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should handle errors when getting resources', async () => {
|
|
261
|
-
const errorClient = new MCPClient({
|
|
262
|
-
id: 'error-test-client',
|
|
263
|
-
servers: {
|
|
264
|
-
weather: {
|
|
265
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
266
|
-
},
|
|
267
|
-
nonexistentServer: {
|
|
268
|
-
command: 'nonexistent-command',
|
|
269
|
-
args: [],
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const resources = await errorClient.resources.list();
|
|
276
|
-
|
|
277
|
-
expect(resources).toHaveProperty('weather');
|
|
278
|
-
expect(resources.weather).toBeDefined();
|
|
279
|
-
expect(resources.weather.length).toBeGreaterThan(0);
|
|
280
|
-
|
|
281
|
-
expect(resources).not.toHaveProperty('nonexistentServer');
|
|
282
|
-
} finally {
|
|
283
|
-
await errorClient.disconnect();
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
describe('Prompts', () => {
|
|
289
|
-
it('should get prompts from connected MCP servers', async () => {
|
|
290
|
-
const prompts = await mcp.prompts.list();
|
|
291
|
-
|
|
292
|
-
expect(prompts).toHaveProperty('weather');
|
|
293
|
-
expect(prompts['weather']).toBeDefined();
|
|
294
|
-
expect(prompts['weather']).toHaveLength(3);
|
|
295
|
-
|
|
296
|
-
// Verify that each expected resource exists with the correct structure
|
|
297
|
-
const promptResources = prompts['weather'];
|
|
298
|
-
const currentWeatherPrompt = promptResources.find(r => r.name === 'current');
|
|
299
|
-
expect(currentWeatherPrompt).toBeDefined();
|
|
300
|
-
expect(currentWeatherPrompt).toMatchObject({
|
|
301
|
-
name: 'current',
|
|
302
|
-
version: 'v1',
|
|
303
|
-
description: expect.any(String),
|
|
304
|
-
mimeType: 'application/json',
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const forecast = promptResources.find(r => r.name === 'forecast');
|
|
308
|
-
expect(forecast).toBeDefined();
|
|
309
|
-
expect(forecast).toMatchObject({
|
|
310
|
-
name: 'forecast',
|
|
311
|
-
version: 'v1',
|
|
312
|
-
description: expect.any(String),
|
|
313
|
-
mimeType: 'application/json',
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const historical = promptResources.find(r => r.name === 'historical');
|
|
317
|
-
expect(historical).toBeDefined();
|
|
318
|
-
expect(historical).toMatchObject({
|
|
319
|
-
name: 'historical',
|
|
320
|
-
version: 'v1',
|
|
321
|
-
description: expect.any(String),
|
|
322
|
-
mimeType: 'application/json',
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('should get a specific prompt from a server', async () => {
|
|
327
|
-
const {prompt, messages} = await mcp.prompts.get({serverName: 'weather', name: 'current'});
|
|
328
|
-
expect(prompt).toBeDefined();
|
|
329
|
-
expect(prompt).toMatchObject({
|
|
330
|
-
name: 'current',
|
|
331
|
-
version: 'v1',
|
|
332
|
-
description: expect.any(String),
|
|
333
|
-
mimeType: 'application/json',
|
|
334
|
-
});
|
|
335
|
-
expect(messages).toBeDefined();
|
|
336
|
-
const messageItem = messages[0];
|
|
337
|
-
let parsedText: any = {};
|
|
338
|
-
if (messageItem.content.text && typeof messageItem.content.text === 'string') {
|
|
339
|
-
try {
|
|
340
|
-
parsedText = JSON.parse(messageItem.content.text);
|
|
341
|
-
} catch {
|
|
342
|
-
// If parsing fails, parsedText remains an empty object
|
|
343
|
-
// console.error("Failed to parse resource content text:", _e);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
expect(parsedText).toHaveProperty('location');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('should receive prompt list changed notification from a specific server', async () => {
|
|
350
|
-
const serverName = 'weather';
|
|
351
|
-
let notificationReceived = false;
|
|
352
|
-
|
|
353
|
-
await mcp.prompts.list();
|
|
354
|
-
|
|
355
|
-
const promptListChangedPromise = new Promise<void>((resolve, reject) => {
|
|
356
|
-
mcp.prompts.onListChanged(serverName, () => {
|
|
357
|
-
notificationReceived = true;
|
|
358
|
-
resolve();
|
|
359
|
-
});
|
|
360
|
-
setTimeout(() => reject(new Error('Timeout waiting for promptListChanged notification')), 4500);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
await expect(promptListChangedPromise).resolves.toBeUndefined();
|
|
364
|
-
|
|
365
|
-
expect(notificationReceived).toBe(true);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('should handle errors when getting prompts', async () => {
|
|
369
|
-
const errorClient = new MCPClient({
|
|
370
|
-
id: 'error-test-client',
|
|
371
|
-
servers: {
|
|
372
|
-
weather: {
|
|
373
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
374
|
-
},
|
|
375
|
-
nonexistentServer: {
|
|
376
|
-
command: 'nonexistent-command',
|
|
377
|
-
args: [],
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const prompts = await errorClient.prompts.list();
|
|
384
|
-
|
|
385
|
-
expect(prompts).toHaveProperty('weather');
|
|
386
|
-
expect(prompts['weather']).toBeDefined();
|
|
387
|
-
expect(prompts['weather'].length).toBeGreaterThan(0);
|
|
388
|
-
|
|
389
|
-
expect(prompts).not.toHaveProperty('nonexistentServer');
|
|
390
|
-
} finally {
|
|
391
|
-
await errorClient.disconnect();
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
describe('Instance Management', () => {
|
|
397
|
-
it('should allow multiple instances with different IDs', async () => {
|
|
398
|
-
const config2 = new MCPClient({
|
|
399
|
-
id: 'custom-id',
|
|
400
|
-
servers: {
|
|
401
|
-
stockPrice: {
|
|
402
|
-
command: 'npx',
|
|
403
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
404
|
-
env: {
|
|
405
|
-
FAKE_CREDS: 'test',
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
expect(config2).not.toBe(mcp);
|
|
412
|
-
await config2.disconnect();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('should allow reuse of configuration after closing', async () => {
|
|
416
|
-
await mcp.disconnect();
|
|
417
|
-
|
|
418
|
-
const config2 = new MCPClient({
|
|
419
|
-
servers: {
|
|
420
|
-
stockPrice: {
|
|
421
|
-
command: 'npx',
|
|
422
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
423
|
-
env: {
|
|
424
|
-
FAKE_CREDS: 'test',
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
weather: {
|
|
428
|
-
url: new URL(`http://localhost:${weatherServerPort}/sse`),
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
expect(config2).not.toBe(mcp);
|
|
434
|
-
await config2.disconnect();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('should throw error when creating duplicate instance without ID', async () => {
|
|
438
|
-
const existingConfig = new MCPClient({
|
|
439
|
-
servers: {
|
|
440
|
-
stockPrice: {
|
|
441
|
-
command: 'npx',
|
|
442
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
443
|
-
env: {
|
|
444
|
-
FAKE_CREDS: 'test',
|
|
445
|
-
},
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
expect(
|
|
451
|
-
() =>
|
|
452
|
-
new MCPClient({
|
|
453
|
-
servers: {
|
|
454
|
-
stockPrice: {
|
|
455
|
-
command: 'npx',
|
|
456
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
457
|
-
env: {
|
|
458
|
-
FAKE_CREDS: 'test',
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
}),
|
|
463
|
-
).toThrow(/MCPClient was initialized multiple times/);
|
|
464
|
-
|
|
465
|
-
await existingConfig.disconnect();
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
describe('MCPClient Operation Timeouts', () => {
|
|
469
|
-
it('should respect custom timeout in configuration', async () => {
|
|
470
|
-
const config = new MCPClient({
|
|
471
|
-
id: 'test-timeout-config',
|
|
472
|
-
timeout: 3000, // 3 second timeout
|
|
473
|
-
servers: {
|
|
474
|
-
test: {
|
|
475
|
-
command: 'node',
|
|
476
|
-
args: [
|
|
477
|
-
'-e',
|
|
478
|
-
`
|
|
479
|
-
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
480
|
-
const server = new Server({ name: 'test', version: '1.0.0' });
|
|
481
|
-
setTimeout(() => process.exit(0), 2000); // 2 second delay
|
|
482
|
-
`,
|
|
483
|
-
],
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const error = await config.getTools().catch(e => e);
|
|
489
|
-
expect(error).toBeDefined(); // Will throw since server exits before responding
|
|
490
|
-
expect(error.message).not.toMatch(/Request timed out/);
|
|
491
|
-
|
|
492
|
-
await config.disconnect();
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('should respect per-server timeout override', async () => {
|
|
496
|
-
const config = new MCPClient({
|
|
497
|
-
id: 'test-server-timeout-config',
|
|
498
|
-
timeout: 500, // Global timeout of 500ms
|
|
499
|
-
servers: {
|
|
500
|
-
test: {
|
|
501
|
-
command: 'node',
|
|
502
|
-
args: [
|
|
503
|
-
'-e',
|
|
504
|
-
`
|
|
505
|
-
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
506
|
-
const server = new Server({ name: 'test', version: '1.0.0' });
|
|
507
|
-
setTimeout(() => process.exit(0), 2000); // 2 second delay
|
|
508
|
-
`,
|
|
509
|
-
],
|
|
510
|
-
timeout: 3000, // Server-specific timeout of 3s
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// This should succeed since server timeout (3s) is longer than delay (2s)
|
|
516
|
-
const error = await config.getTools().catch(e => e);
|
|
517
|
-
expect(error).toBeDefined(); // Will throw since server exits before responding
|
|
518
|
-
expect(error.message).not.toMatch(/Request timed out/);
|
|
519
|
-
|
|
520
|
-
await config.disconnect();
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
describe('MCPClient Connection Timeout', () => {
|
|
525
|
-
it('should throw timeout error for slow starting server', async () => {
|
|
526
|
-
const slowConfig = new MCPClient({
|
|
527
|
-
id: 'test-slow-server',
|
|
528
|
-
servers: {
|
|
529
|
-
slowServer: {
|
|
530
|
-
command: 'node',
|
|
531
|
-
args: ['-e', 'setTimeout(() => process.exit(0), 65000)'], // Simulate a server that takes 65 seconds to start
|
|
532
|
-
timeout: 1000,
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
await expect(slowConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
538
|
-
await slowConfig.disconnect();
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('timeout should be longer than configured timeout', async () => {
|
|
542
|
-
const slowConfig = new MCPClient({
|
|
543
|
-
id: 'test-slow-server',
|
|
544
|
-
timeout: 2000,
|
|
545
|
-
servers: {
|
|
546
|
-
slowServer: {
|
|
547
|
-
command: 'node',
|
|
548
|
-
args: ['-e', 'setTimeout(() => process.exit(0), 1000)'], // Simulate a server that takes 1 second to start
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
const error = await slowConfig.getTools().catch(e => e);
|
|
554
|
-
expect(error).toBeDefined();
|
|
555
|
-
expect(error.message).not.toMatch(/Request timed out/);
|
|
556
|
-
await slowConfig.disconnect();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it('should respect per-server timeout configuration', async () => {
|
|
560
|
-
const mixedConfig = new MCPClient({
|
|
561
|
-
id: 'test-mixed-timeout',
|
|
562
|
-
timeout: 1000, // Short global timeout
|
|
563
|
-
servers: {
|
|
564
|
-
quickServer: {
|
|
565
|
-
command: 'node',
|
|
566
|
-
args: ['-e', 'setTimeout(() => process.exit(0), 2000)'], // Takes 2 seconds to exit
|
|
567
|
-
},
|
|
568
|
-
slowServer: {
|
|
569
|
-
command: 'node',
|
|
570
|
-
args: ['-e', 'setTimeout(() => process.exit(0), 2000)'], // Takes 2 seconds to exit
|
|
571
|
-
timeout: 3000, // But has a longer timeout
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// Quick server should timeout
|
|
577
|
-
await expect(mixedConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
578
|
-
await mixedConfig.disconnect();
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
it('should handle connection errors gracefully', async () => {
|
|
582
|
-
const badConfig = new MCPClient({
|
|
583
|
-
servers: {
|
|
584
|
-
badServer: {
|
|
585
|
-
command: 'nonexistent-command',
|
|
586
|
-
args: [],
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
await expect(badConfig.getTools()).rejects.toThrow();
|
|
592
|
-
await badConfig.disconnect();
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
describe('Schema Handling', () => {
|
|
597
|
-
let complexClient: MCPClient;
|
|
598
|
-
let mockLogHandler: LogHandler & ReturnType<typeof vi.fn>;
|
|
599
|
-
|
|
600
|
-
beforeEach(async () => {
|
|
601
|
-
mockLogHandler = vi.fn();
|
|
602
|
-
|
|
603
|
-
complexClient = new MCPClient({
|
|
604
|
-
id: 'complex-schema-test-client-log-handler-firecrawl',
|
|
605
|
-
servers: {
|
|
606
|
-
'firecrawl-mcp': {
|
|
607
|
-
command: 'npx',
|
|
608
|
-
args: ['-y', 'tsx', path.join(__dirname, '..', '__fixtures__/fire-crawl-complex-schema.ts')],
|
|
609
|
-
logger: mockLogHandler,
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
afterEach(async () => {
|
|
616
|
-
mockLogHandler.mockClear();
|
|
617
|
-
await complexClient?.disconnect().catch(() => { });
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it('should process tools from firecrawl-mcp without crashing', async () => {
|
|
621
|
-
const tools = await complexClient.getTools();
|
|
622
|
-
|
|
623
|
-
Object.keys(allTools).forEach(toolName => {
|
|
624
|
-
expect(tools).toHaveProperty(`${mcpServerName.replace(`-fixture`, ``)}_${toolName}`);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
expect(mockLogHandler.mock.calls.length).toBeGreaterThan(0);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
describe('MCPClient Configuration', () => {
|
|
632
|
-
let clientsToCleanup: MCPClient[] = [];
|
|
633
|
-
|
|
634
|
-
afterEach(async () => {
|
|
635
|
-
await Promise.all(
|
|
636
|
-
clientsToCleanup.map(client =>
|
|
637
|
-
client.disconnect().catch(e => console.error(`Error disconnecting client during test cleanup: ${e}`)),
|
|
638
|
-
),
|
|
639
|
-
);
|
|
640
|
-
clientsToCleanup = []; // Reset for the next test
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
it('should pass runtimeContext to the server logger function during tool execution', async () => {
|
|
644
|
-
type TestContext = { channel: string; userId: string };
|
|
645
|
-
const testContextInstance = new RuntimeContext<TestContext>();
|
|
646
|
-
testContextInstance.set('channel', 'test-channel-123');
|
|
647
|
-
testContextInstance.set('userId', 'user-abc-987');
|
|
648
|
-
const loggerFn = vi.fn();
|
|
649
|
-
|
|
650
|
-
const clientForTest = new MCPClient({
|
|
651
|
-
servers: {
|
|
652
|
-
stockPrice: {
|
|
653
|
-
command: 'npx',
|
|
654
|
-
args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
655
|
-
env: { FAKE_CREDS: 'test' },
|
|
656
|
-
logger: loggerFn,
|
|
657
|
-
},
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
clientsToCleanup.push(clientForTest);
|
|
661
|
-
|
|
662
|
-
const tools = await clientForTest.getTools();
|
|
663
|
-
const stockTool = tools['stockPrice_getStockPrice'];
|
|
664
|
-
expect(stockTool).toBeDefined();
|
|
665
|
-
|
|
666
|
-
await stockTool.execute({
|
|
667
|
-
context: { symbol: 'MSFT' },
|
|
668
|
-
runtimeContext: testContextInstance,
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
expect(loggerFn).toHaveBeenCalled();
|
|
672
|
-
const callWithContext = loggerFn.mock.calls.find(call => {
|
|
673
|
-
const logMessage = call[0] as LogMessage;
|
|
674
|
-
return (
|
|
675
|
-
logMessage.runtimeContext &&
|
|
676
|
-
typeof logMessage.runtimeContext.get === 'function' &&
|
|
677
|
-
logMessage.runtimeContext.get('channel') === 'test-channel-123' &&
|
|
678
|
-
logMessage.runtimeContext.get('userId') === 'user-abc-987'
|
|
679
|
-
);
|
|
680
|
-
});
|
|
681
|
-
expect(callWithContext).toBeDefined();
|
|
682
|
-
const capturedLogMessage = callWithContext?.[0] as LogMessage;
|
|
683
|
-
expect(capturedLogMessage?.serverName).toEqual('stockPrice');
|
|
684
|
-
}, 15000);
|
|
685
|
-
|
|
686
|
-
it('should pass runtimeContext to MCP logger when tool is called via an Agent', async () => {
|
|
687
|
-
type TestAgentContext = { traceId: string; tenant: string };
|
|
688
|
-
const agentTestContext = new RuntimeContext<TestAgentContext>();
|
|
689
|
-
agentTestContext.set('traceId', 'agent-trace-xyz');
|
|
690
|
-
agentTestContext.set('tenant', 'acme-corp');
|
|
691
|
-
const loggerFn = vi.fn();
|
|
692
|
-
|
|
693
|
-
const mcpClientForAgentTest = new MCPClient({
|
|
694
|
-
id: 'mcp-for-agent-test-suite',
|
|
695
|
-
servers: {
|
|
696
|
-
stockPriceServer: {
|
|
697
|
-
command: 'npx',
|
|
698
|
-
args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
699
|
-
env: { FAKE_CREDS: 'test' },
|
|
700
|
-
logger: loggerFn,
|
|
701
|
-
},
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
clientsToCleanup.push(mcpClientForAgentTest);
|
|
705
|
-
|
|
706
|
-
const agentName = 'stockAgentForContextTest';
|
|
707
|
-
const agent = new Agent({
|
|
708
|
-
name: agentName,
|
|
709
|
-
model: openai('gpt-4o'),
|
|
710
|
-
instructions: 'Use the getStockPrice tool to find the price of MSFT.',
|
|
711
|
-
tools: await mcpClientForAgentTest.getTools(),
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
await agent.generate('What is the price of MSFT?', { runtimeContext: agentTestContext });
|
|
715
|
-
|
|
716
|
-
expect(loggerFn).toHaveBeenCalled();
|
|
717
|
-
const callWithAgentContext = loggerFn.mock.calls.find(call => {
|
|
718
|
-
const logMessage = call[0] as LogMessage;
|
|
719
|
-
return (
|
|
720
|
-
logMessage.runtimeContext &&
|
|
721
|
-
typeof logMessage.runtimeContext.get === 'function' &&
|
|
722
|
-
logMessage.runtimeContext.get('traceId') === 'agent-trace-xyz' &&
|
|
723
|
-
logMessage.runtimeContext.get('tenant') === 'acme-corp'
|
|
724
|
-
);
|
|
725
|
-
});
|
|
726
|
-
expect(callWithAgentContext).toBeDefined();
|
|
727
|
-
if (callWithAgentContext) {
|
|
728
|
-
const capturedLogMessage = callWithAgentContext[0] as LogMessage;
|
|
729
|
-
expect(capturedLogMessage?.serverName).toEqual('stockPriceServer');
|
|
730
|
-
}
|
|
731
|
-
}, 20000);
|
|
732
|
-
|
|
733
|
-
it('should correctly use different runtimeContexts on sequential direct tool calls', async () => {
|
|
734
|
-
const loggerFn = vi.fn();
|
|
735
|
-
const clientForSeqTest = new MCPClient({
|
|
736
|
-
id: 'mcp-sequential-context-test',
|
|
737
|
-
servers: {
|
|
738
|
-
stockPriceServer: {
|
|
739
|
-
command: 'npx',
|
|
740
|
-
args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')],
|
|
741
|
-
env: { FAKE_CREDS: 'test' },
|
|
742
|
-
logger: loggerFn,
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
});
|
|
746
|
-
clientsToCleanup.push(clientForSeqTest);
|
|
747
|
-
|
|
748
|
-
const tools = await clientForSeqTest.getTools();
|
|
749
|
-
const stockTool = tools['stockPriceServer_getStockPrice'];
|
|
750
|
-
expect(stockTool).toBeDefined();
|
|
751
|
-
|
|
752
|
-
type ContextA = { callId: string };
|
|
753
|
-
const runtimeContextA = new RuntimeContext<ContextA>();
|
|
754
|
-
runtimeContextA.set('callId', 'call-A-111');
|
|
755
|
-
await stockTool.execute({ context: { symbol: 'MSFT' }, runtimeContext: runtimeContextA });
|
|
756
|
-
|
|
757
|
-
expect(loggerFn).toHaveBeenCalled();
|
|
758
|
-
let callsAfterA = [...loggerFn.mock.calls];
|
|
759
|
-
const logCallForA = callsAfterA.find(
|
|
760
|
-
call => (call[0] as LogMessage).runtimeContext?.get('callId') === 'call-A-111',
|
|
761
|
-
);
|
|
762
|
-
expect(logCallForA).toBeDefined();
|
|
763
|
-
expect((logCallForA?.[0] as LogMessage)?.runtimeContext?.get('callId')).toBe('call-A-111');
|
|
764
|
-
|
|
765
|
-
loggerFn.mockClear();
|
|
766
|
-
|
|
767
|
-
type ContextB = { sessionId: string };
|
|
768
|
-
const runtimeContextB = new RuntimeContext<ContextB>();
|
|
769
|
-
runtimeContextB.set('sessionId', 'session-B-222');
|
|
770
|
-
await stockTool.execute({ context: { symbol: 'GOOG' }, runtimeContext: runtimeContextB });
|
|
771
|
-
|
|
772
|
-
expect(loggerFn).toHaveBeenCalled();
|
|
773
|
-
let callsAfterB = [...loggerFn.mock.calls];
|
|
774
|
-
const logCallForB = callsAfterB.find(
|
|
775
|
-
call => (call[0] as LogMessage).runtimeContext?.get('sessionId') === 'session-B-222',
|
|
776
|
-
);
|
|
777
|
-
expect(logCallForB).toBeDefined();
|
|
778
|
-
expect((logCallForB?.[0] as LogMessage)?.runtimeContext?.get('sessionId')).toBe('session-B-222');
|
|
779
|
-
|
|
780
|
-
const contextALeak = callsAfterB.some(
|
|
781
|
-
call => (call[0] as LogMessage).runtimeContext?.get('callId') === 'call-A-111',
|
|
782
|
-
);
|
|
783
|
-
expect(contextALeak).toBe(false);
|
|
784
|
-
}, 20000);
|
|
785
|
-
|
|
786
|
-
it('should isolate runtimeContext between different servers on the same MCPClient', async () => {
|
|
787
|
-
const sharedLoggerFn = vi.fn();
|
|
788
|
-
|
|
789
|
-
const clientWithTwoServers = new MCPClient({
|
|
790
|
-
id: 'mcp-multi-server-context-isolation',
|
|
791
|
-
servers: {
|
|
792
|
-
serverX: {
|
|
793
|
-
command: 'npx',
|
|
794
|
-
args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')], // Re-use fixture, tool name will differ by server
|
|
795
|
-
logger: sharedLoggerFn,
|
|
796
|
-
env: { FAKE_CREDS: 'serverX-creds' }, // Make env slightly different for clarity if needed
|
|
797
|
-
},
|
|
798
|
-
serverY: {
|
|
799
|
-
command: 'npx',
|
|
800
|
-
args: ['tsx', path.join(__dirname, '..', '__fixtures__/stock-price.ts')], // Re-use fixture
|
|
801
|
-
logger: sharedLoggerFn,
|
|
802
|
-
env: { FAKE_CREDS: 'serverY-creds' },
|
|
803
|
-
},
|
|
804
|
-
},
|
|
805
|
-
});
|
|
806
|
-
clientsToCleanup.push(clientWithTwoServers);
|
|
807
|
-
|
|
808
|
-
const tools = await clientWithTwoServers.getTools();
|
|
809
|
-
const toolX = tools['serverX_getStockPrice'];
|
|
810
|
-
const toolY = tools['serverY_getStockPrice'];
|
|
811
|
-
expect(toolX).toBeDefined();
|
|
812
|
-
expect(toolY).toBeDefined();
|
|
813
|
-
|
|
814
|
-
// --- Call tool on Server X with contextX ---
|
|
815
|
-
type ContextX = { requestId: string };
|
|
816
|
-
const runtimeContextX = new RuntimeContext<ContextX>();
|
|
817
|
-
runtimeContextX.set('requestId', 'req-X-001');
|
|
818
|
-
|
|
819
|
-
await toolX.execute({ context: { symbol: 'AAA' }, runtimeContext: runtimeContextX });
|
|
820
|
-
|
|
821
|
-
expect(sharedLoggerFn).toHaveBeenCalled();
|
|
822
|
-
let callsAfterToolX = [...sharedLoggerFn.mock.calls];
|
|
823
|
-
const logCallForX = callsAfterToolX.find(call => {
|
|
824
|
-
const logMessage = call[0] as LogMessage;
|
|
825
|
-
return logMessage.serverName === 'serverX' && logMessage.runtimeContext?.get('requestId') === 'req-X-001';
|
|
826
|
-
});
|
|
827
|
-
expect(logCallForX).toBeDefined();
|
|
828
|
-
expect((logCallForX?.[0] as LogMessage)?.runtimeContext?.get('requestId')).toBe('req-X-001');
|
|
829
|
-
|
|
830
|
-
sharedLoggerFn.mockClear(); // Clear for next distinct operation
|
|
831
|
-
|
|
832
|
-
// --- Call tool on Server Y with contextY ---
|
|
833
|
-
type ContextY = { customerId: string };
|
|
834
|
-
const runtimeContextY = new RuntimeContext<ContextY>();
|
|
835
|
-
runtimeContextY.set('customerId', 'cust-Y-002');
|
|
836
|
-
|
|
837
|
-
await toolY.execute({ context: { symbol: 'BBB' }, runtimeContext: runtimeContextY });
|
|
838
|
-
|
|
839
|
-
expect(sharedLoggerFn).toHaveBeenCalled();
|
|
840
|
-
let callsAfterToolY = [...sharedLoggerFn.mock.calls];
|
|
841
|
-
const logCallForY = callsAfterToolY.find(call => {
|
|
842
|
-
const logMessage = call[0] as LogMessage;
|
|
843
|
-
return logMessage.serverName === 'serverY' && logMessage.runtimeContext?.get('customerId') === 'cust-Y-002';
|
|
844
|
-
});
|
|
845
|
-
expect(logCallForY).toBeDefined();
|
|
846
|
-
expect((logCallForY?.[0] as LogMessage)?.runtimeContext?.get('customerId')).toBe('cust-Y-002');
|
|
847
|
-
|
|
848
|
-
// Ensure contextX did not leak into logs from serverY's operation
|
|
849
|
-
const contextXLeakInYLogs = callsAfterToolY.some(call => {
|
|
850
|
-
const logMessage = call[0] as LogMessage;
|
|
851
|
-
return logMessage.runtimeContext?.get('requestId') === 'req-X-001';
|
|
852
|
-
});
|
|
853
|
-
expect(contextXLeakInYLogs).toBe(false);
|
|
854
|
-
}, 25000); // Increased timeout for multiple server ops
|
|
855
|
-
});
|
|
856
|
-
});
|