@mastra/mcp 0.5.0-alpha.4 → 0.5.0-alpha.6
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 +13 -0
- package/dist/_tsup-dts-rollup.d.cts +3 -0
- package/dist/_tsup-dts-rollup.d.ts +3 -0
- package/dist/index.cjs +8 -2
- package/dist/index.js +8 -2
- package/package.json +3 -2
- package/src/client.ts +9 -1
- package/src/configuration.test.ts +236 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/mcp@0.5.0-alpha.
|
|
2
|
+
> @mastra/mcp@0.5.0-alpha.6 build /home/runner/work/mastra/mastra/packages/mcp
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.4.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 15333ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 10534ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m32.
|
|
21
|
-
[32mCJS[39m ⚡️ Build success in
|
|
22
|
-
[32mESM[39m [1mdist/index.js [22m[32m32.
|
|
23
|
-
[32mESM[39m ⚡️ Build success in
|
|
20
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m32.75 KB[39m
|
|
21
|
+
[32mCJS[39m ⚡️ Build success in 668ms
|
|
22
|
+
[32mESM[39m [1mdist/index.js [22m[32m32.45 KB[39m
|
|
23
|
+
[32mESM[39m ⚡️ Build success in 669ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @mastra/mcp
|
|
2
2
|
|
|
3
|
+
## 0.5.0-alpha.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [3e9c131]
|
|
8
|
+
- @mastra/core@0.9.4-alpha.4
|
|
9
|
+
|
|
10
|
+
## 0.5.0-alpha.5
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 8baa6c8: passes runtimeContext to the logger function inside MCPClient tool calls
|
|
15
|
+
|
|
3
16
|
## 0.5.0-alpha.4
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
|
@@ -7,6 +7,7 @@ import { MCPServerBase } from '@mastra/core/mcp';
|
|
|
7
7
|
import type { MCPServerHonoSSEOptions } from '@mastra/core/mcp';
|
|
8
8
|
import type { MCPServerSSEOptions } from '@mastra/core/mcp';
|
|
9
9
|
import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
10
|
+
import type { RuntimeContext } from '@mastra/core/di';
|
|
10
11
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
12
|
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
12
13
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
@@ -51,6 +52,7 @@ export declare class InternalMastraMCPClient extends MastraBase {
|
|
|
51
52
|
private enableServerLogs?;
|
|
52
53
|
private serverConfig;
|
|
53
54
|
private transport?;
|
|
55
|
+
private currentOperationContext;
|
|
54
56
|
constructor({ name, version, server, capabilities, timeout, }: InternalMastraMCPClientOptions);
|
|
55
57
|
/**
|
|
56
58
|
* Log a message at the specified level
|
|
@@ -105,6 +107,7 @@ declare interface LogMessage {
|
|
|
105
107
|
timestamp: Date;
|
|
106
108
|
serverName: string;
|
|
107
109
|
details?: Record<string, any>;
|
|
110
|
+
runtimeContext?: RuntimeContext | null;
|
|
108
111
|
}
|
|
109
112
|
export { LogMessage }
|
|
110
113
|
export { LogMessage as LogMessage_alias_1 }
|
|
@@ -7,6 +7,7 @@ import { MCPServerBase } from '@mastra/core/mcp';
|
|
|
7
7
|
import type { MCPServerHonoSSEOptions } from '@mastra/core/mcp';
|
|
8
8
|
import type { MCPServerSSEOptions } from '@mastra/core/mcp';
|
|
9
9
|
import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
10
|
+
import type { RuntimeContext } from '@mastra/core/di';
|
|
10
11
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
12
|
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
12
13
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
@@ -51,6 +52,7 @@ export declare class InternalMastraMCPClient extends MastraBase {
|
|
|
51
52
|
private enableServerLogs?;
|
|
52
53
|
private serverConfig;
|
|
53
54
|
private transport?;
|
|
55
|
+
private currentOperationContext;
|
|
54
56
|
constructor({ name, version, server, capabilities, timeout, }: InternalMastraMCPClientOptions);
|
|
55
57
|
/**
|
|
56
58
|
* Log a message at the specified level
|
|
@@ -105,6 +107,7 @@ declare interface LogMessage {
|
|
|
105
107
|
timestamp: Date;
|
|
106
108
|
serverName: string;
|
|
107
109
|
details?: Record<string, any>;
|
|
110
|
+
runtimeContext?: RuntimeContext | null;
|
|
108
111
|
}
|
|
109
112
|
export { LogMessage }
|
|
110
113
|
export { LogMessage as LogMessage_alias_1 }
|
package/dist/index.cjs
CHANGED
|
@@ -55,6 +55,7 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
55
55
|
enableServerLogs;
|
|
56
56
|
serverConfig;
|
|
57
57
|
transport;
|
|
58
|
+
currentOperationContext = null;
|
|
58
59
|
constructor({
|
|
59
60
|
name,
|
|
60
61
|
version = "1.0.0",
|
|
@@ -95,7 +96,8 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
95
96
|
message: msg,
|
|
96
97
|
timestamp: /* @__PURE__ */ new Date(),
|
|
97
98
|
serverName: this.name,
|
|
98
|
-
details
|
|
99
|
+
details,
|
|
100
|
+
runtimeContext: this.currentOperationContext
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
103
|
}
|
|
@@ -273,7 +275,9 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
273
275
|
id: `${this.name}_${tool.name}`,
|
|
274
276
|
description: tool.description || "",
|
|
275
277
|
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
276
|
-
execute: async ({ context }) => {
|
|
278
|
+
execute: async ({ context, runtimeContext }) => {
|
|
279
|
+
const previousContext = this.currentOperationContext;
|
|
280
|
+
this.currentOperationContext = runtimeContext || null;
|
|
277
281
|
try {
|
|
278
282
|
this.log("debug", `Executing tool: ${tool.name}`, { toolArgs: context });
|
|
279
283
|
const res = await this.client.callTool(
|
|
@@ -294,6 +298,8 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
294
298
|
toolArgs: context
|
|
295
299
|
});
|
|
296
300
|
throw e;
|
|
301
|
+
} finally {
|
|
302
|
+
this.currentOperationContext = previousContext;
|
|
297
303
|
}
|
|
298
304
|
}
|
|
299
305
|
});
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
49
49
|
enableServerLogs;
|
|
50
50
|
serverConfig;
|
|
51
51
|
transport;
|
|
52
|
+
currentOperationContext = null;
|
|
52
53
|
constructor({
|
|
53
54
|
name,
|
|
54
55
|
version = "1.0.0",
|
|
@@ -89,7 +90,8 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
89
90
|
message: msg,
|
|
90
91
|
timestamp: /* @__PURE__ */ new Date(),
|
|
91
92
|
serverName: this.name,
|
|
92
|
-
details
|
|
93
|
+
details,
|
|
94
|
+
runtimeContext: this.currentOperationContext
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -267,7 +269,9 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
267
269
|
id: `${this.name}_${tool.name}`,
|
|
268
270
|
description: tool.description || "",
|
|
269
271
|
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
270
|
-
execute: async ({ context }) => {
|
|
272
|
+
execute: async ({ context, runtimeContext }) => {
|
|
273
|
+
const previousContext = this.currentOperationContext;
|
|
274
|
+
this.currentOperationContext = runtimeContext || null;
|
|
271
275
|
try {
|
|
272
276
|
this.log("debug", `Executing tool: ${tool.name}`, { toolArgs: context });
|
|
273
277
|
const res = await this.client.callTool(
|
|
@@ -288,6 +292,8 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
288
292
|
toolArgs: context
|
|
289
293
|
});
|
|
290
294
|
throw e;
|
|
295
|
+
} finally {
|
|
296
|
+
this.currentOperationContext = previousContext;
|
|
291
297
|
}
|
|
292
298
|
}
|
|
293
299
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mcp",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,13 +29,14 @@
|
|
|
29
29
|
"hono": "^4.7.4",
|
|
30
30
|
"uuid": "^11.1.0",
|
|
31
31
|
"zod-from-json-schema": "^0.0.5",
|
|
32
|
-
"@mastra/core": "^0.9.4-alpha.
|
|
32
|
+
"@mastra/core": "^0.9.4-alpha.4"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"zod": "^3.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@ai-sdk/anthropic": "^1.1.15",
|
|
39
|
+
"@ai-sdk/openai": "^1.3.22",
|
|
39
40
|
"@hono/node-server": "^1.13.8",
|
|
40
41
|
"@mendable/firecrawl-js": "^1.24.0",
|
|
41
42
|
"@microsoft/api-extractor": "^7.52.5",
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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
4
|
import { isZodType } from '@mastra/core/utils';
|
|
4
5
|
import { Client } from '@modelcontextprotocol/sdk/client/index.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
|
}
|
|
@@ -365,7 +369,9 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
365
369
|
id: `${this.name}_${tool.name}`,
|
|
366
370
|
description: tool.description || '',
|
|
367
371
|
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
368
|
-
execute: async ({ context }) => {
|
|
372
|
+
execute: async ({ context, runtimeContext }: { context: any; runtimeContext?: RuntimeContext | null }) => {
|
|
373
|
+
const previousContext = this.currentOperationContext;
|
|
374
|
+
this.currentOperationContext = runtimeContext || null; // Set current context
|
|
369
375
|
try {
|
|
370
376
|
this.log('debug', `Executing tool: ${tool.name}`, { toolArgs: context });
|
|
371
377
|
const res = await this.client.callTool(
|
|
@@ -386,6 +392,8 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
386
392
|
toolArgs: context,
|
|
387
393
|
});
|
|
388
394
|
throw e;
|
|
395
|
+
} finally {
|
|
396
|
+
this.currentOperationContext = previousContext; // Restore previous context
|
|
389
397
|
}
|
|
390
398
|
},
|
|
391
399
|
});
|
|
@@ -1,8 +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';
|
|
4
7
|
import { allTools, mcpServerName } from './__fixtures__/fire-crawl-complex-schema';
|
|
5
|
-
import type { LogHandler } from './client';
|
|
8
|
+
import type { LogHandler, LogMessage } from './client';
|
|
6
9
|
import { MCPClient } from './configuration';
|
|
7
10
|
|
|
8
11
|
vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
@@ -10,6 +13,7 @@ vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
|
10
13
|
describe('MCPClient', () => {
|
|
11
14
|
let mcp: MCPClient;
|
|
12
15
|
let weatherProcess: ReturnType<typeof spawn>;
|
|
16
|
+
let clients: MCPClient[] = [];
|
|
13
17
|
|
|
14
18
|
beforeAll(async () => {
|
|
15
19
|
// Start the weather SSE server
|
|
@@ -52,11 +56,16 @@ describe('MCPClient', () => {
|
|
|
52
56
|
},
|
|
53
57
|
},
|
|
54
58
|
});
|
|
59
|
+
clients.push(mcp);
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
afterEach(async () => {
|
|
58
63
|
// Clean up any connected clients
|
|
59
64
|
await mcp.disconnect();
|
|
65
|
+
const index = clients.indexOf(mcp);
|
|
66
|
+
if (index > -1) {
|
|
67
|
+
clients.splice(index, 1);
|
|
68
|
+
}
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
afterAll(async () => {
|
|
@@ -394,4 +403,230 @@ describe('MCPClient', () => {
|
|
|
394
403
|
expect(mockLogHandler.mock.calls.length).toBeGreaterThan(0);
|
|
395
404
|
});
|
|
396
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
|
+
});
|
|
397
632
|
});
|