@mastra/mcp 0.4.1-alpha.2 → 0.4.1-alpha.4

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/src/client.ts CHANGED
@@ -5,7 +5,8 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
6
6
  import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
7
7
  import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
- import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
8
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
9
+ import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
9
10
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
10
11
  import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
11
12
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
@@ -28,18 +29,43 @@ export interface LogMessage {
28
29
 
29
30
  export type LogHandler = (logMessage: LogMessage) => void;
30
31
 
31
- // Omit the fields we want to control from the SDK options
32
- type SSEClientParameters = {
33
- url: URL;
34
- } & SSEClientTransportOptions;
35
-
36
- export type MastraMCPServerDefinition = (StdioServerParameters | SSEClientParameters) & {
32
+ // Base options common to all server definitions
33
+ type BaseServerOptions = {
37
34
  logger?: LogHandler;
38
35
  timeout?: number;
39
36
  capabilities?: ClientCapabilities;
40
37
  enableServerLogs?: boolean;
41
38
  };
42
39
 
40
+ type StdioServerDefinition = BaseServerOptions & {
41
+ command: string; // 'command' is required for Stdio
42
+ args?: string[];
43
+ env?: Record<string, string>;
44
+
45
+ url?: never; // Exclude 'url' for Stdio
46
+ requestInit?: never; // Exclude HTTP options for Stdio
47
+ eventSourceInit?: never; // Exclude HTTP options for Stdio
48
+ reconnectionOptions?: never; // Exclude Streamable HTTP specific options
49
+ sessionId?: never; // Exclude Streamable HTTP specific options
50
+ };
51
+
52
+ // HTTP Server Definition (Streamable HTTP or SSE fallback)
53
+ type HttpServerDefinition = BaseServerOptions & {
54
+ url: URL; // 'url' is required for HTTP
55
+
56
+ command?: never; // Exclude 'command' for HTTP
57
+ args?: never; // Exclude Stdio options for HTTP
58
+ env?: never; // Exclude Stdio options for HTTP
59
+
60
+ // Include relevant options from SDK HTTP transport types
61
+ requestInit?: StreamableHTTPClientTransportOptions['requestInit'];
62
+ eventSourceInit?: SSEClientTransportOptions['eventSourceInit'];
63
+ reconnectionOptions?: StreamableHTTPClientTransportOptions['reconnectionOptions'];
64
+ sessionId?: StreamableHTTPClientTransportOptions['sessionId'];
65
+ };
66
+
67
+ export type MastraMCPServerDefinition = StdioServerDefinition | HttpServerDefinition;
68
+
43
69
  /**
44
70
  * Convert an MCP LoggingLevel to a logger method name that exists in our logger
45
71
  */
@@ -63,13 +89,15 @@ function convertLogLevelToLoggerMethod(level: LoggingLevel): 'debug' | 'info' |
63
89
  }
64
90
  }
65
91
 
