@mastra/mcp 2.8.0-alpha.0 → 2.8.0-alpha.2
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/client/client.d.ts +6 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/tasks/suspend.d.ts +25 -0
- package/dist/client/tasks/suspend.d.ts.map +1 -0
- package/dist/client/tasks/wait.d.ts +14 -0
- package/dist/client/tasks/wait.d.ts.map +1 -1
- package/dist/client/types.d.ts +30 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/reference-tools-mcp-client.md +45 -1
- package/dist/docs/references/reference-tools-mcp-server.md +67 -0
- package/dist/index.cjs +325 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +325 -18
- package/dist/index.js.map +1 -1
- package/dist/server/model-output-to-mcp.d.ts +14 -0
- package/dist/server/model-output-to-mcp.d.ts.map +1 -0
- package/dist/server/server.d.ts +14 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/types.d.ts +31 -1
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -15397,6 +15397,57 @@ var TaskRequester = class {
|
|
|
15397
15397
|
};
|
|
15398
15398
|
}
|
|
15399
15399
|
};
|
|
15400
|
+
// src/client/tasks/suspend.ts
|
|
15401
|
+
const MCP_INPUT_REQUEST_SUSPEND_TYPE = "mcp-input-request";
|
|
15402
|
+
const SuspendPayloadSchema = z.object({
|
|
15403
|
+
type: z.literal(MCP_INPUT_REQUEST_SUSPEND_TYPE),
|
|
15404
|
+
serverName: z.string(),
|
|
15405
|
+
toolName: z.string(),
|
|
15406
|
+
taskId: z.string().min(1),
|
|
15407
|
+
createdAt: z.string(),
|
|
15408
|
+
requestedAt: z.string().optional(),
|
|
15409
|
+
requests: TaskInputRequestsSchema
|
|
15410
|
+
});
|
|
15411
|
+
const ResumeDataSchema = z.object({ inputResponses: z.record(z.string(), z.object({
|
|
15412
|
+
action: z.enum([
|
|
15413
|
+
"accept",
|
|
15414
|
+
"decline",
|
|
15415
|
+
"cancel"
|
|
15416
|
+
]),
|
|
15417
|
+
content: z.record(z.string(), z.unknown()).optional()
|
|
15418
|
+
})) });
|
|
15419
|
+
/**
|
|
15420
|
+
* Reads a resume of a tool call that suspended on MCP task input. Returns `undefined` when the call
|
|
15421
|
+
* didn't suspend that way, so other resumes (tool approval, for one) take the normal path.
|
|
15422
|
+
*/
|
|
15423
|
+
function readInputRequestResume(host, expected) {
|
|
15424
|
+
const raw = host?.suspendPayload;
|
|
15425
|
+
if (!raw || typeof raw !== "object" || raw.type !== "mcp-input-request") return;
|
|
15426
|
+
const payload = SuspendPayloadSchema.safeParse(raw);
|
|
15427
|
+
if (!payload.success) return {
|
|
15428
|
+
ok: false,
|
|
15429
|
+
reason: `the suspended MCP input request is malformed`
|
|
15430
|
+
};
|
|
15431
|
+
if (payload.data.serverName !== expected.serverName || payload.data.toolName !== expected.toolName) return {
|
|
15432
|
+
ok: false,
|
|
15433
|
+
reason: `the suspended MCP input request belongs to another tool`
|
|
15434
|
+
};
|
|
15435
|
+
const resume = ResumeDataSchema.safeParse(host?.resumeData);
|
|
15436
|
+
if (!resume.success) return {
|
|
15437
|
+
ok: false,
|
|
15438
|
+
reason: `resume data must be { inputResponses: { [key]: { action: 'accept' | 'decline' | 'cancel', content? } } }`
|
|
15439
|
+
};
|
|
15440
|
+
const missing = Object.keys(payload.data.requests).filter((key) => !Object.hasOwn(resume.data.inputResponses, key));
|
|
15441
|
+
if (missing.length > 0) return {
|
|
15442
|
+
ok: false,
|
|
15443
|
+
reason: `resume data has no answer for ${missing.join(", ")}`
|
|
15444
|
+
};
|
|
15445
|
+
return {
|
|
15446
|
+
ok: true,
|
|
15447
|
+
payload: payload.data,
|
|
15448
|
+
inputResponses: resume.data.inputResponses
|
|
15449
|
+
};
|
|
15450
|
+
}
|
|
15400
15451
|
// src/client/tasks/wait.ts
|
|
15401
15452
|
/** Why a task wait stopped without a terminal task. */
|
|
15402
15453
|
var TaskWaitError = class extends Error {
|
|
@@ -15425,12 +15476,6 @@ const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
15425
15476
|
};
|
|
15426
15477
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15427
15478
|
});
|
|
15428
|
-
/**
|
|
15429
|
-
* Polls a remote task until it reaches a terminal status, answering its input requests on the way.
|
|
15430
|
-
*
|
|
15431
|
-
* Aborting the signal or exceeding `maxWaitMs` sends `tasks/cancel` (fire-and-forget) and rejects.
|
|
15432
|
-
* Each input request key is answered at most once, however many polls list it.
|
|
15433
|
-
*/
|
|
15434
15479
|
async function waitForTask(port, created, options = {}) {
|
|
15435
15480
|
const sleep = options.sleep ?? defaultSleep;
|
|
15436
15481
|
const now = options.now ?? Date.now;
|
|
@@ -15481,7 +15526,21 @@ async function waitForTask(port, created, options = {}) {
|
|
|
15481
15526
|
const { inputRequests } = task;
|
|
15482
15527
|
const pending = Object.entries(inputRequests).filter(([key]) => !answered.has(key));
|
|
15483
15528
|
if (pending.length > 0) {
|
|
15484
|
-
const
|
|
15529
|
+
const supplied = options.inputResponses ?? {};
|
|
15530
|
+
const unanswered = pending.filter(([key]) => !Object.hasOwn(supplied, key));
|
|
15531
|
+
const responses = {};
|
|
15532
|
+
for (const [key] of pending) if (Object.hasOwn(supplied, key)) responses[key] = supplied[key];
|
|
15533
|
+
if (unanswered.length > 0 && !options.inputRequests && options.stopAtInput) {
|
|
15534
|
+
if (Object.keys(responses).length > 0) {
|
|
15535
|
+
for (const key of Object.keys(responses)) answered.add(key);
|
|
15536
|
+
await bounded(port.update(task.taskId, responses, { signal: stop.signal }));
|
|
15537
|
+
}
|
|
15538
|
+
return {
|
|
15539
|
+
...task,
|
|
15540
|
+
inputRequests: Object.fromEntries(unanswered)
|
|
15541
|
+
};
|
|
15542
|
+
}
|
|
15543
|
+
if (unanswered.length > 0) Object.assign(responses, await bounded(answerInputRequests(unanswered, task, options.inputRequests, stop.signal)));
|
|
15485
15544
|
for (const key of Object.keys(responses)) answered.add(key);
|
|
15486
15545
|
await bounded(port.update(task.taskId, responses, { signal: stop.signal }));
|
|
15487
15546
|
task = await poll();
|
|
@@ -17001,7 +17060,43 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
17001
17060
|
if (serverMeta.connectFirst) await this.connect();
|
|
17002
17061
|
const operationContext = context?.requestContext ?? null;
|
|
17003
17062
|
return this.operationContextStore.run(operationContext, async () => {
|
|
17063
|
+
const suspendHost = context?.agent ?? context?.workflow;
|
|
17064
|
+
const toolCallSpan = context?.tracingContext?.currentSpan;
|
|
17004
17065
|
const executeToolCall = async () => {
|
|
17066
|
+
const resume = this.taskOptions ? readInputRequestResume(suspendHost, {
|
|
17067
|
+
serverName: this.name,
|
|
17068
|
+
toolName: tool.name
|
|
17069
|
+
}) : void 0;
|
|
17070
|
+
if (resume) {
|
|
17071
|
+
if (!resume.ok) throw new MastraError({
|
|
17072
|
+
id: "MCP_CLIENT_TOOL_TASK_RESUME_INVALID",
|
|
17073
|
+
domain: ErrorDomain.MCP,
|
|
17074
|
+
category: ErrorCategory.USER,
|
|
17075
|
+
text: `Can't resume MCP tool ${tool.name}: ${resume.reason}`,
|
|
17076
|
+
details: {
|
|
17077
|
+
toolName: tool.name,
|
|
17078
|
+
serverName: this.name
|
|
17079
|
+
}
|
|
17080
|
+
});
|
|
17081
|
+
const { payload, inputResponses } = resume;
|
|
17082
|
+
this.log("debug", `Resuming tool ${tool.name} on MCP task ${payload.taskId}`);
|
|
17083
|
+
toolCallSpan?.update({ metadata: { mcpTaskKey: taskKeyOf(payload.taskId) } });
|
|
17084
|
+
const waiting = {
|
|
17085
|
+
taskId: payload.taskId,
|
|
17086
|
+
status: "input_required",
|
|
17087
|
+
createdAt: payload.createdAt,
|
|
17088
|
+
lastUpdatedAt: payload.createdAt,
|
|
17089
|
+
ttlMs: null,
|
|
17090
|
+
inputRequests: payload.requests
|
|
17091
|
+
};
|
|
17092
|
+
const res = await this.awaitToolTask(tool.name, waiting, {
|
|
17093
|
+
signal: context?.abortSignal,
|
|
17094
|
+
writer: context?.writer,
|
|
17095
|
+
suspend: suspendHost?.suspend,
|
|
17096
|
+
inputResponses
|
|
17097
|
+
});
|
|
17098
|
+
return res && finishToolCall(res);
|
|
17099
|
+
}
|
|
17005
17100
|
this.log("debug", `Executing tool: ${tool.name}`, {
|
|
17006
17101
|
toolArgs: input,
|
|
17007
17102
|
runId: context?.runId
|
|
@@ -17019,13 +17114,17 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
17019
17114
|
timeout: this.timeout,
|
|
17020
17115
|
signal: context?.abortSignal
|
|
17021
17116
|
}));
|
|
17022
|
-
const toolCallSpan = context?.tracingContext?.currentSpan;
|
|
17023
17117
|
if (toolCallSpan && serverSpan) toolCallSpan.links = [...toolCallSpan.links ?? [], serverSpan];
|
|
17024
17118
|
const task = this.taskOptions ? takeTaskHandle(res) : void 0;
|
|
17025
17119
|
if (!task) return finishToolCall(res);
|
|
17026
17120
|
this.log("debug", `Tool ${tool.name} continues as MCP task ${task.taskId}`);
|
|
17027
17121
|
toolCallSpan?.update({ metadata: { mcpTaskKey: taskKeyOf(task.taskId) } });
|
|
17028
|
-
|
|
17122
|
+
const taskResult = await this.awaitToolTask(tool.name, task, {
|
|
17123
|
+
signal: context?.abortSignal,
|
|
17124
|
+
writer: context?.writer,
|
|
17125
|
+
suspend: suspendHost?.suspend
|
|
17126
|
+
});
|
|
17127
|
+
return taskResult && finishToolCall(taskResult);
|
|
17029
17128
|
};
|
|
17030
17129
|
const finishToolCall = async (res) => {
|
|
17031
17130
|
if (res.isError && this.onToolError === "throw") {
|
|
@@ -17112,8 +17211,13 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
17112
17211
|
maxRetries: 0
|
|
17113
17212
|
};
|
|
17114
17213
|
}
|
|
17115
|
-
/**
|
|
17116
|
-
|
|
17214
|
+
/**
|
|
17215
|
+
* Waits for a remote task created by a tool call and returns its `CallToolResult`.
|
|
17216
|
+
*
|
|
17217
|
+
* When the task needs input that no `inputRequests` handler answers and the caller can `suspend`,
|
|
17218
|
+
* suspends with the requests and returns `undefined`, leaving the task open for the answers.
|
|
17219
|
+
*/
|
|
17220
|
+
async awaitToolTask(toolName, task, { signal, writer, suspend, inputResponses }) {
|
|
17117
17221
|
const details = {
|
|
17118
17222
|
toolName,
|
|
17119
17223
|
serverName: this.name,
|
|
@@ -17152,6 +17256,8 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
17152
17256
|
}, task, {
|
|
17153
17257
|
signal,
|
|
17154
17258
|
inputRequests: this.serverConfig.inputRequests,
|
|
17259
|
+
stopAtInput: !this.serverConfig.inputRequests && suspend !== void 0,
|
|
17260
|
+
inputResponses,
|
|
17155
17261
|
pollIntervalMs: this.taskOptions?.pollIntervalMs,
|
|
17156
17262
|
maxWaitMs: this.taskOptions?.maxWaitMs,
|
|
17157
17263
|
onStatus
|
|
@@ -17163,6 +17269,17 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
17163
17269
|
throw fail("MCP_CLIENT_TOOL_TASK_POLL_FAILED", `Polling MCP task ${task.taskId} failed: ${reason}`, error);
|
|
17164
17270
|
}
|
|
17165
17271
|
switch (terminal.status) {
|
|
17272
|
+
case "input_required":
|
|
17273
|
+
await suspend({
|
|
17274
|
+
type: MCP_INPUT_REQUEST_SUSPEND_TYPE,
|
|
17275
|
+
serverName: this.name,
|
|
17276
|
+
toolName,
|
|
17277
|
+
taskId: terminal.taskId,
|
|
17278
|
+
createdAt: terminal.createdAt,
|
|
17279
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17280
|
+
requests: terminal.inputRequests
|
|
17281
|
+
});
|
|
17282
|
+
return;
|
|
17166
17283
|
case "completed": return terminal.result;
|
|
17167
17284
|
case "cancelled": throw fail("MCP_CLIENT_TOOL_TASK_CANCELLED", terminal.statusMessage ?? `MCP task ${task.taskId} was cancelled`);
|
|
17168
17285
|
case "failed": throw fail("MCP_CLIENT_TOOL_TASK_FAILED", (typeof terminal.error.message === "string" ? terminal.error.message : void 0) ?? terminal.statusMessage ?? `MCP task ${task.taskId} failed`);
|
|
@@ -20905,6 +21022,161 @@ function echoes(body, challenge) {
|
|
|
20905
21022
|
const b = Buffer.from(challenge);
|
|
20906
21023
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
20907
21024
|
}
|
|
21025
|
+
// src/server/model-output-to-mcp.ts
|
|
21026
|
+
const DATA_URI = /^data:([^;,]*)((?:;[^;,]*)*),(.*)$/is;
|
|
21027
|
+
/**
|
|
21028
|
+
* Maps the value a Mastra tool's `toModelOutput` returns (an AI SDK tool-result output such as
|
|
21029
|
+
* `{ type: 'text' }`, `{ type: 'json' }` or `{ type: 'content', value: [...] }`) to MCP content blocks.
|
|
21030
|
+
*
|
|
21031
|
+
* Returns `undefined` for any other shape, meaning the caller keeps its default result.
|
|
21032
|
+
*/
|
|
21033
|
+
function modelOutputToMcpContent(value, toolName) {
|
|
21034
|
+
if (!isRecord(value)) return void 0;
|
|
21035
|
+
switch (value.type) {
|
|
21036
|
+
case "text": return typeof value.value === "string" ? success([{
|
|
21037
|
+
type: "text",
|
|
21038
|
+
text: value.value
|
|
21039
|
+
}]) : void 0;
|
|
21040
|
+
case "json": return success([{
|
|
21041
|
+
type: "text",
|
|
21042
|
+
text: stringify(value.value)
|
|
21043
|
+
}]);
|
|
21044
|
+
case "error-text": return typeof value.value === "string" ? {
|
|
21045
|
+
isError: true,
|
|
21046
|
+
content: [{
|
|
21047
|
+
type: "text",
|
|
21048
|
+
text: value.value
|
|
21049
|
+
}]
|
|
21050
|
+
} : void 0;
|
|
21051
|
+
case "error-json": return {
|
|
21052
|
+
isError: true,
|
|
21053
|
+
content: [{
|
|
21054
|
+
type: "text",
|
|
21055
|
+
text: stringify(value.value)
|
|
21056
|
+
}]
|
|
21057
|
+
};
|
|
21058
|
+
case "content": return Array.isArray(value.value) ? success(value.value.map((part, index) => partToBlock(part, toolName, index))) : void 0;
|
|
21059
|
+
default: return;
|
|
21060
|
+
}
|
|
21061
|
+
}
|
|
21062
|
+
function partToBlock(part, toolName, index) {
|
|
21063
|
+
if (!isRecord(part)) return {
|
|
21064
|
+
type: "text",
|
|
21065
|
+
text: stringify(part)
|
|
21066
|
+
};
|
|
21067
|
+
switch (part.type) {
|
|
21068
|
+
case "text":
|
|
21069
|
+
if (typeof part.text === "string") return {
|
|
21070
|
+
type: "text",
|
|
21071
|
+
text: part.text
|
|
21072
|
+
};
|
|
21073
|
+
break;
|
|
21074
|
+
case "media":
|
|
21075
|
+
case "image-data":
|
|
21076
|
+
case "file-data": {
|
|
21077
|
+
const fallback = part.type === "image-data" ? "image/jpeg" : "application/octet-stream";
|
|
21078
|
+
const mediaType = typeof part.mediaType === "string" && part.mediaType ? part.mediaType : void 0;
|
|
21079
|
+
if (typeof part.data === "string") {
|
|
21080
|
+
const uri = parseDataUri(part.data);
|
|
21081
|
+
if (uri) return mediaBlock(uri.data, mediaType ?? (uri.mediaType || fallback), toolName, index);
|
|
21082
|
+
if (/^data:/i.test(part.data)) break;
|
|
21083
|
+
return mediaBlock(part.data, mediaType ?? fallback, toolName, index);
|
|
21084
|
+
}
|
|
21085
|
+
if (part.data instanceof Uint8Array) return mediaBlock(Buffer.from(part.data).toString("base64"), mediaType ?? fallback, toolName, index);
|
|
21086
|
+
break;
|
|
21087
|
+
}
|
|
21088
|
+
case "image-url":
|
|
21089
|
+
case "file-url": {
|
|
21090
|
+
if (typeof part.url !== "string") break;
|
|
21091
|
+
const uri = parseDataUri(part.url);
|
|
21092
|
+
if (!uri) return {
|
|
21093
|
+
type: "text",
|
|
21094
|
+
text: part.url
|
|
21095
|
+
};
|
|
21096
|
+
const fallback = part.type === "image-url" ? "image/jpeg" : "application/octet-stream";
|
|
21097
|
+
const mediaType = typeof part.mediaType === "string" && part.mediaType ? part.mediaType : void 0;
|
|
21098
|
+
return mediaBlock(uri.data, mediaType ?? (uri.mediaType || fallback), toolName, index);
|
|
21099
|
+
}
|
|
21100
|
+
}
|
|
21101
|
+
return {
|
|
21102
|
+
type: "text",
|
|
21103
|
+
text: stringify(part)
|
|
21104
|
+
};
|
|
21105
|
+
}
|
|
21106
|
+
function mediaBlock(data, mediaType, toolName, index) {
|
|
21107
|
+
const separator = mediaType.indexOf(";");
|
|
21108
|
+
const mimeType = separator === -1 ? mediaType.toLowerCase() : mediaType.slice(0, separator).toLowerCase() + mediaType.slice(separator);
|
|
21109
|
+
if (mimeType.startsWith("image/")) return {
|
|
21110
|
+
type: "image",
|
|
21111
|
+
data,
|
|
21112
|
+
mimeType
|
|
21113
|
+
};
|
|
21114
|
+
if (mimeType.startsWith("audio/")) return {
|
|
21115
|
+
type: "audio",
|
|
21116
|
+
data,
|
|
21117
|
+
mimeType
|
|
21118
|
+
};
|
|
21119
|
+
return {
|
|
21120
|
+
type: "resource",
|
|
21121
|
+
resource: {
|
|
21122
|
+
uri: `mastra://tool-output/${encodeURIComponent(toolName)}/${index}`,
|
|
21123
|
+
mimeType,
|
|
21124
|
+
blob: data
|
|
21125
|
+
}
|
|
21126
|
+
};
|
|
21127
|
+
}
|
|
21128
|
+
/**
|
|
21129
|
+
* Splits a `data:` URI into its media type (with parameters such as `charset`, minus the `base64`
|
|
21130
|
+
* marker) and Base64 payload. Returns `undefined` if it can't be decoded.
|
|
21131
|
+
*/
|
|
21132
|
+
function parseDataUri(value) {
|
|
21133
|
+
const match = DATA_URI.exec(value);
|
|
21134
|
+
if (!match) return void 0;
|
|
21135
|
+
const [, type = "", params = "", payload = ""] = match;
|
|
21136
|
+
const parameters = params.split(";").map((param) => param.trim()).filter(Boolean);
|
|
21137
|
+
const base64 = parameters.some((param) => param.toLowerCase() === "base64");
|
|
21138
|
+
let data = payload;
|
|
21139
|
+
if (!base64) {
|
|
21140
|
+
const bytes = decodePercentEncoded(payload);
|
|
21141
|
+
if (!bytes) return void 0;
|
|
21142
|
+
data = bytes.toString("base64");
|
|
21143
|
+
}
|
|
21144
|
+
const mediaType = type.trim().toLowerCase();
|
|
21145
|
+
const kept = parameters.filter((param) => param.toLowerCase() !== "base64");
|
|
21146
|
+
return {
|
|
21147
|
+
mediaType: mediaType && kept.length ? [mediaType, ...kept].join(";") : mediaType,
|
|
21148
|
+
data
|
|
21149
|
+
};
|
|
21150
|
+
}
|
|
21151
|
+
/** Decodes `%XX` escapes as raw octets (RFC 2397), not UTF-8 text. Returns `undefined` for a malformed escape. */
|
|
21152
|
+
function decodePercentEncoded(payload) {
|
|
21153
|
+
const input = Buffer.from(payload, "utf8");
|
|
21154
|
+
const output = Buffer.alloc(input.length);
|
|
21155
|
+
let length = 0;
|
|
21156
|
+
for (let index = 0; index < input.length; index++) {
|
|
21157
|
+
if (input[index] !== 37) {
|
|
21158
|
+
output[length++] = input[index];
|
|
21159
|
+
continue;
|
|
21160
|
+
}
|
|
21161
|
+
const hex = input.toString("latin1", index + 1, index + 3);
|
|
21162
|
+
if (!/^[0-9a-f]{2}$/i.test(hex)) return void 0;
|
|
21163
|
+
output[length++] = Number.parseInt(hex, 16);
|
|
21164
|
+
index += 2;
|
|
21165
|
+
}
|
|
21166
|
+
return output.subarray(0, length);
|
|
21167
|
+
}
|
|
21168
|
+
function success(content) {
|
|
21169
|
+
return {
|
|
21170
|
+
isError: false,
|
|
21171
|
+
content
|
|
21172
|
+
};
|
|
21173
|
+
}
|
|
21174
|
+
function stringify(value) {
|
|
21175
|
+
return JSON.stringify(value) ?? String(value);
|
|
21176
|
+
}
|
|
21177
|
+
function isRecord(value) {
|
|
21178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21179
|
+
}
|
|
20908
21180
|
// src/server/tasks/store.ts
|
|
20909
21181
|
/** How a principal is stored and shown. Principals can carry user and subject ids, so only their hash is kept. */
|
|
20910
21182
|
function principalKeyOf(principal) {
|
|
@@ -22634,6 +22906,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
22634
22906
|
tasksInspector;
|
|
22635
22907
|
webhooks;
|
|
22636
22908
|
legacy;
|
|
22909
|
+
toolResultOptions;
|
|
22637
22910
|
tasks;
|
|
22638
22911
|
/** Where tasks are kept: `tasks.store`, or the store picked on first use. */
|
|
22639
22912
|
taskStore;
|
|
@@ -22657,6 +22930,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
22657
22930
|
this.requestStateCodec = this.createRequestStateCodec(config.requestState);
|
|
22658
22931
|
this.eventsOptions = config.eventsOptions ?? {};
|
|
22659
22932
|
this.legacy = config.legacy ?? "reject";
|
|
22933
|
+
this.toolResultOptions = config.toolResult ?? {};
|
|
22660
22934
|
this.tasks = resolveTasksConfig(config.tasks, {
|
|
22661
22935
|
tools: Object.keys(config.tools ?? {}),
|
|
22662
22936
|
workflows: Object.keys(config.workflows ?? {}),
|
|
@@ -23219,10 +23493,10 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23219
23493
|
});
|
|
23220
23494
|
this.setTracedHandler(server, "tools/call", async (request, ctx, trace) => {
|
|
23221
23495
|
const name = request.params.name;
|
|
23222
|
-
const tool = this.convertedTools[name];
|
|
23496
|
+
const tool = Object.hasOwn(this.convertedTools, name) ? this.convertedTools[name] : void 0;
|
|
23223
23497
|
if (!tool) {
|
|
23224
23498
|
this.logger.warn("Unknown tool requested", { tool: name });
|
|
23225
|
-
|
|
23499
|
+
throw new ProtocolError$1(ProtocolErrorCode$1.InvalidParams, `Unknown tool: ${name}`);
|
|
23226
23500
|
}
|
|
23227
23501
|
const args = request.params.arguments ?? {};
|
|
23228
23502
|
const argsHash = hashArguments(args);
|
|
@@ -23277,7 +23551,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23277
23551
|
resumeSchema: execution.resumeSchema
|
|
23278
23552
|
});
|
|
23279
23553
|
}
|
|
23280
|
-
const result = server.projectCallToolResult(this.
|
|
23554
|
+
const result = server.projectCallToolResult(await this.completedToolResult(name, tool, execution.output, requestContext), this.toolInfo(name, tool).outputSchema);
|
|
23281
23555
|
this.logger.info(`Tool '${name}' finished in ${Date.now() - startedAt}ms.`);
|
|
23282
23556
|
return result;
|
|
23283
23557
|
} catch (error) {
|
|
@@ -23344,7 +23618,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23344
23618
|
suspendPayload: options.suspendPayload
|
|
23345
23619
|
},
|
|
23346
23620
|
initialAttempt: () => ({
|
|
23347
|
-
outcome: settled.then((outcome) => "error" in outcome ? this.toolErrorOutcome(name, outcome.error) : this.toTaskOutcome(name, tool, outcome.execution)),
|
|
23621
|
+
outcome: settled.then((outcome) => "error" in outcome ? this.toolErrorOutcome(name, outcome.error) : this.toTaskOutcome(name, tool, outcome.execution, requestContext)),
|
|
23348
23622
|
abort: (reason) => controller.abort(reason)
|
|
23349
23623
|
})
|
|
23350
23624
|
}) };
|
|
@@ -23455,7 +23729,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23455
23729
|
suspendPayload,
|
|
23456
23730
|
tracingContext: span ? { currentSpan: span } : void 0
|
|
23457
23731
|
});
|
|
23458
|
-
return this.toTaskOutcome(name, tool, execution);
|
|
23732
|
+
return await this.toTaskOutcome(name, tool, execution, requestContext);
|
|
23459
23733
|
} catch (error) {
|
|
23460
23734
|
return this.toolErrorOutcome(name, error);
|
|
23461
23735
|
}
|
|
@@ -23539,7 +23813,8 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23539
23813
|
}
|
|
23540
23814
|
};
|
|
23541
23815
|
}
|
|
23542
|
-
|
|
23816
|
+
/** Never rejects: a failure building the result completes the task with the tool error result. */
|
|
23817
|
+
async toTaskOutcome(name, tool, execution, requestContext) {
|
|
23543
23818
|
if (execution.status === "suspended") {
|
|
23544
23819
|
this.logger.debug(`Tool task '${name}' requires client input.`);
|
|
23545
23820
|
return {
|
|
@@ -23549,7 +23824,11 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23549
23824
|
};
|
|
23550
23825
|
}
|
|
23551
23826
|
if (tool.mcp?.toolType === "agent" && execution.output?.finishReason === "suspended") return completed(errorResult$1(`Agent '${name.slice(4)}' paused for tool approval or input, which MCP tasks don't support yet`));
|
|
23552
|
-
|
|
23827
|
+
try {
|
|
23828
|
+
return completed(await this.completedToolResult(name, tool, execution.output, requestContext));
|
|
23829
|
+
} catch (error) {
|
|
23830
|
+
return this.toolErrorOutcome(name, error);
|
|
23831
|
+
}
|
|
23553
23832
|
}
|
|
23554
23833
|
/** A tool that throws inside a task completes it with `isError`, as a synchronous call would answer. */
|
|
23555
23834
|
toolErrorOutcome(name, error) {
|
|
@@ -23864,6 +24143,34 @@ var MCPServer = class extends MCPServerBase {
|
|
|
23864
24143
|
output
|
|
23865
24144
|
};
|
|
23866
24145
|
}
|
|
24146
|
+
/**
|
|
24147
|
+
* The result of a tool call that completed: the default result, built from `toModelOutput` when
|
|
24148
|
+
* `toolResult.modelOutput` is on, then passed through `toolResult.transform`. Throws what
|
|
24149
|
+
* `toModelOutput` or `transform` throw.
|
|
24150
|
+
*/
|
|
24151
|
+
async completedToolResult(name, tool, output, requestContext) {
|
|
24152
|
+
let result = this.toCallToolResult(name, tool, output);
|
|
24153
|
+
const { modelOutput = false, transform } = this.toolResultOptions;
|
|
24154
|
+
if (modelOutput && !result.isError && tool.toModelOutput && output != null) {
|
|
24155
|
+
const mapped = modelOutputToMcpContent(await tool.toModelOutput(output), name);
|
|
24156
|
+
if (mapped?.isError) result = {
|
|
24157
|
+
isError: true,
|
|
24158
|
+
content: mapped.content,
|
|
24159
|
+
...result._meta ? { _meta: result._meta } : {}
|
|
24160
|
+
};
|
|
24161
|
+
else if (mapped) result = {
|
|
24162
|
+
...result,
|
|
24163
|
+
content: result.structuredContent !== void 0 ? [...result.content, ...mapped.content] : mapped.content
|
|
24164
|
+
};
|
|
24165
|
+
}
|
|
24166
|
+
if (transform) result = await transform({
|
|
24167
|
+
toolName: name,
|
|
24168
|
+
output,
|
|
24169
|
+
result,
|
|
24170
|
+
requestContext
|
|
24171
|
+
}) ?? result;
|
|
24172
|
+
return result;
|
|
24173
|
+
}
|
|
23867
24174
|
toCallToolResult(name, tool, value) {
|
|
23868
24175
|
if (isValidationError(value)) {
|
|
23869
24176
|
this.logger.warn(`Tool '${name}' rejected its input.`, { error: value.message });
|