@mastra/mcp 0.5.0-alpha.4 → 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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/mcp@0.5.0-alpha.4 build /home/runner/work/mastra/mastra/packages/mcp
2
+ > @mastra/mcp@0.5.0-alpha.5 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
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.4.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 17734ms
9
+ TSC ⚡️ Build success in 18682ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 13432ms
16
+ DTS ⚡️ Build success in 14171ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 32.44 KB
21
- CJS ⚡️ Build success in 1027ms
22
- ESM dist/index.js 32.14 KB
23
- ESM ⚡️ Build success in 1029ms
20
+ CJS dist/index.cjs 32.75 KB
21
+ CJS ⚡️ Build success in 1159ms
22
+ ESM dist/index.js 32.45 KB
23
+ ESM ⚡️ Build success in 1159ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mastra/mcp
2
2
 
3
+ ## 0.5.0-alpha.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 8baa6c8: passes runtimeContext to the logger function inside MCPClient tool calls
8
+
3
9
  ## 0.5.0-alpha.4
4
10
 
5
11
  ### 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.4",
3
+ "version": "0.5.0-alpha.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,7 @@
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
  });