@mastra/mcp 0.5.0-alpha.3 → 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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +12 -0
- package/dist/_tsup-dts-rollup.d.cts +26 -1
- package/dist/_tsup-dts-rollup.d.ts +26 -1
- package/dist/index.cjs +220 -41
- package/dist/index.js +220 -40
- package/package.json +7 -3
- package/src/__fixtures__/fire-crawl-complex-schema.ts +1013 -0
- package/src/client.test.ts +4 -4
- package/src/client.ts +69 -38
- package/src/configuration.test.ts +37 -0
- package/src/server.test.ts +183 -23
- package/src/server.ts +86 -10
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 {
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
error
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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,10 +500,95 @@ 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;
|
|
478
593
|
listToolsHandlerIsRegistered = false;
|
|
479
594
|
callToolHandlerIsRegistered = false;
|
|
@@ -489,6 +604,12 @@ var MCPServer = class extends MCPServerBase {
|
|
|
489
604
|
getSseTransport() {
|
|
490
605
|
return this.sseTransport;
|
|
491
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Get the current SSE Hono transport.
|
|
609
|
+
*/
|
|
610
|
+
getSseHonoTransport(sessionId) {
|
|
611
|
+
return this.sseHonoTransports.get(sessionId);
|
|
612
|
+
}
|
|
492
613
|
/**
|
|
493
614
|
* Get the current streamable HTTP transport.
|
|
494
615
|
*/
|
|
@@ -507,6 +628,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
507
628
|
this.logger.info(
|
|
508
629
|
`Initialized MCPServer '${name}' v${version} with tools: ${Object.keys(this.convertedTools).join(", ")}`
|
|
509
630
|
);
|
|
631
|
+
this.sseHonoTransports = /* @__PURE__ */ new Map();
|
|
510
632
|
this.registerListToolsHandler();
|
|
511
633
|
this.registerCallToolHandler();
|
|
512
634
|
}
|
|
@@ -678,6 +800,43 @@ var MCPServer = class extends MCPServerBase {
|
|
|
678
800
|
res.end();
|
|
679
801
|
}
|
|
680
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
|
+
}
|
|
681
840
|
/**
|
|
682
841
|
* Handles MCP-over-StreamableHTTP protocol for user-provided HTTP servers.
|
|
683
842
|
* Call this from your HTTP server for the streamable HTTP endpoint.
|
|
@@ -725,14 +884,6 @@ var MCPServer = class extends MCPServerBase {
|
|
|
725
884
|
res.end();
|
|
726
885
|
}
|
|
727
886
|
}
|
|
728
|
-
async handlePostMessage(req, res) {
|
|
729
|
-
if (!this.sseTransport) {
|
|
730
|
-
res.writeHead(503);
|
|
731
|
-
res.end("SSE connection not established");
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
await this.sseTransport.handlePostMessage(req, res);
|
|
735
|
-
}
|
|
736
887
|
async connectSSE({
|
|
737
888
|
messagePath,
|
|
738
889
|
res
|
|
@@ -748,6 +899,29 @@ var MCPServer = class extends MCPServerBase {
|
|
|
748
899
|
this.sseTransport = void 0;
|
|
749
900
|
});
|
|
750
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
|
+
}
|
|
751
925
|
/**
|
|
752
926
|
* Close the MCP server and all its connections
|
|
753
927
|
*/
|
|
@@ -763,6 +937,12 @@ var MCPServer = class extends MCPServerBase {
|
|
|
763
937
|
await this.sseTransport.close?.();
|
|
764
938
|
this.sseTransport = void 0;
|
|
765
939
|
}
|
|
940
|
+
if (this.sseHonoTransports) {
|
|
941
|
+
for (const transport of this.sseHonoTransports.values()) {
|
|
942
|
+
await transport.close?.();
|
|
943
|
+
}
|
|
944
|
+
this.sseHonoTransports.clear();
|
|
945
|
+
}
|
|
766
946
|
if (this.streamableHTTPTransport) {
|
|
767
947
|
await this.streamableHTTPTransport.close?.();
|
|
768
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.
|
|
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
|
-
"
|
|
29
|
+
"hono": "^4.7.4",
|
|
30
30
|
"uuid": "^11.1.0",
|
|
31
|
-
"
|
|
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",
|