@mastra/mcp 0.4.1-alpha.3 → 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.
@@ -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
+ }
61
84
 
62
- await Promise.all(Array.from(this.mcpClientsById.values()).map(client => client.disconnect()));
63
- this.mcpClientsById.clear();
85
+ this.disconnectPromise = (async () => {
86
+ try {
87
+ mcpClientInstances.delete(this.id);
88
+
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() {
@@ -103,32 +137,42 @@ To fix this you have three different options:
103
137
  return sessionIds;
104
138
  }
105
139
 
106
- private mcpClientsById = new Map<string, MastraMCPClient>();
107
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
+
108
147
  const exists = this.mcpClientsById.has(name);
148
+ const existingClient = this.mcpClientsById.get(name);
109
149
 
110
150
  if (exists) {
111
- const mcpClient = this.mcpClientsById.get(name)!;
112
- await mcpClient.connect();
113
-
114
- 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;
115
158
  }
116
159
 
117
160
  this.logger.debug(`Connecting to ${name} MCP server`);
118
161
 
119
162
  // Create client with server configuration including log handler
120
- const mcpClient = new MastraMCPClient({
163
+ const mcpClient = new InternalMastraMCPClient({
121
164
  name,
122
165
  server: config,
123
166
  timeout: config.timeout ?? this.defaultTimeout,
124
167
  });
125
168
 
126
169
  this.mcpClientsById.set(name, mcpClient);
170
+
127
171
  try {
128
172
  await mcpClient.connect();
129
173
  } catch (e) {
130
174
  this.mcpClientsById.delete(name);
131
- this.logger.error(`MCPConfiguration errored connecting to MCP server ${name}`, {
175
+ this.logger.error(`MCPClient errored connecting to MCP server ${name}`, {
132
176
  error: e instanceof Error ? e.message : String(e),
133
177
  });
134
178
  throw new Error(
@@ -145,7 +189,7 @@ To fix this you have three different options:
145
189
  cb: (input: {
146
190
  serverName: string;
147
191
  tools: Record<string, any>; // <- any because we don't have proper tool schemas
148
- client: InstanceType<typeof MastraMCPClient>;
192
+ client: InstanceType<typeof InternalMastraMCPClient>;
149
193
  }) => Promise<void>,
150
194
  ) {
151
195
  await Promise.all(
@@ -157,3 +201,24 @@ To fix this you have three different options:
157
201
  );
158
202
  }
159
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';
@@ -2,7 +2,7 @@ import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
4
4
  import type { LogMessage } from './client';
5
- import { MCPConfiguration } from './configuration';
5
+ import { MCPClient } from './configuration';
6
6
 
7
7
  // Increase test timeout for server operations
8
8
  vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
@@ -58,7 +58,7 @@ describe('MCP Server Logging', () => {
58
58
  const weatherLogHandler = vi.fn();
59
59
  const stockLogHandler = vi.fn();
60
60
 
61
- const config = new MCPConfiguration({
61
+ const config = new MCPClient({
62
62
  id: 'server-log-test',
63
63
  servers: {
64
64
  weather: {
@@ -116,7 +116,7 @@ describe('MCP Server Logging', () => {
116
116
  });
117
117
 
118
118
  // Intentionally use a non-existent command to generate errors
119
- const config = new MCPConfiguration({
119
+ const config = new MCPClient({
120
120
  id: 'error-log-test',
121
121
  servers: {
122
122
  badServer: {
@@ -153,7 +153,7 @@ describe('MCP Server Logging', () => {
153
153
  console.log(formatted);
154
154
  };
155
155
 
156
- const config = new MCPConfiguration({
156
+ const config = new MCPClient({
157
157
  id: 'console-log-test',
158
158
  servers: {
159
159
  echoServer: {
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
5
5
  import { weatherTool } from './__fixtures__/tools';
6
- import { MCPConfiguration } from './configuration';
6
+ import { MCPClient } from './configuration';
7
7
  import { MCPServer } from './server';
8
8
 
9
9
  const PORT = 9100 + Math.floor(Math.random() * 1000);
@@ -94,7 +94,7 @@ describe('MCPServer', () => {
94
94
  expect(server.getStdioTransport()).toBeInstanceOf(StdioServerTransport);
95
95
  });
96
96
  it('should use stdio transport to get tools', async () => {
97
- const existingConfig = new MCPConfiguration({
97
+ const existingConfig = new MCPClient({
98
98
  servers: {
99
99
  weather: {
100
100
  command: 'npx',