@mastra/mcp 0.10.3 → 0.10.4-alpha.0
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 +14 -0
- package/dist/_tsup-dts-rollup.d.cts +2 -7
- package/dist/_tsup-dts-rollup.d.ts +2 -7
- package/dist/index.cjs +163 -46
- package/dist/index.js +163 -46
- package/integration-tests/node_modules/.bin/vitest +2 -2
- package/integration-tests/package.json +3 -2
- package/integration-tests/src/server.test.ts +7 -28
- package/package.json +16 -16
- package/src/client/client.ts +41 -12
- package/src/client/configuration.ts +4 -0
- package/src/server/server.ts +161 -39
- package/integration-tests/node_modules/.bin/mastra +0 -21
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/mcp@0.10.
|
|
2
|
+
> @mastra/mcp@0.10.4-alpha.0 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.5.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 20174ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.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.8.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 20211ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
21
|
-
[32mESM[39m ⚡️ Build success in
|
|
22
|
-
[32mCJS[39m [1mdist/index.cjs [22m[
|
|
23
|
-
[32mCJS[39m ⚡️ Build success in
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m71.81 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 1802ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m71.95 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ Build success in 1784ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mastra/mcp
|
|
2
2
|
|
|
3
|
+
## 0.10.4-alpha.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 63f6b7d: dependencies updates:
|
|
8
|
+
- Updated dependency [`@modelcontextprotocol/sdk@^1.12.1` ↗︎](https://www.npmjs.com/package/@modelcontextprotocol/sdk/v/1.12.1) (from `^1.10.2`, in `dependencies`)
|
|
9
|
+
- Updated dependency [`hono@^4.7.11` ↗︎](https://www.npmjs.com/package/hono/v/4.7.11) (from `^4.7.4`, in `dependencies`)
|
|
10
|
+
- 36f1c36: MCP Client and Server streamable http fixes
|
|
11
|
+
- Updated dependencies [63f6b7d]
|
|
12
|
+
- Updated dependencies [36f1c36]
|
|
13
|
+
- Updated dependencies [10d352e]
|
|
14
|
+
- Updated dependencies [53d3c37]
|
|
15
|
+
- @mastra/core@0.10.6-alpha.0
|
|
16
|
+
|
|
3
17
|
## 0.10.3
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -30,7 +30,6 @@ import type { SSEStreamingApi } from 'hono/streaming';
|
|
|
30
30
|
import { SSETransport } from 'hono-mcp-server-sse-transport';
|
|
31
31
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
32
32
|
import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
33
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
34
33
|
import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
35
34
|
import { Tool } from '@mastra/core/tools';
|
|
36
35
|
import { ToolExecutionContext } from '@mastra/core';
|
|
@@ -185,7 +184,7 @@ export declare class InternalMastraMCPClient extends MastraBase {
|
|
|
185
184
|
* @param args Arguments for the prompt
|
|
186
185
|
* @param version (optional) The prompt version to retrieve
|
|
187
186
|
*/
|
|
188
|
-
getPrompt({ name, args, version }: {
|
|
187
|
+
getPrompt({ name, args, version, }: {
|
|
189
188
|
name: string;
|
|
190
189
|
args?: Record<string, any>;
|
|
191
190
|
version?: string;
|
|
@@ -410,7 +409,7 @@ declare class MCPServer extends MCPServerBase {
|
|
|
410
409
|
private stdioTransport?;
|
|
411
410
|
private sseTransport?;
|
|
412
411
|
private sseHonoTransports;
|
|
413
|
-
private
|
|
412
|
+
private streamableHTTPTransports;
|
|
414
413
|
private listToolsHandlerIsRegistered;
|
|
415
414
|
private callToolHandlerIsRegistered;
|
|
416
415
|
private listResourcesHandlerIsRegistered;
|
|
@@ -440,10 +439,6 @@ declare class MCPServer extends MCPServerBase {
|
|
|
440
439
|
* Get the current SSE Hono transport.
|
|
441
440
|
*/
|
|
442
441
|
getSseHonoTransport(sessionId: string): SSETransport | undefined;
|
|
443
|
-
/**
|
|
444
|
-
* Get the current streamable HTTP transport.
|
|
445
|
-
*/
|
|
446
|
-
getStreamableHTTPTransport(): StreamableHTTPServerTransport | undefined;
|
|
447
442
|
/**
|
|
448
443
|
* Get the current server instance.
|
|
449
444
|
*/
|
|
@@ -30,7 +30,6 @@ import type { SSEStreamingApi } from 'hono/streaming';
|
|
|
30
30
|
import { SSETransport } from 'hono-mcp-server-sse-transport';
|
|
31
31
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
32
32
|
import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
33
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
34
33
|
import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
35
34
|
import { Tool } from '@mastra/core/tools';
|
|
36
35
|
import { ToolExecutionContext } from '@mastra/core';
|
|
@@ -185,7 +184,7 @@ export declare class InternalMastraMCPClient extends MastraBase {
|
|
|
185
184
|
* @param args Arguments for the prompt
|
|
186
185
|
* @param version (optional) The prompt version to retrieve
|
|
187
186
|
*/
|
|
188
|
-
getPrompt({ name, args, version }: {
|
|
187
|
+
getPrompt({ name, args, version, }: {
|
|
189
188
|
name: string;
|
|
190
189
|
args?: Record<string, any>;
|
|
191
190
|
version?: string;
|
|
@@ -410,7 +409,7 @@ declare class MCPServer extends MCPServerBase {
|
|
|
410
409
|
private stdioTransport?;
|
|
411
410
|
private sseTransport?;
|
|
412
411
|
private sseHonoTransports;
|
|
413
|
-
private
|
|
412
|
+
private streamableHTTPTransports;
|
|
414
413
|
private listToolsHandlerIsRegistered;
|
|
415
414
|
private callToolHandlerIsRegistered;
|
|
416
415
|
private listResourcesHandlerIsRegistered;
|
|
@@ -440,10 +439,6 @@ declare class MCPServer extends MCPServerBase {
|
|
|
440
439
|
* Get the current SSE Hono transport.
|
|
441
440
|
*/
|
|
442
441
|
getSseHonoTransport(sessionId: string): SSETransport | undefined;
|
|
443
|
-
/**
|
|
444
|
-
* Get the current streamable HTTP transport.
|
|
445
|
-
*/
|
|
446
|
-
getStreamableHTTPTransport(): StreamableHTTPServerTransport | undefined;
|
|
447
442
|
/**
|
|
448
443
|
* Get the current server instance.
|
|
449
444
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -299,8 +299,7 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
299
299
|
this.log("debug", "Trying Streamable HTTP transport...");
|
|
300
300
|
const streamableTransport = new streamableHttp_js$1.StreamableHTTPClientTransport(url, {
|
|
301
301
|
requestInit,
|
|
302
|
-
reconnectionOptions: this.serverConfig.reconnectionOptions
|
|
303
|
-
sessionId: this.serverConfig.sessionId
|
|
302
|
+
reconnectionOptions: this.serverConfig.reconnectionOptions
|
|
304
303
|
});
|
|
305
304
|
await this.client.connect(streamableTransport, {
|
|
306
305
|
timeout: (
|
|
@@ -331,22 +330,40 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
331
330
|
}
|
|
332
331
|
}
|
|
333
332
|
}
|
|
334
|
-
isConnected =
|
|
333
|
+
isConnected = null;
|
|
335
334
|
async connect() {
|
|
336
|
-
|
|
335
|
+
let res = () => {
|
|
336
|
+
};
|
|
337
|
+
let rej = () => {
|
|
338
|
+
};
|
|
339
|
+
if (this.isConnected === null) {
|
|
340
|
+
this.log("debug", `Creating new isConnected promise`);
|
|
341
|
+
this.isConnected = new Promise((resolve, reject) => {
|
|
342
|
+
res = resolve;
|
|
343
|
+
rej = reject;
|
|
344
|
+
});
|
|
345
|
+
} else if (await this.isConnected) {
|
|
346
|
+
this.log("debug", `MCP server already connected`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
337
349
|
const { command, url } = this.serverConfig;
|
|
338
350
|
if (command) {
|
|
339
|
-
await this.connectStdio(command)
|
|
351
|
+
await this.connectStdio(command).catch((e) => {
|
|
352
|
+
rej(e);
|
|
353
|
+
});
|
|
340
354
|
} else if (url) {
|
|
341
|
-
await this.connectHttp(url)
|
|
355
|
+
await this.connectHttp(url).catch((e) => {
|
|
356
|
+
rej(e);
|
|
357
|
+
});
|
|
342
358
|
} else {
|
|
359
|
+
rej(false);
|
|
343
360
|
throw new Error("Server configuration must include either a command or a url.");
|
|
344
361
|
}
|
|
345
|
-
|
|
362
|
+
res(true);
|
|
346
363
|
const originalOnClose = this.client.onclose;
|
|
347
364
|
this.client.onclose = () => {
|
|
348
365
|
this.log("debug", `MCP server connection closed`);
|
|
349
|
-
|
|
366
|
+
rej(false);
|
|
350
367
|
if (typeof originalOnClose === `function`) {
|
|
351
368
|
originalOnClose();
|
|
352
369
|
}
|
|
@@ -387,7 +404,7 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
387
404
|
throw e;
|
|
388
405
|
} finally {
|
|
389
406
|
this.transport = void 0;
|
|
390
|
-
this.isConnected = false;
|
|
407
|
+
this.isConnected = Promise.resolve(false);
|
|
391
408
|
}
|
|
392
409
|
}
|
|
393
410
|
async listResources() {
|
|
@@ -435,7 +452,11 @@ var InternalMastraMCPClient = class extends base.MastraBase {
|
|
|
435
452
|
* @param args Arguments for the prompt
|
|
436
453
|
* @param version (optional) The prompt version to retrieve
|
|
437
454
|
*/
|
|
438
|
-
async getPrompt({
|
|
455
|
+
async getPrompt({
|
|
456
|
+
name,
|
|
457
|
+
args,
|
|
458
|
+
version
|
|
459
|
+
}) {
|
|
439
460
|
this.log("debug", `Requesting prompt from MCP server: ${name}`);
|
|
440
461
|
return await this.client.request(
|
|
441
462
|
{ method: "prompts/get", params: { name, arguments: args, version } },
|
|
@@ -737,6 +758,7 @@ To fix this you have three different options:
|
|
|
737
758
|
}
|
|
738
759
|
const exists = this.mcpClientsById.has(name);
|
|
739
760
|
const existingClient = this.mcpClientsById.get(name);
|
|
761
|
+
this.logger.debug(`getConnectedClient ${name} exists: ${exists}`);
|
|
740
762
|
if (exists) {
|
|
741
763
|
if (!existingClient) {
|
|
742
764
|
throw new Error(`Client ${name} exists but is undefined`);
|
|
@@ -750,6 +772,7 @@ To fix this you have three different options:
|
|
|
750
772
|
server: config,
|
|
751
773
|
timeout: config.timeout ?? this.defaultTimeout
|
|
752
774
|
});
|
|
775
|
+
mcpClient.__setLogger(this.logger);
|
|
753
776
|
this.mcpClientsById.set(name, mcpClient);
|
|
754
777
|
try {
|
|
755
778
|
await mcpClient.connect();
|
|
@@ -959,7 +982,7 @@ var MCPServer = class extends mcp.MCPServerBase {
|
|
|
959
982
|
stdioTransport;
|
|
960
983
|
sseTransport;
|
|
961
984
|
sseHonoTransports;
|
|
962
|
-
|
|
985
|
+
streamableHTTPTransports = /* @__PURE__ */ new Map();
|
|
963
986
|
listToolsHandlerIsRegistered = false;
|
|
964
987
|
callToolHandlerIsRegistered = false;
|
|
965
988
|
listResourcesHandlerIsRegistered = false;
|
|
@@ -995,12 +1018,6 @@ var MCPServer = class extends mcp.MCPServerBase {
|
|
|
995
1018
|
getSseHonoTransport(sessionId) {
|
|
996
1019
|
return this.sseHonoTransports.get(sessionId);
|
|
997
1020
|
}
|
|
998
|
-
/**
|
|
999
|
-
* Get the current streamable HTTP transport.
|
|
1000
|
-
*/
|
|
1001
|
-
getStreamableHTTPTransport() {
|
|
1002
|
-
return this.streamableHTTPTransport;
|
|
1003
|
-
}
|
|
1004
1021
|
/**
|
|
1005
1022
|
* Get the current server instance.
|
|
1006
1023
|
*/
|
|
@@ -1672,34 +1689,132 @@ var MCPServer = class extends mcp.MCPServerBase {
|
|
|
1672
1689
|
res,
|
|
1673
1690
|
options = { sessionIdGenerator: () => crypto$1.randomUUID() }
|
|
1674
1691
|
}) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
await this.server.connect(this.streamableHTTPTransport);
|
|
1679
|
-
} catch (error) {
|
|
1680
|
-
this.logger.error("Error connecting to MCP server", { error });
|
|
1681
|
-
res.writeHead(500);
|
|
1682
|
-
res.end("Error connecting to MCP server");
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
try {
|
|
1686
|
-
await this.streamableHTTPTransport.handleRequest(req, res);
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
this.logger.error("Error handling MCP connection", { error });
|
|
1689
|
-
res.writeHead(500);
|
|
1690
|
-
res.end("Error handling MCP connection");
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
this.server.onclose = async () => {
|
|
1694
|
-
this.streamableHTTPTransport = void 0;
|
|
1695
|
-
await this.server.close();
|
|
1696
|
-
};
|
|
1697
|
-
res.on("close", () => {
|
|
1698
|
-
this.streamableHTTPTransport = void 0;
|
|
1699
|
-
});
|
|
1700
|
-
} else {
|
|
1692
|
+
this.logger.debug(`startHTTP: Received ${req.method} request to ${url.pathname}`);
|
|
1693
|
+
if (url.pathname !== httpPath) {
|
|
1694
|
+
this.logger.debug(`startHTTP: Pathname ${url.pathname} does not match httpPath ${httpPath}. Returning 404.`);
|
|
1701
1695
|
res.writeHead(404);
|
|
1702
1696
|
res.end();
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1700
|
+
let transport;
|
|
1701
|
+
this.logger.debug(
|
|
1702
|
+
`startHTTP: Session ID from headers: ${sessionId}. Active transports: ${Array.from(this.streamableHTTPTransports.keys()).join(", ")}`
|
|
1703
|
+
);
|
|
1704
|
+
try {
|
|
1705
|
+
if (sessionId && this.streamableHTTPTransports.has(sessionId)) {
|
|
1706
|
+
transport = this.streamableHTTPTransports.get(sessionId);
|
|
1707
|
+
this.logger.debug(`startHTTP: Using existing Streamable HTTP transport for session ID: ${sessionId}`);
|
|
1708
|
+
if (req.method === "GET") {
|
|
1709
|
+
this.logger.debug(
|
|
1710
|
+
`startHTTP: Handling GET request for existing session ${sessionId}. Calling transport.handleRequest.`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
const body = req.method === "POST" ? await new Promise((resolve, reject) => {
|
|
1714
|
+
let data = "";
|
|
1715
|
+
req.on("data", (chunk) => data += chunk);
|
|
1716
|
+
req.on("end", () => {
|
|
1717
|
+
try {
|
|
1718
|
+
resolve(JSON.parse(data));
|
|
1719
|
+
} catch (e) {
|
|
1720
|
+
reject(e);
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
req.on("error", reject);
|
|
1724
|
+
}) : void 0;
|
|
1725
|
+
await transport.handleRequest(req, res, body);
|
|
1726
|
+
} else {
|
|
1727
|
+
this.logger.debug(`startHTTP: No existing Streamable HTTP session ID found. ${req.method}`);
|
|
1728
|
+
if (req.method === "POST") {
|
|
1729
|
+
const body = await new Promise((resolve, reject) => {
|
|
1730
|
+
let data = "";
|
|
1731
|
+
req.on("data", (chunk) => data += chunk);
|
|
1732
|
+
req.on("end", () => {
|
|
1733
|
+
try {
|
|
1734
|
+
resolve(JSON.parse(data));
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
reject(e);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
req.on("error", reject);
|
|
1740
|
+
});
|
|
1741
|
+
const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
|
|
1742
|
+
if (isInitializeRequest(body)) {
|
|
1743
|
+
this.logger.debug("startHTTP: Received Streamable HTTP initialize request, creating new transport.");
|
|
1744
|
+
transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
1745
|
+
...options,
|
|
1746
|
+
sessionIdGenerator: () => crypto$1.randomUUID(),
|
|
1747
|
+
onsessioninitialized: (id) => {
|
|
1748
|
+
this.streamableHTTPTransports.set(id, transport);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
transport.onclose = () => {
|
|
1752
|
+
const closedSessionId = transport?.sessionId;
|
|
1753
|
+
if (closedSessionId && this.streamableHTTPTransports.has(closedSessionId)) {
|
|
1754
|
+
this.logger.debug(
|
|
1755
|
+
`startHTTP: Streamable HTTP transport closed for session ${closedSessionId}, removing from map.`
|
|
1756
|
+
);
|
|
1757
|
+
this.streamableHTTPTransports.delete(closedSessionId);
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
await this.server.connect(transport);
|
|
1761
|
+
if (transport.sessionId) {
|
|
1762
|
+
this.streamableHTTPTransports.set(transport.sessionId, transport);
|
|
1763
|
+
this.logger.debug(
|
|
1764
|
+
`startHTTP: Streamable HTTP session initialized and stored with ID: ${transport.sessionId}`
|
|
1765
|
+
);
|
|
1766
|
+
} else {
|
|
1767
|
+
this.logger.warn("startHTTP: Streamable HTTP transport initialized without a session ID.");
|
|
1768
|
+
}
|
|
1769
|
+
return await transport.handleRequest(req, res, body);
|
|
1770
|
+
} else {
|
|
1771
|
+
this.logger.warn("startHTTP: Received non-initialize POST request without a session ID.");
|
|
1772
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1773
|
+
res.end(
|
|
1774
|
+
JSON.stringify({
|
|
1775
|
+
jsonrpc: "2.0",
|
|
1776
|
+
error: {
|
|
1777
|
+
code: -32e3,
|
|
1778
|
+
message: "Bad Request: No valid session ID provided for non-initialize request"
|
|
1779
|
+
},
|
|
1780
|
+
id: body?.id ?? null
|
|
1781
|
+
// Include original request ID if available
|
|
1782
|
+
})
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
this.logger.warn(`startHTTP: Received ${req.method} request without a session ID.`);
|
|
1787
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1788
|
+
res.end(
|
|
1789
|
+
JSON.stringify({
|
|
1790
|
+
jsonrpc: "2.0",
|
|
1791
|
+
error: {
|
|
1792
|
+
code: -32e3,
|
|
1793
|
+
message: `Bad Request: ${req.method} request requires a valid session ID`
|
|
1794
|
+
},
|
|
1795
|
+
id: null
|
|
1796
|
+
})
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
this.logger.error("startHTTP: Error handling Streamable HTTP request:", { error });
|
|
1802
|
+
if (!res.headersSent) {
|
|
1803
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1804
|
+
res.end(
|
|
1805
|
+
JSON.stringify({
|
|
1806
|
+
jsonrpc: "2.0",
|
|
1807
|
+
error: {
|
|
1808
|
+
code: -32603,
|
|
1809
|
+
message: "Internal server error"
|
|
1810
|
+
},
|
|
1811
|
+
id: null
|
|
1812
|
+
// Cannot determine original request ID in catch
|
|
1813
|
+
})
|
|
1814
|
+
);
|
|
1815
|
+
} else {
|
|
1816
|
+
this.logger.error("startHTTP: Error after headers sent:", error);
|
|
1817
|
+
}
|
|
1703
1818
|
}
|
|
1704
1819
|
}
|
|
1705
1820
|
async connectSSE({
|
|
@@ -1766,9 +1881,11 @@ var MCPServer = class extends mcp.MCPServerBase {
|
|
|
1766
1881
|
}
|
|
1767
1882
|
this.sseHonoTransports.clear();
|
|
1768
1883
|
}
|
|
1769
|
-
if (this.
|
|
1770
|
-
|
|
1771
|
-
|
|
1884
|
+
if (this.streamableHTTPTransports) {
|
|
1885
|
+
for (const transport of this.streamableHTTPTransports.values()) {
|
|
1886
|
+
await transport.close?.();
|
|
1887
|
+
}
|
|
1888
|
+
this.streamableHTTPTransports.clear();
|
|
1772
1889
|
}
|
|
1773
1890
|
await this.server.close();
|
|
1774
1891
|
this.logger.info("MCP server closed.");
|
package/dist/index.js
CHANGED
|
@@ -293,8 +293,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
293
293
|
this.log("debug", "Trying Streamable HTTP transport...");
|
|
294
294
|
const streamableTransport = new StreamableHTTPClientTransport(url, {
|
|
295
295
|
requestInit,
|
|
296
|
-
reconnectionOptions: this.serverConfig.reconnectionOptions
|
|
297
|
-
sessionId: this.serverConfig.sessionId
|
|
296
|
+
reconnectionOptions: this.serverConfig.reconnectionOptions
|
|
298
297
|
});
|
|
299
298
|
await this.client.connect(streamableTransport, {
|
|
300
299
|
timeout: (
|
|
@@ -325,22 +324,40 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
325
324
|
}
|
|
326
325
|
}
|
|
327
326
|
}
|
|
328
|
-
isConnected =
|
|
327
|
+
isConnected = null;
|
|
329
328
|
async connect() {
|
|
330
|
-
|
|
329
|
+
let res = () => {
|
|
330
|
+
};
|
|
331
|
+
let rej = () => {
|
|
332
|
+
};
|
|
333
|
+
if (this.isConnected === null) {
|
|
334
|
+
this.log("debug", `Creating new isConnected promise`);
|
|
335
|
+
this.isConnected = new Promise((resolve, reject) => {
|
|
336
|
+
res = resolve;
|
|
337
|
+
rej = reject;
|
|
338
|
+
});
|
|
339
|
+
} else if (await this.isConnected) {
|
|
340
|
+
this.log("debug", `MCP server already connected`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
331
343
|
const { command, url } = this.serverConfig;
|
|
332
344
|
if (command) {
|
|
333
|
-
await this.connectStdio(command)
|
|
345
|
+
await this.connectStdio(command).catch((e) => {
|
|
346
|
+
rej(e);
|
|
347
|
+
});
|
|
334
348
|
} else if (url) {
|
|
335
|
-
await this.connectHttp(url)
|
|
349
|
+
await this.connectHttp(url).catch((e) => {
|
|
350
|
+
rej(e);
|
|
351
|
+
});
|
|
336
352
|
} else {
|
|
353
|
+
rej(false);
|
|
337
354
|
throw new Error("Server configuration must include either a command or a url.");
|
|
338
355
|
}
|
|
339
|
-
|
|
356
|
+
res(true);
|
|
340
357
|
const originalOnClose = this.client.onclose;
|
|
341
358
|
this.client.onclose = () => {
|
|
342
359
|
this.log("debug", `MCP server connection closed`);
|
|
343
|
-
|
|
360
|
+
rej(false);
|
|
344
361
|
if (typeof originalOnClose === `function`) {
|
|
345
362
|
originalOnClose();
|
|
346
363
|
}
|
|
@@ -381,7 +398,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
381
398
|
throw e;
|
|
382
399
|
} finally {
|
|
383
400
|
this.transport = void 0;
|
|
384
|
-
this.isConnected = false;
|
|
401
|
+
this.isConnected = Promise.resolve(false);
|
|
385
402
|
}
|
|
386
403
|
}
|
|
387
404
|
async listResources() {
|
|
@@ -429,7 +446,11 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
429
446
|
* @param args Arguments for the prompt
|
|
430
447
|
* @param version (optional) The prompt version to retrieve
|
|
431
448
|
*/
|
|
432
|
-
async getPrompt({
|
|
449
|
+
async getPrompt({
|
|
450
|
+
name,
|
|
451
|
+
args,
|
|
452
|
+
version
|
|
453
|
+
}) {
|
|
433
454
|
this.log("debug", `Requesting prompt from MCP server: ${name}`);
|
|
434
455
|
return await this.client.request(
|
|
435
456
|
{ method: "prompts/get", params: { name, arguments: args, version } },
|
|
@@ -731,6 +752,7 @@ To fix this you have three different options:
|
|
|
731
752
|
}
|
|
732
753
|
const exists = this.mcpClientsById.has(name);
|
|
733
754
|
const existingClient = this.mcpClientsById.get(name);
|
|
755
|
+
this.logger.debug(`getConnectedClient ${name} exists: ${exists}`);
|
|
734
756
|
if (exists) {
|
|
735
757
|
if (!existingClient) {
|
|
736
758
|
throw new Error(`Client ${name} exists but is undefined`);
|
|
@@ -744,6 +766,7 @@ To fix this you have three different options:
|
|
|
744
766
|
server: config,
|
|
745
767
|
timeout: config.timeout ?? this.defaultTimeout
|
|
746
768
|
});
|
|
769
|
+
mcpClient.__setLogger(this.logger);
|
|
747
770
|
this.mcpClientsById.set(name, mcpClient);
|
|
748
771
|
try {
|
|
749
772
|
await mcpClient.connect();
|
|
@@ -953,7 +976,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
953
976
|
stdioTransport;
|
|
954
977
|
sseTransport;
|
|
955
978
|
sseHonoTransports;
|
|
956
|
-
|
|
979
|
+
streamableHTTPTransports = /* @__PURE__ */ new Map();
|
|
957
980
|
listToolsHandlerIsRegistered = false;
|
|
958
981
|
callToolHandlerIsRegistered = false;
|
|
959
982
|
listResourcesHandlerIsRegistered = false;
|
|
@@ -989,12 +1012,6 @@ var MCPServer = class extends MCPServerBase {
|
|
|
989
1012
|
getSseHonoTransport(sessionId) {
|
|
990
1013
|
return this.sseHonoTransports.get(sessionId);
|
|
991
1014
|
}
|
|
992
|
-
/**
|
|
993
|
-
* Get the current streamable HTTP transport.
|
|
994
|
-
*/
|
|
995
|
-
getStreamableHTTPTransport() {
|
|
996
|
-
return this.streamableHTTPTransport;
|
|
997
|
-
}
|
|
998
1015
|
/**
|
|
999
1016
|
* Get the current server instance.
|
|
1000
1017
|
*/
|
|
@@ -1666,34 +1683,132 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1666
1683
|
res,
|
|
1667
1684
|
options = { sessionIdGenerator: () => randomUUID() }
|
|
1668
1685
|
}) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
await this.server.connect(this.streamableHTTPTransport);
|
|
1673
|
-
} catch (error) {
|
|
1674
|
-
this.logger.error("Error connecting to MCP server", { error });
|
|
1675
|
-
res.writeHead(500);
|
|
1676
|
-
res.end("Error connecting to MCP server");
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
try {
|
|
1680
|
-
await this.streamableHTTPTransport.handleRequest(req, res);
|
|
1681
|
-
} catch (error) {
|
|
1682
|
-
this.logger.error("Error handling MCP connection", { error });
|
|
1683
|
-
res.writeHead(500);
|
|
1684
|
-
res.end("Error handling MCP connection");
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
this.server.onclose = async () => {
|
|
1688
|
-
this.streamableHTTPTransport = void 0;
|
|
1689
|
-
await this.server.close();
|
|
1690
|
-
};
|
|
1691
|
-
res.on("close", () => {
|
|
1692
|
-
this.streamableHTTPTransport = void 0;
|
|
1693
|
-
});
|
|
1694
|
-
} else {
|
|
1686
|
+
this.logger.debug(`startHTTP: Received ${req.method} request to ${url.pathname}`);
|
|
1687
|
+
if (url.pathname !== httpPath) {
|
|
1688
|
+
this.logger.debug(`startHTTP: Pathname ${url.pathname} does not match httpPath ${httpPath}. Returning 404.`);
|
|
1695
1689
|
res.writeHead(404);
|
|
1696
1690
|
res.end();
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
1694
|
+
let transport;
|
|
1695
|
+
this.logger.debug(
|
|
1696
|
+
`startHTTP: Session ID from headers: ${sessionId}. Active transports: ${Array.from(this.streamableHTTPTransports.keys()).join(", ")}`
|
|
1697
|
+
);
|
|
1698
|
+
try {
|
|
1699
|
+
if (sessionId && this.streamableHTTPTransports.has(sessionId)) {
|
|
1700
|
+
transport = this.streamableHTTPTransports.get(sessionId);
|
|
1701
|
+
this.logger.debug(`startHTTP: Using existing Streamable HTTP transport for session ID: ${sessionId}`);
|
|
1702
|
+
if (req.method === "GET") {
|
|
1703
|
+
this.logger.debug(
|
|
1704
|
+
`startHTTP: Handling GET request for existing session ${sessionId}. Calling transport.handleRequest.`
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const body = req.method === "POST" ? await new Promise((resolve, reject) => {
|
|
1708
|
+
let data = "";
|
|
1709
|
+
req.on("data", (chunk) => data += chunk);
|
|
1710
|
+
req.on("end", () => {
|
|
1711
|
+
try {
|
|
1712
|
+
resolve(JSON.parse(data));
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
reject(e);
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
req.on("error", reject);
|
|
1718
|
+
}) : void 0;
|
|
1719
|
+
await transport.handleRequest(req, res, body);
|
|
1720
|
+
} else {
|
|
1721
|
+
this.logger.debug(`startHTTP: No existing Streamable HTTP session ID found. ${req.method}`);
|
|
1722
|
+
if (req.method === "POST") {
|
|
1723
|
+
const body = await new Promise((resolve, reject) => {
|
|
1724
|
+
let data = "";
|
|
1725
|
+
req.on("data", (chunk) => data += chunk);
|
|
1726
|
+
req.on("end", () => {
|
|
1727
|
+
try {
|
|
1728
|
+
resolve(JSON.parse(data));
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
reject(e);
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
req.on("error", reject);
|
|
1734
|
+
});
|
|
1735
|
+
const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
|
|
1736
|
+
if (isInitializeRequest(body)) {
|
|
1737
|
+
this.logger.debug("startHTTP: Received Streamable HTTP initialize request, creating new transport.");
|
|
1738
|
+
transport = new StreamableHTTPServerTransport({
|
|
1739
|
+
...options,
|
|
1740
|
+
sessionIdGenerator: () => randomUUID(),
|
|
1741
|
+
onsessioninitialized: (id) => {
|
|
1742
|
+
this.streamableHTTPTransports.set(id, transport);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
transport.onclose = () => {
|
|
1746
|
+
const closedSessionId = transport?.sessionId;
|
|
1747
|
+
if (closedSessionId && this.streamableHTTPTransports.has(closedSessionId)) {
|
|
1748
|
+
this.logger.debug(
|
|
1749
|
+
`startHTTP: Streamable HTTP transport closed for session ${closedSessionId}, removing from map.`
|
|
1750
|
+
);
|
|
1751
|
+
this.streamableHTTPTransports.delete(closedSessionId);
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
await this.server.connect(transport);
|
|
1755
|
+
if (transport.sessionId) {
|
|
1756
|
+
this.streamableHTTPTransports.set(transport.sessionId, transport);
|
|
1757
|
+
this.logger.debug(
|
|
1758
|
+
`startHTTP: Streamable HTTP session initialized and stored with ID: ${transport.sessionId}`
|
|
1759
|
+
);
|
|
1760
|
+
} else {
|
|
1761
|
+
this.logger.warn("startHTTP: Streamable HTTP transport initialized without a session ID.");
|
|
1762
|
+
}
|
|
1763
|
+
return await transport.handleRequest(req, res, body);
|
|
1764
|
+
} else {
|
|
1765
|
+
this.logger.warn("startHTTP: Received non-initialize POST request without a session ID.");
|
|
1766
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1767
|
+
res.end(
|
|
1768
|
+
JSON.stringify({
|
|
1769
|
+
jsonrpc: "2.0",
|
|
1770
|
+
error: {
|
|
1771
|
+
code: -32e3,
|
|
1772
|
+
message: "Bad Request: No valid session ID provided for non-initialize request"
|
|
1773
|
+
},
|
|
1774
|
+
id: body?.id ?? null
|
|
1775
|
+
// Include original request ID if available
|
|
1776
|
+
})
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
} else {
|
|
1780
|
+
this.logger.warn(`startHTTP: Received ${req.method} request without a session ID.`);
|
|
1781
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1782
|
+
res.end(
|
|
1783
|
+
JSON.stringify({
|
|
1784
|
+
jsonrpc: "2.0",
|
|
1785
|
+
error: {
|
|
1786
|
+
code: -32e3,
|
|
1787
|
+
message: `Bad Request: ${req.method} request requires a valid session ID`
|
|
1788
|
+
},
|
|
1789
|
+
id: null
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
this.logger.error("startHTTP: Error handling Streamable HTTP request:", { error });
|
|
1796
|
+
if (!res.headersSent) {
|
|
1797
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1798
|
+
res.end(
|
|
1799
|
+
JSON.stringify({
|
|
1800
|
+
jsonrpc: "2.0",
|
|
1801
|
+
error: {
|
|
1802
|
+
code: -32603,
|
|
1803
|
+
message: "Internal server error"
|
|
1804
|
+
},
|
|
1805
|
+
id: null
|
|
1806
|
+
// Cannot determine original request ID in catch
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
} else {
|
|
1810
|
+
this.logger.error("startHTTP: Error after headers sent:", error);
|
|
1811
|
+
}
|
|
1697
1812
|
}
|
|
1698
1813
|
}
|
|
1699
1814
|
async connectSSE({
|
|
@@ -1760,9 +1875,11 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1760
1875
|
}
|
|
1761
1876
|
this.sseHonoTransports.clear();
|
|
1762
1877
|
}
|
|
1763
|
-
if (this.
|
|
1764
|
-
|
|
1765
|
-
|
|
1878
|
+
if (this.streamableHTTPTransports) {
|
|
1879
|
+
for (const transport of this.streamableHTTPTransports.values()) {
|
|
1880
|
+
await transport.close?.();
|
|
1881
|
+
}
|
|
1882
|
+
this.streamableHTTPTransports.clear();
|
|
1766
1883
|
}
|
|
1767
1884
|
await this.server.close();
|
|
1768
1885
|
this.logger.info("MCP server closed.");
|
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.
|
|
13
|
+
export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.3_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_693b2e3fc65baebd9dc5fe66abed9401/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.3_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_693b2e3fc65baebd9dc5fe66abed9401/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.
|
|
15
|
+
export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.3_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_693b2e3fc65baebd9dc5fe66abed9401/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.3_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_693b2e3fc65baebd9dc5fe66abed9401/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
"zod": "^3.25.56"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@mastra/core": "workspace:*",
|
|
18
19
|
"@testing-library/react": "^16.2.0",
|
|
19
20
|
"@types/node": "^20.17.57",
|
|
21
|
+
"get-port": "^7.1.0",
|
|
20
22
|
"mastra": "workspace:*",
|
|
21
23
|
"typescript": "^5.8.2",
|
|
22
|
-
"vitest": "^3.2.2"
|
|
23
|
-
"@mastra/core": "workspace:*"
|
|
24
|
+
"vitest": "^3.2.2"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
27
|
"@mastra/core": "^0.10.0-alpha.0"
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import path from 'path';
|
|
4
3
|
import { MCPClient } from '@mastra/mcp';
|
|
5
4
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
6
5
|
import { ServerInfo } from '@mastra/core/mcp';
|
|
6
|
+
import getPort from 'get-port';
|
|
7
7
|
|
|
8
8
|
vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 });
|
|
9
9
|
|
|
10
|
-
// Helper to find an available port
|
|
11
|
-
async function getAvailablePort(): Promise<number> {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
const server = createServer();
|
|
14
|
-
server.listen(0, () => {
|
|
15
|
-
const { port } = server.address() as { port: number };
|
|
16
|
-
server.close(() => resolve(port));
|
|
17
|
-
});
|
|
18
|
-
server.on('error', reject);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
describe('MCPServer through Mastra HTTP Integration (Subprocess)', () => {
|
|
23
11
|
let mastraServer: ReturnType<typeof spawn>;
|
|
24
12
|
let port: number;
|
|
@@ -27,21 +15,12 @@ describe('MCPServer through Mastra HTTP Integration (Subprocess)', () => {
|
|
|
27
15
|
let client: MCPClient;
|
|
28
16
|
|
|
29
17
|
beforeAll(async () => {
|
|
30
|
-
port = await
|
|
31
|
-
|
|
32
|
-
mastraServer = spawn(
|
|
33
|
-
'
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'dev',
|
|
37
|
-
'--port',
|
|
38
|
-
port.toString(),
|
|
39
|
-
],
|
|
40
|
-
{
|
|
41
|
-
stdio: 'pipe',
|
|
42
|
-
detached: true, // Run in a new process group so we can kill it and children
|
|
43
|
-
},
|
|
44
|
-
);
|
|
18
|
+
port = await getPort();
|
|
19
|
+
|
|
20
|
+
mastraServer = spawn('pnpm', ['mastra', 'dev', '--port', port.toString()], {
|
|
21
|
+
stdio: 'pipe',
|
|
22
|
+
detached: true, // Run in a new process group so we can kill it and children
|
|
23
|
+
});
|
|
45
24
|
|
|
46
25
|
// Wait for server to be ready
|
|
47
26
|
await new Promise<void>((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4-alpha.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,36 +22,36 @@
|
|
|
22
22
|
"author": "",
|
|
23
23
|
"license": "Elastic-2.0",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
26
|
"date-fns": "^4.1.0",
|
|
27
27
|
"exit-hook": "^4.0.0",
|
|
28
28
|
"fast-deep-equal": "^3.1.3",
|
|
29
|
-
"hono": "^4.7.
|
|
29
|
+
"hono": "^4.7.11",
|
|
30
30
|
"uuid": "^11.1.0",
|
|
31
31
|
"zod-from-json-schema": "^0.0.5"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
34
|
+
"@mastra/core": "^0.10.2-alpha.0",
|
|
35
|
+
"zod": "^3.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@ai-sdk/anthropic": "^1.
|
|
38
|
+
"@ai-sdk/anthropic": "^1.2.12",
|
|
39
39
|
"@ai-sdk/openai": "^1.3.22",
|
|
40
|
-
"
|
|
41
|
-
"@
|
|
42
|
-
"@mendable/firecrawl-js": "^1.24.0",
|
|
40
|
+
"@hono/node-server": "^1.14.4",
|
|
41
|
+
"@mendable/firecrawl-js": "^1.25.5",
|
|
43
42
|
"@microsoft/api-extractor": "^7.52.8",
|
|
44
|
-
"@types/node": "^20.
|
|
43
|
+
"@types/node": "^20.19.0",
|
|
44
|
+
"ai": "4.3.16",
|
|
45
45
|
"eslint": "^9.28.0",
|
|
46
46
|
"hono-mcp-server-sse-transport": "0.0.6",
|
|
47
47
|
"tsup": "^8.5.0",
|
|
48
|
-
"tsx": "^4.19.
|
|
49
|
-
"typescript": "^5.8.
|
|
50
|
-
"vitest": "^3.2.
|
|
51
|
-
"zod": "^3.25.
|
|
48
|
+
"tsx": "^4.19.4",
|
|
49
|
+
"typescript": "^5.8.3",
|
|
50
|
+
"vitest": "^3.2.3",
|
|
51
|
+
"zod": "^3.25.57",
|
|
52
52
|
"zod-to-json-schema": "^3.24.5",
|
|
53
|
-
"@internal/lint": "0.0.
|
|
54
|
-
"@mastra/core": "0.10.
|
|
53
|
+
"@internal/lint": "0.0.12",
|
|
54
|
+
"@mastra/core": "0.10.6-alpha.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
package/src/client/client.ts
CHANGED
|
@@ -11,7 +11,12 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
11
11
|
import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
12
12
|
import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
13
13
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ClientCapabilities,
|
|
16
|
+
GetPromptResult,
|
|
17
|
+
ListPromptsResult,
|
|
18
|
+
LoggingLevel,
|
|
19
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
15
20
|
import {
|
|
16
21
|
CallToolResultSchema,
|
|
17
22
|
ListResourcesResultSchema,
|
|
@@ -151,7 +156,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
151
156
|
|
|
152
157
|
// Set up log message capturing
|
|
153
158
|
this.setupLogging();
|
|
154
|
-
|
|
159
|
+
|
|
155
160
|
this.resources = new ResourceClientActions({ client: this, logger: this.logger });
|
|
156
161
|
this.prompts = new PromptClientActions({ client: this, logger: this.logger });
|
|
157
162
|
}
|
|
@@ -234,7 +239,6 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
234
239
|
const streamableTransport = new StreamableHTTPClientTransport(url, {
|
|
235
240
|
requestInit,
|
|
236
241
|
reconnectionOptions: this.serverConfig.reconnectionOptions,
|
|
237
|
-
sessionId: this.serverConfig.sessionId,
|
|
238
242
|
});
|
|
239
243
|
await this.client.connect(streamableTransport, {
|
|
240
244
|
timeout:
|
|
@@ -267,26 +271,43 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
273
|
|
|
270
|
-
private isConnected =
|
|
274
|
+
private isConnected: Promise<boolean> | null = null;
|
|
271
275
|
|
|
272
276
|
async connect() {
|
|
273
|
-
|
|
277
|
+
let res: (value: boolean) => void = () => {};
|
|
278
|
+
let rej: (reason?: any) => void = () => {};
|
|
279
|
+
|
|
280
|
+
if (this.isConnected === null) {
|
|
281
|
+
this.log('debug', `Creating new isConnected promise`);
|
|
282
|
+
this.isConnected = new Promise<boolean>((resolve, reject) => {
|
|
283
|
+
res = resolve;
|
|
284
|
+
rej = reject;
|
|
285
|
+
});
|
|
286
|
+
} else if (await this.isConnected) {
|
|
287
|
+
this.log('debug', `MCP server already connected`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
274
290
|
|
|
275
291
|
const { command, url } = this.serverConfig;
|
|
276
292
|
|
|
277
293
|
if (command) {
|
|
278
|
-
await this.connectStdio(command)
|
|
294
|
+
await this.connectStdio(command).catch(e => {
|
|
295
|
+
rej(e);
|
|
296
|
+
});
|
|
279
297
|
} else if (url) {
|
|
280
|
-
await this.connectHttp(url)
|
|
298
|
+
await this.connectHttp(url).catch(e => {
|
|
299
|
+
rej(e);
|
|
300
|
+
});
|
|
281
301
|
} else {
|
|
302
|
+
rej(false);
|
|
282
303
|
throw new Error('Server configuration must include either a command or a url.');
|
|
283
304
|
}
|
|
284
305
|
|
|
285
|
-
|
|
306
|
+
res(true);
|
|
286
307
|
const originalOnClose = this.client.onclose;
|
|
287
308
|
this.client.onclose = () => {
|
|
288
309
|
this.log('debug', `MCP server connection closed`);
|
|
289
|
-
|
|
310
|
+
rej(false);
|
|
290
311
|
if (typeof originalOnClose === `function`) {
|
|
291
312
|
originalOnClose();
|
|
292
313
|
}
|
|
@@ -330,7 +351,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
330
351
|
throw e;
|
|
331
352
|
} finally {
|
|
332
353
|
this.transport = undefined;
|
|
333
|
-
this.isConnected = false;
|
|
354
|
+
this.isConnected = Promise.resolve(false);
|
|
334
355
|
}
|
|
335
356
|
}
|
|
336
357
|
|
|
@@ -385,12 +406,20 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
385
406
|
* @param args Arguments for the prompt
|
|
386
407
|
* @param version (optional) The prompt version to retrieve
|
|
387
408
|
*/
|
|
388
|
-
async getPrompt({
|
|
409
|
+
async getPrompt({
|
|
410
|
+
name,
|
|
411
|
+
args,
|
|
412
|
+
version,
|
|
413
|
+
}: {
|
|
414
|
+
name: string;
|
|
415
|
+
args?: Record<string, any>;
|
|
416
|
+
version?: string;
|
|
417
|
+
}): Promise<GetPromptResult> {
|
|
389
418
|
this.log('debug', `Requesting prompt from MCP server: ${name}`);
|
|
390
419
|
return await this.client.request(
|
|
391
420
|
{ method: 'prompts/get', params: { name, arguments: args, version } },
|
|
392
421
|
GetPromptResultSchema,
|
|
393
|
-
{ timeout: this.timeout }
|
|
422
|
+
{ timeout: this.timeout },
|
|
394
423
|
);
|
|
395
424
|
}
|
|
396
425
|
|
|
@@ -230,6 +230,8 @@ To fix this you have three different options:
|
|
|
230
230
|
const exists = this.mcpClientsById.has(name);
|
|
231
231
|
const existingClient = this.mcpClientsById.get(name);
|
|
232
232
|
|
|
233
|
+
this.logger.debug(`getConnectedClient ${name} exists: ${exists}`);
|
|
234
|
+
|
|
233
235
|
if (exists) {
|
|
234
236
|
// This is just to satisfy Typescript since technically you could have this.mcpClientsById.set('someKey', undefined);
|
|
235
237
|
// Should never reach this point basically we always create a new MastraMCPClient instance when we add to the Map.
|
|
@@ -249,6 +251,8 @@ To fix this you have three different options:
|
|
|
249
251
|
timeout: config.timeout ?? this.defaultTimeout,
|
|
250
252
|
});
|
|
251
253
|
|
|
254
|
+
mcpClient.__setLogger(this.logger);
|
|
255
|
+
|
|
252
256
|
this.mcpClientsById.set(name, mcpClient);
|
|
253
257
|
|
|
254
258
|
try {
|
package/src/server/server.ts
CHANGED
|
@@ -58,7 +58,7 @@ export class MCPServer extends MCPServerBase {
|
|
|
58
58
|
private stdioTransport?: StdioServerTransport;
|
|
59
59
|
private sseTransport?: SSEServerTransport;
|
|
60
60
|
private sseHonoTransports: Map<string, SSETransport>;
|
|
61
|
-
private
|
|
61
|
+
private streamableHTTPTransports: Map<string, StreamableHTTPServerTransport> = new Map();
|
|
62
62
|
private listToolsHandlerIsRegistered: boolean = false;
|
|
63
63
|
private callToolHandlerIsRegistered: boolean = false;
|
|
64
64
|
private listResourcesHandlerIsRegistered: boolean = false;
|
|
@@ -100,13 +100,6 @@ export class MCPServer extends MCPServerBase {
|
|
|
100
100
|
return this.sseHonoTransports.get(sessionId);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/**
|
|
104
|
-
* Get the current streamable HTTP transport.
|
|
105
|
-
*/
|
|
106
|
-
public getStreamableHTTPTransport(): StreamableHTTPServerTransport | undefined {
|
|
107
|
-
return this.streamableHTTPTransport;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
103
|
/**
|
|
111
104
|
* Get the current server instance.
|
|
112
105
|
*/
|
|
@@ -880,37 +873,163 @@ export class MCPServer extends MCPServerBase {
|
|
|
880
873
|
res: http.ServerResponse<http.IncomingMessage>;
|
|
881
874
|
options?: StreamableHTTPServerTransportOptions;
|
|
882
875
|
}) {
|
|
883
|
-
|
|
884
|
-
this.streamableHTTPTransport = new StreamableHTTPServerTransport(options);
|
|
885
|
-
try {
|
|
886
|
-
await this.server.connect(this.streamableHTTPTransport);
|
|
887
|
-
} catch (error) {
|
|
888
|
-
this.logger.error('Error connecting to MCP server', { error });
|
|
889
|
-
res.writeHead(500);
|
|
890
|
-
res.end('Error connecting to MCP server');
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
await this.streamableHTTPTransport.handleRequest(req, res);
|
|
896
|
-
} catch (error) {
|
|
897
|
-
this.logger.error('Error handling MCP connection', { error });
|
|
898
|
-
res.writeHead(500);
|
|
899
|
-
res.end('Error handling MCP connection');
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
876
|
+
this.logger.debug(`startHTTP: Received ${req.method} request to ${url.pathname}`);
|
|
902
877
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
await this.server.close();
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
res.on('close', () => {
|
|
909
|
-
this.streamableHTTPTransport = undefined;
|
|
910
|
-
});
|
|
911
|
-
} else {
|
|
878
|
+
if (url.pathname !== httpPath) {
|
|
879
|
+
this.logger.debug(`startHTTP: Pathname ${url.pathname} does not match httpPath ${httpPath}. Returning 404.`);
|
|
912
880
|
res.writeHead(404);
|
|
913
881
|
res.end();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
886
|
+
let transport: StreamableHTTPServerTransport | undefined;
|
|
887
|
+
|
|
888
|
+
this.logger.debug(
|
|
889
|
+
`startHTTP: Session ID from headers: ${sessionId}. Active transports: ${Array.from(this.streamableHTTPTransports.keys()).join(', ')}`,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
if (sessionId && this.streamableHTTPTransports.has(sessionId)) {
|
|
894
|
+
// Found existing session
|
|
895
|
+
transport = this.streamableHTTPTransports.get(sessionId)!;
|
|
896
|
+
this.logger.debug(`startHTTP: Using existing Streamable HTTP transport for session ID: ${sessionId}`);
|
|
897
|
+
|
|
898
|
+
if (req.method === 'GET') {
|
|
899
|
+
this.logger.debug(
|
|
900
|
+
`startHTTP: Handling GET request for existing session ${sessionId}. Calling transport.handleRequest.`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Handle the request using the existing transport
|
|
905
|
+
// Need to parse body for POST requests before passing to handleRequest
|
|
906
|
+
const body =
|
|
907
|
+
req.method === 'POST'
|
|
908
|
+
? await new Promise((resolve, reject) => {
|
|
909
|
+
let data = '';
|
|
910
|
+
req.on('data', chunk => (data += chunk));
|
|
911
|
+
req.on('end', () => {
|
|
912
|
+
try {
|
|
913
|
+
resolve(JSON.parse(data));
|
|
914
|
+
} catch (e) {
|
|
915
|
+
reject(e);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
req.on('error', reject);
|
|
919
|
+
})
|
|
920
|
+
: undefined;
|
|
921
|
+
|
|
922
|
+
await transport.handleRequest(req, res, body);
|
|
923
|
+
} else {
|
|
924
|
+
// No session ID or session ID not found
|
|
925
|
+
this.logger.debug(`startHTTP: No existing Streamable HTTP session ID found. ${req.method}`);
|
|
926
|
+
|
|
927
|
+
// Only allow new sessions via POST initialize request
|
|
928
|
+
if (req.method === 'POST') {
|
|
929
|
+
const body = await new Promise((resolve, reject) => {
|
|
930
|
+
let data = '';
|
|
931
|
+
req.on('data', chunk => (data += chunk));
|
|
932
|
+
req.on('end', () => {
|
|
933
|
+
try {
|
|
934
|
+
resolve(JSON.parse(data));
|
|
935
|
+
} catch (e) {
|
|
936
|
+
reject(e);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
req.on('error', reject);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Import isInitializeRequest from the correct path
|
|
943
|
+
const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
|
|
944
|
+
|
|
945
|
+
if (isInitializeRequest(body)) {
|
|
946
|
+
this.logger.debug('startHTTP: Received Streamable HTTP initialize request, creating new transport.');
|
|
947
|
+
|
|
948
|
+
// Create a new transport for the new session
|
|
949
|
+
transport = new StreamableHTTPServerTransport({
|
|
950
|
+
...options,
|
|
951
|
+
sessionIdGenerator: () => randomUUID(),
|
|
952
|
+
onsessioninitialized: id => {
|
|
953
|
+
this.streamableHTTPTransports.set(id, transport!);
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Set up onclose handler to clean up transport when closed
|
|
958
|
+
transport.onclose = () => {
|
|
959
|
+
const closedSessionId = transport?.sessionId;
|
|
960
|
+
if (closedSessionId && this.streamableHTTPTransports.has(closedSessionId)) {
|
|
961
|
+
this.logger.debug(
|
|
962
|
+
`startHTTP: Streamable HTTP transport closed for session ${closedSessionId}, removing from map.`,
|
|
963
|
+
);
|
|
964
|
+
this.streamableHTTPTransports.delete(closedSessionId);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Connect the MCP server instance to the new transport
|
|
969
|
+
await this.server.connect(transport);
|
|
970
|
+
|
|
971
|
+
// Store the transport when the session is initialized
|
|
972
|
+
if (transport.sessionId) {
|
|
973
|
+
this.streamableHTTPTransports.set(transport.sessionId, transport);
|
|
974
|
+
this.logger.debug(
|
|
975
|
+
`startHTTP: Streamable HTTP session initialized and stored with ID: ${transport.sessionId}`,
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
this.logger.warn('startHTTP: Streamable HTTP transport initialized without a session ID.');
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Handle the initialize request
|
|
982
|
+
return await transport.handleRequest(req, res, body);
|
|
983
|
+
} else {
|
|
984
|
+
// POST request but not initialize, and no session ID
|
|
985
|
+
this.logger.warn('startHTTP: Received non-initialize POST request without a session ID.');
|
|
986
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
987
|
+
res.end(
|
|
988
|
+
JSON.stringify({
|
|
989
|
+
jsonrpc: '2.0',
|
|
990
|
+
error: {
|
|
991
|
+
code: -32000,
|
|
992
|
+
message: 'Bad Request: No valid session ID provided for non-initialize request',
|
|
993
|
+
},
|
|
994
|
+
id: (body as any)?.id ?? null, // Include original request ID if available
|
|
995
|
+
}),
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
// Non-POST request (GET/DELETE) without a session ID
|
|
1000
|
+
this.logger.warn(`startHTTP: Received ${req.method} request without a session ID.`);
|
|
1001
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1002
|
+
res.end(
|
|
1003
|
+
JSON.stringify({
|
|
1004
|
+
jsonrpc: '2.0',
|
|
1005
|
+
error: {
|
|
1006
|
+
code: -32000,
|
|
1007
|
+
message: `Bad Request: ${req.method} request requires a valid session ID`,
|
|
1008
|
+
},
|
|
1009
|
+
id: null,
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
this.logger.error('startHTTP: Error handling Streamable HTTP request:', { error });
|
|
1016
|
+
// If headers haven't been sent, send an error response
|
|
1017
|
+
if (!res.headersSent) {
|
|
1018
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1019
|
+
res.end(
|
|
1020
|
+
JSON.stringify({
|
|
1021
|
+
jsonrpc: '2.0',
|
|
1022
|
+
error: {
|
|
1023
|
+
code: -32603,
|
|
1024
|
+
message: 'Internal server error',
|
|
1025
|
+
},
|
|
1026
|
+
id: null, // Cannot determine original request ID in catch
|
|
1027
|
+
}),
|
|
1028
|
+
);
|
|
1029
|
+
} else {
|
|
1030
|
+
// If headers were already sent (e.g., during SSE stream), just log the error
|
|
1031
|
+
this.logger.error('startHTTP: Error after headers sent:', error);
|
|
1032
|
+
}
|
|
914
1033
|
}
|
|
915
1034
|
}
|
|
916
1035
|
|
|
@@ -991,9 +1110,12 @@ export class MCPServer extends MCPServerBase {
|
|
|
991
1110
|
}
|
|
992
1111
|
this.sseHonoTransports.clear();
|
|
993
1112
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
this.
|
|
1113
|
+
// Close all active Streamable HTTP transports
|
|
1114
|
+
if (this.streamableHTTPTransports) {
|
|
1115
|
+
for (const transport of this.streamableHTTPTransports.values()) {
|
|
1116
|
+
await transport.close?.();
|
|
1117
|
+
}
|
|
1118
|
+
this.streamableHTTPTransports.clear();
|
|
997
1119
|
}
|
|
998
1120
|
await this.server.close();
|
|
999
1121
|
this.logger.info('MCP server closed.');
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/mastra/mastra/packages/cli/dist/node_modules:/home/runner/work/mastra/mastra/packages/cli/node_modules:/home/runner/work/mastra/mastra/packages/node_modules:/home/runner/work/mastra/mastra/node_modules:/home/runner/work/mastra/node_modules:/home/runner/work/node_modules:/home/runner/node_modules:/home/node_modules:/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/mastra/mastra/packages/cli/dist/node_modules:/home/runner/work/mastra/mastra/packages/cli/node_modules:/home/runner/work/mastra/mastra/packages/node_modules:/home/runner/work/mastra/mastra/node_modules:/home/runner/work/mastra/node_modules:/home/runner/work/node_modules:/home/runner/node_modules:/home/node_modules:/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../mastra/dist/index.js" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../mastra/dist/index.js" "$@"
|
|
21
|
-
fi
|