@mastra/mcp 0.3.10-alpha.0 → 0.3.10-alpha.2
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 +18 -0
- package/{LICENSE → LICENSE.md} +3 -1
- package/README.md +58 -0
- package/dist/_tsup-dts-rollup.d.cts +10 -3
- package/dist/_tsup-dts-rollup.d.ts +10 -3
- package/dist/index.cjs +11 -4
- package/dist/index.js +11 -4
- package/package.json +5 -5
- package/src/__fixtures__/weather.ts +55 -3
- package/src/client.ts +16 -5
- package/src/configuration.test.ts +131 -1
- package/src/configuration.ts +9 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/mcp@0.3.10-alpha.
|
|
2
|
+
> @mastra/mcp@0.3.10-alpha.2 build /home/runner/work/mastra/mastra/packages/mcp
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
|
|
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.4.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 13988ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.2
|
|
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.8.2
|
|
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 10061ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
20
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m7.53 KB[39m
|
|
21
|
+
[32mCJS[39m ⚡️ Build success in 706ms
|
|
22
|
+
[32mESM[39m [1mdist/index.js [22m[32m7.48 KB[39m
|
|
23
|
+
[32mESM[39m ⚡️ Build success in 706ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @mastra/mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.10-alpha.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [0bcc862]
|
|
8
|
+
- @mastra/core@0.8.3-alpha.2
|
|
9
|
+
|
|
10
|
+
## 0.3.10-alpha.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 37bb612: Add Elastic-2.0 licensing for packages
|
|
15
|
+
- 3e8e000: Added configurable test timeouts to mcp configuration, and updated mcp version
|
|
16
|
+
- 476ce7c: [MASTRA-2997] added documentation on authorization headers in sse connections
|
|
17
|
+
- Updated dependencies [32e7b71]
|
|
18
|
+
- Updated dependencies [37bb612]
|
|
19
|
+
- @mastra/core@0.8.3-alpha.1
|
|
20
|
+
|
|
3
21
|
## 0.3.10-alpha.0
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/{LICENSE → LICENSE.md}
RENAMED
package/README.md
CHANGED
|
@@ -37,6 +37,16 @@ const sseClient = new MastraMCPClient({
|
|
|
37
37
|
requestInit: {
|
|
38
38
|
headers: { Authorization: 'Bearer your-token' },
|
|
39
39
|
},
|
|
40
|
+
eventSourceInit: {
|
|
41
|
+
fetch(input: Request | URL | string, init?: RequestInit) {
|
|
42
|
+
const headers = new Headers(init?.headers || {});
|
|
43
|
+
headers.set('Authorization', 'Bearer your-token');
|
|
44
|
+
return fetch(input, {
|
|
45
|
+
...init,
|
|
46
|
+
headers,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
40
50
|
},
|
|
41
51
|
timeout: 60000, // optional timeout for tool calls in milliseconds
|
|
42
52
|
});
|
|
@@ -140,6 +150,16 @@ const mcp = new MCPConfiguration({
|
|
|
140
150
|
Authorization: 'Bearer user-1-token',
|
|
141
151
|
},
|
|
142
152
|
},
|
|
153
|
+
eventSourceInit: {
|
|
154
|
+
fetch(input: Request | URL | string, init?: RequestInit) {
|
|
155
|
+
const headers = new Headers(init?.headers || {});
|
|
156
|
+
headers.set('Authorization', 'Bearer user-1-token');
|
|
157
|
+
return fetch(input, {
|
|
158
|
+
...init,
|
|
159
|
+
headers,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
},
|
|
143
163
|
},
|
|
144
164
|
},
|
|
145
165
|
});
|
|
@@ -162,6 +182,44 @@ The `MCPConfiguration` class automatically:
|
|
|
162
182
|
- Handles connection lifecycle and cleanup
|
|
163
183
|
- Provides both flat and grouped access to tools
|
|
164
184
|
|
|
185
|
+
## SSE Authentication and Headers
|
|
186
|
+
|
|
187
|
+
When using SSE (Server-Sent Events) connections with authentication or custom headers, you need to configure headers in a specific way. The standard `requestInit` headers won't work alone because SSE connections use the browser's `EventSource` API, which doesn't support custom headers directly.
|
|
188
|
+
|
|
189
|
+
The `eventSourceInit` configuration allows you to customize the underlying fetch request used for the SSE connection, ensuring your authentication headers are properly included.
|
|
190
|
+
|
|
191
|
+
To properly include authentication headers or other custom headers in SSE connections, you need to use both `requestInit` and `eventSourceInit`:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const sseClient = new MastraMCPClient({
|
|
195
|
+
name: 'authenticated-sse-client',
|
|
196
|
+
server: {
|
|
197
|
+
url: new URL('https://your-mcp-server.com/sse'),
|
|
198
|
+
// requestInit alone isn't enough for SSE connections
|
|
199
|
+
requestInit: {
|
|
200
|
+
headers: { Authorization: 'Bearer your-token' },
|
|
201
|
+
},
|
|
202
|
+
// eventSourceInit is required to include headers in the SSE connection
|
|
203
|
+
eventSourceInit: {
|
|
204
|
+
fetch(input: Request | URL | string, init?: RequestInit) {
|
|
205
|
+
const headers = new Headers(init?.headers || {});
|
|
206
|
+
headers.set('Authorization', 'Bearer your-token');
|
|
207
|
+
return fetch(input, {
|
|
208
|
+
...init,
|
|
209
|
+
headers,
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This configuration ensures that:
|
|
218
|
+
|
|
219
|
+
1. The authentication headers are properly included in the SSE connection request
|
|
220
|
+
2. The connection can be established with the required credentials
|
|
221
|
+
3. Subsequent messages can be received through the authenticated connection
|
|
222
|
+
|
|
165
223
|
## Configuration
|
|
166
224
|
|
|
167
225
|
### Required Parameters
|
|
@@ -2,7 +2,7 @@ import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
|
2
2
|
import { MastraBase } from '@mastra/core/base';
|
|
3
3
|
import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
4
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
6
|
import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
7
|
|
|
8
8
|
declare class MastraMCPClient extends MastraBase {
|
|
@@ -26,16 +26,18 @@ declare class MastraMCPClient extends MastraBase {
|
|
|
26
26
|
export { MastraMCPClient }
|
|
27
27
|
export { MastraMCPClient as MastraMCPClient_alias_1 }
|
|
28
28
|
|
|
29
|
-
declare type MastraMCPServerDefinition =
|
|
29
|
+
declare type MastraMCPServerDefinition = StdioServerParametersWithTimeout | SSEClientParameters;
|
|
30
30
|
export { MastraMCPServerDefinition }
|
|
31
31
|
export { MastraMCPServerDefinition as MastraMCPServerDefinition_alias_1 }
|
|
32
32
|
|
|
33
33
|
declare class MCPConfiguration extends MastraBase {
|
|
34
34
|
private serverConfigs;
|
|
35
35
|
private id;
|
|
36
|
+
private defaultTimeout;
|
|
36
37
|
constructor(args: {
|
|
37
38
|
id?: string;
|
|
38
39
|
servers: Record<string, MastraMCPServerDefinition>;
|
|
40
|
+
timeout?: number;
|
|
39
41
|
});
|
|
40
42
|
private addToInstanceCache;
|
|
41
43
|
private makeId;
|
|
@@ -99,6 +101,11 @@ export declare const server_alias_1: Server<{
|
|
|
99
101
|
|
|
100
102
|
declare type SSEClientParameters = {
|
|
101
103
|
url: URL;
|
|
102
|
-
|
|
104
|
+
timeout?: number;
|
|
105
|
+
} & SSEClientTransportOptions;
|
|
106
|
+
|
|
107
|
+
declare type StdioServerParametersWithTimeout = StdioServerParameters & {
|
|
108
|
+
timeout?: number;
|
|
109
|
+
};
|
|
103
110
|
|
|
104
111
|
export { }
|
|
@@ -2,7 +2,7 @@ import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
|
2
2
|
import { MastraBase } from '@mastra/core/base';
|
|
3
3
|
import type { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
4
4
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
6
|
import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
7
|
|
|
8
8
|
declare class MastraMCPClient extends MastraBase {
|
|
@@ -26,16 +26,18 @@ declare class MastraMCPClient extends MastraBase {
|
|
|
26
26
|
export { MastraMCPClient }
|
|
27
27
|
export { MastraMCPClient as MastraMCPClient_alias_1 }
|
|
28
28
|
|
|
29
|
-
declare type MastraMCPServerDefinition =
|
|
29
|
+
declare type MastraMCPServerDefinition = StdioServerParametersWithTimeout | SSEClientParameters;
|
|
30
30
|
export { MastraMCPServerDefinition }
|
|
31
31
|
export { MastraMCPServerDefinition as MastraMCPServerDefinition_alias_1 }
|
|
32
32
|
|
|
33
33
|
declare class MCPConfiguration extends MastraBase {
|
|
34
34
|
private serverConfigs;
|
|
35
35
|
private id;
|
|
36
|
+
private defaultTimeout;
|
|
36
37
|
constructor(args: {
|
|
37
38
|
id?: string;
|
|
38
39
|
servers: Record<string, MastraMCPServerDefinition>;
|
|
40
|
+
timeout?: number;
|
|
39
41
|
});
|
|
40
42
|
private addToInstanceCache;
|
|
41
43
|
private makeId;
|
|
@@ -99,6 +101,11 @@ export declare const server_alias_1: Server<{
|
|
|
99
101
|
|
|
100
102
|
declare type SSEClientParameters = {
|
|
101
103
|
url: URL;
|
|
102
|
-
|
|
104
|
+
timeout?: number;
|
|
105
|
+
} & SSEClientTransportOptions;
|
|
106
|
+
|
|
107
|
+
declare type StdioServerParametersWithTimeout = StdioServerParameters & {
|
|
108
|
+
timeout?: number;
|
|
109
|
+
};
|
|
103
110
|
|
|
104
111
|
export { }
|
package/dist/index.cjs
CHANGED
|
@@ -53,7 +53,9 @@ var MastraMCPClient = class extends base.MastraBase {
|
|
|
53
53
|
async connect() {
|
|
54
54
|
if (this.isConnected) return;
|
|
55
55
|
try {
|
|
56
|
-
await this.client.connect(this.transport
|
|
56
|
+
await this.client.connect(this.transport, {
|
|
57
|
+
timeout: this.timeout
|
|
58
|
+
});
|
|
57
59
|
this.isConnected = true;
|
|
58
60
|
const originalOnClose = this.client.onclose;
|
|
59
61
|
this.client.onclose = () => {
|
|
@@ -84,10 +86,12 @@ ${e instanceof Error ? e.stack : JSON.stringify(e, null, 2)}`
|
|
|
84
86
|
}
|
|
85
87
|
// TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
|
|
86
88
|
async resources() {
|
|
87
|
-
return await this.client.request({ method: "resources/list" }, types_js.ListResourcesResultSchema
|
|
89
|
+
return await this.client.request({ method: "resources/list" }, types_js.ListResourcesResultSchema, {
|
|
90
|
+
timeout: this.timeout
|
|
91
|
+
});
|
|
88
92
|
}
|
|
89
93
|
async tools() {
|
|
90
|
-
const { tools: tools$1 } = await this.client.listTools();
|
|
94
|
+
const { tools: tools$1 } = await this.client.listTools({ timeout: this.timeout });
|
|
91
95
|
const toolsRes = {};
|
|
92
96
|
tools$1.forEach((tool) => {
|
|
93
97
|
const s = utils.jsonSchemaToModel(tool.inputSchema);
|
|
@@ -126,8 +130,10 @@ var mastraMCPConfigurationInstances = /* @__PURE__ */ new Map();
|
|
|
126
130
|
var MCPConfiguration = class extends base.MastraBase {
|
|
127
131
|
serverConfigs = {};
|
|
128
132
|
id;
|
|
133
|
+
defaultTimeout;
|
|
129
134
|
constructor(args) {
|
|
130
135
|
super({ name: "MCPConfiguration" });
|
|
136
|
+
this.defaultTimeout = args.timeout ?? protocol_js.DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
131
137
|
this.serverConfigs = args.servers;
|
|
132
138
|
this.id = args.id ?? this.makeId();
|
|
133
139
|
const existingInstance = mastraMCPConfigurationInstances.get(this.id);
|
|
@@ -198,7 +204,8 @@ To fix this you have three different options:
|
|
|
198
204
|
this.logger.debug(`Connecting to ${name} MCP server`);
|
|
199
205
|
const mcpClient = new MastraMCPClient({
|
|
200
206
|
name,
|
|
201
|
-
server: config
|
|
207
|
+
server: config,
|
|
208
|
+
timeout: config.timeout ?? this.defaultTimeout
|
|
202
209
|
});
|
|
203
210
|
this.mcpClientsById.set(name, mcpClient);
|
|
204
211
|
try {
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,9 @@ var MastraMCPClient = class extends MastraBase {
|
|
|
51
51
|
async connect() {
|
|
52
52
|
if (this.isConnected) return;
|
|
53
53
|
try {
|
|
54
|
-
await this.client.connect(this.transport
|
|
54
|
+
await this.client.connect(this.transport, {
|
|
55
|
+
timeout: this.timeout
|
|
56
|
+
});
|
|
55
57
|
this.isConnected = true;
|
|
56
58
|
const originalOnClose = this.client.onclose;
|
|
57
59
|
this.client.onclose = () => {
|
|
@@ -82,10 +84,12 @@ ${e instanceof Error ? e.stack : JSON.stringify(e, null, 2)}`
|
|
|
82
84
|
}
|
|
83
85
|
// TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
|
|
84
86
|
async resources() {
|
|
85
|
-
return await this.client.request({ method: "resources/list" }, ListResourcesResultSchema
|
|
87
|
+
return await this.client.request({ method: "resources/list" }, ListResourcesResultSchema, {
|
|
88
|
+
timeout: this.timeout
|
|
89
|
+
});
|
|
86
90
|
}
|
|
87
91
|
async tools() {
|
|
88
|
-
const { tools } = await this.client.listTools();
|
|
92
|
+
const { tools } = await this.client.listTools({ timeout: this.timeout });
|
|
89
93
|
const toolsRes = {};
|
|
90
94
|
tools.forEach((tool) => {
|
|
91
95
|
const s = jsonSchemaToModel(tool.inputSchema);
|
|
@@ -124,8 +128,10 @@ var mastraMCPConfigurationInstances = /* @__PURE__ */ new Map();
|
|
|
124
128
|
var MCPConfiguration = class extends MastraBase {
|
|
125
129
|
serverConfigs = {};
|
|
126
130
|
id;
|
|
131
|
+
defaultTimeout;
|
|
127
132
|
constructor(args) {
|
|
128
133
|
super({ name: "MCPConfiguration" });
|
|
134
|
+
this.defaultTimeout = args.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
129
135
|
this.serverConfigs = args.servers;
|
|
130
136
|
this.id = args.id ?? this.makeId();
|
|
131
137
|
const existingInstance = mastraMCPConfigurationInstances.get(this.id);
|
|
@@ -196,7 +202,8 @@ To fix this you have three different options:
|
|
|
196
202
|
this.logger.debug(`Connecting to ${name} MCP server`);
|
|
197
203
|
const mcpClient = new MastraMCPClient({
|
|
198
204
|
name,
|
|
199
|
-
server: config
|
|
205
|
+
server: config,
|
|
206
|
+
timeout: config.timeout ?? this.defaultTimeout
|
|
200
207
|
});
|
|
201
208
|
this.mcpClientsById.set(name, mcpClient);
|
|
202
209
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mcp",
|
|
3
|
-
"version": "0.3.10-alpha.
|
|
3
|
+
"version": "0.3.10-alpha.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
},
|
|
21
21
|
"keywords": [],
|
|
22
22
|
"author": "",
|
|
23
|
-
"license": "
|
|
23
|
+
"license": "Elastic-2.0",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
26
26
|
"date-fns": "^4.1.0",
|
|
27
27
|
"exit-hook": "^4.0.0",
|
|
28
28
|
"uuid": "^11.1.0",
|
|
29
|
-
"@mastra/core": "^0.8.3-alpha.
|
|
29
|
+
"@mastra/core": "^0.8.3-alpha.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@ai-sdk/anthropic": "^1.1.15",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"vitest": "^3.0.9",
|
|
39
39
|
"zod": "^3.24.2",
|
|
40
40
|
"zod-to-json-schema": "^3.22.4",
|
|
41
|
-
"@internal/lint": "0.0.
|
|
41
|
+
"@internal/lint": "0.0.2-alpha.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import { createServer } from 'http';
|
|
1
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import {
|
|
4
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
@@ -127,7 +129,57 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
|
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
// Start the server
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
let transport: SSEServerTransport | undefined;
|
|
133
|
+
|
|
134
|
+
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
135
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
136
|
+
|
|
137
|
+
if (url.pathname === '/sse') {
|
|
138
|
+
console.log('Received SSE connection');
|
|
139
|
+
transport = new SSEServerTransport('/message', res);
|
|
140
|
+
await server.connect(transport);
|
|
141
|
+
|
|
142
|
+
server.onclose = async () => {
|
|
143
|
+
await server.close();
|
|
144
|
+
transport = undefined;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Handle client disconnection
|
|
148
|
+
res.on('close', () => {
|
|
149
|
+
transport = undefined;
|
|
150
|
+
});
|
|
151
|
+
} else if (url.pathname === '/message') {
|
|
152
|
+
console.log('Received message');
|
|
153
|
+
if (!transport) {
|
|
154
|
+
res.writeHead(503);
|
|
155
|
+
res.end('SSE connection not established');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await transport.handlePostMessage(req, res);
|
|
159
|
+
} else {
|
|
160
|
+
console.log('Unknown path:', url.pathname);
|
|
161
|
+
res.writeHead(404);
|
|
162
|
+
res.end();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const PORT = process.env.PORT || 60808;
|
|
167
|
+
httpServer.listen(PORT, () => {
|
|
168
|
+
console.log(`Weather server is running on SSE at http://localhost:${PORT}`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Handle graceful shutdown
|
|
172
|
+
process.on('SIGINT', async () => {
|
|
173
|
+
console.log('Shutting down weather server...');
|
|
174
|
+
if (transport) {
|
|
175
|
+
await server.close();
|
|
176
|
+
transport = undefined;
|
|
177
|
+
}
|
|
178
|
+
// Close the HTTP server
|
|
179
|
+
httpServer.close(() => {
|
|
180
|
+
console.log('Weather server shut down complete');
|
|
181
|
+
process.exit(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
132
184
|
|
|
133
185
|
export { server };
|
package/src/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createTool } from '@mastra/core/tools';
|
|
|
3
3
|
import { jsonSchemaToModel } from '@mastra/core/utils';
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
5
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
|
+
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
7
|
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
8
|
import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
9
|
import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
@@ -13,11 +14,17 @@ import { CallToolResultSchema, ListResourcesResultSchema } from '@modelcontextpr
|
|
|
13
14
|
|
|
14
15
|
import { asyncExitHook, gracefulExit } from 'exit-hook';
|
|
15
16
|
|
|
17
|
+
// Omit the fields we want to control from the SDK options
|
|
16
18
|
type SSEClientParameters = {
|
|
17
19
|
url: URL;
|
|
18
|
-
|
|
20
|
+
timeout?: number;
|
|
21
|
+
} & SSEClientTransportOptions;
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
type StdioServerParametersWithTimeout = StdioServerParameters & {
|
|
24
|
+
timeout?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MastraMCPServerDefinition = StdioServerParametersWithTimeout | SSEClientParameters;
|
|
21
28
|
|
|
22
29
|
export class MastraMCPClient extends MastraBase {
|
|
23
30
|
name: string;
|
|
@@ -70,7 +77,9 @@ export class MastraMCPClient extends MastraBase {
|
|
|
70
77
|
async connect() {
|
|
71
78
|
if (this.isConnected) return;
|
|
72
79
|
try {
|
|
73
|
-
await this.client.connect(this.transport
|
|
80
|
+
await this.client.connect(this.transport, {
|
|
81
|
+
timeout: this.timeout,
|
|
82
|
+
});
|
|
74
83
|
this.isConnected = true;
|
|
75
84
|
const originalOnClose = this.client.onclose;
|
|
76
85
|
this.client.onclose = () => {
|
|
@@ -104,11 +113,13 @@ export class MastraMCPClient extends MastraBase {
|
|
|
104
113
|
// TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
|
|
105
114
|
|
|
106
115
|
async resources(): Promise<ReturnType<Protocol<any, any, any>['request']>> {
|
|
107
|
-
return await this.client.request({ method: 'resources/list' }, ListResourcesResultSchema
|
|
116
|
+
return await this.client.request({ method: 'resources/list' }, ListResourcesResultSchema, {
|
|
117
|
+
timeout: this.timeout,
|
|
118
|
+
});
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
async tools() {
|
|
111
|
-
const { tools } = await this.client.listTools();
|
|
122
|
+
const { tools } = await this.client.listTools({ timeout: this.timeout });
|
|
112
123
|
const toolsRes: Record<string, any> = {};
|
|
113
124
|
tools.forEach(tool => {
|
|
114
125
|
const s = jsonSchemaToModel(tool.inputSchema);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll } from 'vitest';
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from 'vitest';
|
|
4
4
|
import { MCPConfiguration } from './configuration';
|
|
5
5
|
|
|
6
|
+
vi.setConfig({ testTimeout: 80000, hookTimeout: 80000 });
|
|
7
|
+
|
|
6
8
|
describe('MCPConfiguration', () => {
|
|
7
9
|
let mcp: MCPConfiguration;
|
|
8
10
|
let weatherProcess: ReturnType<typeof spawn>;
|
|
@@ -178,4 +180,132 @@ describe('MCPConfiguration', () => {
|
|
|
178
180
|
await existingConfig.disconnect();
|
|
179
181
|
});
|
|
180
182
|
});
|
|
183
|
+
describe('MCPConfiguration Operation Timeouts', () => {
|
|
184
|
+
it('should respect custom timeout in configuration', async () => {
|
|
185
|
+
const config = new MCPConfiguration({
|
|
186
|
+
id: 'test-timeout-config',
|
|
187
|
+
timeout: 3000, // 3 second timeout
|
|
188
|
+
servers: {
|
|
189
|
+
test: {
|
|
190
|
+
command: 'node',
|
|
191
|
+
args: [
|
|
192
|
+
'-e',
|
|
193
|
+
`
|
|
194
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
195
|
+
const server = new Server({ name: 'test', version: '1.0.0' });
|
|
196
|
+
setTimeout(() => process.exit(0), 2000); // 2 second delay
|
|
197
|
+
`,
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const error = await config.getTools().catch(e => e);
|
|
204
|
+
expect(error).toBeDefined(); // Will throw since server exits before responding
|
|
205
|
+
expect(error.message).not.toMatch(/Request timed out/);
|
|
206
|
+
|
|
207
|
+
await config.disconnect();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should respect per-server timeout override', async () => {
|
|
211
|
+
const config = new MCPConfiguration({
|
|
212
|
+
id: 'test-server-timeout-config',
|
|
213
|
+
timeout: 500, // Global timeout of 500ms
|
|
214
|
+
servers: {
|
|
215
|
+
test: {
|
|
216
|
+
command: 'node',
|
|
217
|
+
args: [
|
|
218
|
+
'-e',
|
|
219
|
+
`
|
|
220
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
221
|
+
const server = new Server({ name: 'test', version: '1.0.0' });
|
|
222
|
+
setTimeout(() => process.exit(0), 2000); // 2 second delay
|
|
223
|
+
`,
|
|
224
|
+
],
|
|
225
|
+
timeout: 3000, // Server-specific timeout of 3s
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// This should succeed since server timeout (3s) is longer than delay (2s)
|
|
231
|
+
const error = await config.getTools().catch(e => e);
|
|
232
|
+
expect(error).toBeDefined(); // Will throw since server exits before responding
|
|
233
|
+
expect(error.message).not.toMatch(/Request timed out/);
|
|
234
|
+
|
|
235
|
+
await config.disconnect();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('MCPConfiguration Connection Timeout', () => {
|
|
240
|
+
it('should throw timeout error for slow starting server', async () => {
|
|
241
|
+
const slowConfig = new MCPConfiguration({
|
|
242
|
+
id: 'test-slow-server',
|
|
243
|
+
servers: {
|
|
244
|
+
slowServer: {
|
|
245
|
+
command: 'node',
|
|
246
|
+
args: ['-e', 'setTimeout(() => process.exit(0), 65000)'], // Simulate a server that takes 65 seconds to start
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await expect(slowConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
252
|
+
await slowConfig.disconnect();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('timeout should be longer than default timeout', async () => {
|
|
256
|
+
const slowConfig = new MCPConfiguration({
|
|
257
|
+
id: 'test-slow-server',
|
|
258
|
+
timeout: 70000,
|
|
259
|
+
servers: {
|
|
260
|
+
slowServer: {
|
|
261
|
+
command: 'node',
|
|
262
|
+
args: ['-e', 'setTimeout(() => process.exit(0), 65000)'], // Simulate a server that takes 65 seconds to start
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const error = await slowConfig.getTools().catch(e => e);
|
|
268
|
+
expect(error).toBeDefined();
|
|
269
|
+
expect(error.message).not.toMatch(/Request timed out/);
|
|
270
|
+
await slowConfig.disconnect();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should respect custom timeout configuration', async () => {
|
|
274
|
+
const quickConfig = new MCPConfiguration({
|
|
275
|
+
id: 'test-quick-timeout',
|
|
276
|
+
timeout: 1000, // Very short global timeout
|
|
277
|
+
servers: {
|
|
278
|
+
slowServer: {
|
|
279
|
+
command: 'node',
|
|
280
|
+
args: ['-e', 'setTimeout(() => process.exit(0), 30000)'], // Takes 30 seconds to exit
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await expect(quickConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
286
|
+
await quickConfig.disconnect();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should respect per-server timeout configuration', async () => {
|
|
290
|
+
const mixedConfig = new MCPConfiguration({
|
|
291
|
+
id: 'test-mixed-timeout',
|
|
292
|
+
timeout: 1000, // Short global timeout
|
|
293
|
+
servers: {
|
|
294
|
+
quickServer: {
|
|
295
|
+
command: 'node',
|
|
296
|
+
args: ['-e', 'setTimeout(() => process.exit(0), 2000)'], // Takes 2 seconds to exit
|
|
297
|
+
},
|
|
298
|
+
slowServer: {
|
|
299
|
+
command: 'node',
|
|
300
|
+
args: ['-e', 'setTimeout(() => process.exit(0), 2000)'], // Takes 2 seconds to exit
|
|
301
|
+
timeout: 3000, // But has a longer timeout
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Quick server should timeout
|
|
307
|
+
await expect(mixedConfig.getTools()).rejects.toThrow(/Request timed out/);
|
|
308
|
+
await mixedConfig.disconnect();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
181
311
|
});
|
package/src/configuration.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MastraBase } from '@mastra/core/base';
|
|
2
|
+
import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
2
3
|
import { v5 as uuidv5 } from 'uuid';
|
|
3
4
|
import { MastraMCPClient } from './client';
|
|
4
5
|
import type { MastraMCPServerDefinition } from './client';
|
|
@@ -8,9 +9,15 @@ const mastraMCPConfigurationInstances = new Map<string, InstanceType<typeof MCPC
|
|
|
8
9
|
export class MCPConfiguration extends MastraBase {
|
|
9
10
|
private serverConfigs: Record<string, MastraMCPServerDefinition> = {};
|
|
10
11
|
private id: string;
|
|
12
|
+
private defaultTimeout: number;
|
|
11
13
|
|
|
12
|
-
constructor(args: {
|
|
14
|
+
constructor(args: {
|
|
15
|
+
id?: string;
|
|
16
|
+
servers: Record<string, MastraMCPServerDefinition>;
|
|
17
|
+
timeout?: number; // Optional global timeout
|
|
18
|
+
}) {
|
|
13
19
|
super({ name: 'MCPConfiguration' });
|
|
20
|
+
this.defaultTimeout = args.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
14
21
|
this.serverConfigs = args.servers;
|
|
15
22
|
this.id = args.id ?? this.makeId();
|
|
16
23
|
|
|
@@ -100,6 +107,7 @@ To fix this you have three different options:
|
|
|
100
107
|
const mcpClient = new MastraMCPClient({
|
|
101
108
|
name,
|
|
102
109
|
server: config,
|
|
110
|
+
timeout: config.timeout ?? this.defaultTimeout,
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
this.mcpClientsById.set(name, mcpClient);
|