66
- export class MastraMCPClient extends MastraBase {
92
+ export class InternalMastraMCPClient extends MastraBase {
67
93
  name: string;
68
- private transport: Transport;
69
94
  private client: Client;
70
95
  private readonly timeout: number;
71
96
  private logHandler?: LogHandler;
72
97
  private enableServerLogs?: boolean;
98
+ private static hasWarned = false;
99
+ private serverConfig: MastraMCPServerDefinition;
100
+ private transport?: Transport;
73
101
 
74
102
  constructor({
75
103
  name,
@@ -85,26 +113,18 @@ export class MastraMCPClient extends MastraBase {
85
113
  timeout?: number;
86
114
  }) {
87
115
  super({ name: 'MastraMCPClient' });
116
+ if (!InternalMastraMCPClient.hasWarned) {
117
+ // eslint-disable-next-line no-console
118
+ console.warn(
119
+ '[DEPRECATION] MastraMCPClient is deprecated and will be removed in a future release. Please use MCPClient instead.',
120
+ );
121
+ InternalMastraMCPClient.hasWarned = true;
122
+ }
88
123
  this.name = name;
89
124
  this.timeout = timeout;
90
125
  this.logHandler = server.logger;
91
126
  this.enableServerLogs = server.enableServerLogs ?? true;
92
-
93
- // Extract log handler from server config to avoid passing it to transport
94
- const { logger, enableServerLogs, ...serverConfig } = server;
95
-
96
- if (`url` in serverConfig) {
97
- this.transport = new SSEClientTransport(serverConfig.url, {
98
- requestInit: serverConfig.requestInit,
99
- eventSourceInit: serverConfig.eventSourceInit,
100
- });
101
- } else {
102
- this.transport = new StdioClientTransport({
103
- ...serverConfig,
104
- // without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
105
- env: { ...getDefaultEnvironment(), ...(serverConfig.env || {}) },
106
- });
107
- }
127
+ this.serverConfig = server;
108
128
 
109
129
  this.client = new Client(
110
130
  {
@@ -130,14 +150,16 @@ export class MastraMCPClient extends MastraBase {
130
150
  // Convert MCP logging level to our logger method
131
151
  const loggerMethod = convertLogLevelToLoggerMethod(level);
132
152
 
153
+ const msg = `[${this.name}] ${message}`;
154
+
133
155
  // Log to internal logger
134
- this.logger[loggerMethod](message, details);
156
+ this.logger[loggerMethod](msg, details);
135
157
 
136
158
  // Send to registered handler if available
137
159
  if (this.logHandler) {
138
160
  this.logHandler({
139
161
  level,
140
- message,
162
+ message: msg,
141
163
  timestamp: new Date(),
142
164
  serverName: this.name,
143
165
  details,
@@ -164,46 +186,135 @@ export class MastraMCPClient extends MastraBase {
164
186
  }
165
187
  }
166
188
 
189
+ private async connectStdio(command: string) {
190
+ this.log('debug', `Using Stdio transport for command: ${command}`);
191
+ try {
192
+ this.transport = new StdioClientTransport({
193
+ command,
194
+ args: this.serverConfig.args,
195
+ env: { ...getDefaultEnvironment(), ...(this.serverConfig.env || {}) },
196
+ });
197
+ await this.client.connect(this.transport, { timeout: this.serverConfig.timeout ?? this.timeout });
198
+ this.log('debug', `Successfully connected to MCP server via Stdio`);
199
+ } catch (e) {
200
+ this.log('error', e instanceof Error ? e.stack || e.message : JSON.stringify(e));
201
+ throw e;
202
+ }
203
+ }
204
+
205
+ private async connectHttp(url: URL) {
206
+ const { requestInit, eventSourceInit } = this.serverConfig;
207
+
208
+ this.log('debug', `Attempting to connect to URL: ${url}`);
209
+
210
+ // Assume /sse means sse.
211
+ let shouldTrySSE = url.pathname.endsWith(`/sse`);
212
+
213
+ if (!shouldTrySSE) {
214
+ try {
215
+ // Try Streamable HTTP transport first
216
+ this.log('debug', 'Trying Streamable HTTP transport...');
217
+ const streamableTransport = new StreamableHTTPClientTransport(url, {
218
+ requestInit,
219
+ reconnectionOptions: this.serverConfig.reconnectionOptions,
220
+ sessionId: this.serverConfig.sessionId,
221
+ });
222
+ await this.client.connect(streamableTransport, {
223
+ timeout:
224
+ // this is hardcoded to 3s because the long default timeout would be extremely slow for sse backwards compat (60s)
225
+ 3000,
226
+ });
227
+ this.transport = streamableTransport;
228
+ this.log('debug', 'Successfully connected using Streamable HTTP transport.');
229
+ } catch (error) {
230
+ this.log('debug', `Streamable HTTP transport failed: ${error}`);
231
+ shouldTrySSE = true;
232
+ }
233
+ }
234
+
235
+ if (shouldTrySSE) {
236
+ this.log('debug', 'Falling back to deprecated HTTP+SSE transport...');
237
+ try {
238
+ // Fallback to SSE transport
239
+ const sseTransport = new SSEClientTransport(url, { requestInit, eventSourceInit });
240
+ await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout });
241
+ this.transport = sseTransport;
242
+ this.log('debug', 'Successfully connected using deprecated HTTP+SSE transport.');
243
+ } catch (sseError) {
244
+ this.log(
245
+ 'error',
246
+ `Failed to connect with SSE transport after failing to connect to Streamable HTTP transport first. SSE error: ${sseError}`,
247
+ );
248
+ throw new Error('Could not connect to server with any available HTTP transport');
249
+ }
250
+ }
251
+ }
252
+
167
253
  private isConnected = false;
168
254
 
169
255
  async connect() {
170
256
  if (this.isConnected) return;
171
- try {
172
- this.log('debug', `Connecting to MCP server`);
173
- await this.client.connect(this.transport, {
174
- timeout: this.timeout,
175
- });
176
- this.isConnected = true;
177
- const originalOnClose = this.client.onclose;
178
- this.client.onclose = () => {
179
- this.log('debug', `MCP server connection closed`);
180
- this.isConnected = false;
181
- if (typeof originalOnClose === `function`) {
182
- originalOnClose();
183
- }
184
- };
185
- asyncExitHook(
186
- async () => {
187
- this.log('debug', `Disconnecting MCP server during exit`);
188
- await this.disconnect();
189
- },
190
- { wait: 5000 },
191
- );
192
257
 
193
- process.on('SIGTERM', () => gracefulExit());
194
- this.log('info', `Successfully connected to MCP server`);
195
- } catch (e) {
196
- this.log('error', `Failed connecting to MCP server`, {
197
- error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
198
- });
258
+ const { command, url } = this.serverConfig;
259
+
260
+ if (command) {
261
+ await this.connectStdio(command);
262
+ } else if (url) {
263
+ await this.connectHttp(url);
264
+ } else {
265
+ throw new Error('Server configuration must include either a command or a url.');
266
+ }
267
+
268
+ this.isConnected = true;
269
+ const originalOnClose = this.client.onclose;
270
+ this.client.onclose = () => {
271
+ this.log('debug', `MCP server connection closed`);
199
272
  this.isConnected = false;
200
- throw e;
273
+ if (typeof originalOnClose === `function`) {
274
+ originalOnClose();
275
+ }
276
+ };
277
+ asyncExitHook(
278
+ async () => {
279
+ this.log('debug', `Disconnecting MCP server during exit`);
280
+ await this.disconnect();
281
+ },
282
+ { wait: 5000 },
283
+ );
284
+
285
+ process.on('SIGTERM', () => gracefulExit());
286
+ this.log('debug', `Successfully connected to MCP server`);
287
+ }
288
+
289
+ /**
290
+ * Get the current session ID if using the Streamable HTTP transport.
291
+ * Returns undefined if not connected or not using Streamable HTTP.
292
+ */
293
+ get sessionId(): string | undefined {
294
+ if (this.transport instanceof StreamableHTTPClientTransport) {
295
+ return this.transport.sessionId;
201
296
  }
297
+ return undefined;
202
298
  }
203
299
 
204
300
  async disconnect() {
301
+ if (!this.transport) {
302
+ this.log('debug', 'Disconnect called but no transport was connected.');
303
+ return;
304
+ }
205
305
  this.log('debug', `Disconnecting from MCP server`);
206
- return await this.client.close();
306
+ try {
307
+ await this.transport.close();
308
+ this.log('debug', 'Successfully disconnected from MCP server');
309
+ } catch (e) {
310
+ this.log('error', 'Error during MCP server disconnect', {
311
+ error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
312
+ });
313
+ throw e;
314
+ } finally {
315
+ this.transport = undefined;
316
+ this.isConnected = false;
317
+ }
207
318
  }
208
319
 
209
320
  // TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
@@ -259,3 +370,8 @@ export class MastraMCPClient extends MastraBase {
259
370
  return toolsRes;
260
371
  }
261
372
  }
373
+
374
+ /**
375
+ * @deprecated MastraMCPClient is deprecated and will be removed in a future release. Please use MCPClient instead.
376
+ */
377
+ export const MastraMCPClient = InternalMastraMCPClient;
@@ -1,12 +1,12 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from 'vitest';
4
- import { MCPConfiguration } from './configuration';
4
+ import { MCPClient } from './configuration';
5
5
 
6
6
  vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
7
7
 
8
- describe('MCPConfiguration', () => {
9
- let mcp: MCPConfiguration;
8
+ describe('MCPClient', () => {
9
+ let mcp: MCPClient;
10
10
  let weatherProcess: ReturnType<typeof spawn>;
11
11
 
12
12
  beforeAll(async () => {
@@ -36,7 +36,7 @@ describe('MCPConfiguration', () => {
36
36
  });
37
37
 
38
38
  beforeEach(async () => {
39
- mcp = new MCPConfiguration({
39
+ mcp = new MCPClient({
40
40
  servers: {
41
41
  stockPrice: {
42
42
  command: 'npx',
@@ -95,7 +95,7 @@ describe('MCPConfiguration', () => {
95
95
  });
96
96
 
97
97
  it('should handle connection errors gracefully', async () => {
98
- const badConfig = new MCPConfiguration({
98
+ const badConfig = new MCPClient({
99
99
  servers: {
100
100
  badServer: {
101
101
  command: 'nonexistent-command',
@@ -110,7 +110,7 @@ describe('MCPConfiguration', () => {
110
110
 
111
111
  describe('Instance Management', () => {
112
112
  it('should allow multiple instances with different IDs', async () => {
113
- const config2 = new MCPConfiguration({
113
+ const config2 = new MCPClient({
114
114
  id: 'custom-id',
115
115
  servers: {
116
116
  stockPrice: {
@@ -130,7 +130,7 @@ describe('MCPConfiguration', () => {
130
130
  it('should allow reuse of configuration after closing', async () => {
131
131
  await mcp.disconnect();
132
132
 
133
- const config2 = new MCPConfiguration({
133
+ const config2 = new MCPClient({
134
134
  servers: {
135
135
  stockPrice: {
136
136
  command: 'npx',
@@ -150,7 +150,7 @@ describe('MCPConfiguration', () => {
150
150
  });
151
151
 
152
152
  it('should throw error when creating duplicate instance without ID', async () => {
153
- const existingConfig = new MCPConfiguration({
153
+ const existingConfig = new MCPClient({
154
154
  servers: {
155
155
  stockPrice: {
156
156
  command: 'npx',
@@ -164,7 +164,7 @@ describe('MCPConfiguration', () => {
164
164
 
165
165
  expect(
166
166
  () =>
167
- new MCPConfiguration({
167
+ new MCPClient({
168
168
  servers: {
169
169
  stockPrice: {
170
170
  command: 'npx',
@@ -175,14 +175,14 @@ describe('MCPConfiguration', () => {
175
175
  },
176
176
  },
177
177
  }),
178
- ).toThrow(/MCPConfiguration was initialized multiple times/);
178
+ ).toThrow(/MCPClient was initialized multiple times/);
179
179
 
180
180
  await existingConfig.disconnect();
181
181
  });
182
182
  });
183
- describe('MCPConfiguration Operation Timeouts', () => {
183
+ describe('MCPClient Operation Timeouts', () => {
184
184
  it('should respect custom timeout in configuration', async () => {
185
- const config = new MCPConfiguration({
185
+ const config = new MCPClient({
186
186
  id: 'test-timeout-config',
187
187
  timeout: 3000, // 3 second timeout
188
188
  servers: {
@@ -208,7 +208,7 @@ describe('MCPConfiguration', () => {
208
208
  });
209
209
 
210
210
  it('should respect per-server timeout override', async () => {
211
- const config = new MCPConfiguration({
211
+ const config = new MCPClient({
212
212
  id: 'test-server-timeout-config',
213
213
  timeout: 500, // Global timeout of 500ms
214
214
  servers: {
@@ -236,14 +236,15 @@ describe('MCPConfiguration', () => {
236
236
  });
237
237
  });
238
238
 
239
- describe('MCPConfiguration Connection Timeout', () => {
239
+ describe('MCPClient Connection Timeout', () => {
240
240
  it('should throw timeout error for slow starting server', async () => {
241
- const slowConfig = new MCPConfiguration({
241
+ const slowConfig = new MCPClient({
242
242
  id: 'test-slow-server',
243
243
  servers: {
244
244
  slowServer: {
245
245
  command: 'node',
246
246
  args: ['-e', 'setTimeout(() => process.exit(0), 65000)'], // Simulate a server that takes 65 seconds to start
247
+ timeout: 1000,
247
248
  },
248
249
  },
249
250
  });
@@ -252,14 +253,14 @@ describe('MCPConfiguration', () => {
252
253
  await slowConfig.disconnect();
253
254
  });
254
255
 
255
- it('timeout should be longer than default timeout', async () => {
256
- const slowConfig = new MCPConfiguration({
256
+ it('timeout should be longer than configured timeout', async () => {
257
+ const slowConfig = new MCPClient({
257
258
  id: 'test-slow-server',
258
- timeout: 70000,
259
+ timeout: 2000,
259
260
  servers: {
260
261
  slowServer: {
261
262
  command: 'node',
262
- args: ['-e', 'setTimeout(() => process.exit(0), 65000)'], // Simulate a server that takes 65 seconds to start
263
+ args: ['-e', 'setTimeout(() => process.exit(0), 1000)'], // Simulate a server that takes 1 second to start
263
264
  },
264
265
  },
265
266
  });
@@ -270,24 +271,8 @@ describe('MCPConfiguration', () => {
270
271
  await slowConfig.disconnect();
271
272
  });
272
273
 
273
- it('should respect custom timeout configuration', async () => {
274
- const quickConfig = new MCPConfiguration({
275
- id: 'test-quick-timeout',
276
- timeout: 1000, // Very short global timeout
277
- servers: {
278
- slowServer: {
279
- command: 'node',
280
- args: ['-e', 'setTimeout(() => process.exit(0), 30000)'], // Takes 30 seconds to exit
281
- },
282
- },
283
- });
284
-
285
- await expect(quickConfig.getTools()).rejects.toThrow(/Request timed out/);
286
- await quickConfig.disconnect();
287
- });
288
-
289
274
  it('should respect per-server timeout configuration', async () => {
290
- const mixedConfig = new MCPConfiguration({
275
+ const mixedConfig = new MCPClient({
291
276
  id: 'test-mixed-timeout',
292
277
  timeout: 1000, // Short global timeout
293
278
  servers: {
@@ -1,66 +1,100 @@
1
1
  import { MastraBase } from '@mastra/core/base';
2
2
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
+ import equal from 'fast-deep-equal';
3
4
  import { v5 as uuidv5 } from 'uuid';
4
- import { MastraMCPClient } from './client';
5
+ import { InternalMastraMCPClient } from './client';
5
6
  import type { MastraMCPServerDefinition } from './client';
6
7
 
7
- const mastraMCPConfigurationInstances = new Map<string, InstanceType<typeof MCPConfiguration>>();
8
+ const mcpClientInstances = new Map<string, InstanceType<typeof MCPClient>>();
8
9
 
9
- export interface MCPConfigurationOptions {
10
+ export interface MCPClientOptions {
10
11
  id?: string;
11
12
  servers: Record<string, MastraMCPServerDefinition>;
12
13
  timeout?: number; // Optional global timeout
13
14
  }
14
15
 
15
- export class MCPConfiguration extends MastraBase {
16
+ export class MCPClient extends MastraBase {
16
17
  private serverConfigs: Record<string, MastraMCPServerDefinition> = {};
17
18
  private id: string;
18
19
  private defaultTimeout: number;
20
+ private mcpClientsById = new Map<string, InternalMastraMCPClient>();
21
+ private disconnectPromise: Promise<void> | null = null;
19
22
 
20
- constructor(args: MCPConfigurationOptions) {
21
- super({ name: 'MCPConfiguration' });
23
+ constructor(args: MCPClientOptions) {
24
+ super({ name: 'MCPClient' });
22
25
  this.defaultTimeout = args.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
23
26
  this.serverConfigs = args.servers;
24
27
  this.id = args.id ?? this.makeId();
25
28
 
29
+ if (args.id) {
30
+ this.id = args.id;
31
+ const cached = mcpClientInstances.get(this.id);
32
+
33
+ if (cached && !equal(cached.serverConfigs, args.servers)) {
34
+ const existingInstance = mcpClientInstances.get(this.id);
35
+ if (existingInstance) {
36
+ void existingInstance.disconnect();
37
+ }
38
+ }
39
+ } else {
40
+ this.id = this.makeId();
41
+ }
42
+
26
43
  // to prevent memory leaks return the same MCP server instance when configured the same way multiple times
27
- const existingInstance = mastraMCPConfigurationInstances.get(this.id);
44
+ const existingInstance = mcpClientInstances.get(this.id);
28
45
  if (existingInstance) {
29
46
  if (!args.id) {
30
- throw new Error(`MCPConfiguration was initialized multiple times with the same configuration options.
47
+ throw new Error(`MCPClient was initialized multiple times with the same configuration options.
31
48
 
32
49
  This error is intended to prevent memory leaks.
33
50
 
34
51
  To fix this you have three different options:
35
- 1. If you need multiple MCPConfiguration class instances with identical server configurations, set an id when configuring: new MCPConfiguration({ id: "my-unique-id" })
36
- 2. Call "await configuration.disconnect()" after you're done using the configuration and before you recreate another instance with the same options. If the identical MCPConfiguration instance is already closed at the time of re-creating it, you will not see this error.
37
- 3. If you only need one instance of MCPConfiguration in your app, refactor your code so it's only created one time (ex. move it out of a loop into a higher scope code block)
52
+ 1. If you need multiple MCPClient class instances with identical server configurations, set an id when configuring: new MCPClient({ id: "my-unique-id" })
53
+ 2. Call "await client.disconnect()" after you're done using the client and before you recreate another instance with the same options. If the identical MCPClient instance is already closed at the time of re-creating it, you will not see this error.
54
+ 3. If you only need one instance of MCPClient in your app, refactor your code so it's only created one time (ex. move it out of a loop into a higher scope code block)
38
55
  `);
39
56
  }
40
57
  return existingInstance;
41
58
  }
59
+
60
+ mcpClientInstances.set(this.id, this);
42
61
  this.addToInstanceCache();
43
62
  return this;
44
63
  }
45
64
 
46
65
  private addToInstanceCache() {
47
- if (!mastraMCPConfigurationInstances.has(this.id)) {
48
- mastraMCPConfigurationInstances.set(this.id, this);
66
+ if (!mcpClientInstances.has(this.id)) {
67
+ mcpClientInstances.set(this.id, this);
49
68
  }
50
69
  }
51
70
 
52
71
  private makeId() {
53
72
  const text = JSON.stringify(this.serverConfigs).normalize('NFKC');
54
- const idNamespace = uuidv5(`MCPConfiguration`, uuidv5.DNS);
73
+ const idNamespace = uuidv5(`MCPClient`, uuidv5.DNS);
55
74
 
56
75
  return uuidv5(text, idNamespace);
57
76
  }
58
77
 
59
78
  public async disconnect() {
60
- mastraMCPConfigurationInstances.delete(this.id);
79
+ // Helps to prevent race condition
80
+ // If there is already a disconnect ongoing, return the existing promise.
81
+ if (this.disconnectPromise) {
82
+ return this.disconnectPromise;
83
+ }
84
+
85
+ this.disconnectPromise = (async () => {
86
+ try {
87
+ mcpClientInstances.delete(this.id);
61
88
 
62
- await Promise.all(Array.from(this.mcpClientsById.values()).map(client => client.disconnect()));
63
- this.mcpClientsById.clear();
89
+ // Disconnect all clients in the cache
90
+ await Promise.all(Array.from(this.mcpClientsById.values()).map(client => client.disconnect()));
91
+ this.mcpClientsById.clear();
92
+ } finally {
93
+ this.disconnectPromise = null;
94
+ }
95
+ })();
96
+
97
+ return this.disconnectPromise;
64
98
  }
65
99
 
66
100
  public async getTools() {
@@ -89,35 +123,61 @@ To fix this you have three different options:
89
123
  return connectedToolsets;
90
124
  }
91
125
 
92
- private mcpClientsById = new Map<string, MastraMCPClient>();
126
+ /**
127
+ * Get the current session IDs for all connected MCP clients using the Streamable HTTP transport.
128
+ * Returns an object mapping server names to their session IDs.
129
+ */
130
+ get sessionIds(): Record<string, string> {
131
+ const sessionIds: Record<string, string> = {};
132
+ for (const [serverName, client] of this.mcpClientsById.entries()) {
133
+ if (client.sessionId) {
134
+ sessionIds[serverName] = client.sessionId;
135
+ }
136
+ }
137
+ return sessionIds;
138
+ }
139
+
93
140
  private async getConnectedClient(name: string, config: MastraMCPServerDefinition) {
141
+ // Helps to prevent race condition.
142
+ // If we want to call connect() we need to wait for the disconnect to complete first if any is ongoing.
143
+ if (this.disconnectPromise) {
144
+ await this.disconnectPromise;
145
+ }
146
+
94
147
  const exists = this.mcpClientsById.has(name);
148
+ const existingClient = this.mcpClientsById.get(name);
95
149
 
96
150
  if (exists) {
97
- const mcpClient = this.mcpClientsById.get(name)!;
98
- await mcpClient.connect();
99
-
100
- return mcpClient;
151
+ // This is just to satisfy Typescript since technically you could have this.mcpClientsById.set('someKey', undefined);
152
+ // Should never reach this point basically we always create a new MastraMCPClient instance when we add to the Map.
153
+ if (!existingClient) {
154
+ throw new Error(`Client ${name} exists but is undefined`);
155
+ }
156
+ await existingClient.connect();
157
+ return existingClient;
101
158
  }
102
159
 
103
160
  this.logger.debug(`Connecting to ${name} MCP server`);
104
161
 
105
162
  // Create client with server configuration including log handler
106
- const mcpClient = new MastraMCPClient({
163
+ const mcpClient = new InternalMastraMCPClient({
107
164
  name,
108
165
  server: config,
109
166
  timeout: config.timeout ?? this.defaultTimeout,
110
167
  });
111
168
 
112
169
  this.mcpClientsById.set(name, mcpClient);
170
+
113
171
  try {
114
172
  await mcpClient.connect();
115
173
  } catch (e) {
116
174
  this.mcpClientsById.delete(name);
117
- this.logger.error(`MCPConfiguration errored connecting to MCP server ${name}`, {
175
+ this.logger.error(`MCPClient errored connecting to MCP server ${name}`, {
118
176
  error: e instanceof Error ? e.message : String(e),
119
177
  });
120
- throw new Error(`Failed to connect to MCP server ${name}: ${e instanceof Error ? e.message : String(e)}`);
178
+ throw new Error(
179
+ `Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`,
180
+ );
121
181
  }
122
182
 
123
183
  this.logger.debug(`Connected to ${name} MCP server`);
@@ -129,7 +189,7 @@ To fix this you have three different options:
129
189
  cb: (input: {
130
190
  serverName: string;
131
191
  tools: Record<string, any>; // <- any because we don't have proper tool schemas
132
- client: InstanceType<typeof MastraMCPClient>;
192
+ client: InstanceType<typeof InternalMastraMCPClient>;
133
193
  }) => Promise<void>,
134
194
  ) {
135
195
  await Promise.all(
@@ -141,3 +201,24 @@ To fix this you have three different options:
141
201
  );
142
202
  }
143
203
  }
204
+
205
+ /**
206
+ * @deprecated MCPConfigurationOptions is deprecated and will be removed in a future release. Use MCPClientOptions instead.
207
+ */
208
+ export interface MCPConfigurationOptions {
209
+ id?: string;
210
+ servers: Record<string, MastraMCPServerDefinition>;
211
+ timeout?: number; // Optional global timeout
212
+ }
213
+
214
+ /**
215
+ * @deprecated MCPConfiguration is deprecated and will be removed in a future release. Use MCPClient instead.
216
+ */
217
+ export class MCPConfiguration extends MCPClient {
218
+ constructor(args: MCPClientOptions) {
219
+ super(args);
220
+ this.logger.warn(
221
+ `MCPConfiguration has been renamed to MCPClient and MCPConfiguration is deprecated. The API is identical but the MCPConfiguration export will be removed in the future. Update your imports now to prevent future errors.`,
222
+ );
223
+ }
224
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export * from './client';
1
+ export type { LoggingLevel, LogMessage, LogHandler, MastraMCPServerDefinition } from './client';
2
+ export { MastraMCPClient } from './client';
2
3
  export * from './configuration';
3
4
  export * from './server';