@mastra/mcp 0.5.0-alpha.2 → 0.5.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { MastraBase } from '@mastra/core/base';
2
2
  import { createTool } from '@mastra/core/tools';
3
- import { resolveSerializedZodOutput, isZodType } from '@mastra/core/utils';
3
+ import { isZodType } from '@mastra/core/utils';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
6
6
  import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
7
7
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
8
8
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
9
- import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesResultSchema, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesResultSchema, CallToolResultSchema, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
10
10
  import { asyncExitHook, gracefulExit } from 'exit-hook';
11
- import jsonSchemaToZod from 'json-schema-to-zod';
12
11
  import { z } from 'zod';
12
+ import { jsonSchemaObjectToZodRawShape } from 'zod-from-json-schema';
13
13
  import equal from 'fast-deep-equal';
14
14
  import { v5 } from 'uuid';
15
15
  import { randomUUID } from 'crypto';
@@ -20,6 +20,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
20
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
21
21
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
22
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
23
+ import { streamSSE } from 'hono/streaming';
23
24
 
24
25
  // src/client.ts
25
26
  function convertLogLevelToLoggerMethod(level) {
@@ -231,7 +232,29 @@ var InternalMastraMCPClient = class extends MastraBase {
231
232
  });
232
233
  }
233
234
  convertInputSchema(inputSchema) {
234
- return isZodType(inputSchema) ? inputSchema : resolveSerializedZodOutput(jsonSchemaToZod(inputSchema));
235
+ if (isZodType(inputSchema)) {
236
+ return inputSchema;
237
+ }
238
+ try {
239
+ const rawShape = jsonSchemaObjectToZodRawShape(inputSchema);
240
+ return z.object(rawShape);
241
+ } catch (error) {
242
+ let errorDetails;
243
+ if (error instanceof Error) {
244
+ errorDetails = error.stack;
245
+ } else {
246
+ try {
247
+ errorDetails = JSON.stringify(error);
248
+ } catch {
249
+ errorDetails = String(error);
250
+ }
251
+ }
252
+ this.log("error", "Failed to convert JSON schema to Zod schema using zodFromJsonSchema", {
253
+ error: errorDetails,
254
+ originalJsonSchema: inputSchema
255
+ });
256
+ throw new Error(errorDetails);
257
+ }
235
258
  }
