@mastra/mcp 0.2.7-alpha.8 → 0.3.0-alpha.10

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.2.7-alpha.8 build /home/runner/work/mastra/mastra/packages/mcp
2
+ > @mastra/mcp@0.3.0-alpha.10 build /home/runner/work/mastra/mastra/packages/mcp
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.3.6
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 9772ms
9
+ TSC ⚡️ Build success in 11301ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.7.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.7.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 9475ms
16
+ DTS ⚡️ Build success in 11798ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 2.22 KB
21
- CJS ⚡️ Build success in 344ms
22
- ESM dist/index.js 2.19 KB
23
- ESM ⚡️ Build success in 348ms
20
+ ESM dist/index.js 6.82 KB
21
+ ESM ⚡️ Build success in 800ms
22
+ CJS dist/index.cjs 6.87 KB
23
+ CJS ⚡️ Build success in 798ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @mastra/mcp
2
2
 
3
+ ## 0.3.0-alpha.10
4
+
5
+ ### Minor Changes
6
+
7
+ - dd7a09a: Added new MCPConfiguration class for managing multiple MCP server tools/toolsets. Fixed a bug where MCPClient env would overwrite PATH env var. Fixed a bug where MCP servers would be killed non-gracefully leading to printing huge errors on every code save when running mastra dev
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [a910463]
12
+ - @mastra/core@0.5.0-alpha.10
13
+
14
+ ## 0.2.7-alpha.9
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [e9fbac5]
19
+ - Updated dependencies [1e8bcbc]
20
+ - Updated dependencies [aeb5e36]
21
+ - Updated dependencies [f2301de]
22
+ - @mastra/core@0.5.0-alpha.9
23
+
3
24
  ## 0.2.7-alpha.8
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -53,6 +53,114 @@ const tools = await client.tools();
53
53
  await client.disconnect();
