@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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +7 -0
- package/README.md +72 -49
- package/dist/_tsup-dts-rollup.d.cts +57 -30
- package/dist/_tsup-dts-rollup.d.ts +57 -30
- package/dist/index.cjs +72 -21
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +69 -22
- package/package.json +2 -1
- package/src/client.ts +14 -1
- package/src/configuration.test.ts +17 -17
- package/src/configuration.ts +90 -25
- package/src/index.ts +2 -1
- package/src/server-logging.test.ts +4 -4
- package/src/server.test.ts +2 -2
package/src/configuration.ts
CHANGED
|
@@ -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 {
|
|
5
|
+
import { InternalMastraMCPClient } from './client';
|
|
5
6
|
import type { MastraMCPServerDefinition } from './client';
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const mcpClientInstances = new Map<string, InstanceType<typeof MCPClient>>();
|
|
8
9
|
|
|
9
|
-
export interface
|
|
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
|
|
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:
|
|
21
|
-
super({ name: '
|
|
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 =
|
|
44
|
+
const existingInstance = mcpClientInstances.get(this.id);
|
|
28
45
|
if (existingInstance) {
|
|
29
46
|
if (!args.id) {
|
|
30
|
-
throw new Error(`
|
|
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
|
|
36
|
-
2. Call "await
|
|
37
|
-
3. If you only need one instance of
|
|
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 (!
|
|
48
|
-
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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(`
|
|
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
|
|
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
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
156
|
+
const config = new MCPClient({
|
|
157
157
|
id: 'console-log-test',
|
|
158
158
|
servers: {
|
|
159
159
|
echoServer: {
|
package/src/server.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
97
|
+
const existingConfig = new MCPClient({
|
|
98
98
|
servers: {
|
|
99
99
|
weather: {
|
|
100
100
|
command: 'npx',
|