@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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +21 -0
- package/README.md +108 -0
- package/dist/_tsup-dts-rollup.d.cts +32 -2
- package/dist/_tsup-dts-rollup.d.ts +32 -2
- package/dist/index.cjs +137 -3
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -5
- package/package.json +7 -4
- package/src/__fixtures__/stock-price.ts +44 -0
- package/src/__fixtures__/weather.ts +52 -0
- package/src/client.ts +40 -5
- package/src/configuration.test.ts +181 -0
- package/src/configuration.ts +136 -0
- package/src/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/mcp@0.
|
|
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
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.3.6
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 11301ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.7.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.7.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 11798ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m6.82 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 800ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m6.87 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ 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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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