236
259
  async tools() {
237
260
  this.log("debug", `Requesting tools from MCP server`);
@@ -239,36 +262,43 @@ var InternalMastraMCPClient = class extends MastraBase {
239
262
  const toolsRes = {};
240
263
  tools.forEach((tool) => {
241
264
  this.log("debug", `Processing tool: ${tool.name}`);
242
- const mastraTool = createTool({
243
- id: `${this.name}_${tool.name}`,
244
- description: tool.description || "",
245
- inputSchema: this.convertInputSchema(tool.inputSchema),
246
- execute: async ({ context }) => {
247
- try {
248
- this.log("debug", `Executing tool: ${tool.name}`, { toolArgs: context });
249
- const res = await this.client.callTool(
250
- {
251
- name: tool.name,
252
- arguments: context
253
- },
254
- CallToolResultSchema,
255
- {
256
- timeout: this.timeout
257
- }
258
- );
259
- this.log("debug", `Tool executed successfully: ${tool.name}`);
260
- return res;
261
- } catch (e) {
262
- this.log("error", `Error calling tool: ${tool.name}`, {
263
- error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
264
- toolArgs: context
265
- });
266
- throw e;
265
+ try {
266
+ const mastraTool = createTool({
267
+ id: `${this.name}_${tool.name}`,
268
+ description: tool.description || "",
269
+ inputSchema: this.convertInputSchema(tool.inputSchema),
270
+ execute: async ({ context }) => {
271
+ try {
272
+ this.log("debug", `Executing tool: ${tool.name}`, { toolArgs: context });
273
+ const res = await this.client.callTool(
274
+ {
275
+ name: tool.name,
276
+ arguments: context
277
+ },
278
+ CallToolResultSchema,
279
+ {
280
+ timeout: this.timeout
281
+ }
282
+ );
283
+ this.log("debug", `Tool executed successfully: ${tool.name}`);
284
+ return res;
285
+ } catch (e) {
286
+ this.log("error", `Error calling tool: ${tool.name}`, {
287
+ error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2),
288
+ toolArgs: context
289
+ });
290
+ throw e;
291
+ }
267
292
  }
293
+ });
294
+ if (tool.name) {
295
+ toolsRes[tool.name] = mastraTool;
268
296
  }
269
- });
270
- if (tool.name) {
271
- toolsRes[tool.name] = mastraTool;
297
+ } catch (toolCreationError) {
298
+ this.log("error", `Failed to create Mastra tool wrapper for MCP tool: ${tool.name}`, {
299
+ error: toolCreationError instanceof Error ? toolCreationError.stack : String(toolCreationError),
300
+ mcpToolDefinition: tool
301
+ });
272
302
  }
273
303
  });
274
304
  return toolsRes;
@@ -470,11 +500,98 @@ var MCPConfiguration = class extends MCPClient {
470
500
  );
471
501
  }
472
502
  };
503
+ var MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024;
504
+ var SSETransport = class {
505
+ messageUrl;
506
+ stream;
507
+ _sessionId;
508
+ onclose;
509
+ onerror;
510
+ onmessage;
511
+ /**
512
+ * Creates a new SSETransport, which will direct the MPC client to POST messages to messageUrl
513
+ */
514
+ constructor(messageUrl, stream) {
515
+ this.messageUrl = messageUrl;
516
+ this.stream = stream;
517
+ this._sessionId = crypto.randomUUID();
518
+ this.stream.onAbort(() => {
519
+ void this.close();
520
+ });
521
+ }
522
+ get sessionId() {
523
+ return this._sessionId;
524
+ }
525
+ // start() is automatically called after MCP Server connects to the transport
526
+ async start() {
527
+ if (this.stream == null) {
528
+ throw new Error("Stream not initialized");
529
+ }
530
+ if (this.stream.closed) {
531
+ throw new Error("SSE transport already closed!");
532
+ }
533
+ await this.stream.writeSSE({
534
+ event: "endpoint",
535
+ data: `${this.messageUrl}?sessionId=${this.sessionId}`
536
+ });
537
+ }
538
+ async handlePostMessage(context) {
539
+ if (this.stream?.closed == null) {
540
+ return context.text("SSE connection not established", 500);
541
+ }
542
+ try {
543
+ const contentType = context.req.header("content-type") || "";
544
+ if (!contentType.includes("application/json")) {
545
+ throw new Error(`Unsupported content-type: ${contentType}`);
546
+ }
547
+ const contentLength = Number.parseInt(context.req.header("content-length") || "0", 10);
548
+ if (contentLength > MAXIMUM_MESSAGE_SIZE) {
549
+ throw new Error(`Request body too large: ${contentLength} bytes`);
550
+ }
551
+ const body = await context.req.json();
552
+ await this.handleMessage(body);
553
+ return context.text("Accepted", 202);
554
+ } catch (error) {
555
+ this.onerror?.(error);
556
+ return context.text("Error", 400);
557
+ }
558
+ }
559
+ /**
560
+ * Handle a client message, regardless of how it arrived. This can be used to inform the server of messages that arrive via a means different than HTTP POST.
561
+ */
562
+ async handleMessage(message) {
563
+ let parsedMessage;
564
+ try {
565
+ parsedMessage = JSONRPCMessageSchema.parse(message);
566
+ } catch (error) {
567
+ this.onerror?.(error);
568
+ throw error;
569
+ }
570
+ this.onmessage?.(parsedMessage);
571
+ }
572
+ async close() {
573
+ if (this.stream?.closed) {
574
+ this.stream.abort();
575
+ }
576
+ }
577
+ async send(message) {
578
+ if (this.stream?.closed) {
579
+ throw new Error("Not connected");
580
+ }
581
+ await this.stream.writeSSE({
582
+ event: "message",
583
+ data: JSON.stringify(message)
584
+ });
585
+ }
586
+ };
473
587
  var MCPServer = class extends MCPServerBase {
474
588
  server;
475
589
  stdioTransport;
476
590
  sseTransport;
591
+ sseHonoTransports;
477
592
  streamableHTTPTransport;
593
+ listToolsHandlerIsRegistered = false;
594
+ callToolHandlerIsRegistered = false;
478
595
  /**
479
596
  * Get the current stdio transport.
480
597
  */
@@ -487,6 +604,12 @@ var MCPServer = class extends MCPServerBase {
487
604
  getSseTransport() {
488
605
  return this.sseTransport;
489
606
  }
607
+ /**
608
+ * Get the current SSE Hono transport.
609
+ */
610
+ getSseHonoTransport(sessionId) {
611
+ return this.sseHonoTransports.get(sessionId);
612
+ }
490
613
  /**
491
614
  * Get the current streamable HTTP transport.
492
615
  */
@@ -505,6 +628,7 @@ var MCPServer = class extends MCPServerBase {
505
628
  this.logger.info(
506
629
  `Initialized MCPServer '${name}' v${version} with tools: ${Object.keys(this.convertedTools).join(", ")}`
507
630
  );
631
+ this.sseHonoTransports = /* @__PURE__ */ new Map();
508
632
  this.registerListToolsHandler();
509
633
  this.registerCallToolHandler();
510
634
  }
@@ -548,6 +672,10 @@ var MCPServer = class extends MCPServerBase {
548
672
  * Register the ListTools handler for listing all available tools.
549
673
  */
550
674
  registerListToolsHandler() {
675
+ if (this.listToolsHandlerIsRegistered) {
676
+ return;
677
+ }
678
+ this.listToolsHandlerIsRegistered = true;
551
679
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
552
680
  this.logger.debug("Handling ListTools request");
553
681
  return {
@@ -563,6 +691,10 @@ var MCPServer = class extends MCPServerBase {
563
691
  * Register the CallTool handler for executing a tool by name.
564
692
  */
565
693
  registerCallToolHandler() {
694
+ if (this.callToolHandlerIsRegistered) {
695
+ return;
696
+ }
697
+ this.callToolHandlerIsRegistered = true;
566
698
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
567
699
  const startTime = Date.now();
568
700
  try {
@@ -668,6 +800,43 @@ var MCPServer = class extends MCPServerBase {
668
800
  res.end();
669
801
  }
670
802
  }
803
+ /**
804
+ * Handles MCP-over-SSE protocol for user-provided HTTP servers.
805
+ * Call this from your HTTP server for both the SSE and message endpoints.
806
+ *
807
+ * @param url Parsed URL of the incoming request
808
+ * @param ssePath Path for establishing the SSE connection (e.g. '/sse')
809
+ * @param messagePath Path for POSTing client messages (e.g. '/message')
810
+ * @param context Incoming Hono context
811
+ */
812
+ async startHonoSSE({ url, ssePath, messagePath, context }) {
813
+ if (url.pathname === ssePath) {
814
+ return streamSSE(context, async (stream) => {
815
+ await this.connectHonoSSE({
816
+ messagePath,
817
+ stream
818
+ });
819
+ });
820
+ } else if (url.pathname === messagePath) {
821
+ this.logger.debug("Received message");
822
+ const sessionId = context.req.query("sessionId");
823
+ this.logger.debug("Received message for sessionId", { sessionId });
824
+ if (!sessionId) {
825
+ return context.text("No sessionId provided", 400);
826
+ }
827
+ if (!this.sseHonoTransports.has(sessionId)) {
828
+ return context.text(`No transport found for sessionId ${sessionId}`, 400);
829
+ }
830
+ const message = await this.sseHonoTransports.get(sessionId)?.handlePostMessage(context);
831
+ if (!message) {
832
+ return context.text("Transport not found", 400);
833
+ }
834
+ return message;
835
+ } else {
836
+ this.logger.debug("Unknown path:", { path: url.pathname });
837
+ return context.text("Unknown path", 404);
838
+ }
839
+ }
671
840
  /**
672
841
  * Handles MCP-over-StreamableHTTP protocol for user-provided HTTP servers.
673
842
  * Call this from your HTTP server for the streamable HTTP endpoint.
@@ -715,14 +884,6 @@ var MCPServer = class extends MCPServerBase {
715
884
  res.end();
716
885
  }
717
886
  }
718
- async handlePostMessage(req, res) {
719
- if (!this.sseTransport) {
720
- res.writeHead(503);
721
- res.end("SSE connection not established");
722
- return;
723
- }
724
- await this.sseTransport.handlePostMessage(req, res);
725
- }
726
887
  async connectSSE({
727
888
  messagePath,
728
889
  res
@@ -738,10 +899,35 @@ var MCPServer = class extends MCPServerBase {
738
899
  this.sseTransport = void 0;
739
900
  });
740
901
  }
902
+ async connectHonoSSE({ messagePath, stream }) {
903
+ this.logger.debug("Received SSE connection");
904
+ const sseTransport = new SSETransport(messagePath, stream);
905
+ const sessionId = sseTransport.sessionId;
906
+ this.logger.debug("SSE Transport created with sessionId:", { sessionId });
907
+ this.sseHonoTransports.set(sessionId, sseTransport);
908
+ stream.onAbort(() => {
909
+ this.logger.debug("SSE Transport aborted with sessionId:", { sessionId });
910
+ this.sseHonoTransports.delete(sessionId);
911
+ });
912
+ await this.server.connect(sseTransport);
913
+ this.server.onclose = async () => {
914
+ this.logger.debug("SSE Transport closed with sessionId:", { sessionId });
915
+ this.sseHonoTransports.delete(sessionId);
916
+ await this.server.close();
917
+ };
918
+ while (true) {
919
+ const sessionIds = Array.from(this.sseHonoTransports.keys() || []);
920
+ this.logger.debug("Active Hono SSE sessions:", { sessionIds });
921
+ await stream.write(":keep-alive\n\n");
922
+ await stream.sleep(6e4);
923
+ }
924
+ }
741
925
  /**
742
926
  * Close the MCP server and all its connections
743
927
  */
744
928
  async close() {
929
+ this.callToolHandlerIsRegistered = false;
930
+ this.listToolsHandlerIsRegistered = false;
745
931
  try {
746
932
  if (this.stdioTransport) {
747
933
  await this.stdioTransport.close?.();
@@ -751,6 +937,12 @@ var MCPServer = class extends MCPServerBase {
751
937
  await this.sseTransport.close?.();
752
938
  this.sseTransport = void 0;
753
939
  }
940
+ if (this.sseHonoTransports) {
941
+ for (const transport of this.sseHonoTransports.values()) {
942
+ await transport.close?.();
943
+ }
944
+ this.sseHonoTransports.clear();
945
+ }
754
946
  if (this.streamableHTTPTransport) {
755
947
  await this.streamableHTTPTransport.close?.();
756
948
  this.streamableHTTPTransport = void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.5.0-alpha.2",
3
+ "version": "0.5.0-alpha.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,18 +26,22 @@
26
26
  "date-fns": "^4.1.0",
27
27
  "exit-hook": "^4.0.0",
28
28
  "fast-deep-equal": "^3.1.3",
29
- "json-schema-to-zod": "^2.6.0",
29
+ "hono": "^4.7.4",
30
30
  "uuid": "^11.1.0",
31
- "@mastra/core": "^0.9.4-alpha.2"
31
+ "zod-from-json-schema": "^0.0.5",
32
+ "@mastra/core": "^0.9.4-alpha.3"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "zod": "^3.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@ai-sdk/anthropic": "^1.1.15",
39
+ "@hono/node-server": "^1.13.8",
40
+ "@mendable/firecrawl-js": "^1.24.0",
38
41
  "@microsoft/api-extractor": "^7.52.5",
39
42
  "@types/node": "^20.17.27",
40
43
  "eslint": "^9.23.0",
44
+ "hono-mcp-server-sse-transport": "0.0.6",
41
45
  "tsup": "^8.4.0",
42
46
  "tsx": "^4.19.3",
43
47
  "typescript": "^5.8.2",