@mastra/mcp 0.5.0-alpha.3 → 0.5.0-alpha.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +18 -0
- package/dist/_tsup-dts-rollup.d.cts +29 -1
- package/dist/_tsup-dts-rollup.d.ts +29 -1
- package/dist/index.cjs +227 -42
- package/dist/index.js +227 -41
- package/package.json +8 -3
- package/src/__fixtures__/fire-crawl-complex-schema.ts +1013 -0
- package/src/client.test.ts +4 -4
- package/src/client.ts +77 -38
- package/src/configuration.test.ts +272 -0
- package/src/server.test.ts +183 -23
- package/src/server.ts +86 -10
package/src/client.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
8
8
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { InternalMastraMCPClient } from './client.js';
|
|
12
12
|
|
|
13
13
|
async function setupTestServer(withSessionManagement: boolean) {
|
|
14
14
|
const httpServer: HttpServer = createServer();
|
|
@@ -62,12 +62,12 @@ describe('MastraMCPClient with Streamable HTTP', () => {
|
|
|
62
62
|
serverTransport: StreamableHTTPServerTransport;
|
|
63
63
|
baseUrl: URL;
|
|
64
64
|
};
|
|
65
|
-
let client:
|
|
65
|
+
let client: InternalMastraMCPClient;
|
|
66
66
|
|
|
67
67
|
describe('Stateless Mode', () => {
|
|
68
68
|
beforeEach(async () => {
|
|
69
69
|
testServer = await setupTestServer(false);
|
|
70
|
-
client = new
|
|
70
|
+
client = new InternalMastraMCPClient({
|
|
71
71
|
name: 'test-stateless-client',
|
|
72
72
|
server: {
|
|
73
73
|
url: testServer.baseUrl,
|
|
@@ -99,7 +99,7 @@ describe('MastraMCPClient with Streamable HTTP', () => {
|
|
|
99
99
|
describe('Stateful Mode', () => {
|
|
100
100
|
beforeEach(async () => {
|
|
101
101
|
testServer = await setupTestServer(true);
|
|
102
|
-
client = new
|
|
102
|
+
client = new InternalMastraMCPClient({
|
|
103
103
|
name: 'test-stateful-client',
|
|
104
104
|
server: {
|
|
105
105
|
url: testServer.baseUrl,
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MastraBase } from '@mastra/core/base';
|
|
2
|
+
import type { RuntimeContext } from '@mastra/core/di';
|
|
2
3
|
import { createTool } from '@mastra/core/tools';
|
|
3
|
-
import { isZodType
|
|
4
|
+
import { isZodType } from '@mastra/core/utils';
|
|
4
5
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
6
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
7
|
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
@@ -14,9 +15,9 @@ import type { ClientCapabilities, LoggingLevel } from '@modelcontextprotocol/sdk
|
|
|
14
15
|
import { CallToolResultSchema, ListResourcesResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
16
|
|
|
16
17
|
import { asyncExitHook, gracefulExit } from 'exit-hook';
|
|
17
|
-
import jsonSchemaToZod from 'json-schema-to-zod';
|
|
18
|
-
import type { JsonSchema } from 'json-schema-to-zod';
|
|
19
18
|
import { z } from 'zod';
|
|
19
|
+
import { jsonSchemaObjectToZodRawShape } from 'zod-from-json-schema';
|
|
20
|
+
import type { JSONSchema } from 'zod-from-json-schema';
|
|
20
21
|
|
|
21
22
|
// Re-export MCP SDK LoggingLevel for convenience
|
|
22
23
|
export type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -27,6 +28,7 @@ export interface LogMessage {
|
|
|
27
28
|
timestamp: Date;
|
|
28
29
|
serverName: string;
|
|
29
30
|
details?: Record<string, any>;
|
|
31
|
+
runtimeContext?: RuntimeContext | null;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export type LogHandler = (logMessage: LogMessage) => void;
|
|
@@ -107,6 +109,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
107
109
|
private enableServerLogs?: boolean;
|
|
108
110
|
private serverConfig: MastraMCPServerDefinition;
|
|
109
111
|
private transport?: Transport;
|
|
112
|
+
private currentOperationContext: RuntimeContext | null = null;
|
|
110
113
|
|
|
111
114
|
constructor({
|
|
112
115
|
name,
|
|
@@ -159,6 +162,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
159
162
|
timestamp: new Date(),
|
|
160
163
|
serverName: this.name,
|
|
161
164
|
details,
|
|
165
|
+
runtimeContext: this.currentOperationContext,
|
|
162
166
|
});
|
|
163
167
|
}
|
|
164
168
|
}
|
|
@@ -323,11 +327,35 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
323
327
|
}
|
|
324
328
|
|
|
325
329
|
private convertInputSchema(
|
|
326
|
-
inputSchema: Awaited<ReturnType<Client['listTools']>>['tools'][0]['inputSchema'] |
|
|
330
|
+
inputSchema: Awaited<ReturnType<Client['listTools']>>['tools'][0]['inputSchema'] | JSONSchema,
|
|
327
331
|
): z.ZodType {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
332
|
+
if (isZodType(inputSchema)) {
|
|
333
|
+
return inputSchema;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Assuming inputSchema is a JSONSchema object type for tool inputs
|
|
338
|
+
const rawShape = jsonSchemaObjectToZodRawShape(inputSchema as JSONSchema);
|
|
339
|
+
return z.object(rawShape); // Wrap the raw shape to return a ZodType (object)
|
|
340
|
+
} catch (error: unknown) {
|
|
341
|
+
let errorDetails: string | undefined;
|
|
342
|
+
if (error instanceof Error) {
|
|
343
|
+
errorDetails = error.stack;
|
|
344
|
+
} else {
|
|
345
|
+
// Attempt to stringify, fallback to String()
|
|
346
|
+
try {
|
|
347
|
+
errorDetails = JSON.stringify(error);
|
|
348
|
+
} catch {
|
|
349
|
+
errorDetails = String(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this.log('error', 'Failed to convert JSON schema to Zod schema using zodFromJsonSchema', {
|
|
353
|
+
error: errorDetails,
|
|
354
|
+
originalJsonSchema: inputSchema,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
throw new Error(errorDetails);
|
|
358
|
+
}
|
|
331
359
|
}
|
|
332
360
|
|
|
333
361
|
async tools() {
|
|
@@ -336,38 +364,49 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
336
364
|
const toolsRes: Record<string, any> = {};
|
|
337
365
|
tools.forEach(tool => {
|
|
338
366
|
this.log('debug', `Processing tool: ${tool.name}`);
|
|
367
|
+
try {
|
|
368
|
+
const mastraTool = createTool({
|
|
369
|
+
id: `${this.name}_${tool.name}`,
|
|
370
|
+
description: tool.description || '',
|
|
371
|
+
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
372
|
+
execute: async ({ context, runtimeContext }: { context: any; runtimeContext?: RuntimeContext | null }) => {
|
|
373
|
+
const previousContext = this.currentOperationContext;
|
|
374
|
+
this.currentOperationContext = runtimeContext || null; // Set current context
|
|
375
|
+
try {
|
|
376
|
+
this.log('debug', `Executing tool: ${tool.name}`, { toolArgs: context });
|
|
377
|
+
const res = await this.client.callTool(
|
|
378
|
+
{
|
|
379
|
+
name: tool.name,
|
|
380
|
+
arguments: context,
|
|
381
|
+
},
|
|
382
|
+
CallToolResultSchema,
|
|
383
|
+
{
|
|
384
|
+
timeout: this.timeout,
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
this.log('debug', `Tool executed successfully: ${tool.name}`);
|
|
388
|
+
return res;
|
|
389
|
+
} catch (e) {
|
|
390
|
+
this.log('error', `Error calling tool: ${tool.name}`, {
|
|
391
|
+
error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
|
|
392
|
+
toolArgs: context,
|
|
393
|
+
});
|
|
394
|
+
throw e;
|
|
395
|
+
} finally {
|
|
396
|
+
this.currentOperationContext = previousContext; // Restore previous context
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
});
|
|
339
400
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
name: tool.name,
|
|
350
|
-
arguments: context,
|
|
351
|
-
},
|
|
352
|
-
CallToolResultSchema,
|
|
353
|
-
{
|
|
354
|
-
timeout: this.timeout,
|
|
355
|
-
},
|
|
356
|
-
);
|
|
357
|
-
this.log('debug', `Tool executed successfully: ${tool.name}`);
|
|
358
|
-
return res;
|
|
359
|
-
} catch (e) {
|
|
360
|
-
this.log('error', `Error calling tool: ${tool.name}`, {
|
|
361
|
-
error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
|
|
362
|
-
toolArgs: context,
|
|
363
|
-
});
|
|
364
|
-
throw e;
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (tool.name) {
|
|
370
|
-
toolsRes[tool.name] = mastraTool;
|
|
401
|
+
if (tool.name) {
|
|
402
|
+
toolsRes[tool.name] = mastraTool;
|
|
403
|
+
}
|
|
404
|
+
} catch (toolCreationError: unknown) {
|
|
405
|
+
// Catch errors during tool creation itself (e.g., if createTool has issues)
|
|
406
|
+
this.log('error', `Failed to create Mastra tool wrapper for MCP tool: ${tool.name}`, {
|
|
407
|
+
error: toolCreationError instanceof Error ? toolCreationError.stack : String(toolCreationError),
|
|
408
|
+
mcpToolDefinition: tool,
|
|
409
|
+
});
|
|
371
410
|
}
|
|
372
411
|
});
|
|
373
412
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
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';
|
|
3
6
|
import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from 'vitest';
|
|
7
|
+
import { allTools, mcpServerName } from './__fixtures__/fire-crawl-complex-schema';
|
|
8
|
+
import type { LogHandler, LogMessage } from './client';
|
|
4
9
|
import { MCPClient } from './configuration';
|
|
5
10
|
|
|
6
11
|
vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
@@ -8,6 +13,7 @@ vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
|
8
13
|
describe('MCPClient', () => {
|
|
9
14
|
let mcp: MCPClient;
|
|
10
15
|
let weatherProcess: ReturnType<typeof spawn>;
|
|
16
|
+
let clients: MCPClient[] = [];
|
|
11
17
|
|
|
12
18
|
beforeAll(async () => {
|
|
13
19
|
// Start the weather SSE server
|
|
@@ -50,11 +56,16 @@ describe('MCPClient', () => {
|
|
|
50
56
|
},
|
|
51
57
|
},
|
|
52
58
|
});
|
|
59
|
+
clients.push(mcp);
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
afterEach(async () => {
|
|
56
63
|
// Clean up any connected clients
|
|
57
64
|
await mcp.disconnect();
|
|
65
|
+
const index = clients.indexOf(mcp);
|
|
66
|
+
if (index > -1) {
|
|
67
|
+
clients.splice(index, 1);
|
|
68
|
+
}
|
|
58
69
|
});
|
|
59
70
|
|
|
60
71
|
afterAll(async () => {
|
|
@@ -357,4 +368,265 @@ describe('MCPClient', () => {
|
|
|
357
368
|
await mixedConfig.disconnect();
|
|
358
369
|
});
|
|
359
370
|
});
|
|
371
|
+
|
|
372
|
+
describe('Schema Handling', () => {
|
|
373
|
+
let complexClient: MCPClient;
|
|
374
|
+
let mockLogHandler: LogHandler & ReturnType<typeof vi.fn>;
|
|
375
|
+
|
|
376
|
+
beforeEach(async () => {
|
|
377
|
+
mockLogHandler = vi.fn();
|
|
378
|
+
|
|
379
|
+
complexClient = new MCPClient({
|
|
380
|
+
id: 'complex-schema-test-client-log-handler-firecrawl',
|
|
381
|
+
servers: {
|
|
382
|
+
'firecrawl-mcp': {
|
|
383
|
+
command: 'npx',
|
|
384
|
+
args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/fire-crawl-complex-schema.ts')],
|
|
385
|
+
logger: mockLogHandler,
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
afterEach(async () => {
|
|
392
|
+
mockLogHandler.mockClear();
|
|
393
|
+
await complexClient?.disconnect().catch(() => {});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should process tools from firecrawl-mcp without crashing', async () => {
|
|
397
|
+
const tools = await complexClient.getTools();
|
|
398
|
+
|
|
399
|
+
Object.keys(allTools).forEach(toolName => {
|
|
400
|
+
expect(tools).toHaveProperty(`${mcpServerName.replace(`-fixture`, ``)}_${toolName}`);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(mockLogHandler.mock.calls.length).toBeGreaterThan(0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('MCPClient Configuration', () => {
|
|
408
|
+
let clientsToCleanup: MCPClient[] = [];
|
|
409
|
+
|
|
410
|
+
afterEach(async () => {
|
|
411
|
+
await Promise.all(
|
|
412
|
+
clientsToCleanup.map(client =>
|
|
413
|
+
client.disconnect().catch(e => console.error(`Error disconnecting client during test cleanup: ${e}`)),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
clientsToCleanup = []; // Reset for the next test
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should pass runtimeContext to the server logger function during tool execution', async () => {
|
|
420
|
+
type TestContext = { channel: string; userId: string };
|
|
421
|
+
const testContextInstance = new RuntimeContext<TestContext>();
|
|
422
|
+
testContextInstance.set('channel', 'test-channel-123');
|
|
423
|
+
testContextInstance.set('userId', 'user-abc-987');
|
|
424
|
+
const loggerFn = vi.fn();
|
|
425
|
+
|
|
426
|
+
const clientForTest = new MCPClient({
|
|
427
|
+
servers: {
|
|
428
|
+
stockPrice: {
|
|
429
|
+
command: 'npx',
|
|
430
|
+
args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
|
|
431
|
+
env: { FAKE_CREDS: 'test' },
|
|
432
|
+
logger: loggerFn,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
clientsToCleanup.push(clientForTest);
|
|
437
|
+
|
|
438
|
+
const tools = await clientForTest.getTools();
|
|
439
|
+
const stockTool = tools['stockPrice_getStockPrice'];
|
|
440
|
+
expect(stockTool).toBeDefined();
|
|
441
|
+
|
|
442
|
+
await stockTool.execute({
|
|
443
|
+
context: { symbol: 'MSFT' },
|
|
444
|
+
runtimeContext: testContextInstance,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(loggerFn).toHaveBeenCalled();
|
|
448
|
+
const callWithContext = loggerFn.mock.calls.find(call => {
|
|
449
|
+
const logMessage = call[0] as LogMessage;
|
|
450
|
+
return (
|
|
451
|
+
logMessage.runtimeContext &&
|
|
452
|
+
typeof logMessage.runtimeContext.get === 'function' &&
|
|
453
|
+
logMessage.runtimeContext.get('channel') === 'test-channel-123' &&
|
|
454
|
+
logMessage.runtimeContext.get('userId') === 'user-abc-987'
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
expect(callWithContext).toBeDefined();
|
|
458
|
+
const capturedLogMessage = callWithContext?.[0] as LogMessage;
|
|
459
|
+
expect(capturedLogMessage?.serverName).toEqual('stockPrice');
|
|
460
|
+
}, 15000);
|
|
461
|
+
|
|
462
|
+
it('should pass runtimeContext to MCP logger when tool is called via an Agent', async () => {
|
|
463
|
+
type TestAgentContext = { traceId: string; tenant: string };
|
|
464
|
+
const agentTestContext = new RuntimeContext<TestAgentContext>();
|
|
465
|
+
agentTestContext.set('traceId', 'agent-trace-xyz');
|
|
466
|
+
agentTestContext.set('tenant', 'acme-corp');
|
|
467
|
+
const loggerFn = vi.fn();
|
|
468
|
+
|
|
469
|
+
const mcpClientForAgentTest = new MCPClient({
|
|
470
|
+
id: 'mcp-for-agent-test-suite',
|
|
471
|
+
servers: {
|
|
472
|
+
stockPriceServer: {
|
|
473
|
+
command: 'npx',
|
|
474
|
+
args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
|
|
475
|
+
env: { FAKE_CREDS: 'test' },
|
|
476
|
+
logger: loggerFn,
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
clientsToCleanup.push(mcpClientForAgentTest);
|
|
481
|
+
|
|
482
|
+
const agentName = 'stockAgentForContextTest';
|
|
483
|
+
const agent = new Agent({
|
|
484
|
+
name: agentName,
|
|
485
|
+
model: openai('gpt-4o'),
|
|
486
|
+
instructions: 'Use the getStockPrice tool to find the price of MSFT.',
|
|
487
|
+
tools: await mcpClientForAgentTest.getTools(),
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await agent.generate('What is the price of MSFT?', { runtimeContext: agentTestContext });
|
|
491
|
+
|
|
492
|
+
expect(loggerFn).toHaveBeenCalled();
|
|
493
|
+
const callWithAgentContext = loggerFn.mock.calls.find(call => {
|
|
494
|
+
const logMessage = call[0] as LogMessage;
|
|
495
|
+
return (
|
|
496
|
+
logMessage.runtimeContext &&
|
|
497
|
+
typeof logMessage.runtimeContext.get === 'function' &&
|
|
498
|
+
logMessage.runtimeContext.get('traceId') === 'agent-trace-xyz' &&
|
|
499
|
+
logMessage.runtimeContext.get('tenant') === 'acme-corp'
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
expect(callWithAgentContext).toBeDefined();
|
|
503
|
+
if (callWithAgentContext) {
|
|
504
|
+
const capturedLogMessage = callWithAgentContext[0] as LogMessage;
|
|
505
|
+
expect(capturedLogMessage?.serverName).toEqual('stockPriceServer');
|
|
506
|
+
}
|
|
507
|
+
}, 20000);
|
|
508
|
+
|
|
509
|
+
it('should correctly use different runtimeContexts on sequential direct tool calls', async () => {
|
|
510
|
+
const loggerFn = vi.fn();
|
|
511
|
+
const clientForSeqTest = new MCPClient({
|
|
512
|
+
id: 'mcp-sequential-context-test',
|
|
513
|
+
servers: {
|
|
514
|
+
stockPriceServer: {
|
|
515
|
+
command: 'npx',
|
|
516
|
+
args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
|
|
517
|
+
env: { FAKE_CREDS: 'test' },
|
|
518
|
+
logger: loggerFn,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
clientsToCleanup.push(clientForSeqTest);
|
|
523
|
+
|
|
524
|
+
const tools = await clientForSeqTest.getTools();
|
|
525
|
+
const stockTool = tools['stockPriceServer_getStockPrice'];
|
|
526
|
+
expect(stockTool).toBeDefined();
|
|
527
|
+
|
|
528
|
+
type ContextA = { callId: string };
|
|
529
|
+
const runtimeContextA = new RuntimeContext<ContextA>();
|
|
530
|
+
runtimeContextA.set('callId', 'call-A-111');
|
|
531
|
+
await stockTool.execute({ context: { symbol: 'MSFT' }, runtimeContext: runtimeContextA });
|
|
532
|
+
|
|
533
|
+
expect(loggerFn).toHaveBeenCalled();
|
|
534
|
+
let callsAfterA = [...loggerFn.mock.calls];
|
|
535
|
+
const logCallForA = callsAfterA.find(
|
|
536
|
+
call => (call[0] as LogMessage).runtimeContext?.get('callId') === 'call-A-111',
|
|
537
|
+
);
|
|
538
|
+
expect(logCallForA).toBeDefined();
|
|
539
|
+
expect((logCallForA?.[0] as LogMessage)?.runtimeContext?.get('callId')).toBe('call-A-111');
|
|
540
|
+
|
|
541
|
+
loggerFn.mockClear();
|
|
542
|
+
|
|
543
|
+
type ContextB = { sessionId: string };
|
|
544
|
+
const runtimeContextB = new RuntimeContext<ContextB>();
|
|
545
|
+
runtimeContextB.set('sessionId', 'session-B-222');
|
|
546
|
+
await stockTool.execute({ context: { symbol: 'GOOG' }, runtimeContext: runtimeContextB });
|
|
547
|
+
|
|
548
|
+
expect(loggerFn).toHaveBeenCalled();
|
|
549
|
+
let callsAfterB = [...loggerFn.mock.calls];
|
|
550
|
+
const logCallForB = callsAfterB.find(
|
|
551
|
+
call => (call[0] as LogMessage).runtimeContext?.get('sessionId') === 'session-B-222',
|
|
552
|
+
);
|
|
553
|
+
expect(logCallForB).toBeDefined();
|
|
554
|
+
expect((logCallForB?.[0] as LogMessage)?.runtimeContext?.get('sessionId')).toBe('session-B-222');
|
|
555
|
+
|
|
556
|
+
const contextALeak = callsAfterB.some(
|
|
557
|
+
call => (call[0] as LogMessage).runtimeContext?.get('callId') === 'call-A-111',
|
|
558
|
+
);
|
|
559
|
+
expect(contextALeak).toBe(false);
|
|
560
|
+
}, 20000);
|
|
561
|
+
|
|
562
|
+
it('should isolate runtimeContext between different servers on the same MCPClient', async () => {
|
|
563
|
+
const sharedLoggerFn = vi.fn();
|
|
564
|
+
|
|
565
|
+
const clientWithTwoServers = new MCPClient({
|
|
566
|
+
id: 'mcp-multi-server-context-isolation',
|
|
567
|
+
servers: {
|
|
568
|
+
serverX: {
|
|
569
|
+
command: 'npx',
|
|
570
|
+
args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')], // Re-use fixture, tool name will differ by server
|
|
571
|
+
logger: sharedLoggerFn,
|
|
572
|
+
env: { FAKE_CREDS: 'serverX-creds' }, // Make env slightly different for clarity if needed
|
|
573
|
+
},
|
|
574
|
+
serverY: {
|
|
575
|
+
command: 'npx',
|
|
576
|
+
args: ['tsx', path.join(__dirname, '__fixtures__/stock-price.ts')], // Re-use fixture
|
|
577
|
+
logger: sharedLoggerFn,
|
|
578
|
+
env: { FAKE_CREDS: 'serverY-creds' },
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
clientsToCleanup.push(clientWithTwoServers);
|
|
583
|
+
|
|
584
|
+
const tools = await clientWithTwoServers.getTools();
|
|
585
|
+
const toolX = tools['serverX_getStockPrice'];
|
|
586
|
+
const toolY = tools['serverY_getStockPrice'];
|
|
587
|
+
expect(toolX).toBeDefined();
|
|
588
|
+
expect(toolY).toBeDefined();
|
|
589
|
+
|
|
590
|
+
// --- Call tool on Server X with contextX ---
|
|
591
|
+
type ContextX = { requestId: string };
|
|
592
|
+
const runtimeContextX = new RuntimeContext<ContextX>();
|
|
593
|
+
runtimeContextX.set('requestId', 'req-X-001');
|
|
594
|
+
|
|
595
|
+
await toolX.execute({ context: { symbol: 'AAA' }, runtimeContext: runtimeContextX });
|
|
596
|
+
|
|
597
|
+
expect(sharedLoggerFn).toHaveBeenCalled();
|
|
598
|
+
let callsAfterToolX = [...sharedLoggerFn.mock.calls];
|
|
599
|
+
const logCallForX = callsAfterToolX.find(call => {
|
|
600
|
+
const logMessage = call[0] as LogMessage;
|
|
601
|
+
return logMessage.serverName === 'serverX' && logMessage.runtimeContext?.get('requestId') === 'req-X-001';
|
|
602
|
+
});
|
|
603
|
+
expect(logCallForX).toBeDefined();
|
|
604
|
+
expect((logCallForX?.[0] as LogMessage)?.runtimeContext?.get('requestId')).toBe('req-X-001');
|
|
605
|
+
|
|
606
|
+
sharedLoggerFn.mockClear(); // Clear for next distinct operation
|
|
607
|
+
|
|
608
|
+
// --- Call tool on Server Y with contextY ---
|
|
609
|
+
type ContextY = { customerId: string };
|
|
610
|
+
const runtimeContextY = new RuntimeContext<ContextY>();
|
|
611
|
+
runtimeContextY.set('customerId', 'cust-Y-002');
|
|
612
|
+
|
|
613
|
+
await toolY.execute({ context: { symbol: 'BBB' }, runtimeContext: runtimeContextY });
|
|
614
|
+
|
|
615
|
+
expect(sharedLoggerFn).toHaveBeenCalled();
|
|
616
|
+
let callsAfterToolY = [...sharedLoggerFn.mock.calls];
|
|
617
|
+
const logCallForY = callsAfterToolY.find(call => {
|
|
618
|
+
const logMessage = call[0] as LogMessage;
|
|
619
|
+
return logMessage.serverName === 'serverY' && logMessage.runtimeContext?.get('customerId') === 'cust-Y-002';
|
|
620
|
+
});
|
|
621
|
+
expect(logCallForY).toBeDefined();
|
|
622
|
+
expect((logCallForY?.[0] as LogMessage)?.runtimeContext?.get('customerId')).toBe('cust-Y-002');
|
|
623
|
+
|
|
624
|
+
// Ensure contextX did not leak into logs from serverY's operation
|
|
625
|
+
const contextXLeakInYLogs = callsAfterToolY.some(call => {
|
|
626
|
+
const logMessage = call[0] as LogMessage;
|
|
627
|
+
return logMessage.runtimeContext?.get('requestId') === 'req-X-001';
|
|
628
|
+
});
|
|
629
|
+
expect(contextXLeakInYLogs).toBe(false);
|
|
630
|
+
}, 25000); // Increased timeout for multiple server ops
|
|
631
|
+
});
|
|
360
632
|
});
|