@mastra/mcp 0.10.3-alpha.0 → 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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/mcp@0.10.3-alpha.0 build /home/runner/work/mastra/mastra/packages/mcp
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
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 21509ms
9
+ TSC ⚡️ Build success in 20174ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/packages/mcp/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 23126ms
16
+ DTS ⚡️ Build success in 20211ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 67.33 KB
21
- CJS ⚡️ Build success in 1667ms
22
- ESM dist/index.js 67.20 KB
23
- ESM ⚡️ Build success in 1667ms
20
+ ESM dist/index.js 71.81 KB
21
+ ESM ⚡️ Build success in 1802ms
22
+ CJS dist/index.cjs 71.95 KB
23
+ CJS ⚡️ Build success in 1784ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
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
+
17
+ ## 0.10.3
18
+
19
+ ### Patch Changes
20
+
21
+ - fc579cd: [MASTRA-3554] Support MCP prompts for MCPServer and MCPClient
22
+ - Updated dependencies [d1ed912]
23
+ - Updated dependencies [f6fd25f]
24
+ - Updated dependencies [dffb67b]
25
+ - Updated dependencies [f1f1f1b]
26
+ - Updated dependencies [925ab94]
27
+ - Updated dependencies [f9816ae]
28
+ - Updated dependencies [82090c1]
29
+ - Updated dependencies [1b443fd]
30
+ - Updated dependencies [ce97900]
31
+ - Updated dependencies [f1309d3]
32
+ - Updated dependencies [14a2566]
33
+ - Updated dependencies [f7f8293]
34
+ - Updated dependencies [48eddb9]
35
+ - @mastra/core@0.10.4
36
+
3
37
  ## 0.10.3-alpha.0
4
38
 
5
39
  ### 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 streamableHTTPTransport?;
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 streamableHTTPTransport?;
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 = false;
333
+ isConnected = null;
335
334
  async connect() {
336
- if (this.isConnected) return;
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
- this.isConnected = true;
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
- this.isConnected = false;
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({ name, args, version }) {
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
- streamableHTTPTransport;
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
- if (url.pathname === httpPath) {
1676
- this.streamableHTTPTransport = new streamableHttp_js.StreamableHTTPServerTransport(options);
1677
- try {
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.streamableHTTPTransport) {
1770
- await this.streamableHTTPTransport.close?.();
1771
- this.streamableHTTPTransport = void 0;
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 = false;
327
+ isConnected = null;
329
328
  async connect() {
330
- if (this.isConnected) return;
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
- this.isConnected = true;
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
- this.isConnected = false;
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({ name, args, version }) {
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
- streamableHTTPTransport;
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
- if (url.pathname === httpPath) {
1670
- this.streamableHTTPTransport = new StreamableHTTPServerTransport(options);
1671
- try {
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.streamableHTTPTransport) {
1764
- await this.streamableHTTPTransport.close?.();
1765
- this.streamableHTTPTransport = void 0;
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.2_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_f2f8c5e73aab614bca86ded968276706/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.2_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_f2f8c5e73aab614bca86ded968276706/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
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.2_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_f2f8c5e73aab614bca86ded968276706/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.2_@edge-runtime+vm@3.2.0_@types+debug@4.1.12_@types+node@20.19.0_@vitest+ui@_f2f8c5e73aab614bca86ded968276706/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
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 getAvailablePort();
31
-
32
- mastraServer = spawn(
33
- 'pnpm',
34
- [
35
- path.resolve(import.meta.dirname, `..`, `..`, `..`, `cli`, `dist`, `index.js`),
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-alpha.0",
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.10.2",
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.4",
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
- "zod": "^3.0.0",
35
- "@mastra/core": "^0.10.2-alpha.0"
34
+ "@mastra/core": "^0.10.2-alpha.0",
35
+ "zod": "^3.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@ai-sdk/anthropic": "^1.1.15",
38
+ "@ai-sdk/anthropic": "^1.2.12",
39
39
  "@ai-sdk/openai": "^1.3.22",
40
- "ai": "4.3.16",
41
- "@hono/node-server": "^1.13.8",
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.17.57",
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.3",
49
- "typescript": "^5.8.2",
50
- "vitest": "^3.2.2",
51
- "zod": "^3.25.56",
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.10",
54
- "@mastra/core": "0.10.4-alpha.1"
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",
@@ -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 { ClientCapabilities, GetPromptResult, ListPromptsResult, LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
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 = false;
274
+ private isConnected: Promise<boolean> | null = null;
271
275
 
272
276
  async connect() {
273
- if (this.isConnected) return;
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
- this.isConnected = true;
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
- this.isConnected = false;
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({ name, args, version }: { name: string; args?: Record<string, any>; version?: string }): Promise<GetPromptResult> {
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 {
@@ -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 streamableHTTPTransport?: StreamableHTTPServerTransport;
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
- if (url.pathname === httpPath) {
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
- this.server.onclose = async () => {
904
- this.streamableHTTPTransport = undefined;
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
- if (this.streamableHTTPTransport) {
995
- await this.streamableHTTPTransport.close?.();
996
- this.streamableHTTPTransport = undefined;
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.');