54
54
  ```
55
55
 
56
+ ## Managing Multiple MCP Servers
57
+
58
+ For applications that need to interact with multiple MCP servers, the `MCPConfiguration` class provides a convenient way to manage multiple server connections and their tools:
59
+
60
+ ```typescript
61
+ import { MCPConfiguration } from '@mastra/mcp';
62
+
63
+ const mcp = new MCPConfiguration({
64
+ servers: {
65
+ // Stdio-based server
66
+ stockPrice: {
67
+ command: 'npx',
68
+ args: ['tsx', 'stock-price.ts'],
69
+ env: {
70
+ API_KEY: 'your-api-key',
71
+ },
72
+ },
73
+ // SSE-based server
74
+ weather: {
75
+ url: new URL('http://localhost:8080/sse'),
76
+ },
77
+ },
78
+ });
79
+
80
+ // Get all tools from all configured servers namespaced with the server name
81
+ const tools = await mcp.getTools();
82
+
83
+ // Get tools grouped into a toolset object per-server
84
+ const toolsets = await mcp.getToolsets();
85
+ ```
86
+
87
+ ### Tools vs Toolsets
88
+
89
+ The MCPConfiguration class provides two ways to access MCP tools:
90
+
91
+ #### Tools (`getTools()`)
92
+
93
+ Use this when:
94
+
95
+ - You have a single MCP connection
96
+ - The tools are used by a single user/context (CLI tools, automation scripts, etc)
97
+ - Tool configuration (API keys, credentials) remains constant
98
+ - You want to initialize an Agent with a fixed set of tools
99
+
100
+ ```typescript
101
+ const agent = new Agent({
102
+ name: 'CLI Assistant',
103
+ instructions: 'You help users with CLI tasks',
104
+ model: openai('gpt-4'),
105
+ tools: await mcp.getTools(), // Tools are fixed at agent creation
106
+ });
107
+ ```
108
+
109
+ #### Toolsets (`getToolsets()`)
110
+
111
+ Use this when:
112
+
113
+ - You need per-request tool configuration
114
+ - Tools need different credentials per user
115
+ - Running in a multi-user environment (web app, API, etc)
116
+ - Tool configuration needs to change dynamically
117
+
118
+ ```typescript
119
+ import { MCPConfiguration } from '@mastra/mcp';
120
+ import { Agent } from '@mastra/core/agent';
121
+ import { openai } from '@ai-sdk/openai';
122
+
123
+ // Configure MCP servers with user-specific settings before getting toolsets
124
+ const mcp = new MCPConfiguration({
125
+ servers: {
126
+ stockPrice: {
127
+ command: 'npx',
128
+ args: ['tsx', 'weather-mcp.ts'],
129
+ env: {
130
+ // These would be different per user
131
+ API_KEY: 'user-1-api-key',
132
+ },
133
+ },
134
+ weather: {
135
+ url: new URL('http://localhost:8080/sse'),
136
+ requestInit: {
137
+ headers: {
138
+ // These would be different per user
139
+ Authorization: 'Bearer user-1-token',
140
+ },
141
+ },
142
+ },
143
+ },
144
+ });
145
+
146
+ // Get the current toolsets configured for this user
147
+ const toolsets = await mcp.getToolsets();
148
+
149
+ // Use the agent with user-specific tool configurations
150
+ const response = await agent.generate('What is the weather in London?', {
151
+ toolsets,
152
+ });
153
+
154
+ console.log(response.text);
155
+ ```
156
+
157
+ The `MCPConfiguration` class automatically:
158
+
159
+ - Manages connections to multiple MCP servers
160
+ - Namespaces tools to prevent naming conflicts
161
+ - Handles connection lifecycle and cleanup
162
+ - Provides both flat and grouped access to tools
163
+
56
164
  ## Configuration
57
165
 
58
166
  ### Required Parameters
@@ -1,18 +1,21 @@
1
1
  import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
2
+ import { FastMCP } from 'fastmcp';
3
+ import { MastraBase } from '@mastra/core/base';
2
4
  import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
6
  import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
5
7
 
6
- declare class MastraMCPClient {
8
+ declare class MastraMCPClient extends MastraBase {
7
9
  name: string;
8
10
  private transport;
9
11
  private client;
10
12
  constructor({ name, version, server, capabilities, }: {
11
13
  name: string;
12
- server: StdioServerParameters | SSEClientParameters;
14
+ server: MastraMCPServerDefinition;
13
15
  capabilities?: ClientCapabilities;
14
16
  version?: string;
15
17
  });
18
+ private isConnected;
16
19
  connect(): Promise<void>;
17
20
  disconnect(): Promise<void>;
18
21
  resources(): Promise<ReturnType<Protocol<any, any, any>['request']>>;
@@ -21,6 +24,33 @@ declare class MastraMCPClient {
21
24
  export { MastraMCPClient }
22
25
  export { MastraMCPClient as MastraMCPClient_alias_1 }
23
26
 
27
+ declare type MastraMCPServerDefinition = StdioServerParameters | SSEClientParameters;
28
+ export { MastraMCPServerDefinition }
29
+ export { MastraMCPServerDefinition as MastraMCPServerDefinition_alias_1 }
30
+
31
+ declare class MCPConfiguration extends MastraBase {
32
+ private serverConfigs;
33
+ private id;
34
+ constructor(args: {
35
+ id?: string;
36
+ servers: Record<string, MastraMCPServerDefinition>;
37
+ });
38
+ private addToInstanceCache;
39
+ private makeId;
40
+ disconnect(): Promise<void>;
41
+ getTools(): Promise<Record<string, any>>;
42
+ getToolsets(): Promise<Record<string, Record<string, any>>>;
43
+ private mcpClientsById;
44
+ private getConnectedClient;
45
+ private eachClientTools;
46
+ }
47
+ export { MCPConfiguration }
48
+ export { MCPConfiguration as MCPConfiguration_alias_1 }
49
+
50
+ export declare const server: FastMCP<undefined>;
51
+
52
+ export declare const server_alias_1: FastMCP<undefined>;
53
+
24
54
  declare type SSEClientParameters = {
25
55
  url: URL;
26
56
  } & ConstructorParameters<typeof SSEClientTransport>[1];
@@ -1,18 +1,21 @@
1
1
  import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
2
+ import { FastMCP } from 'fastmcp';
3
+ import { MastraBase } from '@mastra/core/base';
2
4
  import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
4
6
  import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
5
7
 
6
- declare class MastraMCPClient {
8
+ declare class MastraMCPClient extends MastraBase {
7
9
  name: string;
8
10
  private transport;
9
11
  private client;
10
12
  constructor({ name, version, server, capabilities, }: {
11
13
  name: string;
12
- server: StdioServerParameters | SSEClientParameters;
14
+ server: MastraMCPServerDefinition;
13
15
  capabilities?: ClientCapabilities;
14
16
  version?: string;
15
17
  });
18
+ private isConnected;
16
19
  connect(): Promise<void>;
17
20
  disconnect(): Promise<void>;
18
21
  resources(): Promise<ReturnType<Protocol<any, any, any>['request']>>;
@@ -21,6 +24,33 @@ declare class MastraMCPClient {
21
24
  export { MastraMCPClient }
22
25
  export { MastraMCPClient as MastraMCPClient_alias_1 }
23
26
 
27
+ declare type MastraMCPServerDefinition = StdioServerParameters | SSEClientParameters;
28
+ export { MastraMCPServerDefinition }
29
+ export { MastraMCPServerDefinition as MastraMCPServerDefinition_alias_1 }
30
+
31
+ declare class MCPConfiguration extends MastraBase {
32
+ private serverConfigs;
33
+ private id;
34
+ constructor(args: {
35
+ id?: string;
36
+ servers: Record<string, MastraMCPServerDefinition>;
37
+ });
38
+ private addToInstanceCache;
39
+ private makeId;
40
+ disconnect(): Promise<void>;
41
+ getTools(): Promise<Record<string, any>>;
42
+ getToolsets(): Promise<Record<string, Record<string, any>>>;
43
+ private mcpClientsById;
44
+ private getConnectedClient;
45
+ private eachClientTools;
46
+ }
47
+ export { MCPConfiguration }
48
+ export { MCPConfiguration as MCPConfiguration_alias_1 }
49
+
50
+ export declare const server: FastMCP<undefined>;
51
+
52
+ export declare const server_alias_1: FastMCP<undefined>;
53
+
24
54
  declare type SSEClientParameters = {
25
55
  url: URL;
26
56
  } & ConstructorParameters<typeof SSEClientTransport>[1];
package/dist/index.cjs CHANGED
@@ -1,14 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ var base = require('@mastra/core/base');
3
4
  var tools = require('@mastra/core/tools');
4
5
  var utils = require('@mastra/core/utils');
5
6
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
6
7
  var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
7
8
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
9
  var types_js = require('@modelcontextprotocol/sdk/types.js');
10
+ var exitHook = require('exit-hook');
11
+ var uuid = require('uuid');
9
12
 
10
13
  // src/client.ts
11
- var MastraMCPClient = class {
14
+ var MastraMCPClient = class extends base.MastraBase {
12
15
  name;
13
16
  transport;
14
17
  client;
@@ -18,6 +21,7 @@ var MastraMCPClient = class {
18
21
  server,
19
22
  capabilities = {}
20
23
  }) {
24
+ super({ name: "MastraMCPClient" });
21
25
  this.name = name;
22
26
  if (`url` in server) {
23
27
  this.transport = new sse_js.SSEClientTransport(server.url, {
@@ -25,7 +29,11 @@ var MastraMCPClient = class {
25
29
  eventSourceInit: server.eventSourceInit
26
30
  });
27
31
  } else {
28
- this.transport = new stdio_js.StdioClientTransport(server);
32
+ this.transport = new stdio_js.StdioClientTransport({
33
+ ...server,
34
+ // without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
35
+ env: { ...stdio_js.getDefaultEnvironment(), ...server.env || {} }
36
+ });
29
37
  }
30
38
  this.client = new index_js.Client(
31
39
  {
@@ -37,8 +45,32 @@ var MastraMCPClient = class {
37
45
  }
38
46
  );
39
47
  }
48
+ isConnected = false;
40
49
  async connect() {
41
- return await this.client.connect(this.transport);
50
+ if (this.isConnected) return;
51
+ try {
52
+ await this.client.connect(this.transport);
53
+ this.isConnected = true;
54
+ this.client.onclose = () => {
55
+ this.isConnected = false;
56
+ };
57
+ await this.client.setLoggingLevel(`critical`);
58
+ exitHook.asyncExitHook(
59
+ async () => {
60
+ this.logger.debug(`Disconnecting ${this.name} MCP server`);
61
+ await this.disconnect();
62
+ },
63
+ { wait: 5e3 }
64
+ );
65
+ process.on("SIGTERM", () => exitHook.gracefulExit());
66
+ } catch (e) {
67
+ this.logger.error(
68
+ `Failed connecting to MCPClient with name ${this.name}.
69
+ ${e instanceof Error ? e.stack : JSON.stringify(e, null, 2)}`
70
+ );
71
+ this.isConnected = false;
72
+ throw e;
73
+ }
42
74
  }
43
75
  async disconnect() {
44
76
  return await this.client.close();
@@ -77,5 +109,107 @@ var MastraMCPClient = class {
77
109
  return toolsRes;
78
110
  }
79
111
  };
112
+ var mastraMCPConfigurationInstances = /* @__PURE__ */ new Map();
113
+ var MCPConfiguration = class extends base.MastraBase {
114
+ serverConfigs = {};
115
+ id;
116
+ constructor(args) {
117
+ super({ name: "MCPConfiguration" });
118
+ this.serverConfigs = args.servers;
119
+ this.id = args.id ?? this.makeId();
120
+ const existingInstance = mastraMCPConfigurationInstances.get(this.id);
121
+ if (existingInstance) {
122
+ if (!args.id) {
123
+ throw new Error(`MCPConfiguration was initialized multiple times with the same configuration options.
124
+
125
+ This error is intended to prevent memory leaks.
126
+
127
+ To fix this you have three different options:
128
+ 1. If you need multiple MCPConfiguration class instances with identical server configurations, set an id when configuring: new MCPConfiguration({ id: "my-unique-id" })
129
+ 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.
130
+ 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)
131
+ `);
132
+ }
133
+ return existingInstance;
134
+ }
135
+ this.addToInstanceCache();
136
+ return this;
137
+ }
138
+ addToInstanceCache() {
139
+ if (!mastraMCPConfigurationInstances.has(this.id)) {
140
+ mastraMCPConfigurationInstances.set(this.id, this);
141
+ }
142
+ }
143
+ makeId() {
144
+ const text = JSON.stringify(this.serverConfigs).normalize("NFKC");
145
+ const idNamespace = uuid.v5(`MCPConfiguration`, uuid.v5.DNS);
146
+ return uuid.v5(text, idNamespace);
147
+ }
148
+ async disconnect() {
149
+ mastraMCPConfigurationInstances.delete(this.id);
150
+ for (const serverName of Object.keys(this.serverConfigs)) {
151
+ const client = this.mcpClientsById.get(serverName);
152
+ if (client) {
153
+ await client.disconnect();
154
+ }
155
+ }
156
+ }
157
+ async getTools() {
158
+ this.addToInstanceCache();
159
+ const connectedTools = {};
160
+ await this.eachClientTools(async ({ serverName, tools }) => {
161
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
162
+ connectedTools[`${serverName}_${toolName}`] = toolConfig;
163
+ }
164
+ });
165
+ return connectedTools;
166
+ }
167
+ async getToolsets() {
168
+ this.addToInstanceCache();
169
+ const connectedToolsets = {};
170
+ await this.eachClientTools(async ({ serverName, tools }) => {
171
+ if (tools) {
172
+ connectedToolsets[serverName] = tools;
173
+ }
174
+ });
175
+ return connectedToolsets;
176
+ }
177
+ mcpClientsById = /* @__PURE__ */ new Map();
178
+ async getConnectedClient(name, config) {
179
+ const exists = this.mcpClientsById.has(name);
180
+ if (exists) {
181
+ const mcpClient2 = this.mcpClientsById.get(name);
182
+ await mcpClient2.connect();
183
+ return mcpClient2;
184
+ }
185
+ this.logger.debug(`Connecting to ${name} MCP server`);
186
+ const mcpClient = new MastraMCPClient({
187
+ name,
188
+ server: config
189
+ });
190
+ this.mcpClientsById.set(name, mcpClient);
191
+ try {
192
+ await mcpClient.connect();
193
+ } catch (e) {
194
+ this.mcpClientsById.delete(name);
195
+ this.logger.error(`MCPConfiguraiton errored connecting to MCP server ${name}`);
196
+ throw e;
197
+ }
198
+ this.logger.debug(`Connected to ${name} MCP server`);
199
+ return mcpClient;
200
+ }
201
+ async eachClientTools(cb) {
202
+ for (const [serverName, serverConfig] of Object.entries(this.serverConfigs)) {
203
+ const client = await this.getConnectedClient(serverName, serverConfig);
204
+ const tools = await client.tools();
205
+ await cb({
206
+ serverName,
207
+ tools,
208
+ client
209
+ });
210
+ }
211
+ }
212
+ };
80
213
 
214
+ exports.MCPConfiguration = MCPConfiguration;
81
215
  exports.MastraMCPClient = MastraMCPClient;
package/dist/index.d.cts CHANGED
@@ -1 +1,3 @@
1
+ export { MastraMCPServerDefinition_alias_1 as MastraMCPServerDefinition } from './_tsup-dts-rollup.cjs';
1
2
  export { MastraMCPClient_alias_1 as MastraMCPClient } from './_tsup-dts-rollup.cjs';
3
+ export { MCPConfiguration_alias_1 as MCPConfiguration } from './_tsup-dts-rollup.cjs';
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
+ export { MastraMCPServerDefinition_alias_1 as MastraMCPServerDefinition } from './_tsup-dts-rollup.js';
1
2
  export { MastraMCPClient_alias_1 as MastraMCPClient } from './_tsup-dts-rollup.js';
3
+ export { MCPConfiguration_alias_1 as MCPConfiguration } from './_tsup-dts-rollup.js';
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { MastraBase } from '@mastra/core/base';
1
2
  import { createTool } from '@mastra/core/tools';
2
3
  import { jsonSchemaToModel } from '@mastra/core/utils';
3
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
6
7
  import { ListResourcesResultSchema } from '@modelcontextprotocol/sdk/types.js';
8
+ import { asyncExitHook, gracefulExit } from 'exit-hook';
9
+ import { v5 } from 'uuid';
7
10
 
8
11
  // src/client.ts
9
- var MastraMCPClient = class {
12
+ var MastraMCPClient = class extends MastraBase {
10
13
  name;
11
14
  transport;
12
15
  client;
@@ -16,6 +19,7 @@ var MastraMCPClient = class {
16
19
  server,
17
20
  capabilities = {}
18
21
  }) {
22
+ super({ name: "MastraMCPClient" });
19
23
  this.name = name;
20
24
  if (`url` in server) {
21
25
  this.transport = new SSEClientTransport(server.url, {
@@ -23,7 +27,11 @@ var MastraMCPClient = class {
23
27
  eventSourceInit: server.eventSourceInit
24
28
  });
25
29
  } else {
26
- this.transport = new StdioClientTransport(server);
30
+ this.transport = new StdioClientTransport({
31
+ ...server,
32
+ // without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
33
+ env: { ...getDefaultEnvironment(), ...server.env || {} }
34
+ });
27
35
  }
28
36
  this.client = new Client(
29
37
  {
@@ -35,8 +43,32 @@ var MastraMCPClient = class {
35
43
  }
36
44
  );
37
45
  }
46
+ isConnected = false;
38
47
  async connect() {
39
- return await this.client.connect(this.transport);
48
+ if (this.isConnected) return;
49
+ try {
50
+ await this.client.connect(this.transport);
51
+ this.isConnected = true;
52
+ this.client.onclose = () => {
53
+ this.isConnected = false;
54
+ };
55
+ await this.client.setLoggingLevel(`critical`);
56
+ asyncExitHook(
57
+ async () => {
58
+ this.logger.debug(`Disconnecting ${this.name} MCP server`);
59
+ await this.disconnect();
60
+ },
61
+ { wait: 5e3 }
62
+ );
63
+ process.on("SIGTERM", () => gracefulExit());
64
+ } catch (e) {
65
+ this.logger.error(
66
+ `Failed connecting to MCPClient with name ${this.name}.
67
+ ${e instanceof Error ? e.stack : JSON.stringify(e, null, 2)}`
68
+ );
69
+ this.isConnected = false;
70
+ throw e;
71
+ }
40
72
  }
41
73
  async disconnect() {
42
74
  return await this.client.close();
@@ -75,5 +107,106 @@ var MastraMCPClient = class {
75
107
  return toolsRes;
76
108
  }
77
109
  };
110
+ var mastraMCPConfigurationInstances = /* @__PURE__ */ new Map();
111
+ var MCPConfiguration = class extends MastraBase {
112
+ serverConfigs = {};
113
+ id;
114
+ constructor(args) {
115
+ super({ name: "MCPConfiguration" });
116
+ this.serverConfigs = args.servers;
117
+ this.id = args.id ?? this.makeId();
118
+ const existingInstance = mastraMCPConfigurationInstances.get(this.id);
119
+ if (existingInstance) {
120
+ if (!args.id) {
121
+ throw new Error(`MCPConfiguration was initialized multiple times with the same configuration options.
122
+
123
+ This error is intended to prevent memory leaks.
124
+
125
+ To fix this you have three different options:
126
+ 1. If you need multiple MCPConfiguration class instances with identical server configurations, set an id when configuring: new MCPConfiguration({ id: "my-unique-id" })
127
+ 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.
128
+ 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)
129
+ `);
130
+ }
131
+ return existingInstance;
132
+ }
133
+ this.addToInstanceCache();
134
+ return this;
135
+ }
136
+ addToInstanceCache() {
137
+ if (!mastraMCPConfigurationInstances.has(this.id)) {
138
+ mastraMCPConfigurationInstances.set(this.id, this);
139
+ }
140
+ }
141
+ makeId() {
142
+ const text = JSON.stringify(this.serverConfigs).normalize("NFKC");
143
+ const idNamespace = v5(`MCPConfiguration`, v5.DNS);
144
+ return v5(text, idNamespace);
145
+ }
146
+ async disconnect() {
147
+ mastraMCPConfigurationInstances.delete(this.id);
148
+ for (const serverName of Object.keys(this.serverConfigs)) {
149
+ const client = this.mcpClientsById.get(serverName);
150
+ if (client) {
151
+ await client.disconnect();
152
+ }
153
+ }
154
+ }
155
+ async getTools() {
156
+ this.addToInstanceCache();
157
+ const connectedTools = {};
158
+ await this.eachClientTools(async ({ serverName, tools }) => {
159
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
160
+ connectedTools[`${serverName}_${toolName}`] = toolConfig;
161
+ }
162
+ });
163
+ return connectedTools;
164
+ }
165
+ async getToolsets() {
166
+ this.addToInstanceCache();
167
+ const connectedToolsets = {};
168
+ await this.eachClientTools(async ({ serverName, tools }) => {
169
+ if (tools) {
170
+ connectedToolsets[serverName] = tools;
171
+ }
172
+ });
173
+ return connectedToolsets;
174
+ }
175
+ mcpClientsById = /* @__PURE__ */ new Map();
176
+ async getConnectedClient(name, config) {
177
+ const exists = this.mcpClientsById.has(name);
178
+ if (exists) {
179
+ const mcpClient2 = this.mcpClientsById.get(name);
180
+ await mcpClient2.connect();
181
+ return mcpClient2;
182
+ }
183
+ this.logger.debug(`Connecting to ${name} MCP server`);
184
+ const mcpClient = new MastraMCPClient({
185
+ name,
186
+ server: config
187
+ });
188
+ this.mcpClientsById.set(name, mcpClient);
189
+ try {
190
+ await mcpClient.connect();
191
+ } catch (e) {
192
+ this.mcpClientsById.delete(name);
193
+ this.logger.error(`MCPConfiguraiton errored connecting to MCP server ${name}`);
194
+ throw e;
195
+ }
196
+ this.logger.debug(`Connected to ${name} MCP server`);
197
+ return mcpClient;
198
+ }
199
+ async eachClientTools(cb) {
200
+ for (const [serverName, serverConfig] of Object.entries(this.serverConfigs)) {
201
+ const client = await this.getConnectedClient(serverName, serverConfig);
202
+ const tools = await client.tools();
203
+ await cb({
204
+ serverName,
205
+ tools,
206
+ client
207
+ });
208
+ }
209
+ }
210
+ };
78
211
 
79
- export { MastraMCPClient };
212
+ export { MCPConfiguration, MastraMCPClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.2.7-alpha.8",
3
+ "version": "0.3.0-alpha.10",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,17 +24,20 @@
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.1.1",
26
26
  "date-fns": "^4.1.0",
27
- "zod": "^3.24.1",
28
- "@mastra/core": "^0.5.0-alpha.8"
27
+ "exit-hook": "^4.0.0",
28
+ "uuid": "^11.1.0",
29
+ "@mastra/core": "^0.5.0-alpha.10"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@ai-sdk/anthropic": "^1.1.6",
32
33
  "@microsoft/api-extractor": "^7.49.2",
33
34
  "@types/node": "^22.13.1",
35
+ "eslint": "^9.20.1",
36
+ "fastmcp": "^1.20.2",
34
37
  "tsup": "^8.0.1",
35
38
  "typescript": "^5.7.3",
36
39
  "vitest": "^3.0.4",
37
- "eslint": "^9.20.1",
40
+ "zod": "^3.24.2",
38
41
  "@internal/lint": "0.0.0"
39
42
  },
40
43
  "scripts": {
@@ -0,0 +1,44 @@
1
+ import { FastMCP } from 'fastmcp';
2
+ import { z } from 'zod';
3
+
4
+ const getStockPrice = async (symbol: string) => {
5
+ // Return mock data for testing
6
+ return {
7
+ symbol,
8
+ currentPrice: '150.00',
9
+ };
10
+ };
11
+
12
+ const server = new FastMCP({
13
+ name: 'Stock Price Server',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ const stockSchema = z.object({
18
+ symbol: z.string(),
19
+ });
20
+
21
+ server.addTool({
22
+ name: 'getStockPrice',
23
+ description: "Fetches the last day's closing stock price for a given symbol",
24
+ parameters: stockSchema,
25
+ execute: async (args: z.infer<typeof stockSchema>) => {
26
+ try {
27
+ const priceData = await getStockPrice(args.symbol);
28
+ return JSON.stringify(priceData);
29
+ } catch (error) {
30
+ if (error instanceof Error) {
31
+ throw new Error(`Stock price fetch failed: ${error.message}`);
32
+ }
33
+ throw error;
34
+ }
35
+ },
36
+ });
37
+
38
+ // Start the server with stdio transport
39
+ void server.start({
40
+ transportType: 'stdio',
41
+ });
42
+
43
+ export { server };
44
+
@@ -0,0 +1,52 @@
1
+ import { FastMCP } from 'fastmcp';
2
+ import { z } from 'zod';
3
+
4
+ const getWeather = async (location: string) => {
5
+ // Return mock data for testing
6
+ return {
7
+ temperature: 20,
8
+ feelsLike: 18,
9
+ humidity: 65,
10
+ windSpeed: 10,
11
+ windGust: 15,
12
+ conditions: 'Clear sky',
13
+ location,
14
+ };
15
+ };
16
+
17
+ const server = new FastMCP({
18
+ name: 'Weather Server',
19
+ version: '1.0.0',
20
+ });
21
+
22
+ const weatherSchema = z.object({
23
+ location: z.string().describe('City name'),
24
+ });
25
+
26
+ server.addTool({
27
+ name: 'getWeather',
28
+ description: 'Get current weather for a location',
29
+ parameters: weatherSchema,
30
+ execute: async (args: z.infer<typeof weatherSchema>) => {
31
+ try {
32
+ const weatherData = await getWeather(args.location);
33
+ return JSON.stringify(weatherData);
34
+ } catch (error) {
35
+ if (error instanceof Error) {
36
+ throw new Error(`Weather fetch failed: ${error.message}`);
37
+ }
38
+ throw error;
39
+ }
40
+ },
41
+ });
42
+
43
+ // Start the server with SSE support
44
+ void server.start({
45
+ transportType: 'sse',
46
+ sse: {
47
+ endpoint: '/sse',
48
+ port: 60808,
49
+ },
50
+ });
51
+
52
+ export { server };
package/src/client.ts CHANGED
@@ -1,19 +1,24 @@
1
+ import { MastraBase } from '@mastra/core/base';
1
2
  import { createTool } from '@mastra/core/tools';
2
3
  import { jsonSchemaToModel } from '@mastra/core/utils';
3
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
7
  import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
7
8
  import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
8
9
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
9
10
  import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
10
11
  import { ListResourcesResultSchema } from '@modelcontextprotocol/sdk/types.js';
11
12
 
13
+ import { asyncExitHook, gracefulExit } from 'exit-hook';
14
+
12
15
  type SSEClientParameters = {
13
16
  url: URL;
14
17
  } & ConstructorParameters<typeof SSEClientTransport>[1];
15
18
 
16
- export class MastraMCPClient {
19
+ export type MastraMCPServerDefinition = StdioServerParameters | SSEClientParameters;
20
+
21
+ export class MastraMCPClient extends MastraBase {
17
22
  name: string;
18
23
  private transport: Transport;
19
24
  private client: Client;
@@ -24,10 +29,11 @@ export class MastraMCPClient {
24
29
  capabilities = {},
25
30
  }: {
26
31
  name: string;
27
- server: StdioServerParameters | SSEClientParameters;
32
+ server: MastraMCPServerDefinition;
28
33
  capabilities?: ClientCapabilities;
29
34
  version?: string;
30
35
  }) {
36
+ super({ name: 'MastraMCPClient' });
31
37
  this.name = name;
32
38
 
33
39
  if (`url` in server) {
@@ -36,7 +42,11 @@ export class MastraMCPClient {
36
42
  eventSourceInit: server.eventSourceInit,
37
43
  });
38
44
  } else {
39
- this.transport = new StdioClientTransport(server);
45
+ this.transport = new StdioClientTransport({
46
+ ...server,
47
+ // without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
48
+ env: { ...getDefaultEnvironment(), ...(server.env || {}) },
49
+ });
40
50
  }
41
51
 
42
52
  this.client = new Client(
@@ -50,8 +60,33 @@ export class MastraMCPClient {
50
60
  );
51
61
  }
52
62
 
63
+ private isConnected = false;
64
+
53
65
  async connect() {
54
- return await this.client.connect(this.transport);
66
+ if (this.isConnected) return;
67
+ try {
68
+ await this.client.connect(this.transport);
69
+ this.isConnected = true;
70
+ this.client.onclose = () => {
71
+ this.isConnected = false;
72
+ };
73
+ await this.client.setLoggingLevel(`critical`);
74
+ asyncExitHook(
75
+ async () => {
76
+ this.logger.debug(`Disconnecting ${this.name} MCP server`);
77
+ await this.disconnect();
78
+ },
79
+ { wait: 5000 },
80
+ );
81
+
82
+ process.on('SIGTERM', () => gracefulExit());
83
+ } catch (e) {
84
+ this.logger.error(
85
+ `Failed connecting to MCPClient with name ${this.name}.\n${e instanceof Error ? e.stack : JSON.stringify(e, null, 2)}`,
86
+ );
87
+ this.isConnected = false;
88
+ throw e;
89
+ }
55
90
  }
56
91
 
57
92
  async disconnect() {
@@ -0,0 +1,181 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll } from 'vitest';
4
+ import { MCPConfiguration } from './configuration';
5
+
6
+ describe('MCPConfiguration', () => {
7
+ let mcp: MCPConfiguration;
8
+ let weatherProcess: ReturnType<typeof spawn>;
9
+
10
+ beforeAll(async () => {
11
+ // Start the weather SSE server
12
+ weatherProcess = spawn('npx', ['-y', 'tsx', path.join(__dirname, '__fixtures__/weather.ts')]);
13
+
14
+ // Wait for SSE server to be ready
15
+ let resolved = false;
16
+ await new Promise<void>((resolve, reject) => {
17
+ weatherProcess.on(`exit`, () => {
18
+ if (!resolved) reject();
19
+ });
20
+ if (weatherProcess.stderr) {
21
+ weatherProcess.stderr.on(`data`, chunk => {
22
+ console.error(chunk.toString());
23
+ });
24
+ }
25
+ if (weatherProcess.stdout) {
26
+ weatherProcess.stdout.on('data', chunk => {
27
+ if (chunk.toString().includes('server is running on SSE')) {
28
+ resolve();
29
+ resolved = true;
30
+ }
31
+ });
32
+ }
33
+ });
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ mcp = new MCPConfiguration({
38
+ servers: {
39
+ stockPrice: {
40
+ command: 'npx',
41
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
42
+ env: {
43
+ FAKE_CREDS: 'test',
44
+ },
45
+ },
46
+ weather: {
47
+ url: new URL('http://localhost:60808/sse'),
48
+ },
49
+ },
50
+ });
51
+ });
52
+
53
+ afterEach(async () => {
54
+ // Clean up any connected clients
55
+ await mcp.disconnect();
56
+ });
57
+
58
+ afterAll(async () => {
59
+ // Kill the weather SSE server
60
+ weatherProcess.kill('SIGINT');
61
+ });
62
+
63
+ it('should initialize with server configurations', () => {
64
+ expect(mcp['serverConfigs']).toEqual({
65
+ stockPrice: {
66
+ command: 'npx',
67
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
68
+ env: {
69
+ FAKE_CREDS: 'test',
70
+ },
71
+ },
72
+ weather: {
73
+ url: new URL('http://localhost:60808/sse'),
74
+ },
75
+ });
76
+ });
77
+
78
+ it('should get connected tools with namespaced tool names', async () => {
79
+ const connectedTools = await mcp.getTools();
80
+
81
+ // Each tool should be namespaced with its server name
82
+ expect(connectedTools).toHaveProperty('stockPrice_getStockPrice');
83
+ expect(connectedTools).toHaveProperty('weather_getWeather');
84
+ });
85
+
86
+ it('should get connected toolsets grouped by server', async () => {
87
+ const connectedToolsets = await mcp.getToolsets();
88
+
89
+ expect(connectedToolsets).toHaveProperty('stockPrice');
90
+ expect(connectedToolsets).toHaveProperty('weather');
91
+ expect(connectedToolsets.stockPrice).toHaveProperty('getStockPrice');
92
+ expect(connectedToolsets.weather).toHaveProperty('getWeather');
93
+ });
94
+
95
+ it('should handle connection errors gracefully', async () => {
96
+ const badConfig = new MCPConfiguration({
97
+ servers: {
98
+ badServer: {
99
+ command: 'nonexistent-command',
100
+ args: [],
101
+ },
102
+ },
103
+ });
104
+
105
+ await expect(badConfig.getTools()).rejects.toThrow();
106
+ await badConfig.disconnect();
107
+ });
108
+
109
+ describe('Instance Management', () => {
110
+ it('should allow multiple instances with different IDs', async () => {
111
+ const config2 = new MCPConfiguration({
112
+ id: 'custom-id',
113
+ servers: {
114
+ stockPrice: {
115
+ command: 'npx',
116
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
117
+ env: {
118
+ FAKE_CREDS: 'test',
119
+ },
120
+ },
121
+ },
122
+ });
123
+
124
+ expect(config2).not.toBe(mcp);
125
+ await config2.disconnect();
126
+ });
127
+
128
+ it('should allow reuse of configuration after closing', async () => {
129
+ await mcp.disconnect();
130
+
131
+ const config2 = new MCPConfiguration({
132
+ servers: {
133
+ stockPrice: {
134
+ command: 'npx',
135
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
136
+ env: {
137
+ FAKE_CREDS: 'test',
138
+ },
139
+ },
140
+ weather: {
141
+ url: new URL('http://localhost:60808/sse'),
142
+ },
143
+ },
144
+ });
145
+
146
+ expect(config2).not.toBe(mcp);
147
+ await config2.disconnect();
148
+ });
149
+
150
+ it('should throw error when creating duplicate instance without ID', async () => {
151
+ const existingConfig = new MCPConfiguration({
152
+ servers: {
153
+ stockPrice: {
154
+ command: 'npx',
155
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
156
+ env: {
157
+ FAKE_CREDS: 'test',
158
+ },
159
+ },
160
+ },
161
+ });
162
+
163
+ expect(
164
+ () =>
165
+ new MCPConfiguration({
166
+ servers: {
167
+ stockPrice: {
168
+ command: 'npx',
169
+ args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/stock-price.ts')],
170
+ env: {
171
+ FAKE_CREDS: 'test',
172
+ },
173
+ },
174
+ },
175
+ }),
176
+ ).toThrow(/MCPConfiguration was initialized multiple times/);
177
+
178
+ await existingConfig.disconnect();
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,136 @@
1
+ import { MastraBase } from '@mastra/core/base';
2
+ import { v5 as uuidv5 } from 'uuid';
3
+ import { MastraMCPClient } from './client';
4
+ import type { MastraMCPServerDefinition } from './client';
5
+
6
+ const mastraMCPConfigurationInstances = new Map<string, InstanceType<typeof MCPConfiguration>>();
7
+
8
+ export class MCPConfiguration extends MastraBase {
9
+ private serverConfigs: Record<string, MastraMCPServerDefinition> = {};
10
+ private id: string;
11
+
12
+ constructor(args: { id?: string; servers: Record<string, MastraMCPServerDefinition> }) {
13
+ super({ name: 'MCPConfiguration' });
14
+ this.serverConfigs = args.servers;
15
+ this.id = args.id ?? this.makeId();
16
+
17
+ // to prevent memory leaks return the same MCP server instance when configured the same way multiple times
18
+ const existingInstance = mastraMCPConfigurationInstances.get(this.id);
19
+ if (existingInstance) {
20
+ if (!args.id) {
21
+ throw new Error(`MCPConfiguration was initialized multiple times with the same configuration options.
22
+
23
+ This error is intended to prevent memory leaks.
24
+
25
+ To fix this you have three different options:
26
+ 1. If you need multiple MCPConfiguration class instances with identical server configurations, set an id when configuring: new MCPConfiguration({ id: "my-unique-id" })
27
+ 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.
28
+ 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)
29
+ `);
30
+ }
31
+ return existingInstance;
32
+ }
33
+ this.addToInstanceCache();
34
+ return this;
35
+ }
36
+
37
+ private addToInstanceCache() {
38
+ if (!mastraMCPConfigurationInstances.has(this.id)) {
39
+ mastraMCPConfigurationInstances.set(this.id, this);
40
+ }
41
+ }
42
+
43
+ private makeId() {
44
+ const text = JSON.stringify(this.serverConfigs).normalize('NFKC');
45
+ const idNamespace = uuidv5(`MCPConfiguration`, uuidv5.DNS);
46
+
47
+ return uuidv5(text, idNamespace);
48
+ }
49
+
50
+ public async disconnect() {
51
+ mastraMCPConfigurationInstances.delete(this.id);
52
+
53
+ for (const serverName of Object.keys(this.serverConfigs)) {
54
+ const client = this.mcpClientsById.get(serverName);
55
+ if (client) {
56
+ await client.disconnect();
57
+ }
58
+ }
59
+ }
60
+
61
+ public async getTools() {
62
+ this.addToInstanceCache();
63
+ const connectedTools: Record<string, any> = {}; // <- any because we don't have proper tool schemas
64
+
65
+ await this.eachClientTools(async ({ serverName, tools }) => {
66
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
67
+ connectedTools[`${serverName}_${toolName}`] = toolConfig; // namespace tool to prevent tool name conflicts between servers
68
+ }
69
+ });
70
+
71
+ return connectedTools;
72
+ }
73
+
74
+ public async getToolsets() {
75
+ this.addToInstanceCache();
76
+ const connectedToolsets: Record<string, Record<string, any>> = {}; // <- any because we don't have proper tool schemas
77
+
78
+ await this.eachClientTools(async ({ serverName, tools }) => {
79
+ if (tools) {
80
+ connectedToolsets[serverName] = tools;
81
+ }
82
+ });
83
+
84
+ return connectedToolsets;
85
+ }
86
+
87
+ private mcpClientsById = new Map<string, MastraMCPClient>();
88
+ private async getConnectedClient(name: string, config: MastraMCPServerDefinition) {
89
+ const exists = this.mcpClientsById.has(name);
90
+
91
+ if (exists) {
92
+ const mcpClient = this.mcpClientsById.get(name)!;
93
+ await mcpClient.connect();
94
+
95
+ return mcpClient;
96
+ }
97
+
98
+ this.logger.debug(`Connecting to ${name} MCP server`);
99
+
100
+ const mcpClient = new MastraMCPClient({
101
+ name,
102
+ server: config,
103
+ });
104
+
105
+ this.mcpClientsById.set(name, mcpClient);
106
+ try {
107
+ await mcpClient.connect();
108
+ } catch (e) {
109
+ this.mcpClientsById.delete(name);
110
+ this.logger.error(`MCPConfiguraiton errored connecting to MCP server ${name}`);
111
+ throw e;
112
+ }
113
+
114
+ this.logger.debug(`Connected to ${name} MCP server`);
115
+
116
+ return mcpClient;
117
+ }
118
+
119
+ private async eachClientTools(
120
+ cb: (input: {
121
+ serverName: string;
122
+ tools: Record<string, any>; // <- any because we don't have proper tool schemas
123
+ client: InstanceType<typeof MastraMCPClient>;
124
+ }) => Promise<void>,
125
+ ) {
126
+ for (const [serverName, serverConfig] of Object.entries(this.serverConfigs)) {
127
+ const client = await this.getConnectedClient(serverName, serverConfig);
128
+ const tools = await client.tools();
129
+ await cb({
130
+ serverName,
131
+ tools,
132
+ client,
133
+ });
134
+ }
135
+ }
136
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './client';
2
+ export * from './configuration';