@mastra/mcp 0.10.5-alpha.0 → 0.10.5-alpha.1
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 +15 -0
- package/dist/_tsup-dts-rollup.d.cts +88 -80
- package/dist/_tsup-dts-rollup.d.ts +88 -80
- package/dist/index.cjs +258 -30
- package/dist/index.js +253 -25
- package/integration-tests/node_modules/.bin/vitest +2 -2
- package/integration-tests/package.json +2 -2
- package/package.json +7 -7
- package/src/__fixtures__/tools.ts +0 -9
- package/src/client/client.ts +45 -1
- package/src/server/server.test.ts +84 -0
- package/src/server/server.ts +51 -16
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MastraBase } from '@mastra/core/base';
|
|
2
|
+
import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
|
|
2
3
|
import { createTool as createTool$1 } from '@mastra/core/tools';
|
|
3
4
|
import { isZodType } from '@mastra/core/utils';
|
|
4
5
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -10,7 +11,6 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSche
|
|
|
10
11
|
import { asyncExitHook, gracefulExit } from 'exit-hook';
|
|
11
12
|
import { z } from 'zod';
|
|
12
13
|
import { convertJsonSchemaToZod } from 'zod-from-json-schema';
|
|
13
|
-
import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
|
|
14
14
|
import equal from 'fast-deep-equal';
|
|
15
15
|
import { v5 } from 'uuid';
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
@@ -21,7 +21,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
21
21
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
22
22
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
23
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
24
|
-
import { streamSSE } from 'hono/streaming';
|
|
25
24
|
|
|
26
25
|
// src/client/client.ts
|
|
27
26
|
var PromptClientActions = class {
|
|
@@ -493,7 +492,42 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
493
492
|
error: errorDetails,
|
|
494
493
|
originalJsonSchema: inputSchema
|
|
495
494
|
});
|
|
496
|
-
throw new
|
|
495
|
+
throw new MastraError({
|
|
496
|
+
id: "MCP_TOOL_INPUT_SCHEMA_CONVERSION_FAILED",
|
|
497
|
+
domain: ErrorDomain.MCP,
|
|
498
|
+
category: ErrorCategory.USER,
|
|
499
|
+
details: { error: errorDetails ?? "Unknown error" }
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
convertOutputSchema(outputSchema) {
|
|
504
|
+
if (!outputSchema) return;
|
|
505
|
+
if (isZodType(outputSchema)) {
|
|
506
|
+
return outputSchema;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
return convertJsonSchemaToZod(outputSchema);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
let errorDetails;
|
|
512
|
+
if (error instanceof Error) {
|
|
513
|
+
errorDetails = error.stack;
|
|
514
|
+
} else {
|
|
515
|
+
try {
|
|
516
|
+
errorDetails = JSON.stringify(error);
|
|
517
|
+
} catch {
|
|
518
|
+
errorDetails = String(error);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
this.log("error", "Failed to convert JSON schema to Zod schema using zodFromJsonSchema", {
|
|
522
|
+
error: errorDetails,
|
|
523
|
+
originalJsonSchema: outputSchema
|
|
524
|
+
});
|
|
525
|
+
throw new MastraError({
|
|
526
|
+
id: "MCP_TOOL_OUTPUT_SCHEMA_CONVERSION_FAILED",
|
|
527
|
+
domain: ErrorDomain.MCP,
|
|
528
|
+
category: ErrorCategory.USER,
|
|
529
|
+
details: { error: errorDetails ?? "Unknown error" }
|
|
530
|
+
});
|
|
497
531
|
}
|
|
498
532
|
}
|
|
499
533
|
async tools() {
|
|
@@ -507,6 +541,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
507
541
|
id: `${this.name}_${tool.name}`,
|
|
508
542
|
description: tool.description || "",
|
|
509
543
|
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
544
|
+
outputSchema: this.convertOutputSchema(tool.outputSchema),
|
|
510
545
|
execute: async ({ context, runtimeContext }) => {
|
|
511
546
|
const previousContext = this.currentOperationContext;
|
|
512
547
|
this.currentOperationContext = runtimeContext || null;
|
|
@@ -929,6 +964,165 @@ var MCPConfiguration = class extends MCPClient {
|
|
|
929
964
|
);
|
|
930
965
|
}
|
|
931
966
|
};
|
|
967
|
+
|
|
968
|
+
// ../../node_modules/.pnpm/hono@4.8.1/node_modules/hono/dist/utils/stream.js
|
|
969
|
+
var StreamingApi = class {
|
|
970
|
+
writer;
|
|
971
|
+
encoder;
|
|
972
|
+
writable;
|
|
973
|
+
abortSubscribers = [];
|
|
974
|
+
responseReadable;
|
|
975
|
+
aborted = false;
|
|
976
|
+
closed = false;
|
|
977
|
+
constructor(writable, _readable) {
|
|
978
|
+
this.writable = writable;
|
|
979
|
+
this.writer = writable.getWriter();
|
|
980
|
+
this.encoder = new TextEncoder();
|
|
981
|
+
const reader = _readable.getReader();
|
|
982
|
+
this.abortSubscribers.push(async () => {
|
|
983
|
+
await reader.cancel();
|
|
984
|
+
});
|
|
985
|
+
this.responseReadable = new ReadableStream({
|
|
986
|
+
async pull(controller) {
|
|
987
|
+
const { done, value } = await reader.read();
|
|
988
|
+
done ? controller.close() : controller.enqueue(value);
|
|
989
|
+
},
|
|
990
|
+
cancel: () => {
|
|
991
|
+
this.abort();
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
async write(input) {
|
|
996
|
+
try {
|
|
997
|
+
if (typeof input === "string") {
|
|
998
|
+
input = this.encoder.encode(input);
|
|
999
|
+
}
|
|
1000
|
+
await this.writer.write(input);
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
async writeln(input) {
|
|
1006
|
+
await this.write(input + "\n");
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
sleep(ms) {
|
|
1010
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
1011
|
+
}
|
|
1012
|
+
async close() {
|
|
1013
|
+
try {
|
|
1014
|
+
await this.writer.close();
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
this.closed = true;
|
|
1018
|
+
}
|
|
1019
|
+
async pipe(body) {
|
|
1020
|
+
this.writer.releaseLock();
|
|
1021
|
+
await body.pipeTo(this.writable, { preventClose: true });
|
|
1022
|
+
this.writer = this.writable.getWriter();
|
|
1023
|
+
}
|
|
1024
|
+
onAbort(listener) {
|
|
1025
|
+
this.abortSubscribers.push(listener);
|
|
1026
|
+
}
|
|
1027
|
+
abort() {
|
|
1028
|
+
if (!this.aborted) {
|
|
1029
|
+
this.aborted = true;
|
|
1030
|
+
this.abortSubscribers.forEach((subscriber) => subscriber());
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// ../../node_modules/.pnpm/hono@4.8.1/node_modules/hono/dist/helper/streaming/utils.js
|
|
1036
|
+
var isOldBunVersion = () => {
|
|
1037
|
+
const version = typeof Bun !== "undefined" ? Bun.version : void 0;
|
|
1038
|
+
if (version === void 0) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
|
|
1042
|
+
isOldBunVersion = () => result;
|
|
1043
|
+
return result;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// ../../node_modules/.pnpm/hono@4.8.1/node_modules/hono/dist/utils/html.js
|
|
1047
|
+
var HtmlEscapedCallbackPhase = {
|
|
1048
|
+
Stringify: 1};
|
|
1049
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
|
|
1050
|
+
if (typeof str === "object" && !(str instanceof String)) {
|
|
1051
|
+
if (!(str instanceof Promise)) {
|
|
1052
|
+
str = str.toString();
|
|
1053
|
+
}
|
|
1054
|
+
if (str instanceof Promise) {
|
|
1055
|
+
str = await str;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const callbacks = str.callbacks;
|
|
1059
|
+
if (!callbacks?.length) {
|
|
1060
|
+
return Promise.resolve(str);
|
|
1061
|
+
}
|
|
1062
|
+
if (buffer) {
|
|
1063
|
+
buffer[0] += str;
|
|
1064
|
+
} else {
|
|
1065
|
+
buffer = [str];
|
|
1066
|
+
}
|
|
1067
|
+
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then(
|
|
1068
|
+
(res) => Promise.all(
|
|
1069
|
+
res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context, buffer))
|
|
1070
|
+
).then(() => buffer[0])
|
|
1071
|
+
);
|
|
1072
|
+
{
|
|
1073
|
+
return resStr;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// ../../node_modules/.pnpm/hono@4.8.1/node_modules/hono/dist/helper/streaming/sse.js
|
|
1078
|
+
var SSEStreamingApi = class extends StreamingApi {
|
|
1079
|
+
constructor(writable, readable) {
|
|
1080
|
+
super(writable, readable);
|
|
1081
|
+
}
|
|
1082
|
+
async writeSSE(message) {
|
|
1083
|
+
const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
|
|
1084
|
+
const dataLines = data.split("\n").map((line) => {
|
|
1085
|
+
return `data: ${line}`;
|
|
1086
|
+
}).join("\n");
|
|
1087
|
+
const sseData = [
|
|
1088
|
+
message.event && `event: ${message.event}`,
|
|
1089
|
+
dataLines,
|
|
1090
|
+
message.id && `id: ${message.id}`,
|
|
1091
|
+
message.retry && `retry: ${message.retry}`
|
|
1092
|
+
].filter(Boolean).join("\n") + "\n\n";
|
|
1093
|
+
await this.write(sseData);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
var run = async (stream2, cb, onError) => {
|
|
1097
|
+
try {
|
|
1098
|
+
await cb(stream2);
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
{
|
|
1101
|
+
console.error(e);
|
|
1102
|
+
}
|
|
1103
|
+
} finally {
|
|
1104
|
+
stream2.close();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
var contextStash = /* @__PURE__ */ new WeakMap();
|
|
1108
|
+
var streamSSE = (c, cb, onError) => {
|
|
1109
|
+
const { readable, writable } = new TransformStream();
|
|
1110
|
+
const stream2 = new SSEStreamingApi(writable, readable);
|
|
1111
|
+
if (isOldBunVersion()) {
|
|
1112
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
1113
|
+
if (!stream2.closed) {
|
|
1114
|
+
stream2.abort();
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
contextStash.set(stream2.responseReadable, c);
|
|
1119
|
+
c.header("Transfer-Encoding", "chunked");
|
|
1120
|
+
c.header("Content-Type", "text/event-stream");
|
|
1121
|
+
c.header("Cache-Control", "no-cache");
|
|
1122
|
+
c.header("Connection", "keep-alive");
|
|
1123
|
+
run(stream2, cb);
|
|
1124
|
+
return c.newResponse(stream2.responseReadable);
|
|
1125
|
+
};
|
|
932
1126
|
var MAXIMUM_MESSAGE_SIZE = 4 * 1024 * 1024;
|
|
933
1127
|
var SSETransport = class {
|
|
934
1128
|
messageUrl;
|
|
@@ -940,9 +1134,9 @@ var SSETransport = class {
|
|
|
940
1134
|
/**
|
|
941
1135
|
* Creates a new SSETransport, which will direct the MPC client to POST messages to messageUrl
|
|
942
1136
|
*/
|
|
943
|
-
constructor(messageUrl,
|
|
1137
|
+
constructor(messageUrl, stream2) {
|
|
944
1138
|
this.messageUrl = messageUrl;
|
|
945
|
-
this.stream =
|
|
1139
|
+
this.stream = stream2;
|
|
946
1140
|
this._sessionId = crypto.randomUUID();
|
|
947
1141
|
this.stream.onAbort(() => {
|
|
948
1142
|
void this.close();
|
|
@@ -959,6 +1153,10 @@ var SSETransport = class {
|
|
|
959
1153
|
if (this.stream.closed) {
|
|
960
1154
|
throw new Error("SSE transport already closed!");
|
|
961
1155
|
}
|
|
1156
|
+
await this.stream.writeSSE({
|
|
1157
|
+
event: "ping",
|
|
1158
|
+
data: ""
|
|
1159
|
+
});
|
|
962
1160
|
await this.stream.writeSSE({
|
|
963
1161
|
event: "endpoint",
|
|
964
1162
|
data: `${this.messageUrl}?sessionId=${this.sessionId}`
|
|
@@ -1330,8 +1528,8 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1330
1528
|
context
|
|
1331
1529
|
);
|
|
1332
1530
|
try {
|
|
1333
|
-
const
|
|
1334
|
-
const response = await
|
|
1531
|
+
const run2 = workflow.createRun({ runId: runtimeContext?.get("runId") });
|
|
1532
|
+
const response = await run2.start({ inputData: context, runtimeContext });
|
|
1335
1533
|
return response;
|
|
1336
1534
|
} catch (error) {
|
|
1337
1535
|
this.logger.error(
|
|
@@ -1354,6 +1552,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1354
1552
|
name: workflowToolName,
|
|
1355
1553
|
description: coreTool.description,
|
|
1356
1554
|
parameters: coreTool.parameters,
|
|
1555
|
+
outputSchema: coreTool.outputSchema,
|
|
1357
1556
|
execute: coreTool.execute,
|
|
1358
1557
|
toolType: "workflow"
|
|
1359
1558
|
};
|
|
@@ -1393,6 +1592,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1393
1592
|
name: toolName,
|
|
1394
1593
|
description: coreTool.description,
|
|
1395
1594
|
parameters: coreTool.parameters,
|
|
1595
|
+
outputSchema: coreTool.outputSchema,
|
|
1396
1596
|
execute: coreTool.execute
|
|
1397
1597
|
};
|
|
1398
1598
|
this.logger.info(`Registered explicit tool: '${toolName}'`);
|
|
@@ -1439,11 +1639,17 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1439
1639
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1440
1640
|
this.logger.debug("Handling ListTools request");
|
|
1441
1641
|
return {
|
|
1442
|
-
tools: Object.values(this.convertedTools).map((tool) =>
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1642
|
+
tools: Object.values(this.convertedTools).map((tool) => {
|
|
1643
|
+
const toolSpec = {
|
|
1644
|
+
name: tool.name,
|
|
1645
|
+
description: tool.description,
|
|
1646
|
+
inputSchema: tool.parameters.jsonSchema
|
|
1647
|
+
};
|
|
1648
|
+
if (tool.outputSchema) {
|
|
1649
|
+
toolSpec.outputSchema = tool.outputSchema.jsonSchema;
|
|
1650
|
+
}
|
|
1651
|
+
return toolSpec;
|
|
1652
|
+
})
|
|
1447
1653
|
};
|
|
1448
1654
|
});
|
|
1449
1655
|
}
|
|
@@ -1466,7 +1672,6 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1466
1672
|
isError: true
|
|
1467
1673
|
};
|
|
1468
1674
|
}
|
|
1469
|
-
this.logger.debug(`CallTool: Invoking '${request.params.name}' with arguments:`, request.params.arguments);
|
|
1470
1675
|
const validation = tool.parameters.validate?.(request.params.arguments ?? {});
|
|
1471
1676
|
if (validation && !validation.success) {
|
|
1472
1677
|
this.logger.warn(`CallTool: Invalid tool arguments for '${request.params.name}'`, {
|
|
@@ -1485,17 +1690,38 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1485
1690
|
};
|
|
1486
1691
|
}
|
|
1487
1692
|
const result = await tool.execute(validation?.value, { messages: [], toolCallId: "" });
|
|
1693
|
+
this.logger.debug(`CallTool: Tool '${request.params.name}' executed successfully with result:`, result);
|
|
1488
1694
|
const duration = Date.now() - startTime;
|
|
1489
1695
|
this.logger.info(`Tool '${request.params.name}' executed successfully in ${duration}ms.`);
|
|
1490
|
-
|
|
1491
|
-
|
|
1696
|
+
const response = { isError: false, content: [] };
|
|
1697
|
+
if (tool.outputSchema) {
|
|
1698
|
+
if (!result.structuredContent) {
|
|
1699
|
+
throw new Error(`Tool ${request.params.name} has an output schema but no structured content was provided.`);
|
|
1700
|
+
}
|
|
1701
|
+
const outputValidation = tool.outputSchema.validate?.(result.structuredContent ?? {});
|
|
1702
|
+
if (outputValidation && !outputValidation.success) {
|
|
1703
|
+
this.logger.warn(`CallTool: Invalid structured content for '${request.params.name}'`, {
|
|
1704
|
+
errors: outputValidation.error
|
|
1705
|
+
});
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`Invalid structured content for tool ${request.params.name}: ${JSON.stringify(outputValidation.error)}`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
response.structuredContent = result.structuredContent;
|
|
1711
|
+
}
|
|
1712
|
+
if (result.content) {
|
|
1713
|
+
response.content = result.content;
|
|
1714
|
+
} else if (response.structuredContent) {
|
|
1715
|
+
response.content = [{ type: "text", text: JSON.stringify(response.structuredContent) }];
|
|
1716
|
+
} else {
|
|
1717
|
+
response.content = [
|
|
1492
1718
|
{
|
|
1493
1719
|
type: "text",
|
|
1494
1720
|
text: typeof result === "string" ? result : JSON.stringify(result)
|
|
1495
1721
|
}
|
|
1496
|
-
]
|
|
1497
|
-
|
|
1498
|
-
|
|
1722
|
+
];
|
|
1723
|
+
}
|
|
1724
|
+
return response;
|
|
1499
1725
|
} catch (error) {
|
|
1500
1726
|
const duration = Date.now() - startTime;
|
|
1501
1727
|
if (error instanceof z.ZodError) {
|
|
@@ -1849,10 +2075,10 @@ var MCPServer = class extends MCPServerBase {
|
|
|
1849
2075
|
async startHonoSSE({ url, ssePath, messagePath, context }) {
|
|
1850
2076
|
try {
|
|
1851
2077
|
if (url.pathname === ssePath) {
|
|
1852
|
-
return streamSSE(context, async (
|
|
2078
|
+
return streamSSE(context, async (stream2) => {
|
|
1853
2079
|
await this.connectHonoSSE({
|
|
1854
2080
|
messagePath,
|
|
1855
|
-
stream
|
|
2081
|
+
stream: stream2
|
|
1856
2082
|
});
|
|
1857
2083
|
});
|
|
1858
2084
|
} else if (url.pathname === messagePath) {
|
|
@@ -2078,13 +2304,13 @@ var MCPServer = class extends MCPServerBase {
|
|
|
2078
2304
|
throw mastraError;
|
|
2079
2305
|
}
|
|
2080
2306
|
}
|
|
2081
|
-
async connectHonoSSE({ messagePath, stream }) {
|
|
2307
|
+
async connectHonoSSE({ messagePath, stream: stream2 }) {
|
|
2082
2308
|
this.logger.debug("Received SSE connection");
|
|
2083
|
-
const sseTransport = new SSETransport(messagePath,
|
|
2309
|
+
const sseTransport = new SSETransport(messagePath, stream2);
|
|
2084
2310
|
const sessionId = sseTransport.sessionId;
|
|
2085
2311
|
this.logger.debug("SSE Transport created with sessionId:", { sessionId });
|
|
2086
2312
|
this.sseHonoTransports.set(sessionId, sseTransport);
|
|
2087
|
-
|
|
2313
|
+
stream2.onAbort(() => {
|
|
2088
2314
|
this.logger.debug("SSE Transport aborted with sessionId:", { sessionId });
|
|
2089
2315
|
this.sseHonoTransports.delete(sessionId);
|
|
2090
2316
|
});
|
|
@@ -2098,8 +2324,8 @@ var MCPServer = class extends MCPServerBase {
|
|
|
2098
2324
|
while (true) {
|
|
2099
2325
|
const sessionIds = Array.from(this.sseHonoTransports.keys() || []);
|
|
2100
2326
|
this.logger.debug("Active Hono SSE sessions:", { sessionIds });
|
|
2101
|
-
await
|
|
2102
|
-
await
|
|
2327
|
+
await stream2.write(":keep-alive\n\n");
|
|
2328
|
+
await stream2.sleep(6e4);
|
|
2103
2329
|
}
|
|
2104
2330
|
} catch (e) {
|
|
2105
2331
|
const mastraError = new MastraError(
|
|
@@ -2208,6 +2434,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
2208
2434
|
name: tool.name,
|
|
2209
2435
|
description: tool.description,
|
|
2210
2436
|
inputSchema: tool.parameters?.jsonSchema || tool.parameters,
|
|
2437
|
+
outputSchema: tool.outputSchema?.jsonSchema || tool.outputSchema,
|
|
2211
2438
|
toolType: tool.toolType
|
|
2212
2439
|
}))
|
|
2213
2440
|
};
|
|
@@ -2228,6 +2455,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
2228
2455
|
name: tool.name,
|
|
2229
2456
|
description: tool.description,
|
|
2230
2457
|
inputSchema: tool.parameters?.jsonSchema || tool.parameters,
|
|
2458
|
+
outputSchema: tool.outputSchema?.jsonSchema || tool.outputSchema,
|
|
2231
2459
|
toolType: tool.toolType
|
|
2232
2460
|
};
|
|
2233
2461
|
}
|
|
@@ -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.4_@types+debug@4.1.12_@types+node@20.19.1_@vitest+ui@3.2.3_jiti@2.4.2_jsdom@_9ed2428c73777463a3d92e2c0ddae976/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@20.19.1_@vitest+ui@3.2.3_jiti@2.4.2_jsdom@_9ed2428c73777463a3d92e2c0ddae976/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.4_@types+debug@4.1.12_@types+node@20.19.1_@vitest+ui@3.2.3_jiti@2.4.2_jsdom@_9ed2428c73777463a3d92e2c0ddae976/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@20.19.1_@vitest+ui@3.2.3_jiti@2.4.2_jsdom@_9ed2428c73777463a3d92e2c0ddae976/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" "$@"
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"@mastra/client-js": "workspace:*",
|
|
13
13
|
"@mastra/mcp": "workspace:*",
|
|
14
14
|
"dotenv": "^16.5.0",
|
|
15
|
-
"zod": "^3.25.
|
|
15
|
+
"zod": "^3.25.67"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@mastra/core": "workspace:*",
|
|
19
|
-
"@testing-library/react": "^16.
|
|
19
|
+
"@testing-library/react": "^16.3.0",
|
|
20
20
|
"@types/node": "^20.17.57",
|
|
21
21
|
"get-port": "^7.1.0",
|
|
22
22
|
"mastra": "workspace:*",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mcp",
|
|
3
|
-
"version": "0.10.5-alpha.
|
|
3
|
+
"version": "0.10.5-alpha.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
"author": "",
|
|
23
23
|
"license": "Elastic-2.0",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
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.11",
|
|
30
29
|
"uuid": "^11.1.0",
|
|
31
30
|
"zod-from-json-schema": "^0.0.5"
|
|
32
31
|
},
|
|
@@ -42,16 +41,17 @@
|
|
|
42
41
|
"@microsoft/api-extractor": "^7.52.8",
|
|
43
42
|
"@types/node": "^20.19.0",
|
|
44
43
|
"ai": "4.3.16",
|
|
45
|
-
"eslint": "^9.
|
|
46
|
-
"hono-mcp-server-sse-transport": "0.0.
|
|
44
|
+
"eslint": "^9.29.0",
|
|
45
|
+
"hono-mcp-server-sse-transport": "0.0.7",
|
|
46
|
+
"hono": "^4.7.11",
|
|
47
47
|
"tsup": "^8.5.0",
|
|
48
48
|
"tsx": "^4.19.4",
|
|
49
49
|
"typescript": "^5.8.3",
|
|
50
50
|
"vitest": "^3.2.3",
|
|
51
|
-
"zod": "^3.25.
|
|
51
|
+
"zod": "^3.25.67",
|
|
52
52
|
"zod-to-json-schema": "^3.24.5",
|
|
53
53
|
"@internal/lint": "0.0.13",
|
|
54
|
-
"@mastra/core": "0.10.7-alpha.
|
|
54
|
+
"@mastra/core": "0.10.7-alpha.2"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
@@ -26,15 +26,6 @@ export const weatherTool = createTool({
|
|
|
26
26
|
inputSchema: z.object({
|
|
27
27
|
location: z.string().describe('City name'),
|
|
28
28
|
}),
|
|
29
|
-
outputSchema: z.object({
|
|
30
|
-
temperature: z.number(),
|
|
31
|
-
feelsLike: z.number(),
|
|
32
|
-
humidity: z.number(),
|
|
33
|
-
windSpeed: z.number(),
|
|
34
|
-
windGust: z.number(),
|
|
35
|
-
conditions: z.string(),
|
|
36
|
-
location: z.string(),
|
|
37
|
-
}),
|
|
38
29
|
execute: async ({ context }) => {
|
|
39
30
|
console.log('weather tool', context);
|
|
40
31
|
return await getWeather(context.location);
|
package/src/client/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MastraBase } from '@mastra/core/base';
|
|
2
2
|
|
|
3
3
|
import type { RuntimeContext } from '@mastra/core/di';
|
|
4
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
4
5
|
import { createTool } from '@mastra/core/tools';
|
|
5
6
|
import { isZodType } from '@mastra/core/utils';
|
|
6
7
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -475,7 +476,48 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
475
476
|
originalJsonSchema: inputSchema,
|
|
476
477
|
});
|
|
477
478
|
|
|
478
|
-
throw new
|
|
479
|
+
throw new MastraError({
|
|
480
|
+
id: 'MCP_TOOL_INPUT_SCHEMA_CONVERSION_FAILED',
|
|
481
|
+
domain: ErrorDomain.MCP,
|
|
482
|
+
category: ErrorCategory.USER,
|
|
483
|
+
details: { error: errorDetails ?? 'Unknown error' },
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private convertOutputSchema(
|
|
489
|
+
outputSchema: Awaited<ReturnType<Client['listTools']>>['tools'][0]['outputSchema'] | JSONSchema,
|
|
490
|
+
): z.ZodType | undefined {
|
|
491
|
+
if (!outputSchema) return
|
|
492
|
+
if (isZodType(outputSchema)) {
|
|
493
|
+
return outputSchema;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
return convertJsonSchemaToZod(outputSchema as JSONSchema);
|
|
498
|
+
} catch (error: unknown) {
|
|
499
|
+
let errorDetails: string | undefined;
|
|
500
|
+
if (error instanceof Error) {
|
|
501
|
+
errorDetails = error.stack;
|
|
502
|
+
} else {
|
|
503
|
+
// Attempt to stringify, fallback to String()
|
|
504
|
+
try {
|
|
505
|
+
errorDetails = JSON.stringify(error);
|
|
506
|
+
} catch {
|
|
507
|
+
errorDetails = String(error);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
this.log('error', 'Failed to convert JSON schema to Zod schema using zodFromJsonSchema', {
|
|
511
|
+
error: errorDetails,
|
|
512
|
+
originalJsonSchema: outputSchema,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
throw new MastraError({
|
|
516
|
+
id: 'MCP_TOOL_OUTPUT_SCHEMA_CONVERSION_FAILED',
|
|
517
|
+
domain: ErrorDomain.MCP,
|
|
518
|
+
category: ErrorCategory.USER,
|
|
519
|
+
details: { error: errorDetails ?? 'Unknown error' },
|
|
520
|
+
});
|
|
479
521
|
}
|
|
480
522
|
}
|
|
481
523
|
|
|
@@ -490,6 +532,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
490
532
|
id: `${this.name}_${tool.name}`,
|
|
491
533
|
description: tool.description || '',
|
|
492
534
|
inputSchema: this.convertInputSchema(tool.inputSchema),
|
|
535
|
+
outputSchema: this.convertOutputSchema(tool.outputSchema),
|
|
493
536
|
execute: async ({ context, runtimeContext }: { context: any; runtimeContext?: RuntimeContext | null }) => {
|
|
494
537
|
const previousContext = this.currentOperationContext;
|
|
495
538
|
this.currentOperationContext = runtimeContext || null; // Set current context
|
|
@@ -505,6 +548,7 @@ export class InternalMastraMCPClient extends MastraBase {
|
|
|
505
548
|
timeout: this.timeout,
|
|
506
549
|
},
|
|
507
550
|
);
|
|
551
|
+
|
|
508
552
|
this.log('debug', `Tool executed successfully: ${tool.name}`);
|
|
509
553
|
return res;
|
|
510
554
|
} catch (e) {
|
|
@@ -1347,3 +1347,87 @@ describe('MCPServer - Workflow to Tool Conversion', () => {
|
|
|
1347
1347
|
expect(server.tools()['run_unique_workflow_key_789']).toBeDefined();
|
|
1348
1348
|
});
|
|
1349
1349
|
});
|
|
1350
|
+
|
|
1351
|
+
describe('MCPServer with Tool Output Schema', () => {
|
|
1352
|
+
let serverWithOutputSchema: MCPServer;
|
|
1353
|
+
let clientWithOutputSchema: MCPClient;
|
|
1354
|
+
const PORT = 9600 + Math.floor(Math.random() * 1000);
|
|
1355
|
+
let httpServerWithOutputSchema: http.Server;
|
|
1356
|
+
|
|
1357
|
+
const structuredTool: ToolsInput = {
|
|
1358
|
+
structuredTool: {
|
|
1359
|
+
description: 'A test tool with structured output',
|
|
1360
|
+
parameters: z.object({ input: z.string() }),
|
|
1361
|
+
outputSchema: z.object({
|
|
1362
|
+
processedInput: z.string(),
|
|
1363
|
+
timestamp: z.string(),
|
|
1364
|
+
}),
|
|
1365
|
+
execute: async ({ input }: { input: string }) => ({
|
|
1366
|
+
structuredContent: {
|
|
1367
|
+
processedInput: `processed: ${input}`,
|
|
1368
|
+
timestamp: mockDateISO,
|
|
1369
|
+
},
|
|
1370
|
+
}),
|
|
1371
|
+
},
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
beforeAll(async () => {
|
|
1375
|
+
serverWithOutputSchema = new MCPServer({
|
|
1376
|
+
name: 'Test MCP Server with OutputSchema',
|
|
1377
|
+
version: '0.1.0',
|
|
1378
|
+
tools: structuredTool,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
httpServerWithOutputSchema = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
1382
|
+
const url = new URL(req.url || '', `http://localhost:${PORT}`);
|
|
1383
|
+
await serverWithOutputSchema.startHTTP({
|
|
1384
|
+
url,
|
|
1385
|
+
httpPath: '/http',
|
|
1386
|
+
req,
|
|
1387
|
+
res,
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
await new Promise<void>(resolve => httpServerWithOutputSchema.listen(PORT, () => resolve()));
|
|
1392
|
+
|
|
1393
|
+
clientWithOutputSchema = new MCPClient({
|
|
1394
|
+
servers: {
|
|
1395
|
+
local: {
|
|
1396
|
+
url: new URL(`http://localhost:${PORT}/http`),
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
afterAll(async () => {
|
|
1403
|
+
httpServerWithOutputSchema.closeAllConnections?.();
|
|
1404
|
+
await new Promise<void>(resolve =>
|
|
1405
|
+
httpServerWithOutputSchema.close(() => {
|
|
1406
|
+
resolve();
|
|
1407
|
+
}),
|
|
1408
|
+
);
|
|
1409
|
+
await serverWithOutputSchema.close();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
it('should list tool with outputSchema', async () => {
|
|
1413
|
+
const tools = await clientWithOutputSchema.getTools();
|
|
1414
|
+
const tool = tools['local_structuredTool'];
|
|
1415
|
+
expect(tool).toBeDefined();
|
|
1416
|
+
expect(tool.outputSchema).toBeDefined();
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('should call tool and receive structuredContent', async () => {
|
|
1420
|
+
const tools = await clientWithOutputSchema.getTools();
|
|
1421
|
+
const tool = tools['local_structuredTool'];
|
|
1422
|
+
const result = await tool.execute({ context: { input: 'hello' } });
|
|
1423
|
+
|
|
1424
|
+
expect(result).toBeDefined();
|
|
1425
|
+
expect(result.structuredContent).toBeDefined();
|
|
1426
|
+
expect(result.structuredContent.processedInput).toBe('processed: hello');
|
|
1427
|
+
expect(result.structuredContent.timestamp).toBe(mockDateISO);
|
|
1428
|
+
|
|
1429
|
+
expect(result.content).toBeDefined();
|
|
1430
|
+
expect(result.content[0].type).toBe('text');
|
|
1431
|
+
expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent);
|
|
1432
|
+
});
|
|
1433
|
+
});
|