@mastra/mcp 2.6.0 → 2.7.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.
- package/dist/client/client.d.ts +12 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/configuration.d.ts +40 -0
- package/dist/client/configuration.d.ts.map +1 -1
- package/dist/client/error-utils.d.ts +9 -0
- package/dist/client/error-utils.d.ts.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/tasks/intercept.d.ts +22 -0
- package/dist/client/tasks/intercept.d.ts.map +1 -0
- package/dist/client/tasks/options.d.ts +4 -0
- package/dist/client/tasks/options.d.ts.map +1 -0
- package/dist/client/tasks/requester.d.ts +26 -0
- package/dist/client/tasks/requester.d.ts.map +1 -0
- package/dist/client/tasks/wait.d.ts +60 -0
- package/dist/client/tasks/wait.d.ts.map +1 -0
- package/dist/client/types.d.ts +47 -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/docs-auth-fga.md +1 -1
- package/dist/docs/references/reference-tools-mcp-client.md +48 -0
- package/dist/docs/references/reference-tools-mcp-server.md +216 -1
- package/dist/index.cjs +3206 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3204 -112
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/request.d.ts +6 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/server.d.ts +131 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/tasks/background-store.d.ts +107 -0
- package/dist/server/tasks/background-store.d.ts.map +1 -0
- package/dist/server/tasks/config.d.ts +226 -0
- package/dist/server/tasks/config.d.ts.map +1 -0
- package/dist/server/tasks/emit.d.ts +16 -0
- package/dist/server/tasks/emit.d.ts.map +1 -0
- package/dist/server/tasks/handlers.d.ts +33 -0
- package/dist/server/tasks/handlers.d.ts.map +1 -0
- package/dist/server/tasks/in-memory-store.d.ts +71 -0
- package/dist/server/tasks/in-memory-store.d.ts.map +1 -0
- package/dist/server/tasks/index.d.ts +9 -0
- package/dist/server/tasks/index.d.ts.map +1 -0
- package/dist/server/tasks/inspector.d.ts +13 -0
- package/dist/server/tasks/inspector.d.ts.map +1 -0
- package/dist/server/tasks/store.d.ts +260 -0
- package/dist/server/tasks/store.d.ts.map +1 -0
- package/dist/server/tasks/workflow-runs.d.ts +63 -0
- package/dist/server/tasks/workflow-runs.d.ts.map +1 -0
- package/dist/shared/tasks/capability.d.ts +18 -0
- package/dist/shared/tasks/capability.d.ts.map +1 -0
- package/dist/shared/tasks/constants.d.ts +34 -0
- package/dist/shared/tasks/constants.d.ts.map +1 -0
- package/dist/shared/tasks/index.d.ts +6 -0
- package/dist/shared/tasks/index.d.ts.map +1 -0
- package/dist/shared/tasks/schemas.d.ts +187 -0
- package/dist/shared/tasks/schemas.d.ts.map +1 -0
- package/dist/shared/tasks/task-id.d.ts +27 -0
- package/dist/shared/tasks/task-id.d.ts.map +1 -0
- package/dist/shared/tasks/types.d.ts +21 -0
- package/dist/shared/tasks/types.d.ts.map +1 -0
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "module";
|
|
|
2
2
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3
3
|
import { MastraBase } from "@mastra/core/base";
|
|
4
4
|
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
5
|
-
import { createTool, isValidationError, noopObserve, validateToolOutput } from "@mastra/core/tools";
|
|
5
|
+
import { createTool, isValidationError, noopObserve, validateToolInput, validateToolOutput } from "@mastra/core/tools";
|
|
6
6
|
import z3, { ZodFirstPartyTypeKind } from "zod/v3";
|
|
7
7
|
import { toJSONSchema, z } from "zod/v4";
|
|
8
8
|
import { z as z$1 } from "zod";
|
|
@@ -18,12 +18,12 @@ import http, { createServer } from "http";
|
|
|
18
18
|
import { MCPServerBase, MCP_SERVER_EVENT_ERROR_IDS } from "@mastra/core/mcp";
|
|
19
19
|
import { isStandardSchemaWithJSON, standardSchemaToJSONSchema, toStandardSchema } from "@mastra/core/schema";
|
|
20
20
|
import { readFileSync } from "fs";
|
|
21
|
-
import { RequestContext } from "@mastra/core/request-context";
|
|
21
|
+
import { MASTRA_AUTH_TOKEN_KEY, RequestContext } from "@mastra/core/request-context";
|
|
22
22
|
import { makeCoreTool } from "@mastra/core/utils";
|
|
23
23
|
import { PromptSchema } from "@modelcontextprotocol/core";
|
|
24
24
|
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps";
|
|
25
25
|
import { hostHeaderValidation, originValidation, toNodeHandler } from "@modelcontextprotocol/node";
|
|
26
|
-
import { CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY, ProtocolError as ProtocolError$1, ProtocolErrorCode as ProtocolErrorCode$1, Server, createMcpHandler, createRequestStateCodec, inputRequired, inputResponse, specTypeSchemas } from "@modelcontextprotocol/server";
|
|
26
|
+
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, MissingRequiredClientCapabilityError, PROTOCOL_VERSION_META_KEY, ProtocolError as ProtocolError$1, ProtocolErrorCode as ProtocolErrorCode$1, Server, createMcpHandler, createRequestStateCodec, inputRequired, inputResponse, specTypeSchemas } from "@modelcontextprotocol/server";
|
|
27
27
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
28
28
|
import https from "https";
|
|
29
29
|
import { SignalProvider } from "@mastra/core/signals";
|
|
@@ -12508,8 +12508,8 @@ const WEBHOOK_HEADERS = {
|
|
|
12508
12508
|
*
|
|
12509
12509
|
* @experimental SEP-3415 is a draft Extensions Track proposal. Shapes may change.
|
|
12510
12510
|
*/
|
|
12511
|
-
const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
12512
|
-
const MetaSchema = JsonObjectSchema.optional();
|
|
12511
|
+
const JsonObjectSchema$1 = z.record(z.string(), z.unknown());
|
|
12512
|
+
const MetaSchema$1 = JsonObjectSchema$1.optional();
|
|
12513
12513
|
const CursorSchema = z.string().nullable();
|
|
12514
12514
|
const ResultTypeSchema = z.string().optional();
|
|
12515
12515
|
const RequestIdSchema = z.union([z.string(), z.number()]);
|
|
@@ -12520,17 +12520,17 @@ const EventDescriptorSchema = z.looseObject({
|
|
|
12520
12520
|
name: z.string(),
|
|
12521
12521
|
description: z.string().optional(),
|
|
12522
12522
|
delivery: z.array(z.string()).min(1),
|
|
12523
|
-
inputSchema: JsonObjectSchema.optional(),
|
|
12524
|
-
payloadSchema: JsonObjectSchema.optional(),
|
|
12525
|
-
_meta: MetaSchema
|
|
12523
|
+
inputSchema: JsonObjectSchema$1.optional(),
|
|
12524
|
+
payloadSchema: JsonObjectSchema$1.optional(),
|
|
12525
|
+
_meta: MetaSchema$1
|
|
12526
12526
|
});
|
|
12527
12527
|
const EventOccurrenceSchema = z.looseObject({
|
|
12528
12528
|
eventId: z.string(),
|
|
12529
12529
|
name: z.string(),
|
|
12530
12530
|
timestamp: z.string(),
|
|
12531
|
-
data: JsonObjectSchema,
|
|
12531
|
+
data: JsonObjectSchema$1,
|
|
12532
12532
|
cursor: CursorSchema.optional(),
|
|
12533
|
-
_meta: MetaSchema
|
|
12533
|
+
_meta: MetaSchema$1
|
|
12534
12534
|
});
|
|
12535
12535
|
const EventsErrorSchema = z.looseObject({
|
|
12536
12536
|
code: z.number().int(),
|
|
@@ -12539,7 +12539,7 @@ const EventsErrorSchema = z.looseObject({
|
|
|
12539
12539
|
});
|
|
12540
12540
|
const ListEventsRequestParamsSchema = z.looseObject({
|
|
12541
12541
|
cursor: z.string().optional(),
|
|
12542
|
-
_meta: MetaSchema
|
|
12542
|
+
_meta: MetaSchema$1
|
|
12543
12543
|
});
|
|
12544
12544
|
z.object({
|
|
12545
12545
|
method: z.literal(EVENTS_METHODS.list),
|
|
@@ -12549,14 +12549,14 @@ const ListEventsResultSchema = z.looseObject({
|
|
|
12549
12549
|
resultType: ResultTypeSchema,
|
|
12550
12550
|
events: z.array(EventDescriptorSchema),
|
|
12551
12551
|
nextCursor: z.string().optional(),
|
|
12552
|
-
_meta: MetaSchema
|
|
12552
|
+
_meta: MetaSchema$1
|
|
12553
12553
|
});
|
|
12554
12554
|
const SubscriptionParamsShape = {
|
|
12555
12555
|
name: z.string(),
|
|
12556
|
-
arguments: JsonObjectSchema.optional(),
|
|
12556
|
+
arguments: JsonObjectSchema$1.optional(),
|
|
12557
12557
|
cursor: CursorSchema.optional(),
|
|
12558
12558
|
maxAgeMs: z.number().int().nonnegative().optional(),
|
|
12559
|
-
_meta: MetaSchema
|
|
12559
|
+
_meta: MetaSchema$1
|
|
12560
12560
|
};
|
|
12561
12561
|
const PollEventsRequestParamsSchema = z.looseObject({
|
|
12562
12562
|
...SubscriptionParamsShape,
|
|
@@ -12573,7 +12573,7 @@ const PollEventsResultSchema = z.looseObject({
|
|
|
12573
12573
|
truncated: z.boolean().optional(),
|
|
12574
12574
|
hasMore: z.boolean().optional(),
|
|
12575
12575
|
nextPollMs: z.number().nonnegative().optional(),
|
|
12576
|
-
_meta: MetaSchema
|
|
12576
|
+
_meta: MetaSchema$1
|
|
12577
12577
|
});
|
|
12578
12578
|
const StreamEventsRequestParamsSchema = z.looseObject(SubscriptionParamsShape);
|
|
12579
12579
|
z.object({
|
|
@@ -12582,7 +12582,7 @@ z.object({
|
|
|
12582
12582
|
});
|
|
12583
12583
|
const StreamEventsResultSchema = z.looseObject({
|
|
12584
12584
|
resultType: ResultTypeSchema,
|
|
12585
|
-
_meta: MetaSchema
|
|
12585
|
+
_meta: MetaSchema$1
|
|
12586
12586
|
});
|
|
12587
12587
|
/** `_meta` on every `notifications/events/*` message delivered on a push stream. */
|
|
12588
12588
|
const StreamNotificationMetaSchema = z.looseObject({ [EVENTS_SUBSCRIPTION_ID_META_KEY]: RequestIdSchema });
|
|
@@ -12601,7 +12601,7 @@ const EventErrorNotificationParamsSchema = z.looseObject({
|
|
|
12601
12601
|
_meta: StreamNotificationMetaSchema
|
|
12602
12602
|
});
|
|
12603
12603
|
const EventTerminatedNotificationParamsSchema = EventErrorNotificationParamsSchema;
|
|
12604
|
-
const EventsListChangedNotificationParamsSchema = z.looseObject({ _meta: MetaSchema }).optional();
|
|
12604
|
+
const EventsListChangedNotificationParamsSchema = z.looseObject({ _meta: MetaSchema$1 }).optional();
|
|
12605
12605
|
z.object({
|
|
12606
12606
|
method: z.literal(EVENTS_NOTIFICATIONS.active),
|
|
12607
12607
|
params: EventActiveNotificationParamsSchema
|
|
@@ -12661,13 +12661,13 @@ const SubscribeEventsResultSchema = z.looseObject({
|
|
|
12661
12661
|
cursor: CursorSchema,
|
|
12662
12662
|
truncated: z.boolean().optional(),
|
|
12663
12663
|
deliveryStatus: WebhookDeliveryStatusSchema.optional(),
|
|
12664
|
-
_meta: MetaSchema
|
|
12664
|
+
_meta: MetaSchema$1
|
|
12665
12665
|
});
|
|
12666
12666
|
const UnsubscribeEventsRequestParamsSchema = z.looseObject({
|
|
12667
12667
|
name: z.string(),
|
|
12668
|
-
arguments: JsonObjectSchema.optional(),
|
|
12668
|
+
arguments: JsonObjectSchema$1.optional(),
|
|
12669
12669
|
delivery: z.looseObject({ url: z.string().url() }),
|
|
12670
|
-
_meta: MetaSchema
|
|
12670
|
+
_meta: MetaSchema$1
|
|
12671
12671
|
});
|
|
12672
12672
|
z.object({
|
|
12673
12673
|
method: z.literal(EVENTS_METHODS.unsubscribe),
|
|
@@ -12675,7 +12675,7 @@ z.object({
|
|
|
12675
12675
|
});
|
|
12676
12676
|
const UnsubscribeEventsResultSchema = z.looseObject({
|
|
12677
12677
|
resultType: ResultTypeSchema,
|
|
12678
|
-
_meta: MetaSchema
|
|
12678
|
+
_meta: MetaSchema$1
|
|
12679
12679
|
});
|
|
12680
12680
|
const WebhookGapBodySchema = z.looseObject({
|
|
12681
12681
|
type: z.literal("gap"),
|
|
@@ -13224,6 +13224,176 @@ function extractBearerToken(authHeader) {
|
|
|
13224
13224
|
if (authHeader.slice(0, 7).toLowerCase() !== prefix) return void 0;
|
|
13225
13225
|
return authHeader.slice(7).trim() || void 0;
|
|
13226
13226
|
}
|
|
13227
|
+
// src/shared/tasks/constants.ts
|
|
13228
|
+
/**
|
|
13229
|
+
* Wire constants for the MCP Tasks extension (SEP-2663, protocol revision 2026-07-28).
|
|
13230
|
+
*
|
|
13231
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13232
|
+
*/
|
|
13233
|
+
/** Extension identifier used as the key under `capabilities.extensions`. */
|
|
13234
|
+
const TASKS_EXTENSION_ID = "io.modelcontextprotocol/tasks";
|
|
13235
|
+
const TASKS_METHODS = {
|
|
13236
|
+
get: "tasks/get",
|
|
13237
|
+
update: "tasks/update",
|
|
13238
|
+
cancel: "tasks/cancel"
|
|
13239
|
+
};
|
|
13240
|
+
/** `resultType` of a `tools/call` result that is a task handle instead of a `CallToolResult`. */
|
|
13241
|
+
const TASK_RESULT_TYPE = "task";
|
|
13242
|
+
const TASK_STATUSES = [
|
|
13243
|
+
"working",
|
|
13244
|
+
"input_required",
|
|
13245
|
+
"completed",
|
|
13246
|
+
"failed",
|
|
13247
|
+
"cancelled"
|
|
13248
|
+
];
|
|
13249
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
13250
|
+
"completed",
|
|
13251
|
+
"failed",
|
|
13252
|
+
"cancelled"
|
|
13253
|
+
]);
|
|
13254
|
+
/** `completed`, `failed` and `cancelled` are terminal: the task never changes again. */
|
|
13255
|
+
function isTerminalTaskStatus(status) {
|
|
13256
|
+
return TERMINAL_STATUSES.has(status);
|
|
13257
|
+
}
|
|
13258
|
+
const TASKS_ERROR_CODES = {
|
|
13259
|
+
/** Unknown, expired or foreign task id. Also used for a principal mismatch, so existence is not revealed. */
|
|
13260
|
+
InvalidParams: -32602,
|
|
13261
|
+
InternalError: -32603,
|
|
13262
|
+
/** The request needs a task but the client did not declare `io.modelcontextprotocol/tasks`. */
|
|
13263
|
+
MissingRequiredClientCapability: -32021
|
|
13264
|
+
};
|
|
13265
|
+
/** How long a server keeps a task by default: 24 hours. */
|
|
13266
|
+
const DEFAULT_TASK_TTL_MS = 864e5;
|
|
13267
|
+
/** Suggested poll interval once a task is older than {@link DEFAULT_TASK_INITIAL_POLL_WINDOW_MS}. */
|
|
13268
|
+
const DEFAULT_TASK_POLL_INTERVAL_MS = 5e3;
|
|
13269
|
+
// src/shared/tasks/schemas.ts
|
|
13270
|
+
/**
|
|
13271
|
+
* Zod schemas for the MCP Tasks extension (SEP-2663) wire format.
|
|
13272
|
+
*
|
|
13273
|
+
* Objects are loose so fields added by later revisions pass through. `resultType` is required on
|
|
13274
|
+
* the raw `tools/call` task handle, as the 2026-07-28 revision requires it there.
|
|
13275
|
+
*
|
|
13276
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13277
|
+
*/
|
|
13278
|
+
const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
13279
|
+
const MetaSchema = JsonObjectSchema.optional();
|
|
13280
|
+
const TaskStatusSchema = z.enum(TASK_STATUSES);
|
|
13281
|
+
/** An embedded elicitation, sampling or roots request (`{ method, params }`). */
|
|
13282
|
+
const TaskInputRequestSchema = z.looseObject({
|
|
13283
|
+
method: z.string(),
|
|
13284
|
+
params: JsonObjectSchema.optional()
|
|
13285
|
+
});
|
|
13286
|
+
/** Keyed by identifiers that are unique over the task's lifetime. */
|
|
13287
|
+
const TaskInputRequestsSchema = z.record(z.string(), TaskInputRequestSchema);
|
|
13288
|
+
/** The bare result object answering the input request with the same key. */
|
|
13289
|
+
const TaskInputResponsesSchema = z.record(z.string(), JsonObjectSchema);
|
|
13290
|
+
const TaskShape = {
|
|
13291
|
+
taskId: z.string().min(1),
|
|
13292
|
+
status: TaskStatusSchema,
|
|
13293
|
+
statusMessage: z.string().optional(),
|
|
13294
|
+
createdAt: z.string(),
|
|
13295
|
+
lastUpdatedAt: z.string(),
|
|
13296
|
+
ttlMs: z.number().int().nonnegative().nullable(),
|
|
13297
|
+
pollIntervalMs: z.number().int().nonnegative().optional()
|
|
13298
|
+
};
|
|
13299
|
+
z.looseObject(TaskShape);
|
|
13300
|
+
const DetailedTaskSchema = z.discriminatedUnion("status", [
|
|
13301
|
+
z.looseObject({
|
|
13302
|
+
...TaskShape,
|
|
13303
|
+
status: z.literal("working")
|
|
13304
|
+
}),
|
|
13305
|
+
z.looseObject({
|
|
13306
|
+
...TaskShape,
|
|
13307
|
+
status: z.literal("input_required"),
|
|
13308
|
+
inputRequests: TaskInputRequestsSchema
|
|
13309
|
+
}),
|
|
13310
|
+
z.looseObject({
|
|
13311
|
+
...TaskShape,
|
|
13312
|
+
status: z.literal("completed"),
|
|
13313
|
+
result: JsonObjectSchema
|
|
13314
|
+
}),
|
|
13315
|
+
z.looseObject({
|
|
13316
|
+
...TaskShape,
|
|
13317
|
+
status: z.literal("failed"),
|
|
13318
|
+
error: JsonObjectSchema
|
|
13319
|
+
}),
|
|
13320
|
+
z.looseObject({
|
|
13321
|
+
...TaskShape,
|
|
13322
|
+
status: z.literal("cancelled")
|
|
13323
|
+
})
|
|
13324
|
+
]);
|
|
13325
|
+
/** What a server returns from `tools/call` instead of a `CallToolResult` when it creates a task. */
|
|
13326
|
+
const CreateTaskResultSchema = z.looseObject({
|
|
13327
|
+
...TaskShape,
|
|
13328
|
+
resultType: z.literal(TASK_RESULT_TYPE),
|
|
13329
|
+
_meta: MetaSchema
|
|
13330
|
+
});
|
|
13331
|
+
const TaskIdParamsSchema = z.looseObject({
|
|
13332
|
+
taskId: z.string().min(1),
|
|
13333
|
+
_meta: MetaSchema
|
|
13334
|
+
});
|
|
13335
|
+
const GetTaskRequestParamsSchema = TaskIdParamsSchema;
|
|
13336
|
+
const CancelTaskRequestParamsSchema = TaskIdParamsSchema;
|
|
13337
|
+
TaskIdParamsSchema.extend({ inputResponses: TaskInputResponsesSchema });
|
|
13338
|
+
const UpdateTaskHandlerParamsSchema = TaskIdParamsSchema;
|
|
13339
|
+
const CompleteResultShape = {
|
|
13340
|
+
resultType: z.literal("complete").optional(),
|
|
13341
|
+
_meta: MetaSchema
|
|
13342
|
+
};
|
|
13343
|
+
/** `tasks/get` result: the full task. */
|
|
13344
|
+
const GetTaskResultSchema = z.intersection(z.looseObject(CompleteResultShape), DetailedTaskSchema);
|
|
13345
|
+
/** `tasks/update` and `tasks/cancel` only acknowledge. */
|
|
13346
|
+
const TaskAckResultSchema = z.looseObject(CompleteResultShape);
|
|
13347
|
+
// src/shared/tasks/capability.ts
|
|
13348
|
+
/**
|
|
13349
|
+
* Whether a capabilities object declares the Tasks extension. A non-object entry counts as absent,
|
|
13350
|
+
* and the legacy 2025-11-25 top-level `capabilities.tasks` key is ignored: it is not wire-compatible.
|
|
13351
|
+
*
|
|
13352
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13353
|
+
*/
|
|
13354
|
+
function hasTasksCapability(capabilities) {
|
|
13355
|
+
const entry = capabilities?.extensions?.[TASKS_EXTENSION_ID];
|
|
13356
|
+
return typeof entry === "object" && entry !== null && !Array.isArray(entry);
|
|
13357
|
+
}
|
|
13358
|
+
/**
|
|
13359
|
+
* The `extensions` entry a peer advertises to declare the Tasks extension. SEP-2663 defines no
|
|
13360
|
+
* settings, so the value is an empty object.
|
|
13361
|
+
*
|
|
13362
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13363
|
+
*/
|
|
13364
|
+
function tasksCapabilityEntry() {
|
|
13365
|
+
return { [TASKS_EXTENSION_ID]: {} };
|
|
13366
|
+
}
|
|
13367
|
+
// src/shared/tasks/task-id.ts
|
|
13368
|
+
/**
|
|
13369
|
+
* A new task id: 192 random bits, base64url. Task ids work as bearer tokens, so they must not be
|
|
13370
|
+
* guessable or derived from anything else (such as a workflow run id).
|
|
13371
|
+
*
|
|
13372
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13373
|
+
*/
|
|
13374
|
+
function createTaskId() {
|
|
13375
|
+
return randomBytes(24).toString("base64url");
|
|
13376
|
+
}
|
|
13377
|
+
/**
|
|
13378
|
+
* A stable key for a task that is safe to store and show: the SHA-256 of the task id, base64url.
|
|
13379
|
+
* Client and server compute the same key, so traces on both sides can be matched without
|
|
13380
|
+
* recording the task id itself.
|
|
13381
|
+
*
|
|
13382
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13383
|
+
*/
|
|
13384
|
+
function taskKeyOf(taskId) {
|
|
13385
|
+
return createHash("sha256").update(taskId).digest("base64url");
|
|
13386
|
+
}
|
|
13387
|
+
/**
|
|
13388
|
+
* The poll interval a server suggests for a task created at `createdAt`: frequent while the task is
|
|
13389
|
+
* young, then slower.
|
|
13390
|
+
*
|
|
13391
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13392
|
+
*/
|
|
13393
|
+
function suggestedPollIntervalMs(createdAt, now, options = {}) {
|
|
13394
|
+
const window = options.initialWindowMs ?? 6e4;
|
|
13395
|
+
return now - createdAt < window ? options.initialMs ?? 2e3 : options.steadyMs ?? 5e3;
|
|
13396
|
+
}
|
|
13227
13397
|
// src/shared/trace-context.ts
|
|
13228
13398
|
/** `_meta` keys defined by the MCP specification for W3C trace propagation. */
|
|
13229
13399
|
const TRACEPARENT_META_KEY = "traceparent";
|
|
@@ -14215,6 +14385,26 @@ function isReconnectableMCPError(error) {
|
|
|
14215
14385
|
const errorMessage = error.message.toLowerCase();
|
|
14216
14386
|
return errorMessage.includes("not connected") || errorMessage.includes("http 400") || errorMessage.includes("http 401") || errorMessage.includes("http 403") || errorMessage.includes("http 404") || errorMessage.includes("econnrefused") || errorMessage.includes("fetch failed") || errorMessage.includes("connection refused") || errorMessage.includes("connection closed") || errorMessage.includes("typeerror: terminated");
|
|
14217
14387
|
}
|
|
14388
|
+
const GATEWAY_UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
14389
|
+
502,
|
|
14390
|
+
503,
|
|
14391
|
+
504
|
|
14392
|
+
]);
|
|
14393
|
+
/**
|
|
14394
|
+
* Whether a failed `tasks/get` poll should be retried while waiting out an outage.
|
|
14395
|
+
*
|
|
14396
|
+
* Adds HTTP 502/503/504 (a proxy or load balancer answering while the server
|
|
14397
|
+
* restarts or deploys) to the reconnectable failures. Kept separate from
|
|
14398
|
+
* {@link isReconnectableMCPError}: `tasks/get` is idempotent, but a `tools/call`
|
|
14399
|
+
* retried after a gateway 5xx could run its work twice.
|
|
14400
|
+
*/
|
|
14401
|
+
function isRecoverableTaskPollError(error) {
|
|
14402
|
+
if (isReconnectableMCPError(error)) return true;
|
|
14403
|
+
if (!(error instanceof Error)) return false;
|
|
14404
|
+
const { httpStatus } = getMCPDiscoveryErrorDetails(error);
|
|
14405
|
+
if (httpStatus !== void 0) return GATEWAY_UNAVAILABLE_STATUSES.has(httpStatus);
|
|
14406
|
+
return /\bhttp 50[234]\b/i.test(error.message);
|
|
14407
|
+
}
|
|
14218
14408
|
const MAX_DISCOVERY_ERROR_CAUSE_DEPTH = 8;
|
|
14219
14409
|
function isHttpStatus(value) {
|
|
14220
14410
|
return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599;
|
|
@@ -15096,6 +15286,270 @@ function getInputSchemaError(schema) {
|
|
|
15096
15286
|
}
|
|
15097
15287
|
return result.valid ? void 0 : result.errorMessage.split("; ").at(-1);
|
|
15098
15288
|
}
|
|
15289
|
+
// src/client/tasks/intercept.ts
|
|
15290
|
+
/**
|
|
15291
|
+
* `_meta` key carrying an intercepted task handle. The `mastra.ai/` prefix keeps it out of the
|
|
15292
|
+
* reserved MCP namespace.
|
|
15293
|
+
*/
|
|
15294
|
+
const TASK_HANDLE_META_KEY = "mastra.ai/mcp-task";
|
|
15295
|
+
/**
|
|
15296
|
+
* Lets task handles through on a connection that declared the Tasks extension.
|
|
15297
|
+
*
|
|
15298
|
+
* The SDK rejects `resultType: "task"` before any caller code runs. This rewrites such a response
|
|
15299
|
+
* into a complete `CallToolResult` with `isError: true` that carries the task under
|
|
15300
|
+
* {@link TASK_HANDLE_META_KEY}: `isError` keeps the SDK from applying the tool's output schema, and
|
|
15301
|
+
* if the marker is ever missed the call fails instead of returning a wrong result.
|
|
15302
|
+
* Call after `connect()`, which installs the protocol's own message handler.
|
|
15303
|
+
*/
|
|
15304
|
+
function interceptTaskResults(transport) {
|
|
15305
|
+
const deliver = transport.onmessage;
|
|
15306
|
+
if (!deliver) throw new Error("interceptTaskResults must run after the transport is connected");
|
|
15307
|
+
transport.onmessage = (message, extra) => {
|
|
15308
|
+
const result = "result" in message ? message.result : void 0;
|
|
15309
|
+
if (result?.resultType === "task") {
|
|
15310
|
+
const parsed = CreateTaskResultSchema.safeParse(result);
|
|
15311
|
+
if (parsed.success) {
|
|
15312
|
+
const { resultType: _resultType, _meta, ...task } = parsed.data;
|
|
15313
|
+
message = {
|
|
15314
|
+
...message,
|
|
15315
|
+
result: {
|
|
15316
|
+
resultType: "complete",
|
|
15317
|
+
isError: true,
|
|
15318
|
+
content: [{
|
|
15319
|
+
type: "text",
|
|
15320
|
+
text: `MCP task ${task.taskId} was not awaited`
|
|
15321
|
+
}],
|
|
15322
|
+
_meta: {
|
|
15323
|
+
..._meta ?? {},
|
|
15324
|
+
[TASK_HANDLE_META_KEY]: task
|
|
15325
|
+
}
|
|
15326
|
+
}
|
|
15327
|
+
};
|
|
15328
|
+
}
|
|
15329
|
+
}
|
|
15330
|
+
deliver.call(transport, message, extra);
|
|
15331
|
+
};
|
|
15332
|
+
}
|
|
15333
|
+
/** The task handle a rewritten result carries, if any. */
|
|
15334
|
+
function takeTaskHandle(result) {
|
|
15335
|
+
const handle = result._meta?.[TASK_HANDLE_META_KEY];
|
|
15336
|
+
return handle && typeof handle === "object" ? handle : void 0;
|
|
15337
|
+
}
|
|
15338
|
+
// src/client/tasks/options.ts
|
|
15339
|
+
/** `setTimeout` fires at once for longer delays. */
|
|
15340
|
+
const MAX_TIMER_DELAY_MS$1 = 2147483647;
|
|
15341
|
+
/** Normalises a server's `tasks` option; `undefined` means tasks are off. */
|
|
15342
|
+
function resolveTasksOptions(serverName, option) {
|
|
15343
|
+
if (!option) return void 0;
|
|
15344
|
+
if (option === true) return {};
|
|
15345
|
+
for (const key of ["pollIntervalMs", "maxWaitMs"]) {
|
|
15346
|
+
const value = option[key];
|
|
15347
|
+
if (value !== void 0 && (!Number.isInteger(value) || value <= 0 || value > MAX_TIMER_DELAY_MS$1)) throw new RangeError(`MCP server '${serverName}': tasks.${key} must be a positive integer of at most ${MAX_TIMER_DELAY_MS$1}`);
|
|
15348
|
+
}
|
|
15349
|
+
return option;
|
|
15350
|
+
}
|
|
15351
|
+
// src/client/tasks/requester.ts
|
|
15352
|
+
/**
|
|
15353
|
+
* Sends `tasks/get`, `tasks/update` and `tasks/cancel` to one server. Each is sent with an explicit
|
|
15354
|
+
* result schema, which the SDK requires for the Tasks extension's methods on the 2026-07-28 revision.
|
|
15355
|
+
* The SDK sets the `Mcp-Name: <taskId>` routing header on Streamable HTTP.
|
|
15356
|
+
*/
|
|
15357
|
+
var TaskRequester = class {
|
|
15358
|
+
client;
|
|
15359
|
+
options;
|
|
15360
|
+
constructor(client, options = {}) {
|
|
15361
|
+
this.client = client;
|
|
15362
|
+
this.options = options;
|
|
15363
|
+
}
|
|
15364
|
+
async get(taskId, options = {}) {
|
|
15365
|
+
const { resultType: _resultType, _meta, ...task } = await this.client.request({
|
|
15366
|
+
method: TASKS_METHODS.get,
|
|
15367
|
+
params: this.params({ taskId })
|
|
15368
|
+
}, GetTaskResultSchema, this.requestOptions(options));
|
|
15369
|
+
return task;
|
|
15370
|
+
}
|
|
15371
|
+
async update(taskId, inputResponses, options = {}) {
|
|
15372
|
+
await this.client.request({
|
|
15373
|
+
method: TASKS_METHODS.update,
|
|
15374
|
+
params: this.params({
|
|
15375
|
+
taskId,
|
|
15376
|
+
inputResponses
|
|
15377
|
+
})
|
|
15378
|
+
}, TaskAckResultSchema, this.requestOptions(options));
|
|
15379
|
+
}
|
|
15380
|
+
async cancel(taskId, options = {}) {
|
|
15381
|
+
await this.client.request({
|
|
15382
|
+
method: TASKS_METHODS.cancel,
|
|
15383
|
+
params: this.params({ taskId })
|
|
15384
|
+
}, TaskAckResultSchema, this.requestOptions(options));
|
|
15385
|
+
}
|
|
15386
|
+
params(params) {
|
|
15387
|
+
const meta = this.options.meta?.();
|
|
15388
|
+
return meta ? {
|
|
15389
|
+
...params,
|
|
15390
|
+
_meta: meta
|
|
15391
|
+
} : params;
|
|
15392
|
+
}
|
|
15393
|
+
requestOptions(options) {
|
|
15394
|
+
return {
|
|
15395
|
+
timeout: this.options.timeout,
|
|
15396
|
+
signal: options.signal
|
|
15397
|
+
};
|
|
15398
|
+
}
|
|
15399
|
+
};
|
|
15400
|
+
// src/client/tasks/wait.ts
|
|
15401
|
+
/** Why a task wait stopped without a terminal task. */
|
|
15402
|
+
var TaskWaitError = class extends Error {
|
|
15403
|
+
reason;
|
|
15404
|
+
task;
|
|
15405
|
+
constructor(reason, message, task, cause) {
|
|
15406
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
15407
|
+
this.reason = reason;
|
|
15408
|
+
this.task = task;
|
|
15409
|
+
this.name = "TaskWaitError";
|
|
15410
|
+
}
|
|
15411
|
+
};
|
|
15412
|
+
const DEFAULT_MAX_OUTAGE_MS = 3e5;
|
|
15413
|
+
const MAX_OUTAGE_BACKOFF_MS = 3e4;
|
|
15414
|
+
/** Polls at least this often, whatever the server suggests. */
|
|
15415
|
+
const MAX_TASK_POLL_INTERVAL_MS = 36e5;
|
|
15416
|
+
const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
15417
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
15418
|
+
const timer = setTimeout(() => {
|
|
15419
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15420
|
+
resolve();
|
|
15421
|
+
}, ms);
|
|
15422
|
+
const onAbort = () => {
|
|
15423
|
+
clearTimeout(timer);
|
|
15424
|
+
reject(signal.reason);
|
|
15425
|
+
};
|
|
15426
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15427
|
+
});
|
|
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
|
+
async function waitForTask(port, created, options = {}) {
|
|
15435
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
15436
|
+
const now = options.now ?? Date.now;
|
|
15437
|
+
const maxOutageMs = options.maxOutageMs ?? DEFAULT_MAX_OUTAGE_MS;
|
|
15438
|
+
const deadline = options.maxWaitMs === void 0 ? void 0 : now() + options.maxWaitMs;
|
|
15439
|
+
const answered = /* @__PURE__ */ new Set();
|
|
15440
|
+
let task = created;
|
|
15441
|
+
const startedAt = now();
|
|
15442
|
+
const ttlEndOf = (current) => {
|
|
15443
|
+
if (current.ttlMs === null) return Infinity;
|
|
15444
|
+
const createdAt = Date.parse(current.createdAt);
|
|
15445
|
+
return (Number.isNaN(createdAt) ? startedAt : createdAt) + current.ttlMs;
|
|
15446
|
+
};
|
|
15447
|
+
let cancelSent = false;
|
|
15448
|
+
const cancel = () => {
|
|
15449
|
+
if (cancelSent) return;
|
|
15450
|
+
cancelSent = true;
|
|
15451
|
+
port.cancel(created.taskId).catch(() => {});
|
|
15452
|
+
};
|
|
15453
|
+
const timedOut = () => new TaskWaitError("timeout", `Gave up on MCP task ${task.taskId} after ${options.maxWaitMs}ms`, task);
|
|
15454
|
+
const stop = new AbortController();
|
|
15455
|
+
const stopped = new Promise((_, reject) => {
|
|
15456
|
+
stop.signal.addEventListener("abort", () => reject(stop.signal.reason), { once: true });
|
|
15457
|
+
});
|
|
15458
|
+
stopped.catch(() => {});
|
|
15459
|
+
const bounded = (operation) => Promise.race([operation, stopped]);
|
|
15460
|
+
const onAbort = () => {
|
|
15461
|
+
cancel();
|
|
15462
|
+
stop.abort(options.signal.reason);
|
|
15463
|
+
};
|
|
15464
|
+
if (options.signal?.aborted) onAbort();
|
|
15465
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15466
|
+
const deadlineTimer = options.maxWaitMs === void 0 ? void 0 : setTimeout(() => {
|
|
15467
|
+
cancel();
|
|
15468
|
+
stop.abort(timedOut());
|
|
15469
|
+
}, options.maxWaitMs);
|
|
15470
|
+
deadlineTimer?.unref?.();
|
|
15471
|
+
try {
|
|
15472
|
+
let reported;
|
|
15473
|
+
for (;;) {
|
|
15474
|
+
if (task.status !== reported) {
|
|
15475
|
+
reported = task.status;
|
|
15476
|
+
options.onStatus?.(task);
|
|
15477
|
+
}
|
|
15478
|
+
stop.signal.throwIfAborted();
|
|
15479
|
+
if (isTerminalTaskStatus(task.status)) return task;
|
|
15480
|
+
if (task.status === "input_required" && "inputRequests" in task) {
|
|
15481
|
+
const { inputRequests } = task;
|
|
15482
|
+
const pending = Object.entries(inputRequests).filter(([key]) => !answered.has(key));
|
|
15483
|
+
if (pending.length > 0) {
|
|
15484
|
+
const responses = await bounded(answerInputRequests(pending, task, options.inputRequests, stop.signal));
|
|
15485
|
+
for (const key of Object.keys(responses)) answered.add(key);
|
|
15486
|
+
await bounded(port.update(task.taskId, responses, { signal: stop.signal }));
|
|
15487
|
+
task = await poll();
|
|
15488
|
+
continue;
|
|
15489
|
+
}
|
|
15490
|
+
}
|
|
15491
|
+
const ttlLeft = ttlEndOf(task) - now();
|
|
15492
|
+
if (ttlLeft <= 0) throw new TaskWaitError("expired", `MCP task ${task.taskId} outlived its ${task.ttlMs}ms TTL`, task);
|
|
15493
|
+
let interval = Math.min(pollIntervalOf(task), ttlLeft);
|
|
15494
|
+
if (deadline !== void 0) {
|
|
15495
|
+
const remaining = deadline - now();
|
|
15496
|
+
if (remaining <= 0) {
|
|
15497
|
+
cancel();
|
|
15498
|
+
throw timedOut();
|
|
15499
|
+
}
|
|
15500
|
+
interval = Math.min(interval, remaining);
|
|
15501
|
+
}
|
|
15502
|
+
await bounded(sleep(interval, stop.signal));
|
|
15503
|
+
task = await poll();
|
|
15504
|
+
}
|
|
15505
|
+
function pollIntervalOf(current) {
|
|
15506
|
+
const suggested = current.pollIntervalMs ?? options.pollIntervalMs ?? 5e3;
|
|
15507
|
+
return Number.isFinite(suggested) ? Math.min(MAX_TASK_POLL_INTERVAL_MS, Math.max(100, suggested)) : DEFAULT_TASK_POLL_INTERVAL_MS;
|
|
15508
|
+
}
|
|
15509
|
+
async function poll() {
|
|
15510
|
+
let outageStartedAt;
|
|
15511
|
+
let backoff;
|
|
15512
|
+
for (;;) try {
|
|
15513
|
+
return await bounded(port.get(created.taskId, { signal: stop.signal }));
|
|
15514
|
+
} catch (error) {
|
|
15515
|
+
stop.signal.throwIfAborted();
|
|
15516
|
+
if (!port.isRecoverable(error)) throw error;
|
|
15517
|
+
const at = now();
|
|
15518
|
+
outageStartedAt ??= at;
|
|
15519
|
+
if (deadline !== void 0 && at >= deadline) {
|
|
15520
|
+
cancel();
|
|
15521
|
+
throw timedOut();
|
|
15522
|
+
}
|
|
15523
|
+
const giveUpAt = Math.min(outageStartedAt + maxOutageMs, ttlEndOf(task));
|
|
15524
|
+
if (at >= giveUpAt) throw new TaskWaitError("unreachable", `Couldn't reach the MCP server for ${Math.round((at - outageStartedAt) / 1e3)}s while waiting for task ${task.taskId}; the task may still finish on the server`, task, error);
|
|
15525
|
+
try {
|
|
15526
|
+
await bounded(port.recover(error));
|
|
15527
|
+
} catch (recoverError) {
|
|
15528
|
+
stop.signal.throwIfAborted();
|
|
15529
|
+
if (!port.isRecoverable(recoverError)) throw recoverError;
|
|
15530
|
+
}
|
|
15531
|
+
backoff = Math.min(MAX_OUTAGE_BACKOFF_MS, backoff === void 0 ? pollIntervalOf(task) : backoff * 2);
|
|
15532
|
+
await bounded(sleep(Math.max(0, Math.min(backoff, giveUpAt - at, (deadline ?? Infinity) - at)), stop.signal));
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
15535
|
+
} finally {
|
|
15536
|
+
clearTimeout(deadlineTimer);
|
|
15537
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
15538
|
+
}
|
|
15539
|
+
}
|
|
15540
|
+
async function answerInputRequests(pending, task, handler, signal) {
|
|
15541
|
+
if (!handler) throw new TaskWaitError("input_unhandled", `MCP task ${task.taskId} needs input, but no inputRequests handler is configured`, task);
|
|
15542
|
+
const responses = {};
|
|
15543
|
+
for (const [key, request] of pending) {
|
|
15544
|
+
if (request.method !== "elicitation/create") throw new TaskWaitError("input_unsupported", `MCP task ${task.taskId} sent an unsupported input request '${request.method}'`, task);
|
|
15545
|
+
responses[key] = await handler({
|
|
15546
|
+
key,
|
|
15547
|
+
params: request.params,
|
|
15548
|
+
signal
|
|
15549
|
+
});
|
|
15550
|
+
}
|
|
15551
|
+
return responses;
|
|
15552
|
+
}
|
|
15099
15553
|
// src/client/url-policy.ts
|
|
15100
15554
|
/**
|
|
15101
15555
|
* Redirect status codes the manual redirect loop follows. Other 3xx statuses
|
|
@@ -15302,6 +15756,13 @@ function shouldDetachPersistentTransportRequest(init) {
|
|
|
15302
15756
|
return false;
|
|
15303
15757
|
}
|
|
15304
15758
|
}
|
|
15759
|
+
const TASK_WAIT_ERROR_IDS = {
|
|
15760
|
+
timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
|
|
15761
|
+
expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
|
|
15762
|
+
input_unhandled: "MCP_CLIENT_TOOL_TASK_INPUT_UNHANDLED",
|
|
15763
|
+
input_unsupported: "MCP_CLIENT_TOOL_TASK_INPUT_UNSUPPORTED",
|
|
15764
|
+
unreachable: "MCP_CLIENT_TOOL_TASK_POLL_FAILED"
|
|
15765
|
+
};
|
|
15305
15766
|
/**
|
|
15306
15767
|
* Reports the JSON-RPC id of each `events/stream` request as it goes out on `transport`. The
|
|
15307
15768
|
* server identifies a push stream by that id, and the SDK doesn't return it to the caller.
|
|
@@ -15488,6 +15949,8 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
15488
15949
|
priorDiscovery;
|
|
15489
15950
|
requireToolApproval;
|
|
15490
15951
|
onToolError;
|
|
15952
|
+
taskOptions;
|
|
15953
|
+
taskRequester;
|
|
15491
15954
|
jsonSchemaValidator;
|
|
15492
15955
|
jsonSchemaValidatorPromise;
|
|
15493
15956
|
/** Provides access to resource operations (list, read, notifications) */
|
|
@@ -15519,13 +15982,15 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
15519
15982
|
this.requireToolApproval = server.requireToolApproval;
|
|
15520
15983
|
this.onToolError = server.onToolError ?? "throw";
|
|
15521
15984
|
this.jsonSchemaValidator = server.jsonSchemaValidator;
|
|
15985
|
+
this.taskOptions = resolveTasksOptions(name, server.tasks);
|
|
15522
15986
|
const configured = server.capabilities ?? {};
|
|
15523
15987
|
if (configured.elicitation !== void 0 && !server.inputRequests) throw new Error(`MCP server '${name}' advertises the elicitation capability without an inputRequests handler. Configure inputRequests or remove capabilities.elicitation.`);
|
|
15524
15988
|
const clientCapabilities = {
|
|
15525
15989
|
...server.inputRequests ? { elicitation: configured.elicitation ?? { form: {} } } : {},
|
|
15526
15990
|
extensions: {
|
|
15527
15991
|
...configured.extensions ?? {},
|
|
15528
|
-
"io.modelcontextprotocol/ui": {}
|
|
15992
|
+
"io.modelcontextprotocol/ui": {},
|
|
15993
|
+
...this.taskOptions ? tasksCapabilityEntry() : {}
|
|
15529
15994
|
}
|
|
15530
15995
|
};
|
|
15531
15996
|
this.client = new Client({
|
|
@@ -15536,6 +16001,10 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
15536
16001
|
...server.jsonSchemaValidator ? { jsonSchemaValidator: server.jsonSchemaValidator } : {},
|
|
15537
16002
|
versionNegotiation: { mode: negotiationMode(server.protocolVersion) }
|
|
15538
16003
|
});
|
|
16004
|
+
this.taskRequester = new TaskRequester(this.client, {
|
|
16005
|
+
timeout: this.timeout,
|
|
16006
|
+
meta: () => this.requestMeta()
|
|
16007
|
+
});
|
|
15539
16008
|
if (server.inputRequests) {
|
|
15540
16009
|
const handler = server.inputRequests;
|
|
15541
16010
|
this.client.setRequestHandler("elicitation/create", async (request, ctx) => {
|
|
@@ -15645,6 +16114,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
15645
16114
|
timeout: this.serverConfig.timeout ?? this.timeout,
|
|
15646
16115
|
prior: this.priorDiscovery
|
|
15647
16116
|
});
|
|
16117
|
+
if (this.taskOptions) interceptTaskResults(transport);
|
|
15648
16118
|
this.transport = transport;
|
|
15649
16119
|
this.log("debug", `Successfully connected to MCP server via Stdio`);
|
|
15650
16120
|
} catch (e) {
|
|
@@ -15678,6 +16148,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
15678
16148
|
timeout: connectTimeout ?? DEFAULT_SERVER_CONNECT_TIMEOUT_MSEC,
|
|
15679
16149
|
prior: this.priorDiscovery
|
|
15680
16150
|
});
|
|
16151
|
+
if (this.taskOptions) interceptTaskResults(transport);
|
|
15681
16152
|
this.transport = transport;
|
|
15682
16153
|
this.log("debug", "Successfully connected using Streamable HTTP transport.");
|
|
15683
16154
|
this.closePendingAuthTransport();
|
|
@@ -16474,6 +16945,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
16474
16945
|
strict: getMastraToolStrictMeta(toolMeta),
|
|
16475
16946
|
...mcpToolProps,
|
|
16476
16947
|
requireApproval,
|
|
16948
|
+
...this.taskOptions ? { background: this.taskToolBackgroundConfig() } : {},
|
|
16477
16949
|
mcpMetadata: {
|
|
16478
16950
|
serverName: this.name,
|
|
16479
16951
|
serverVersion: serverMeta.version,
|
|
@@ -16506,6 +16978,13 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
16506
16978
|
}));
|
|
16507
16979
|
const toolCallSpan = context?.tracingContext?.currentSpan;
|
|
16508
16980
|
if (toolCallSpan && serverSpan) toolCallSpan.links = [...toolCallSpan.links ?? [], serverSpan];
|
|
16981
|
+
const task = this.taskOptions ? takeTaskHandle(res) : void 0;
|
|
16982
|
+
if (!task) return finishToolCall(res);
|
|
16983
|
+
this.log("debug", `Tool ${tool.name} continues as MCP task ${task.taskId}`);
|
|
16984
|
+
toolCallSpan?.update({ metadata: { mcpTaskKey: taskKeyOf(task.taskId) } });
|
|
16985
|
+
return finishToolCall(await this.awaitToolTask(tool.name, task, context?.abortSignal, context?.writer));
|
|
16986
|
+
};
|
|
16987
|
+
const finishToolCall = async (res) => {
|
|
16509
16988
|
if (res.isError && this.onToolError === "throw") {
|
|
16510
16989
|
const errorText = extractToolErrorText(res.content);
|
|
16511
16990
|
this.log("debug", `Tool reported an error: ${tool.name}`, { error: errorText });
|
|
@@ -16540,7 +17019,7 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
16540
17019
|
try {
|
|
16541
17020
|
return await executeToolCall();
|
|
16542
17021
|
} catch (e) {
|
|
16543
|
-
if (!(e instanceof MastraError && e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED") && isReconnectableMCPError(e)) {
|
|
17022
|
+
if (!(e instanceof MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"))) && isReconnectableMCPError(e)) {
|
|
16544
17023
|
this.log("debug", `Transport error detected for tool ${tool.name}, attempting reconnection...`, { error: e instanceof Error ? e.message : String(e) });
|
|
16545
17024
|
try {
|
|
16546
17025
|
await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
|
|
@@ -16574,6 +17053,78 @@ var InternalMastraMCPClient = class extends MastraBase {
|
|
|
16574
17053
|
return;
|
|
16575
17054
|
}
|
|
16576
17055
|
}
|
|
17056
|
+
/** Reads, answers and cancels this server's tasks (SEP-2663). */
|
|
17057
|
+
get tasks() {
|
|
17058
|
+
return this.taskRequester;
|
|
17059
|
+
}
|
|
17060
|
+
/**
|
|
17061
|
+
* Background settings for tools of a tasks-enabled server: they may run for as long as the
|
|
17062
|
+
* remote task, and must never be retried, as a retry would start a second task.
|
|
17063
|
+
*/
|
|
17064
|
+
taskToolBackgroundConfig() {
|
|
17065
|
+
return {
|
|
17066
|
+
enabled: true,
|
|
17067
|
+
defaultDisposition: "deferred",
|
|
17068
|
+
timeoutMs: this.taskOptions?.maxWaitMs ?? 864e5,
|
|
17069
|
+
maxRetries: 0
|
|
17070
|
+
};
|
|
17071
|
+
}
|
|
17072
|
+
/** Waits for a remote task created by a tool call and returns its `CallToolResult`. */
|
|
17073
|
+
async awaitToolTask(toolName, task, signal, writer) {
|
|
17074
|
+
const details = {
|
|
17075
|
+
toolName,
|
|
17076
|
+
serverName: this.name,
|
|
17077
|
+
taskId: task.taskId
|
|
17078
|
+
};
|
|
17079
|
+
const onStatus = (current) => {
|
|
17080
|
+
const progress = {
|
|
17081
|
+
type: "mcp-task",
|
|
17082
|
+
taskId: current.taskId,
|
|
17083
|
+
status: current.status,
|
|
17084
|
+
createdAt: current.createdAt,
|
|
17085
|
+
serverName: this.name
|
|
17086
|
+
};
|
|
17087
|
+
writer?.write(progress).catch(() => {});
|
|
17088
|
+
};
|
|
17089
|
+
const fail = (id, text, cause) => new MastraError({
|
|
17090
|
+
id,
|
|
17091
|
+
domain: ErrorDomain.MCP,
|
|
17092
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
17093
|
+
text,
|
|
17094
|
+
details
|
|
17095
|
+
}, cause);
|
|
17096
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
17097
|
+
let polledTransport;
|
|
17098
|
+
let terminal;
|
|
17099
|
+
try {
|
|
17100
|
+
terminal = await waitForTask({
|
|
17101
|
+
get: (taskId, options) => {
|
|
17102
|
+
polledTransport = this.transport;
|
|
17103
|
+
return this.taskRequester.get(taskId, options);
|
|
17104
|
+
},
|
|
17105
|
+
update: (taskId, responses, options) => this.taskRequester.update(taskId, responses, options),
|
|
17106
|
+
cancel: (taskId) => this.taskRequester.cancel(taskId),
|
|
17107
|
+
isRecoverable: isRecoverableTaskPollError,
|
|
17108
|
+
recover: () => this.reconnectAfterTransportFailure(polledTransport, lifecycleGeneration)
|
|
17109
|
+
}, task, {
|
|
17110
|
+
signal,
|
|
17111
|
+
inputRequests: this.serverConfig.inputRequests,
|
|
17112
|
+
pollIntervalMs: this.taskOptions?.pollIntervalMs,
|
|
17113
|
+
maxWaitMs: this.taskOptions?.maxWaitMs,
|
|
17114
|
+
onStatus
|
|
17115
|
+
});
|
|
17116
|
+
} catch (error) {
|
|
17117
|
+
if (signal?.aborted) throw error;
|
|
17118
|
+
if (error instanceof TaskWaitError) throw fail(TASK_WAIT_ERROR_IDS[error.reason], error.message, error.cause);
|
|
17119
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
17120
|
+
throw fail("MCP_CLIENT_TOOL_TASK_POLL_FAILED", `Polling MCP task ${task.taskId} failed: ${reason}`, error);
|
|
17121
|
+
}
|
|
17122
|
+
switch (terminal.status) {
|
|
17123
|
+
case "completed": return terminal.result;
|
|
17124
|
+
case "cancelled": throw fail("MCP_CLIENT_TOOL_TASK_CANCELLED", terminal.statusMessage ?? `MCP task ${task.taskId} was cancelled`);
|
|
17125
|
+
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`);
|
|
17126
|
+
}
|
|
17127
|
+
}
|
|
16577
17128
|
stampServerIdInMeta(meta) {
|
|
16578
17129
|
const ui = meta.ui;
|
|
16579
17130
|
if (!ui?.resourceUri) return meta;
|
|
@@ -17592,6 +18143,63 @@ To fix this you have three different options:
|
|
|
17592
18143
|
};
|
|
17593
18144
|
}
|
|
17594
18145
|
/**
|
|
18146
|
+
* Reads, answers and cancels MCP tasks (SEP-2663) on configured servers. Tool calls to a server
|
|
18147
|
+
* with `tasks` enabled already wait for their tasks; use this to inspect or steer one directly.
|
|
18148
|
+
*
|
|
18149
|
+
* @experimental SEP-2663 extension; this API may change.
|
|
18150
|
+
*
|
|
18151
|
+
* @example
|
|
18152
|
+
* ```typescript
|
|
18153
|
+
* const task = await mcp.tasks.get('reports', taskId);
|
|
18154
|
+
* if (task.status === 'working') await mcp.tasks.cancel('reports', taskId);
|
|
18155
|
+
* ```
|
|
18156
|
+
*/
|
|
18157
|
+
get tasks() {
|
|
18158
|
+
this.addToInstanceCache();
|
|
18159
|
+
const wrap = async (id, details, run) => {
|
|
18160
|
+
try {
|
|
18161
|
+
return await run();
|
|
18162
|
+
} catch (error) {
|
|
18163
|
+
throw new MastraError({
|
|
18164
|
+
id,
|
|
18165
|
+
domain: ErrorDomain.MCP,
|
|
18166
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
18167
|
+
details
|
|
18168
|
+
}, error);
|
|
18169
|
+
}
|
|
18170
|
+
};
|
|
18171
|
+
return {
|
|
18172
|
+
/**
|
|
18173
|
+
* Returns a task's current state, including its result once completed.
|
|
18174
|
+
*
|
|
18175
|
+
* @throws {MastraError} `MCP_CLIENT_GET_TASK_FAILED`; the cause is a `-32602` protocol error
|
|
18176
|
+
* when the server does not know the task.
|
|
18177
|
+
*/
|
|
18178
|
+
get: (serverName, taskId, options) => wrap("MCP_CLIENT_GET_TASK_FAILED", {
|
|
18179
|
+
serverName,
|
|
18180
|
+
taskId
|
|
18181
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.get(taskId, options)),
|
|
18182
|
+
/**
|
|
18183
|
+
* Answers input requests of an `input_required` task, keyed as in its `inputRequests`.
|
|
18184
|
+
*
|
|
18185
|
+
* @throws {MastraError} `MCP_CLIENT_UPDATE_TASK_FAILED`
|
|
18186
|
+
*/
|
|
18187
|
+
update: (serverName, taskId, inputResponses, options) => wrap("MCP_CLIENT_UPDATE_TASK_FAILED", {
|
|
18188
|
+
serverName,
|
|
18189
|
+
taskId
|
|
18190
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.update(taskId, inputResponses, options)),
|
|
18191
|
+
/**
|
|
18192
|
+
* Asks the server to stop a task. Cancellation is cooperative: the task may still complete.
|
|
18193
|
+
*
|
|
18194
|
+
* @throws {MastraError} `MCP_CLIENT_CANCEL_TASK_FAILED`
|
|
18195
|
+
*/
|
|
18196
|
+
cancel: (serverName, taskId, options) => wrap("MCP_CLIENT_CANCEL_TASK_FAILED", {
|
|
18197
|
+
serverName,
|
|
18198
|
+
taskId
|
|
18199
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.cancel(taskId, options))
|
|
18200
|
+
};
|
|
18201
|
+
}
|
|
18202
|
+
/**
|
|
17595
18203
|
* Provides access to resource-related operations across all configured servers.
|
|
17596
18204
|
*
|
|
17597
18205
|
* Resources represent data exposed by MCP servers (files, database records, API responses, etc.).
|
|
@@ -18802,14 +19410,45 @@ async function toRequestContext(ctx, mapAuthInfoToUser) {
|
|
|
18802
19410
|
* never share a principal. A token refreshed mid-round therefore starts over.
|
|
18803
19411
|
*/
|
|
18804
19412
|
function principalOf(ctx, requestContext) {
|
|
19413
|
+
return callerOf(ctx, requestContext).principal;
|
|
19414
|
+
}
|
|
19415
|
+
/**
|
|
19416
|
+
* A readable name for the caller, for operators debugging a server: the principal, except that a
|
|
19417
|
+
* token principal keeps only a short prefix of the token's hash. Never a credential, so it can be
|
|
19418
|
+
* stored and shown; it is not unique enough to authorize anything.
|
|
19419
|
+
*/
|
|
19420
|
+
function clientLabelOf(ctx, requestContext) {
|
|
19421
|
+
return callerOf(ctx, requestContext).label;
|
|
19422
|
+
}
|
|
19423
|
+
const TOKEN_LABEL_HASH_LENGTH = 8;
|
|
19424
|
+
function callerOf(ctx, requestContext) {
|
|
18805
19425
|
const authInfo = ctx.http?.authInfo;
|
|
18806
|
-
if (!authInfo) return
|
|
19426
|
+
if (!authInfo) return {
|
|
19427
|
+
principal: "anonymous",
|
|
19428
|
+
label: "anonymous"
|
|
19429
|
+
};
|
|
18807
19430
|
const subject = authInfo.extra?.sub ?? authInfo.extra?.subject;
|
|
18808
|
-
if (typeof subject === "string" && subject)
|
|
19431
|
+
if (typeof subject === "string" && subject) {
|
|
19432
|
+
const principal = `${authInfo.clientId}:sub:${subject}`;
|
|
19433
|
+
return {
|
|
19434
|
+
principal,
|
|
19435
|
+
label: principal
|
|
19436
|
+
};
|
|
19437
|
+
}
|
|
18809
19438
|
const user = requestContext.get("user");
|
|
18810
19439
|
const userId = user && typeof user === "object" ? user.id : void 0;
|
|
18811
|
-
if (typeof userId === "string" && userId)
|
|
18812
|
-
|
|
19440
|
+
if (typeof userId === "string" && userId) {
|
|
19441
|
+
const principal = `${authInfo.clientId}:user:${userId}`;
|
|
19442
|
+
return {
|
|
19443
|
+
principal,
|
|
19444
|
+
label: principal
|
|
19445
|
+
};
|
|
19446
|
+
}
|
|
19447
|
+
const tokenHash = createHash("sha256").update(authInfo.token).digest("base64url");
|
|
19448
|
+
return {
|
|
19449
|
+
principal: `${authInfo.clientId}:token:${tokenHash}`,
|
|
19450
|
+
label: `${authInfo.clientId}:token:${tokenHash.slice(0, TOKEN_LABEL_HASH_LENGTH)}`
|
|
19451
|
+
};
|
|
18813
19452
|
}
|
|
18814
19453
|
function hashArguments(value) {
|
|
18815
19454
|
return createHash("sha256").update(canonicalJSON(value)).digest("base64url");
|
|
@@ -20198,65 +20837,1742 @@ function echoes(body, challenge) {
|
|
|
20198
20837
|
const b = Buffer.from(challenge);
|
|
20199
20838
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
20200
20839
|
}
|
|
20201
|
-
// src/server/
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
}
|
|
20840
|
+
// src/server/tasks/store.ts
|
|
20841
|
+
/** How a principal is stored and shown. Principals can carry user and subject ids, so only their hash is kept. */
|
|
20842
|
+
function principalKeyOf(principal) {
|
|
20843
|
+
return createHash("sha256").update(principal).digest("base64url");
|
|
20844
|
+
}
|
|
20206
20845
|
/**
|
|
20207
|
-
*
|
|
20208
|
-
* Protocol (MCP) clients using protocol revision 2026-07-28: self-contained requests
|
|
20209
|
-
* over Streamable HTTP or stdio, native `input_required` continuation and
|
|
20210
|
-
* per-request logging.
|
|
20211
|
-
*
|
|
20212
|
-
* @example
|
|
20213
|
-
* `yourTool` is a tool you have already configured.
|
|
20214
|
-
* ```typescript
|
|
20215
|
-
* import { MCPServer } from '@mastra/mcp';
|
|
20216
|
-
*
|
|
20217
|
-
* const server = new MCPServer({
|
|
20218
|
-
* id: 'my-server',
|
|
20219
|
-
* name: 'My Server',
|
|
20220
|
-
* version: '1.0.0',
|
|
20221
|
-
* tools: { yourTool },
|
|
20222
|
-
* });
|
|
20223
|
-
* await server.startStdio();
|
|
20224
|
-
* ```
|
|
20225
|
-
*
|
|
20226
|
-
* @see For documentation bundled with your installed package, locate
|
|
20227
|
-
* `@mastra/mcp/package.json` with your project's resolver or package-manager
|
|
20228
|
-
* tooling, then read `dist/docs/SKILL.md` from that package root and follow its
|
|
20229
|
-
* reference links. Use package-manager tools for virtual or archived packages.
|
|
20846
|
+
* Thrown by {@link MCPTaskStore.create} when the task would go over one of its `limits`.
|
|
20230
20847
|
*
|
|
20231
|
-
* @
|
|
20232
|
-
* if packaged docs are unavailable.
|
|
20848
|
+
* @experimental SEP-2663 extension; API may change.
|
|
20233
20849
|
*/
|
|
20234
|
-
var
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
|
|
20246
|
-
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20850
|
+
var MCPTaskLimitError = class extends Error {
|
|
20851
|
+
scope;
|
|
20852
|
+
limit;
|
|
20853
|
+
kind;
|
|
20854
|
+
constructor(scope, limit, kind = "open") {
|
|
20855
|
+
super(`Too many ${kind} MCP tasks ${scope === "principal" ? "for this principal" : "in this store"} (limit ${limit})`);
|
|
20856
|
+
this.scope = scope;
|
|
20857
|
+
this.limit = limit;
|
|
20858
|
+
this.kind = kind;
|
|
20859
|
+
this.name = "MCPTaskLimitError";
|
|
20860
|
+
}
|
|
20861
|
+
};
|
|
20862
|
+
function decodeTaskListCursor(cursor) {
|
|
20863
|
+
if (!cursor) return void 0;
|
|
20864
|
+
try {
|
|
20865
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
20866
|
+
if (typeof parsed.before === "string" && !Number.isNaN(Date.parse(parsed.before)) && Array.isArray(parsed.seen)) return {
|
|
20867
|
+
before: parsed.before,
|
|
20868
|
+
seen: parsed.seen.filter((key) => typeof key === "string")
|
|
20869
|
+
};
|
|
20870
|
+
} catch {}
|
|
20871
|
+
}
|
|
20872
|
+
/** Whether a task comes after the cursor in newest-first order. */
|
|
20873
|
+
function isPastTaskListCursor(listing, cursor) {
|
|
20874
|
+
if (!cursor) return true;
|
|
20875
|
+
const created = Date.parse(listing.createdAt);
|
|
20876
|
+
const before = Date.parse(cursor.before);
|
|
20877
|
+
return created < before || created === before && !cursor.seen.includes(listing.taskKey);
|
|
20878
|
+
}
|
|
20879
|
+
/** The cursor for the page after `listings`, newest first. */
|
|
20880
|
+
function nextTaskListCursor(listings, previous) {
|
|
20881
|
+
const last = listings.at(-1);
|
|
20882
|
+
if (!last) return void 0;
|
|
20883
|
+
const before = Date.parse(last.createdAt);
|
|
20884
|
+
const seen = listings.filter((listing) => Date.parse(listing.createdAt) === before).map((listing) => listing.taskKey);
|
|
20885
|
+
if (previous && Date.parse(previous.before) === before) seen.push(...previous.seen);
|
|
20886
|
+
const cursor = {
|
|
20887
|
+
before: new Date(before).toISOString(),
|
|
20888
|
+
seen
|
|
20889
|
+
};
|
|
20890
|
+
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
20891
|
+
}
|
|
20892
|
+
/** Whether a listing matches the filters a store could not apply while reading. */
|
|
20893
|
+
function matchesTaskListOptions(listing, options) {
|
|
20894
|
+
if (options.status && listing.status !== options.status) return false;
|
|
20895
|
+
if (options.surface && listing.context?.toolName !== options.surface) return false;
|
|
20896
|
+
if (options.principalKey && listing.principalKey !== options.principalKey) return false;
|
|
20897
|
+
if (options.taskKey && listing.taskKey !== options.taskKey) return false;
|
|
20898
|
+
return true;
|
|
20899
|
+
}
|
|
20900
|
+
/** @experimental SEP-2663 extension; API may change. */
|
|
20901
|
+
function isMCPTaskStoreAdmin(store) {
|
|
20902
|
+
const admin = store;
|
|
20903
|
+
return typeof admin.list === "function" && typeof admin.updateByKey === "function" && typeof admin.cancelByKey === "function";
|
|
20904
|
+
}
|
|
20905
|
+
/** Projects a detailed task onto a listing. */
|
|
20906
|
+
function toListing(taskKey, principalKey, context, task, clientLabel) {
|
|
20907
|
+
return {
|
|
20908
|
+
taskKey,
|
|
20909
|
+
principalKey,
|
|
20910
|
+
...clientLabel !== void 0 && { clientLabel },
|
|
20911
|
+
...context && { context },
|
|
20912
|
+
status: task.status,
|
|
20913
|
+
...task.statusMessage !== void 0 && { statusMessage: task.statusMessage },
|
|
20914
|
+
createdAt: task.createdAt,
|
|
20915
|
+
lastUpdatedAt: task.lastUpdatedAt,
|
|
20916
|
+
ttlMs: task.ttlMs,
|
|
20917
|
+
...task.status === "input_required" && { inputRequests: task.inputRequests },
|
|
20918
|
+
...task.status === "completed" && { result: task.result },
|
|
20919
|
+
...task.status === "failed" && { error: task.error }
|
|
20920
|
+
};
|
|
20921
|
+
}
|
|
20922
|
+
// src/server/tasks/background-store.ts
|
|
20923
|
+
const ENVELOPE_VERSION = 1;
|
|
20924
|
+
/** For `ttlMs: null`: a deadline cleanup never reaches. */
|
|
20925
|
+
const NEVER = /* @__PURE__ */ new Date("9999-12-31T00:00:00.000Z");
|
|
20926
|
+
/** The longest delay `setTimeout` honours; larger values fire at once. */
|
|
20927
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
20928
|
+
/** How long an expired task still answers `failed` before it is forgotten. */
|
|
20929
|
+
const EXPIRED_RETENTION_MS = 6e4;
|
|
20930
|
+
/** What core writes when recovery reclaims a crashed attempt it won't run again. */
|
|
20931
|
+
const CORE_LOST_MESSAGE = "Worker process terminated before task completed";
|
|
20932
|
+
const TASK_LOST_MESSAGE = "task lost: the server restarted while it was running";
|
|
20933
|
+
const TASK_EXPIRED_MESSAGE = "Task expired before it finished";
|
|
20934
|
+
const TERMINAL_ROW_STATUSES = /* @__PURE__ */ new Set([
|
|
20935
|
+
"completed",
|
|
20936
|
+
"failed",
|
|
20937
|
+
"cancelled",
|
|
20938
|
+
"timed_out"
|
|
20939
|
+
]);
|
|
20940
|
+
const OPEN_ROW_STATUSES = [
|
|
20941
|
+
"pending",
|
|
20942
|
+
"running",
|
|
20943
|
+
"suspended"
|
|
20944
|
+
];
|
|
20945
|
+
const UPDATE_ATTEMPTS = 3;
|
|
20946
|
+
/**
|
|
20947
|
+
* The row statuses {@link projectTask} can turn into each task status: a suspended row whose
|
|
20948
|
+
* requests are all answered is working, and an open row past its TTL is failed.
|
|
20949
|
+
*/
|
|
20950
|
+
const ROW_STATUSES_FOR = {
|
|
20951
|
+
working: [
|
|
20952
|
+
"pending",
|
|
20953
|
+
"running",
|
|
20954
|
+
"suspended"
|
|
20955
|
+
],
|
|
20956
|
+
input_required: ["suspended"],
|
|
20957
|
+
completed: ["completed"],
|
|
20958
|
+
cancelled: ["cancelled"],
|
|
20959
|
+
failed: [
|
|
20960
|
+
"failed",
|
|
20961
|
+
"timed_out",
|
|
20962
|
+
...OPEN_ROW_STATUSES
|
|
20963
|
+
]
|
|
20964
|
+
};
|
|
20965
|
+
/** How long an aborted attempt's row is watched for core to record why: from 50ms apart, backing off to 1s. */
|
|
20966
|
+
const ABORT_SETTLE_FIRST_READ_MS = 50;
|
|
20967
|
+
const ABORT_SETTLE_MAX_READ_MS = 1e3;
|
|
20968
|
+
/** The most rows one sweep reads, in each of its two listings. */
|
|
20969
|
+
const SWEEP_BATCH = 100;
|
|
20970
|
+
/** Running, or pending but pinned (`pinToWorker`) to the worker that holds its attempt. */
|
|
20971
|
+
function isOwned(row) {
|
|
20972
|
+
return row.status === "running" || row.status === "pending" && Boolean(row.ownerId);
|
|
20973
|
+
}
|
|
20974
|
+
function isTerminalRow(row) {
|
|
20975
|
+
return TERMINAL_ROW_STATUSES.has(row.status);
|
|
20976
|
+
}
|
|
20977
|
+
function envelopeOf(row) {
|
|
20978
|
+
const metadata = row.metadata;
|
|
20979
|
+
return metadata?.v === ENVELOPE_VERSION && typeof metadata.principalKey === "string" ? metadata : void 0;
|
|
20980
|
+
}
|
|
20981
|
+
function openInputRequests(envelope) {
|
|
20982
|
+
const answered = envelope.inputResponses ?? {};
|
|
20983
|
+
return Object.fromEntries(Object.entries(envelope.inputRequests ?? {}).filter(([key]) => !(key in answered)));
|
|
20984
|
+
}
|
|
20985
|
+
function isFullyAnswered(row, envelope) {
|
|
20986
|
+
return row.status === "suspended" && Object.keys(envelope.inputRequests ?? {}).length > 0 && Object.keys(openInputRequests(envelope)).length === 0;
|
|
20987
|
+
}
|
|
20988
|
+
/**
|
|
20989
|
+
* Stopped by a client, by expiry (the sweep cancels the row, then records it as failed), or by core
|
|
20990
|
+
* timing the attempt out at the task's TTL.
|
|
20991
|
+
*/
|
|
20992
|
+
function wasStopped(row, now) {
|
|
20993
|
+
return row.status === "cancelled" || row.status === "timed_out" || row.status === "failed" && isExpired(row, now);
|
|
20994
|
+
}
|
|
20995
|
+
function isExpired(row, now) {
|
|
20996
|
+
return row.expiresAt !== void 0 && row.expiresAt.getTime() <= now;
|
|
20997
|
+
}
|
|
20998
|
+
/** Terminal past its deadline: gone, whether or not cleanup has deleted the row yet. */
|
|
20999
|
+
function isForgotten(row, now) {
|
|
21000
|
+
return isTerminalRow(row) && row.expiresAt !== void 0 && row.expiresAt.getTime() + EXPIRED_RETENTION_MS <= now;
|
|
21001
|
+
}
|
|
21002
|
+
function latest(...dates) {
|
|
21003
|
+
return new Date(Math.max(...dates.filter((date) => date !== void 0).map((date) => date.getTime())));
|
|
21004
|
+
}
|
|
21005
|
+
/**
|
|
21006
|
+
* Projects a background-task row onto the MCP task it backs, as `tasks/get` answers it.
|
|
21007
|
+
*
|
|
21008
|
+
* A row still running past its TTL reads as `failed` before the sweep gets to it.
|
|
21009
|
+
*/
|
|
21010
|
+
function projectTask(row, envelope, taskId, now) {
|
|
21011
|
+
const base = {
|
|
21012
|
+
taskId,
|
|
21013
|
+
status: "working",
|
|
21014
|
+
createdAt: row.createdAt.toISOString(),
|
|
21015
|
+
lastUpdatedAt: latest(row.createdAt, row.startedAt, row.suspendedAt, row.completedAt).toISOString(),
|
|
21016
|
+
ttlMs: envelope.ttlMs,
|
|
21017
|
+
pollIntervalMs: envelope.pollIntervalMs ?? suggestedPollIntervalMs(row.createdAt.getTime(), now)
|
|
21018
|
+
};
|
|
21019
|
+
const failed = (message) => ({
|
|
21020
|
+
...base,
|
|
21021
|
+
status: "failed",
|
|
21022
|
+
error: {
|
|
21023
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21024
|
+
message
|
|
21025
|
+
}
|
|
21026
|
+
});
|
|
21027
|
+
if (!isTerminalRow(row) && isExpired(row, now)) return failed(TASK_EXPIRED_MESSAGE);
|
|
21028
|
+
switch (row.status) {
|
|
21029
|
+
case "pending":
|
|
21030
|
+
case "running": return base;
|
|
21031
|
+
case "suspended": {
|
|
21032
|
+
const open = openInputRequests(envelope);
|
|
21033
|
+
return Object.keys(open).length === 0 ? base : {
|
|
21034
|
+
...base,
|
|
21035
|
+
status: "input_required",
|
|
21036
|
+
inputRequests: open
|
|
21037
|
+
};
|
|
21038
|
+
}
|
|
21039
|
+
case "completed": return {
|
|
21040
|
+
...base,
|
|
21041
|
+
status: "completed",
|
|
21042
|
+
result: { ...row.result }
|
|
21043
|
+
};
|
|
21044
|
+
case "cancelled": return {
|
|
21045
|
+
...base,
|
|
21046
|
+
status: "cancelled"
|
|
21047
|
+
};
|
|
21048
|
+
case "timed_out": return failed(isExpired(row, now) ? TASK_EXPIRED_MESSAGE : row.error?.message ?? "Task timed out");
|
|
21049
|
+
case "failed": {
|
|
21050
|
+
const message = row.error?.message ?? "Task failed";
|
|
21051
|
+
return failed(message === CORE_LOST_MESSAGE ? TASK_LOST_MESSAGE : message);
|
|
21052
|
+
}
|
|
21053
|
+
}
|
|
21054
|
+
}
|
|
21055
|
+
/**
|
|
21056
|
+
* An {@link MCPTaskStore} on Mastra's background tasks: every MCP task is a background-task row in
|
|
21057
|
+
* Mastra's storage, so any instance sharing that storage can read, answer and cancel it, and run it.
|
|
21058
|
+
*
|
|
21059
|
+
* Rows are owned by `mcp-server:<serverId>` and keyed by a hash of the task id. Each attempt runs
|
|
21060
|
+
* the tool's work rebuilt from the row (`workFor`), through a static executor registered for
|
|
21061
|
+
* every task tool, so a worker other than the creating one can run it.
|
|
21062
|
+
*/
|
|
21063
|
+
var BackgroundTaskMCPTaskStore = class {
|
|
21064
|
+
#manager;
|
|
21065
|
+
#owner;
|
|
21066
|
+
#workFor;
|
|
21067
|
+
#recoverFor;
|
|
21068
|
+
#getLogger;
|
|
21069
|
+
#gates = /* @__PURE__ */ new Map();
|
|
21070
|
+
/**
|
|
21071
|
+
* Aborts for first attempts already running under their request, by row id, until the row's
|
|
21072
|
+
* executor takes the attempt over. A row pinned here can wait for a slot while its attempt runs,
|
|
21073
|
+
* and the manager can't abort an attempt it never started.
|
|
21074
|
+
*/
|
|
21075
|
+
#firstAttempts = /* @__PURE__ */ new Map();
|
|
21076
|
+
#executors = /* @__PURE__ */ new Set();
|
|
21077
|
+
#onCancel;
|
|
21078
|
+
#sweepTimer;
|
|
21079
|
+
#closed = false;
|
|
21080
|
+
#sweeping;
|
|
21081
|
+
/** The page of open rows the next sweep checks. */
|
|
21082
|
+
#sweepPage = 0;
|
|
21083
|
+
constructor(options) {
|
|
21084
|
+
this.#manager = options.manager;
|
|
21085
|
+
this.#owner = `mcp-server:${options.serverId}`;
|
|
21086
|
+
this.#workFor = options.workFor;
|
|
21087
|
+
this.#recoverFor = options.recoverFor;
|
|
21088
|
+
this.#onCancel = options.onCancel;
|
|
21089
|
+
this.#getLogger = options.logger;
|
|
21090
|
+
}
|
|
21091
|
+
/** The `agentId` every row of this server carries. */
|
|
21092
|
+
get owner() {
|
|
21093
|
+
return this.#owner;
|
|
21094
|
+
}
|
|
21095
|
+
/**
|
|
21096
|
+
* The row's `toolName`: `mcp-server:<serverId>:<tool>`. Core looks an executor up as
|
|
21097
|
+
* `<agentId>:<toolName>`, then as the bare `toolName`; the bare lookup hits the executor
|
|
21098
|
+
* registered under this name, never a Mastra tool named like the MCP tool. Where no executor is
|
|
21099
|
+
* registered (the server closed, an older instance, a worker without the server), the attempt
|
|
21100
|
+
* fails instead of running a same-named tool with the task's arguments.
|
|
21101
|
+
*/
|
|
21102
|
+
#rowToolName(toolName) {
|
|
21103
|
+
return `${this.#owner}:${toolName}`;
|
|
21104
|
+
}
|
|
21105
|
+
/**
|
|
21106
|
+
* Registers the executor that runs `toolName`'s tasks on any worker. It must be registered on
|
|
21107
|
+
* every instance before the manager recovers rows, or recovered attempts find no executor.
|
|
21108
|
+
*/
|
|
21109
|
+
registerTool(toolName) {
|
|
21110
|
+
const key = this.#rowToolName(toolName);
|
|
21111
|
+
this.#manager.registerStaticExecutor(key, { execute: (args, options) => {
|
|
21112
|
+
const { context } = args;
|
|
21113
|
+
return this.#execute(this.#workFor(context), args, options);
|
|
21114
|
+
} });
|
|
21115
|
+
this.#executors.add(key);
|
|
21116
|
+
}
|
|
21117
|
+
async create(params, { pinToWorker = false, clientLabel } = {}) {
|
|
21118
|
+
const context = params.context;
|
|
21119
|
+
if (!context || typeof context.toolName !== "string") throw new Error("A background-task MCP task needs the tool it runs in its context");
|
|
21120
|
+
if (!this.#executors.has(this.#rowToolName(context.toolName))) this.registerTool(context.toolName);
|
|
21121
|
+
this.#startSweeping();
|
|
21122
|
+
const taskId = createTaskId();
|
|
21123
|
+
const taskKey = taskKeyOf(taskId);
|
|
21124
|
+
const recover = this.#recoverFor(context.toolName);
|
|
21125
|
+
const principalKey = principalKeyOf(params.principal);
|
|
21126
|
+
if (params.limits) await this.#checkLimits(params.limits, principalKey);
|
|
21127
|
+
const envelope = {
|
|
21128
|
+
v: ENVELOPE_VERSION,
|
|
21129
|
+
principalKey,
|
|
21130
|
+
...clientLabel !== void 0 && { clientLabel },
|
|
21131
|
+
...params.trace && { trace: {
|
|
21132
|
+
traceId: params.trace.traceId,
|
|
21133
|
+
spanId: params.trace.spanId
|
|
21134
|
+
} },
|
|
21135
|
+
ttlMs: params.ttlMs,
|
|
21136
|
+
...params.pollIntervalMs !== void 0 && { pollIntervalMs: params.pollIntervalMs },
|
|
21137
|
+
recover,
|
|
21138
|
+
usedInputKeys: []
|
|
21139
|
+
};
|
|
21140
|
+
const createdAt = Date.now();
|
|
21141
|
+
const gate = {};
|
|
21142
|
+
gate.promise = new Promise((resolve) => gate.resolve = resolve);
|
|
21143
|
+
this.#gates.set(taskId, gate);
|
|
21144
|
+
let first = true;
|
|
21145
|
+
const taskContext = { executor: { execute: async (args, options) => {
|
|
21146
|
+
const { work, options: runOptions } = await gate.promise;
|
|
21147
|
+
const initial = first ? runOptions.initialAttempt : void 0;
|
|
21148
|
+
first = false;
|
|
21149
|
+
if (initial && gate.rowId) this.#firstAttempts.delete(gate.rowId);
|
|
21150
|
+
return this.#execute(work, args, options, initial);
|
|
21151
|
+
} } };
|
|
21152
|
+
const args = {
|
|
21153
|
+
taskKey,
|
|
21154
|
+
context
|
|
21155
|
+
};
|
|
21156
|
+
try {
|
|
21157
|
+
const { task, fallbackToSync } = await this.#manager.enqueue({
|
|
21158
|
+
runId: taskKey,
|
|
21159
|
+
toolName: this.#rowToolName(context.toolName),
|
|
21160
|
+
toolCallId: taskKey,
|
|
21161
|
+
args,
|
|
21162
|
+
agentId: this.#owner,
|
|
21163
|
+
resourceId: `${this.#owner}:${principalKey}`,
|
|
21164
|
+
timeoutMs: params.ttlMs === null ? MAX_TIMEOUT_MS : Math.min(Math.max(params.ttlMs, 1), MAX_TIMEOUT_MS),
|
|
21165
|
+
maxRetries: recover ? 1 : 0,
|
|
21166
|
+
expiresAt: params.ttlMs === null ? NEVER : new Date(createdAt + params.ttlMs),
|
|
21167
|
+
metadata: envelope
|
|
21168
|
+
}, taskContext, { pinToWorker });
|
|
21169
|
+
if (fallbackToSync) throw new Error("Background tasks are at capacity and backpressure is 'fallback-sync'; MCP tasks need 'queue'");
|
|
21170
|
+
gate.rowId = task.id;
|
|
21171
|
+
} catch (error) {
|
|
21172
|
+
this.#gates.delete(taskId);
|
|
21173
|
+
throw error;
|
|
21174
|
+
}
|
|
21175
|
+
return {
|
|
21176
|
+
taskId,
|
|
21177
|
+
status: "working",
|
|
21178
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
21179
|
+
lastUpdatedAt: new Date(createdAt).toISOString(),
|
|
21180
|
+
ttlMs: params.ttlMs,
|
|
21181
|
+
pollIntervalMs: params.pollIntervalMs ?? suggestedPollIntervalMs(createdAt, createdAt)
|
|
21182
|
+
};
|
|
21183
|
+
}
|
|
21184
|
+
/**
|
|
21185
|
+
* Counts open rows (not finished and not past their TTL) in storage, so the limits hold across
|
|
21186
|
+
* every instance sharing it. Counting and inserting are separate writes, so creates racing on
|
|
21187
|
+
* different instances can each pass the count and overshoot a limit by up to their number.
|
|
21188
|
+
*/
|
|
21189
|
+
async #checkLimits(limits, principalKey) {
|
|
21190
|
+
const open = {
|
|
21191
|
+
agentId: this.#owner,
|
|
21192
|
+
status: OPEN_ROW_STATUSES,
|
|
21193
|
+
dateFilterBy: "expiresAt",
|
|
21194
|
+
fromDate: new Date(Date.now() + 1),
|
|
21195
|
+
perPage: 1
|
|
21196
|
+
};
|
|
21197
|
+
if ((await this.#manager.listTasks({
|
|
21198
|
+
...open,
|
|
21199
|
+
resourceId: `${this.#owner}:${principalKey}`
|
|
21200
|
+
})).total >= limits.perPrincipal) throw new MCPTaskLimitError("principal", limits.perPrincipal);
|
|
21201
|
+
if ((await this.#manager.listTasks(open)).total >= limits.total) throw new MCPTaskLimitError("total", limits.total);
|
|
21202
|
+
}
|
|
21203
|
+
run(taskId, work, options = {}) {
|
|
21204
|
+
const gate = this.#gates.get(taskId);
|
|
21205
|
+
if (!gate) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21206
|
+
this.#gates.delete(taskId);
|
|
21207
|
+
const { initialAttempt } = options;
|
|
21208
|
+
if (initialAttempt && gate.rowId) {
|
|
21209
|
+
const rowId = gate.rowId;
|
|
21210
|
+
this.#firstAttempts.set(rowId, initialAttempt.abort);
|
|
21211
|
+
const done = () => {
|
|
21212
|
+
if (this.#firstAttempts.get(rowId) === initialAttempt.abort) this.#firstAttempts.delete(rowId);
|
|
21213
|
+
};
|
|
21214
|
+
initialAttempt.outcome.then(done, done);
|
|
21215
|
+
}
|
|
21216
|
+
gate.resolve({
|
|
21217
|
+
work,
|
|
21218
|
+
options
|
|
21219
|
+
});
|
|
21220
|
+
}
|
|
21221
|
+
async get(taskId, access) {
|
|
21222
|
+
this.#startSweeping();
|
|
21223
|
+
const found = await this.#find(taskKeyOf(taskId), access);
|
|
21224
|
+
if (!found) return void 0;
|
|
21225
|
+
const { row, envelope } = found;
|
|
21226
|
+
if (isTerminalRow(row)) this.#manager.deregisterTaskContext(row.id);
|
|
21227
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21228
|
+
return projectTask(row, envelope, taskId, Date.now());
|
|
21229
|
+
}
|
|
21230
|
+
async update(taskId, inputResponses, access) {
|
|
21231
|
+
const taskKey = taskKeyOf(taskId);
|
|
21232
|
+
return await this.#update(taskKey, inputResponses, `with key "${taskKey}"`, access) !== "not_found";
|
|
21233
|
+
}
|
|
21234
|
+
async cancel(taskId, access) {
|
|
21235
|
+
return await this.#cancel(taskKeyOf(taskId), access) !== "not_found";
|
|
21236
|
+
}
|
|
21237
|
+
/** Lists this server's tasks across principals, most recently created first. */
|
|
21238
|
+
async list(options = {}) {
|
|
21239
|
+
this.#startSweeping();
|
|
21240
|
+
const limit = options.limit ?? 50;
|
|
21241
|
+
const cursor = decodeTaskListCursor(options.cursor);
|
|
21242
|
+
const now = Date.now();
|
|
21243
|
+
const matching = [];
|
|
21244
|
+
for (let page = 0; matching.length <= limit; page++) {
|
|
21245
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21246
|
+
agentId: this.#owner,
|
|
21247
|
+
...options.surface && { toolName: this.#rowToolName(options.surface) },
|
|
21248
|
+
...options.principalKey && { resourceId: `${this.#owner}:${options.principalKey}` },
|
|
21249
|
+
...options.taskKey && { toolCallId: options.taskKey },
|
|
21250
|
+
...options.status && { status: ROW_STATUSES_FOR[options.status] },
|
|
21251
|
+
...cursor && { toDate: new Date(Date.parse(cursor.before) + 1) },
|
|
21252
|
+
orderBy: "createdAt",
|
|
21253
|
+
orderDirection: "desc",
|
|
21254
|
+
page,
|
|
21255
|
+
perPage: limit + 1
|
|
21256
|
+
});
|
|
21257
|
+
for (const row of tasks) {
|
|
21258
|
+
const envelope = envelopeOf(row);
|
|
21259
|
+
if (!envelope || isForgotten(row, now)) continue;
|
|
21260
|
+
const { context } = row.args;
|
|
21261
|
+
const listing = toListing(row.toolCallId, envelope.principalKey, context, projectTask(row, envelope, "", now), envelope.clientLabel);
|
|
21262
|
+
if (!isPastTaskListCursor(listing, cursor) || !matchesTaskListOptions(listing, options)) continue;
|
|
21263
|
+
matching.push(listing);
|
|
21264
|
+
if (matching.length > limit) break;
|
|
21265
|
+
}
|
|
21266
|
+
if (tasks.length <= limit) break;
|
|
21267
|
+
}
|
|
21268
|
+
const listings = matching.slice(0, limit);
|
|
21269
|
+
return {
|
|
21270
|
+
listings,
|
|
21271
|
+
...matching.length > limit && { nextCursor: nextTaskListCursor(listings, cursor) }
|
|
21272
|
+
};
|
|
21273
|
+
}
|
|
21274
|
+
updateByKey(taskKey, inputResponses) {
|
|
21275
|
+
return this.#update(taskKey, inputResponses, `with key "${taskKey}"`);
|
|
21276
|
+
}
|
|
21277
|
+
cancelByKey(taskKey) {
|
|
21278
|
+
return this.#cancel(taskKey);
|
|
21279
|
+
}
|
|
21280
|
+
/** Without `access`, any principal's task (admin). `label` names the task in errors. */
|
|
21281
|
+
async #update(taskKey, inputResponses, label, access) {
|
|
21282
|
+
this.#startSweeping();
|
|
21283
|
+
const found = await this.#find(taskKey, access);
|
|
21284
|
+
if (!found) return "not_found";
|
|
21285
|
+
let { row, envelope } = found;
|
|
21286
|
+
let stored = false;
|
|
21287
|
+
const storage = await this.#manager.getStorage();
|
|
21288
|
+
for (let attempt = 0; attempt < UPDATE_ATTEMPTS && row.status === "suspended"; attempt++) {
|
|
21289
|
+
const open = openInputRequests(envelope);
|
|
21290
|
+
const accepted = Object.entries(inputResponses).filter(([key]) => key in open);
|
|
21291
|
+
if (accepted.length === 0) break;
|
|
21292
|
+
const next = {
|
|
21293
|
+
...envelope,
|
|
21294
|
+
inputResponses: {
|
|
21295
|
+
...envelope.inputResponses,
|
|
21296
|
+
...Object.fromEntries(accepted)
|
|
21297
|
+
}
|
|
21298
|
+
};
|
|
21299
|
+
const written = await storage.updateTask(row.id, { metadata: next }, {
|
|
21300
|
+
expectedStatus: "suspended",
|
|
21301
|
+
expectedSuspendedAt: row.suspendedAt ?? null
|
|
21302
|
+
});
|
|
21303
|
+
const reread = await storage.getTask(row.id);
|
|
21304
|
+
const rereadEnvelope = reread && envelopeOf(reread);
|
|
21305
|
+
if (!reread || !rereadEnvelope) return "not_found";
|
|
21306
|
+
row = reread;
|
|
21307
|
+
envelope = rereadEnvelope;
|
|
21308
|
+
if (written && accepted.every(([key]) => key in (envelope.inputResponses ?? {}))) {
|
|
21309
|
+
stored = true;
|
|
21310
|
+
break;
|
|
21311
|
+
}
|
|
21312
|
+
}
|
|
21313
|
+
if (!stored && row.status === "suspended") {
|
|
21314
|
+
const open = openInputRequests(envelope);
|
|
21315
|
+
if (Object.keys(inputResponses).some((key) => key in open && !(key in (envelope.inputResponses ?? {})))) throw new Error(`Could not store the input responses for MCP task ${label}`);
|
|
21316
|
+
}
|
|
21317
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21318
|
+
return stored ? "done" : "ignored";
|
|
21319
|
+
}
|
|
21320
|
+
async traceOf(taskId, access) {
|
|
21321
|
+
return (await this.#find(taskKeyOf(taskId), access))?.envelope.trace;
|
|
21322
|
+
}
|
|
21323
|
+
async #cancel(taskKey, access) {
|
|
21324
|
+
this.#startSweeping();
|
|
21325
|
+
const found = await this.#find(taskKey, access);
|
|
21326
|
+
if (!found) return "not_found";
|
|
21327
|
+
if (isTerminalRow(found.row)) return "ignored";
|
|
21328
|
+
try {
|
|
21329
|
+
await this.#manager.cancel(found.row.id);
|
|
21330
|
+
} catch (error) {
|
|
21331
|
+
this.#getLogger?.().warn("Could not cancel MCP task", { error });
|
|
21332
|
+
}
|
|
21333
|
+
this.#abortFirstAttempt(found.row.id, /* @__PURE__ */ new Error("Task cancelled"));
|
|
21334
|
+
this.#notifyCancel(found.row);
|
|
21335
|
+
return "done";
|
|
21336
|
+
}
|
|
21337
|
+
/** Fire and forget: a slow hook must hold up neither the `tasks/cancel` ack nor the sweep. */
|
|
21338
|
+
#notifyCancel(row) {
|
|
21339
|
+
const onCancel = this.#onCancel;
|
|
21340
|
+
if (!onCancel) return;
|
|
21341
|
+
const context = row.args.context;
|
|
21342
|
+
Promise.resolve().then(() => onCancel(context)).catch((error) => this.#getLogger?.().warn("Could not stop what a cancelled MCP task stands for", { error }));
|
|
21343
|
+
}
|
|
21344
|
+
/**
|
|
21345
|
+
* An attempt here was aborted. The abort says why only in core's message, so read the row: if
|
|
21346
|
+
* another instance cancelled or expired the task, or core timed the attempt out, run `onCancel`
|
|
21347
|
+
* here as well, where the work may be executing (a workflow run, say). Core records a timeout only
|
|
21348
|
+
* once the aborted attempt settles, so a row still running under this worker is read again, less
|
|
21349
|
+
* often each time and again after a failed read, until core records how the attempt ended. A shutdown releases the lease and a
|
|
21350
|
+
* lost lease changes the owner: either leaves the row running without this worker, and the work
|
|
21351
|
+
* alone.
|
|
21352
|
+
*/
|
|
21353
|
+
#notifyIfCancelled(taskKey) {
|
|
21354
|
+
const check = async () => {
|
|
21355
|
+
for (let wait = ABORT_SETTLE_FIRST_READ_MS; !this.#closed; wait = Math.min(wait * 2, ABORT_SETTLE_MAX_READ_MS)) {
|
|
21356
|
+
let row;
|
|
21357
|
+
try {
|
|
21358
|
+
row = (await this.#manager.listTasks({
|
|
21359
|
+
agentId: this.#owner,
|
|
21360
|
+
toolCallId: taskKey,
|
|
21361
|
+
perPage: 1
|
|
21362
|
+
})).tasks[0];
|
|
21363
|
+
} catch (error) {
|
|
21364
|
+
this.#getLogger?.().warn("Could not read an aborted MCP task attempt; reading it again", { error });
|
|
21365
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
21366
|
+
continue;
|
|
21367
|
+
}
|
|
21368
|
+
if (!row) return;
|
|
21369
|
+
if (wasStopped(row, Date.now())) return this.#notifyCancel(row);
|
|
21370
|
+
if (row.status !== "running" || row.ownerId !== this.#manager.ownerId) return;
|
|
21371
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
21372
|
+
}
|
|
21373
|
+
};
|
|
21374
|
+
check().catch((error) => this.#getLogger?.().warn("Could not check why an MCP task attempt was aborted", { error }));
|
|
21375
|
+
}
|
|
21376
|
+
#abortFirstAttempt(rowId, reason) {
|
|
21377
|
+
const abort = this.#firstAttempts.get(rowId);
|
|
21378
|
+
if (!abort) return;
|
|
21379
|
+
this.#firstAttempts.delete(rowId);
|
|
21380
|
+
abort(reason);
|
|
21381
|
+
}
|
|
21382
|
+
/**
|
|
21383
|
+
* Stops sweeping, waits for a sweep already running to stop at its next step, then unregisters
|
|
21384
|
+
* this store's executors. Rows stay in storage.
|
|
21385
|
+
*/
|
|
21386
|
+
async close() {
|
|
21387
|
+
this.#closed = true;
|
|
21388
|
+
clearInterval(this.#sweepTimer);
|
|
21389
|
+
this.#sweepTimer = void 0;
|
|
21390
|
+
await this.#sweeping?.catch(() => {});
|
|
21391
|
+
for (const key of this.#executors) this.#manager.unregisterStaticExecutor(key);
|
|
21392
|
+
this.#executors.clear();
|
|
21393
|
+
}
|
|
21394
|
+
/**
|
|
21395
|
+
* Repairs what no request would: expires rows past their TTL, resumes answered rows whose
|
|
21396
|
+
* process-local resume was lost, and settles rows whose worker died while this manager was
|
|
21397
|
+
* already running (the manager itself only recovers rows when it starts).
|
|
21398
|
+
*/
|
|
21399
|
+
sweep() {
|
|
21400
|
+
if (this.#closed) return Promise.resolve();
|
|
21401
|
+
this.#sweeping ??= this.#sweep().finally(() => this.#sweeping = void 0);
|
|
21402
|
+
return this.#sweeping;
|
|
21403
|
+
}
|
|
21404
|
+
/**
|
|
21405
|
+
* Starts the periodic sweep before any task request, so this instance settles rows a dead worker
|
|
21406
|
+
* left behind even if no client ever asks it about a task. Does nothing once the store is closed.
|
|
21407
|
+
*/
|
|
21408
|
+
startSweeping() {
|
|
21409
|
+
this.#startSweeping();
|
|
21410
|
+
}
|
|
21411
|
+
#startSweeping() {
|
|
21412
|
+
if (this.#sweepTimer || this.#closed) return;
|
|
21413
|
+
this.#sweepTimer = setInterval(() => {
|
|
21414
|
+
this.sweep().catch((error) => this.#getLogger?.().debug("MCP task sweep failed", { error }));
|
|
21415
|
+
}, this.#manager.config.leaseDurationMs);
|
|
21416
|
+
this.#sweepTimer.unref?.();
|
|
21417
|
+
}
|
|
21418
|
+
/**
|
|
21419
|
+
* Bounded per tick, however many rows are open: it expires at most {@link SWEEP_BATCH} rows past
|
|
21420
|
+
* their TTL, and checks one page of {@link SWEEP_BATCH} other open rows, moving to the next page
|
|
21421
|
+
* on the next tick. Rows that need a restart are restarted only while slots are free.
|
|
21422
|
+
*/
|
|
21423
|
+
async #sweep() {
|
|
21424
|
+
const storage = await this.#manager.getStorage();
|
|
21425
|
+
const now = Date.now();
|
|
21426
|
+
const lease = this.#manager.config.leaseDurationMs;
|
|
21427
|
+
for (const rowId of this.#firstAttempts.keys()) {
|
|
21428
|
+
if (this.#closed) return;
|
|
21429
|
+
const current = await storage.getTask(rowId).catch(() => void 0);
|
|
21430
|
+
if (current && isTerminalRow(current) && !this.#closed) this.#abortFirstAttempt(rowId, /* @__PURE__ */ new Error("Task cancelled"));
|
|
21431
|
+
}
|
|
21432
|
+
if (this.#closed) return;
|
|
21433
|
+
if (storage.supportsTaskExpiry) await storage.deleteTasks({
|
|
21434
|
+
agentId: this.#owner,
|
|
21435
|
+
status: [...TERMINAL_ROW_STATUSES],
|
|
21436
|
+
dateFilterBy: "expiresAt",
|
|
21437
|
+
toDate: /* @__PURE__ */ new Date(now - EXPIRED_RETENTION_MS)
|
|
21438
|
+
}).catch((error) => this.#getLogger?.().debug("Could not delete expired MCP tasks", { error }));
|
|
21439
|
+
if (this.#closed) return;
|
|
21440
|
+
const { tasks: expired } = await this.#manager.listTasks({
|
|
21441
|
+
agentId: this.#owner,
|
|
21442
|
+
status: OPEN_ROW_STATUSES,
|
|
21443
|
+
dateFilterBy: "expiresAt",
|
|
21444
|
+
toDate: new Date(now + 1),
|
|
21445
|
+
perPage: SWEEP_BATCH
|
|
21446
|
+
});
|
|
21447
|
+
for (const row of expired) {
|
|
21448
|
+
if (this.#closed) return;
|
|
21449
|
+
if (!envelopeOf(row)) continue;
|
|
21450
|
+
await this.#expire(storage, row).catch((error) => this.#getLogger?.().debug("Could not expire MCP task", { error }));
|
|
21451
|
+
}
|
|
21452
|
+
if (this.#closed) return;
|
|
21453
|
+
const { tasks: rows } = await this.#manager.listTasks({
|
|
21454
|
+
agentId: this.#owner,
|
|
21455
|
+
status: OPEN_ROW_STATUSES,
|
|
21456
|
+
orderBy: "createdAt",
|
|
21457
|
+
orderDirection: "asc",
|
|
21458
|
+
page: this.#sweepPage,
|
|
21459
|
+
perPage: SWEEP_BATCH
|
|
21460
|
+
});
|
|
21461
|
+
this.#sweepPage = rows.length < SWEEP_BATCH ? 0 : this.#sweepPage + 1;
|
|
21462
|
+
const restarts = [];
|
|
21463
|
+
for (const row of rows) {
|
|
21464
|
+
if (this.#closed) return;
|
|
21465
|
+
const envelope = envelopeOf(row);
|
|
21466
|
+
if (!envelope || isExpired(row, now)) continue;
|
|
21467
|
+
try {
|
|
21468
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21469
|
+
else if (isOwned(row) && (!row.leaseExpiresAt || row.leaseExpiresAt.getTime() <= now)) {
|
|
21470
|
+
if (await this.#reclaim(storage, row, envelope) && !this.#closed) restarts.push(row.id);
|
|
21471
|
+
} else if (row.status === "pending" && !row.ownerId && now - row.createdAt.getTime() >= lease) restarts.push(row.id);
|
|
21472
|
+
} catch (error) {
|
|
21473
|
+
this.#getLogger?.().debug("Could not sweep MCP task", { error });
|
|
21474
|
+
}
|
|
21475
|
+
}
|
|
21476
|
+
if (restarts.length === 0 || this.#closed) return;
|
|
21477
|
+
const free = await this.#freeSlots(storage);
|
|
21478
|
+
for (const rowId of restarts.slice(0, free)) {
|
|
21479
|
+
if (this.#closed) return;
|
|
21480
|
+
await this.#restart(rowId);
|
|
21481
|
+
}
|
|
21482
|
+
}
|
|
21483
|
+
/** Slots this server's rows can take now, by the manager's global and per-owner limits. */
|
|
21484
|
+
async #freeSlots(storage) {
|
|
21485
|
+
const { globalConcurrency, perAgentConcurrency, perOwnerConcurrency } = this.#manager.config;
|
|
21486
|
+
const ownerLimit = perOwnerConcurrency && Object.hasOwn(perOwnerConcurrency, this.#owner) ? perOwnerConcurrency[this.#owner] : perAgentConcurrency;
|
|
21487
|
+
const [running, mine] = await Promise.all([storage.getRunningCount(), storage.getRunningCountByAgent(this.#owner)]);
|
|
21488
|
+
return Math.max(0, Math.min(globalConcurrency - running, ownerLimit - mine));
|
|
21489
|
+
}
|
|
21490
|
+
/** Aborts the work, then records the task as failed until its retention ends. */
|
|
21491
|
+
async #expire(storage, row) {
|
|
21492
|
+
await this.#manager.cancel(row.id);
|
|
21493
|
+
this.#abortFirstAttempt(row.id, /* @__PURE__ */ new Error(TASK_EXPIRED_MESSAGE));
|
|
21494
|
+
this.#notifyCancel(row);
|
|
21495
|
+
await storage.updateTask(row.id, {
|
|
21496
|
+
status: "failed",
|
|
21497
|
+
error: { message: TASK_EXPIRED_MESSAGE },
|
|
21498
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
21499
|
+
expiresAt: new Date(Date.now() + EXPIRED_RETENTION_MS)
|
|
21500
|
+
}, { expectedStatus: "cancelled" });
|
|
21501
|
+
}
|
|
21502
|
+
/**
|
|
21503
|
+
* A running row, or a pending row pinned to the worker running its first attempt, whose lease
|
|
21504
|
+
* lapsed or was released: its worker died, or shut down, with the attempt in flight. Fenced like
|
|
21505
|
+
* the manager's own recovery, so of all instances (and managers) noticing it, exactly one
|
|
21506
|
+
* settles it. Returns whether it won the row back to pending, for the sweep to restart.
|
|
21507
|
+
*/
|
|
21508
|
+
async #reclaim(storage, row, envelope) {
|
|
21509
|
+
const fence = {
|
|
21510
|
+
expectedStatus: row.status,
|
|
21511
|
+
expectedOwnerId: row.ownerId ?? null,
|
|
21512
|
+
expectedLeaseExpiresAt: row.leaseExpiresAt ?? null
|
|
21513
|
+
};
|
|
21514
|
+
if (envelope.recover && row.retryCount < row.maxRetries) return await storage.updateTask(row.id, {
|
|
21515
|
+
status: "pending",
|
|
21516
|
+
retryCount: row.retryCount + 1,
|
|
21517
|
+
ownerId: void 0,
|
|
21518
|
+
leaseExpiresAt: void 0
|
|
21519
|
+
}, fence);
|
|
21520
|
+
await storage.updateTask(row.id, {
|
|
21521
|
+
status: "failed",
|
|
21522
|
+
error: { message: TASK_LOST_MESSAGE },
|
|
21523
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
21524
|
+
ownerId: void 0,
|
|
21525
|
+
leaseExpiresAt: void 0
|
|
21526
|
+
}, fence);
|
|
21527
|
+
return false;
|
|
21528
|
+
}
|
|
21529
|
+
/** Dispatches a reclaimed row; the dispatch claim is fenced, so it runs once however often this is called. */
|
|
21530
|
+
async #restart(rowId) {
|
|
21531
|
+
try {
|
|
21532
|
+
await this.#manager.restart(rowId);
|
|
21533
|
+
} catch (error) {
|
|
21534
|
+
this.#getLogger?.().debug("Could not restart MCP task", { error });
|
|
21535
|
+
}
|
|
21536
|
+
}
|
|
21537
|
+
/** Without `access`, any principal's task (admin). */
|
|
21538
|
+
async #find(taskKey, access) {
|
|
21539
|
+
const principalKey = access && principalKeyOf(access.principal);
|
|
21540
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21541
|
+
agentId: this.#owner,
|
|
21542
|
+
...principalKey !== void 0 && { resourceId: `${this.#owner}:${principalKey}` },
|
|
21543
|
+
toolCallId: taskKey,
|
|
21544
|
+
perPage: 1
|
|
21545
|
+
});
|
|
21546
|
+
const row = tasks[0];
|
|
21547
|
+
const envelope = row && envelopeOf(row);
|
|
21548
|
+
if (!row || !envelope || principalKey !== void 0 && envelope.principalKey !== principalKey) return void 0;
|
|
21549
|
+
if (isForgotten(row, Date.now())) return void 0;
|
|
21550
|
+
return {
|
|
21551
|
+
row,
|
|
21552
|
+
envelope
|
|
21553
|
+
};
|
|
21554
|
+
}
|
|
21555
|
+
async #resume(row, envelope) {
|
|
21556
|
+
const resumeData = {
|
|
21557
|
+
inputResponses: envelope.inputResponses ?? {},
|
|
21558
|
+
suspendPayload: row.suspendPayload
|
|
21559
|
+
};
|
|
21560
|
+
try {
|
|
21561
|
+
await this.#manager.resume(row.id, resumeData, { onNoSlot: "queue" });
|
|
21562
|
+
} catch (error) {
|
|
21563
|
+
this.#getLogger?.().debug("Could not resume MCP task", { error });
|
|
21564
|
+
}
|
|
21565
|
+
}
|
|
21566
|
+
/**
|
|
21567
|
+
* Counts the attempt this worker just claimed, for a traced task, and describes it for its span.
|
|
21568
|
+
* The write is fenced on the claim; losing it only costs the span its number.
|
|
21569
|
+
*/
|
|
21570
|
+
async #countAttempt(taskKey, resumed) {
|
|
21571
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21572
|
+
agentId: this.#owner,
|
|
21573
|
+
toolCallId: taskKey,
|
|
21574
|
+
perPage: 1
|
|
21575
|
+
});
|
|
21576
|
+
const row = tasks[0];
|
|
21577
|
+
const envelope = row && envelopeOf(row);
|
|
21578
|
+
if (!row || !envelope?.trace) return void 0;
|
|
21579
|
+
const attempt = (envelope.attempts ?? 0) + 1;
|
|
21580
|
+
await (await this.#manager.getStorage()).updateTask(row.id, { metadata: {
|
|
21581
|
+
...envelope,
|
|
21582
|
+
attempts: attempt
|
|
21583
|
+
} }, {
|
|
21584
|
+
expectedStatus: "running",
|
|
21585
|
+
expectedOwnerId: row.ownerId ?? null
|
|
21586
|
+
}).catch(() => void 0);
|
|
21587
|
+
return {
|
|
21588
|
+
...envelope.trace,
|
|
21589
|
+
taskKey,
|
|
21590
|
+
attempt,
|
|
21591
|
+
resumed,
|
|
21592
|
+
recovered: !resumed && attempt > 1
|
|
21593
|
+
};
|
|
21594
|
+
}
|
|
21595
|
+
/** Runs one attempt and turns its outcome into what the background-task workflow records. */
|
|
21596
|
+
async #execute(work, args, options, initial) {
|
|
21597
|
+
const signal = options?.abortSignal ?? new AbortController().signal;
|
|
21598
|
+
const resumeData = options?.resumeData;
|
|
21599
|
+
const trace = await this.#countAttempt(args.taskKey, resumeData !== void 0);
|
|
21600
|
+
let outcome;
|
|
21601
|
+
if (initial) {
|
|
21602
|
+
const abort = () => initial.abort(signal.reason);
|
|
21603
|
+
if (signal.aborted) abort();
|
|
21604
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
21605
|
+
outcome = initial.outcome.finally(() => signal.removeEventListener("abort", abort));
|
|
21606
|
+
} else {
|
|
21607
|
+
const attempt = {
|
|
21608
|
+
signal,
|
|
21609
|
+
...trace && { trace },
|
|
21610
|
+
...resumeData && { resume: {
|
|
21611
|
+
inputResponses: resumeData.inputResponses,
|
|
21612
|
+
suspendPayload: resumeData.suspendPayload
|
|
21613
|
+
} }
|
|
21614
|
+
};
|
|
21615
|
+
outcome = Promise.resolve().then(() => work(attempt));
|
|
21616
|
+
}
|
|
21617
|
+
const result = await new Promise((resolve, reject) => {
|
|
21618
|
+
const onAbort = () => {
|
|
21619
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("Task cancelled"));
|
|
21620
|
+
this.#notifyIfCancelled(args.taskKey);
|
|
21621
|
+
};
|
|
21622
|
+
if (signal.aborted) onAbort();
|
|
21623
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
21624
|
+
outcome.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
21625
|
+
});
|
|
21626
|
+
if (result.status === "completed") return result.result;
|
|
21627
|
+
const storage = await this.#manager.getStorage();
|
|
21628
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21629
|
+
agentId: this.#owner,
|
|
21630
|
+
toolCallId: args.taskKey,
|
|
21631
|
+
perPage: 1
|
|
21632
|
+
});
|
|
21633
|
+
const row = tasks[0];
|
|
21634
|
+
const envelope = row && envelopeOf(row);
|
|
21635
|
+
if (!row || !envelope) throw new Error("MCP task row is gone");
|
|
21636
|
+
const keys = Object.keys(result.inputRequests);
|
|
21637
|
+
if (keys.length === 0) throw new Error("Task asked for input without any input request");
|
|
21638
|
+
const reused = keys.find((key) => envelope.usedInputKeys.includes(key));
|
|
21639
|
+
if (reused !== void 0) throw new Error(`Input request key "${reused}" was already used by this task`);
|
|
21640
|
+
const next = {
|
|
21641
|
+
...envelope,
|
|
21642
|
+
inputRequests: result.inputRequests,
|
|
21643
|
+
inputResponses: {},
|
|
21644
|
+
usedInputKeys: [...envelope.usedInputKeys, ...keys]
|
|
21645
|
+
};
|
|
21646
|
+
if (!await storage.updateTask(row.id, { metadata: next }, {
|
|
21647
|
+
expectedStatus: "running",
|
|
21648
|
+
expectedOwnerId: this.#manager.ownerId
|
|
21649
|
+
})) throw new Error("MCP task is no longer running here");
|
|
21650
|
+
if (!options?.suspend) throw new Error("Background task runtime cannot suspend this task");
|
|
21651
|
+
await options.suspend(result.suspendPayload);
|
|
21652
|
+
}
|
|
21653
|
+
};
|
|
21654
|
+
// src/server/tasks/in-memory-store.ts
|
|
21655
|
+
const DEFAULT_EXPIRED_RETENTION_MS = 6e4;
|
|
21656
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
21657
|
+
/**
|
|
21658
|
+
* Default {@link MCPTaskStore}: tasks live in this process. A task created here can only be read on
|
|
21659
|
+
* the same instance and does not survive a restart, so use it for single-instance servers.
|
|
21660
|
+
*
|
|
21661
|
+
* When a task's TTL elapses its work is aborted and it turns `failed`; it is forgotten after
|
|
21662
|
+
* `expiredRetentionMs`. Finished tasks stay in memory until then, so `limits.retainedPerPrincipal`
|
|
21663
|
+
* and `limits.retainedTotal` bound how many tasks it holds; a task with a `null` TTL counts until
|
|
21664
|
+
* the store closes.
|
|
21665
|
+
*
|
|
21666
|
+
* @experimental SEP-2663 extension; API may change.
|
|
21667
|
+
*/
|
|
21668
|
+
var InMemoryMCPTaskStore = class {
|
|
21669
|
+
#tasks = /* @__PURE__ */ new Map();
|
|
21670
|
+
/** The same records, by task key. */
|
|
21671
|
+
#byKey = /* @__PURE__ */ new Map();
|
|
21672
|
+
/** Open (non-terminal) tasks per principal, and in total. */
|
|
21673
|
+
#openByPrincipal = /* @__PURE__ */ new Map();
|
|
21674
|
+
#open = 0;
|
|
21675
|
+
/** Every record held (open or finished, until purged) per principal, and in total. */
|
|
21676
|
+
#retainedByPrincipal = /* @__PURE__ */ new Map();
|
|
21677
|
+
#now;
|
|
21678
|
+
#expiredRetentionMs;
|
|
21679
|
+
#onCancel;
|
|
21680
|
+
constructor(options = {}) {
|
|
21681
|
+
this.#now = options.now ?? Date.now;
|
|
21682
|
+
this.#expiredRetentionMs = options.expiredRetentionMs ?? DEFAULT_EXPIRED_RETENTION_MS;
|
|
21683
|
+
this.#onCancel = options.onCancel;
|
|
21684
|
+
}
|
|
21685
|
+
async create(params, { clientLabel } = {}) {
|
|
21686
|
+
assertNonNegativeInteger("ttlMs", params.ttlMs);
|
|
21687
|
+
assertNonNegativeInteger("pollIntervalMs", params.pollIntervalMs);
|
|
21688
|
+
const openForPrincipal = this.#openByPrincipal.get(params.principal) ?? 0;
|
|
21689
|
+
const retainedForPrincipal = this.#retainedByPrincipal.get(params.principal) ?? 0;
|
|
21690
|
+
const { limits } = params;
|
|
21691
|
+
if (limits) {
|
|
21692
|
+
if (openForPrincipal >= limits.perPrincipal) throw new MCPTaskLimitError("principal", limits.perPrincipal);
|
|
21693
|
+
if (this.#open >= limits.total) throw new MCPTaskLimitError("total", limits.total);
|
|
21694
|
+
if (limits.retainedPerPrincipal !== void 0 && retainedForPrincipal >= limits.retainedPerPrincipal) throw new MCPTaskLimitError("principal", limits.retainedPerPrincipal, "retained");
|
|
21695
|
+
if (limits.retainedTotal !== void 0 && this.#tasks.size >= limits.retainedTotal) throw new MCPTaskLimitError("total", limits.retainedTotal, "retained");
|
|
21696
|
+
}
|
|
21697
|
+
const now = this.#now();
|
|
21698
|
+
const taskId = createTaskId();
|
|
21699
|
+
const record = {
|
|
21700
|
+
taskId,
|
|
21701
|
+
taskKey: taskKeyOf(taskId),
|
|
21702
|
+
principal: params.principal,
|
|
21703
|
+
clientLabel,
|
|
21704
|
+
context: params.context,
|
|
21705
|
+
...params.trace && { trace: params.trace },
|
|
21706
|
+
attempts: 0,
|
|
21707
|
+
status: "working",
|
|
21708
|
+
createdAt: now,
|
|
21709
|
+
lastUpdatedAt: now,
|
|
21710
|
+
ttlMs: params.ttlMs,
|
|
21711
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
21712
|
+
usedInputKeys: /* @__PURE__ */ new Set(),
|
|
21713
|
+
abort: new AbortController()
|
|
21714
|
+
};
|
|
21715
|
+
if (record.ttlMs !== null) this.#arm(record, now + record.ttlMs, () => this.#expire(record));
|
|
21716
|
+
this.#tasks.set(record.taskId, record);
|
|
21717
|
+
this.#byKey.set(record.taskKey, record);
|
|
21718
|
+
this.#openByPrincipal.set(record.principal, openForPrincipal + 1);
|
|
21719
|
+
this.#open++;
|
|
21720
|
+
this.#retainedByPrincipal.set(record.principal, retainedForPrincipal + 1);
|
|
21721
|
+
return this.#snapshot(record);
|
|
21722
|
+
}
|
|
21723
|
+
async get(taskId, access) {
|
|
21724
|
+
const record = this.#lookup(taskKeyOf(taskId), access);
|
|
21725
|
+
return record && this.#detailed(record);
|
|
21726
|
+
}
|
|
21727
|
+
async update(taskId, inputResponses, access) {
|
|
21728
|
+
return this.#update(taskKeyOf(taskId), inputResponses, access) !== "not_found";
|
|
21729
|
+
}
|
|
21730
|
+
async cancel(taskId, access) {
|
|
21731
|
+
return this.#cancel(taskKeyOf(taskId), access) !== "not_found";
|
|
21732
|
+
}
|
|
21733
|
+
async list(options = {}) {
|
|
21734
|
+
const limit = options.limit ?? 50;
|
|
21735
|
+
const cursor = decodeTaskListCursor(options.cursor);
|
|
21736
|
+
const matching = [];
|
|
21737
|
+
const now = this.#now();
|
|
21738
|
+
for (const record of [...this.#tasks.values()].reverse()) {
|
|
21739
|
+
if (record.purgeAt !== void 0 && now >= record.purgeAt) {
|
|
21740
|
+
this.#purge(record);
|
|
21741
|
+
continue;
|
|
21742
|
+
}
|
|
21743
|
+
if (options.taskKey && record.taskKey !== options.taskKey) continue;
|
|
21744
|
+
const listing = toListing(record.taskKey, principalKeyOf(record.principal), record.context, this.#detailed(record), record.clientLabel);
|
|
21745
|
+
if (!isPastTaskListCursor(listing, cursor) || !matchesTaskListOptions(listing, options)) continue;
|
|
21746
|
+
matching.push(listing);
|
|
21747
|
+
if (matching.length > limit) break;
|
|
21748
|
+
}
|
|
21749
|
+
const listings = matching.slice(0, limit);
|
|
21750
|
+
return {
|
|
21751
|
+
listings,
|
|
21752
|
+
...matching.length > limit && { nextCursor: nextTaskListCursor(listings, cursor) }
|
|
21753
|
+
};
|
|
21754
|
+
}
|
|
21755
|
+
async updateByKey(taskKey, inputResponses) {
|
|
21756
|
+
return this.#update(taskKey, inputResponses);
|
|
21757
|
+
}
|
|
21758
|
+
async cancelByKey(taskKey) {
|
|
21759
|
+
return this.#cancel(taskKey);
|
|
21760
|
+
}
|
|
21761
|
+
#update(taskKey, inputResponses, access) {
|
|
21762
|
+
const record = this.#lookup(taskKey, access);
|
|
21763
|
+
if (!record) return "not_found";
|
|
21764
|
+
const pending = record.pendingInput;
|
|
21765
|
+
if (!pending) return "ignored";
|
|
21766
|
+
let accepted = false;
|
|
21767
|
+
for (const [key, response] of Object.entries(inputResponses)) if (Object.hasOwn(pending.requests, key) && !pending.responses.has(key)) {
|
|
21768
|
+
pending.responses.set(key, response);
|
|
21769
|
+
accepted = true;
|
|
21770
|
+
}
|
|
21771
|
+
if (Object.keys(pending.requests).every((key) => pending.responses.has(key))) {
|
|
21772
|
+
record.pendingInput = void 0;
|
|
21773
|
+
this.#transition(record, "working");
|
|
21774
|
+
pending.resolve(Object.fromEntries(pending.responses));
|
|
21775
|
+
}
|
|
21776
|
+
return accepted ? "done" : "ignored";
|
|
21777
|
+
}
|
|
21778
|
+
#cancel(taskKey, access) {
|
|
21779
|
+
const record = this.#lookup(taskKey, access);
|
|
21780
|
+
if (!record) return "not_found";
|
|
21781
|
+
if (isTerminalTaskStatus(record.status)) return "ignored";
|
|
21782
|
+
this.#finish(record, "cancelled", /* @__PURE__ */ new Error("Task was cancelled"));
|
|
21783
|
+
this.#notifyCancel(record);
|
|
21784
|
+
return "done";
|
|
21785
|
+
}
|
|
21786
|
+
async traceOf(taskId, access) {
|
|
21787
|
+
return this.#lookup(taskKeyOf(taskId), access)?.trace;
|
|
21788
|
+
}
|
|
21789
|
+
/** Whether this store holds the task, for any caller. */
|
|
21790
|
+
has(taskId) {
|
|
21791
|
+
return this.#tasks.has(taskId);
|
|
21792
|
+
}
|
|
21793
|
+
/** The context recorded at creation. */
|
|
21794
|
+
context(taskId) {
|
|
21795
|
+
return this.#tasks.get(taskId)?.context;
|
|
21796
|
+
}
|
|
21797
|
+
run(taskId, work, options = {}) {
|
|
21798
|
+
const record = this.#tasks.get(taskId);
|
|
21799
|
+
if (!record) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21800
|
+
const initial = options.initialAttempt;
|
|
21801
|
+
if (!initial) {
|
|
21802
|
+
this.#attempt(record, work);
|
|
21803
|
+
return;
|
|
21804
|
+
}
|
|
21805
|
+
record.attempts++;
|
|
21806
|
+
const signal = record.abort.signal;
|
|
21807
|
+
const abort = () => initial.abort(signal.reason);
|
|
21808
|
+
if (signal.aborted) abort();
|
|
21809
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
21810
|
+
this.#settle(record, work, initial.outcome.finally(() => signal.removeEventListener("abort", abort)));
|
|
21811
|
+
}
|
|
21812
|
+
#attempt(record, work, resume) {
|
|
21813
|
+
record.attempts++;
|
|
21814
|
+
const attempt = {
|
|
21815
|
+
signal: record.abort.signal,
|
|
21816
|
+
...resume && { resume },
|
|
21817
|
+
...record.trace && { trace: {
|
|
21818
|
+
...record.trace,
|
|
21819
|
+
taskKey: taskKeyOf(record.taskId),
|
|
21820
|
+
attempt: record.attempts,
|
|
21821
|
+
resumed: resume !== void 0,
|
|
21822
|
+
recovered: false
|
|
21823
|
+
} }
|
|
21824
|
+
};
|
|
21825
|
+
this.#settle(record, work, Promise.resolve().then(() => work(attempt)));
|
|
21826
|
+
}
|
|
21827
|
+
#settle(record, work, outcome) {
|
|
21828
|
+
outcome.then((result) => this.#record(record, work, result), (error) => this.#fail(record, {
|
|
21829
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21830
|
+
message: error instanceof Error ? error.message : String(error)
|
|
21831
|
+
}));
|
|
21832
|
+
}
|
|
21833
|
+
#record(record, work, outcome) {
|
|
21834
|
+
if (isTerminalTaskStatus(record.status)) return;
|
|
21835
|
+
if (outcome.status === "completed") {
|
|
21836
|
+
record.result = outcome.result;
|
|
21837
|
+
this.#finish(record, "completed");
|
|
21838
|
+
return;
|
|
21839
|
+
}
|
|
21840
|
+
const keys = Object.keys(outcome.inputRequests);
|
|
21841
|
+
const reused = keys.find((key) => record.usedInputKeys.has(key));
|
|
21842
|
+
if (keys.length === 0 || reused !== void 0) {
|
|
21843
|
+
this.#fail(record, {
|
|
21844
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21845
|
+
message: reused === void 0 ? "Task asked for input without any input request" : `Input request key "${reused}" was already used by this task`
|
|
21846
|
+
});
|
|
21847
|
+
return;
|
|
21848
|
+
}
|
|
21849
|
+
for (const key of keys) record.usedInputKeys.add(key);
|
|
21850
|
+
const { suspendPayload } = outcome;
|
|
21851
|
+
record.pendingInput = {
|
|
21852
|
+
requests: outcome.inputRequests,
|
|
21853
|
+
responses: /* @__PURE__ */ new Map(),
|
|
21854
|
+
resolve: (inputResponses) => this.#attempt(record, work, {
|
|
21855
|
+
inputResponses,
|
|
21856
|
+
suspendPayload
|
|
21857
|
+
}),
|
|
21858
|
+
reject: () => {}
|
|
21859
|
+
};
|
|
21860
|
+
this.#transition(record, "input_required");
|
|
21861
|
+
}
|
|
21862
|
+
#fail(record, error) {
|
|
21863
|
+
if (isTerminalTaskStatus(record.status)) return;
|
|
21864
|
+
record.error = error;
|
|
21865
|
+
this.#finish(record, "failed", new Error(error.message));
|
|
21866
|
+
}
|
|
21867
|
+
/** The writer for a task this store holds. Writes after the task is terminal are ignored. */
|
|
21868
|
+
handle(taskId) {
|
|
21869
|
+
const record = this.#tasks.get(taskId);
|
|
21870
|
+
if (!record) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21871
|
+
const live = () => !isTerminalTaskStatus(record.status);
|
|
21872
|
+
return {
|
|
21873
|
+
taskId,
|
|
21874
|
+
signal: record.abort.signal,
|
|
21875
|
+
status: (message) => {
|
|
21876
|
+
if (!live()) return;
|
|
21877
|
+
record.statusMessage = message;
|
|
21878
|
+
record.lastUpdatedAt = this.#now();
|
|
21879
|
+
},
|
|
21880
|
+
requireInput: (requests) => {
|
|
21881
|
+
if (!live()) return Promise.reject(/* @__PURE__ */ new Error(`MCP task ${taskKeyOf(taskId)} is no longer running`));
|
|
21882
|
+
if (record.pendingInput) return Promise.reject(/* @__PURE__ */ new Error(`MCP task ${taskKeyOf(taskId)} is already waiting for input`));
|
|
21883
|
+
const keys = Object.keys(requests);
|
|
21884
|
+
if (keys.length === 0) return Promise.reject(/* @__PURE__ */ new Error("requireInput needs at least one input request"));
|
|
21885
|
+
const reused = keys.find((key) => record.usedInputKeys.has(key));
|
|
21886
|
+
if (reused !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`Input request key "${reused}" was already used by MCP task ${taskKeyOf(taskId)}`));
|
|
21887
|
+
for (const key of keys) record.usedInputKeys.add(key);
|
|
21888
|
+
return new Promise((resolve, reject) => {
|
|
21889
|
+
record.pendingInput = {
|
|
21890
|
+
requests,
|
|
21891
|
+
responses: /* @__PURE__ */ new Map(),
|
|
21892
|
+
resolve,
|
|
21893
|
+
reject
|
|
21894
|
+
};
|
|
21895
|
+
this.#transition(record, "input_required");
|
|
21896
|
+
});
|
|
21897
|
+
},
|
|
21898
|
+
complete: (result) => {
|
|
21899
|
+
if (!live()) return;
|
|
21900
|
+
record.result = result;
|
|
21901
|
+
this.#finish(record, "completed");
|
|
21902
|
+
},
|
|
21903
|
+
fail: (error) => {
|
|
21904
|
+
if (!live()) return;
|
|
21905
|
+
record.error = error;
|
|
21906
|
+
this.#finish(record, "failed");
|
|
21907
|
+
}
|
|
21908
|
+
};
|
|
21909
|
+
}
|
|
21910
|
+
/** Cancels all running work, invalidates every handle and forgets every task. */
|
|
21911
|
+
close() {
|
|
21912
|
+
for (const record of this.#tasks.values()) {
|
|
21913
|
+
clearTimeout(record.expiryTimer);
|
|
21914
|
+
if (!isTerminalTaskStatus(record.status)) this.#finish(record, "cancelled", /* @__PURE__ */ new Error("MCP task store closed"));
|
|
21915
|
+
}
|
|
21916
|
+
this.#tasks.clear();
|
|
21917
|
+
this.#byKey.clear();
|
|
21918
|
+
this.#openByPrincipal.clear();
|
|
21919
|
+
this.#open = 0;
|
|
21920
|
+
this.#retainedByPrincipal.clear();
|
|
21921
|
+
}
|
|
21922
|
+
/** Without `access`, any principal's task (admin). */
|
|
21923
|
+
#lookup(taskKey, access) {
|
|
21924
|
+
const record = this.#byKey.get(taskKey);
|
|
21925
|
+
if (!record || access && record.principal !== access.principal) return void 0;
|
|
21926
|
+
if (record.purgeAt !== void 0 && this.#now() >= record.purgeAt) {
|
|
21927
|
+
this.#purge(record);
|
|
21928
|
+
return;
|
|
21929
|
+
}
|
|
21930
|
+
return record;
|
|
21931
|
+
}
|
|
21932
|
+
/** Forgets an expired task, freeing its retained slot. */
|
|
21933
|
+
#purge(record) {
|
|
21934
|
+
if (this.#tasks.get(record.taskId) !== record) return;
|
|
21935
|
+
clearTimeout(record.expiryTimer);
|
|
21936
|
+
this.#tasks.delete(record.taskId);
|
|
21937
|
+
this.#byKey.delete(record.taskKey);
|
|
21938
|
+
const retained = this.#retainedByPrincipal.get(record.principal) - 1;
|
|
21939
|
+
if (retained === 0) this.#retainedByPrincipal.delete(record.principal);
|
|
21940
|
+
else this.#retainedByPrincipal.set(record.principal, retained);
|
|
21941
|
+
}
|
|
21942
|
+
/** setTimeout fires after 1 ms past 2^31-1 ms (~24.8 days), so a longer wait goes in steps. */
|
|
21943
|
+
#arm(record, at, fire) {
|
|
21944
|
+
const delay = at - this.#now();
|
|
21945
|
+
record.expiryTimer = delay > MAX_TIMER_DELAY_MS ? setTimeout(() => this.#arm(record, at, fire), MAX_TIMER_DELAY_MS) : setTimeout(fire, delay);
|
|
21946
|
+
record.expiryTimer.unref?.();
|
|
21947
|
+
}
|
|
21948
|
+
#expire(record) {
|
|
21949
|
+
if (!isTerminalTaskStatus(record.status)) {
|
|
21950
|
+
record.error = {
|
|
21951
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21952
|
+
message: "Task expired before it finished"
|
|
21953
|
+
};
|
|
21954
|
+
this.#finish(record, "failed", /* @__PURE__ */ new Error("Task expired"));
|
|
21955
|
+
this.#notifyCancel(record);
|
|
21956
|
+
}
|
|
21957
|
+
record.purgeAt = this.#now() + this.#expiredRetentionMs;
|
|
21958
|
+
this.#arm(record, record.purgeAt, () => this.#purge(record));
|
|
21959
|
+
}
|
|
21960
|
+
/** Fire and forget: a slow hook must hold up neither the `tasks/cancel` ack nor expiry. */
|
|
21961
|
+
#notifyCancel(record) {
|
|
21962
|
+
const onCancel = this.#onCancel;
|
|
21963
|
+
const context = record.context;
|
|
21964
|
+
if (!onCancel || !context) return;
|
|
21965
|
+
Promise.resolve().then(() => onCancel(context)).catch(() => {});
|
|
21966
|
+
}
|
|
21967
|
+
#transition(record, status) {
|
|
21968
|
+
record.status = status;
|
|
21969
|
+
record.lastUpdatedAt = this.#now();
|
|
21970
|
+
}
|
|
21971
|
+
/** The only way a task turns terminal, so it is also where it stops counting as open. */
|
|
21972
|
+
#finish(record, status, reason) {
|
|
21973
|
+
const open = this.#openByPrincipal.get(record.principal) - 1;
|
|
21974
|
+
if (open === 0) this.#openByPrincipal.delete(record.principal);
|
|
21975
|
+
else this.#openByPrincipal.set(record.principal, open);
|
|
21976
|
+
this.#open--;
|
|
21977
|
+
this.#transition(record, status);
|
|
21978
|
+
const pending = record.pendingInput;
|
|
21979
|
+
record.pendingInput = void 0;
|
|
21980
|
+
pending?.reject(reason ?? /* @__PURE__ */ new Error(`MCP task "${taskKeyOf(record.taskId)}" ${status} while waiting for input`));
|
|
21981
|
+
if (reason) record.abort.abort(reason);
|
|
21982
|
+
}
|
|
21983
|
+
#snapshot(record) {
|
|
21984
|
+
return {
|
|
21985
|
+
taskId: record.taskId,
|
|
21986
|
+
status: record.status,
|
|
21987
|
+
...record.statusMessage !== void 0 && { statusMessage: record.statusMessage },
|
|
21988
|
+
createdAt: new Date(record.createdAt).toISOString(),
|
|
21989
|
+
lastUpdatedAt: new Date(record.lastUpdatedAt).toISOString(),
|
|
21990
|
+
ttlMs: record.ttlMs,
|
|
21991
|
+
pollIntervalMs: record.pollIntervalMs ?? suggestedPollIntervalMs(record.createdAt, this.#now())
|
|
21992
|
+
};
|
|
21993
|
+
}
|
|
21994
|
+
#detailed(record) {
|
|
21995
|
+
const base = this.#snapshot(record);
|
|
21996
|
+
switch (record.status) {
|
|
21997
|
+
case "input_required": {
|
|
21998
|
+
const { requests, responses } = record.pendingInput;
|
|
21999
|
+
const open = Object.fromEntries(Object.entries(requests).filter(([key]) => !responses.has(key)));
|
|
22000
|
+
return {
|
|
22001
|
+
...base,
|
|
22002
|
+
status: "input_required",
|
|
22003
|
+
inputRequests: open
|
|
22004
|
+
};
|
|
22005
|
+
}
|
|
22006
|
+
case "completed": return {
|
|
22007
|
+
...base,
|
|
22008
|
+
status: "completed",
|
|
22009
|
+
result: { ...record.result }
|
|
22010
|
+
};
|
|
22011
|
+
case "failed": return {
|
|
22012
|
+
...base,
|
|
22013
|
+
status: "failed",
|
|
22014
|
+
error: { ...record.error }
|
|
22015
|
+
};
|
|
22016
|
+
case "working":
|
|
22017
|
+
case "cancelled": return {
|
|
22018
|
+
...base,
|
|
22019
|
+
status: record.status
|
|
22020
|
+
};
|
|
22021
|
+
}
|
|
22022
|
+
}
|
|
22023
|
+
};
|
|
22024
|
+
function assertNonNegativeInteger(name, value) {
|
|
22025
|
+
if (value === null || value === void 0) return;
|
|
22026
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative integer, got ${value}`);
|
|
22027
|
+
}
|
|
22028
|
+
// src/server/tasks/config.ts
|
|
22029
|
+
const MODES = [
|
|
22030
|
+
"off",
|
|
22031
|
+
"auto",
|
|
22032
|
+
"always"
|
|
22033
|
+
];
|
|
22034
|
+
const DEFAULT_SYNC_WINDOW_MS = 2e3;
|
|
22035
|
+
const DEFAULT_MAX_OPEN_TASKS_PER_CLIENT = 100;
|
|
22036
|
+
const DEFAULT_MAX_OPEN_TASKS = 1e3;
|
|
22037
|
+
const DEFAULT_MAX_RETAINED_TASKS_PER_CLIENT = 1e3;
|
|
22038
|
+
const DEFAULT_MAX_RETAINED_TASKS = 1e4;
|
|
22039
|
+
function invalid(text) {
|
|
22040
|
+
return new MastraError({
|
|
22041
|
+
id: "MCP_SERVER_TASKS_INVALID_CONFIG",
|
|
22042
|
+
domain: ErrorDomain.MCP,
|
|
22043
|
+
category: ErrorCategory.USER,
|
|
22044
|
+
text
|
|
22045
|
+
});
|
|
22046
|
+
}
|
|
22047
|
+
function assertMode(owner, mode) {
|
|
22048
|
+
if (!MODES.includes(mode)) throw invalid(`${owner} has an unknown task mode "${String(mode)}"; use 'off', 'auto' or 'always'.`);
|
|
22049
|
+
}
|
|
22050
|
+
function assertDuration(owner, value) {
|
|
22051
|
+
if (value === null || value === void 0) return;
|
|
22052
|
+
if (!Number.isSafeInteger(value) || value < 0) throw invalid(`${owner} must be a non-negative integer number of milliseconds, got ${value}.`);
|
|
22053
|
+
}
|
|
22054
|
+
function assertBoolean(owner, value) {
|
|
22055
|
+
if (value !== void 0 && typeof value !== "boolean") throw invalid(`${owner} must be a boolean, got ${String(value)}.`);
|
|
22056
|
+
}
|
|
22057
|
+
function positiveInteger(owner, value) {
|
|
22058
|
+
if (value === void 0) return void 0;
|
|
22059
|
+
if (!Number.isSafeInteger(value) || value < 1) throw invalid(`${owner} must be a positive integer, got ${value}.`);
|
|
22060
|
+
return value;
|
|
22061
|
+
}
|
|
22062
|
+
/**
|
|
22063
|
+
* Resolves an open cap and the retained cap that contains it. Every open task is also retained, so
|
|
22064
|
+
* an unset cap follows the one that was set, and setting both with retained below open is refused.
|
|
22065
|
+
*/
|
|
22066
|
+
function caps([openName, open, defaultOpen], [retainedName, retained, defaultRetained]) {
|
|
22067
|
+
const openCap = positiveInteger(openName, open);
|
|
22068
|
+
const retainedCap = positiveInteger(retainedName, retained);
|
|
22069
|
+
if (openCap !== void 0 && retainedCap !== void 0 && retainedCap < openCap) throw invalid(`${retainedName} (${retainedCap}) must be at least ${openName} (${openCap}).`);
|
|
22070
|
+
return [openCap ?? Math.min(defaultOpen, retainedCap ?? defaultOpen), retainedCap ?? Math.max(defaultRetained, openCap ?? 0)];
|
|
22071
|
+
}
|
|
22072
|
+
/** Validates `tasks` against the configured tools, workflows and agents. Throws on anything a server could not serve. */
|
|
22073
|
+
function resolveTasksConfig(options, surfaces) {
|
|
22074
|
+
const config = options ?? {};
|
|
22075
|
+
const defaultMode = config.default ?? "off";
|
|
22076
|
+
assertMode("tasks.default", defaultMode);
|
|
22077
|
+
assertDuration("tasks.ttlMs", config.ttlMs);
|
|
22078
|
+
const ttlMs = config.ttlMs === void 0 ? DEFAULT_TASK_TTL_MS : config.ttlMs;
|
|
22079
|
+
assertBoolean("tasks.recover", config.recover);
|
|
22080
|
+
const recover = config.recover ?? false;
|
|
22081
|
+
const [perPrincipal, retainedPerPrincipal] = caps([
|
|
22082
|
+
"tasks.maxOpenTasksPerClient",
|
|
22083
|
+
config.maxOpenTasksPerClient,
|
|
22084
|
+
DEFAULT_MAX_OPEN_TASKS_PER_CLIENT
|
|
22085
|
+
], [
|
|
22086
|
+
"tasks.maxRetainedTasksPerClient",
|
|
22087
|
+
config.maxRetainedTasksPerClient,
|
|
22088
|
+
DEFAULT_MAX_RETAINED_TASKS_PER_CLIENT
|
|
22089
|
+
]);
|
|
22090
|
+
const [total, retainedTotal] = caps([
|
|
22091
|
+
"tasks.maxOpenTasks",
|
|
22092
|
+
config.maxOpenTasks,
|
|
22093
|
+
DEFAULT_MAX_OPEN_TASKS
|
|
22094
|
+
], [
|
|
22095
|
+
"tasks.maxRetainedTasks",
|
|
22096
|
+
config.maxRetainedTasks,
|
|
22097
|
+
DEFAULT_MAX_RETAINED_TASKS
|
|
22098
|
+
]);
|
|
22099
|
+
const limits = {
|
|
22100
|
+
perPrincipal,
|
|
22101
|
+
total,
|
|
22102
|
+
retainedPerPrincipal,
|
|
22103
|
+
retainedTotal
|
|
22104
|
+
};
|
|
22105
|
+
/** Resolves one surface's per-key entries, keyed by the tool name each key is exposed as. */
|
|
22106
|
+
const resolveSurface = (option, keys, exposedAs, surfaceRecover) => {
|
|
22107
|
+
const known = new Set(keys);
|
|
22108
|
+
const tools = new Set(surfaces.tools);
|
|
22109
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
22110
|
+
const entries = config[option] ?? {};
|
|
22111
|
+
const noun = option.slice(0, -1);
|
|
22112
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
22113
|
+
if (!known.has(key)) throw invalid(`tasks.${option} names "${key}", which is not one of this server's ${option}.`);
|
|
22114
|
+
const owner = `tasks.${option}.${key}`;
|
|
22115
|
+
if (option !== "tools" && tools.has(exposedAs(key))) throw invalid(`${owner} names a ${noun} hidden by the tool "${exposedAs(key)}", so it is never served.`);
|
|
22116
|
+
const surface = typeof entry === "string" ? { mode: entry } : entry;
|
|
22117
|
+
assertMode(owner, surface.mode);
|
|
22118
|
+
assertDuration(`${owner}.syncWindowMs`, surface.syncWindowMs);
|
|
22119
|
+
assertDuration(`${owner}.ttlMs`, surface.ttlMs);
|
|
22120
|
+
if (option === "workflows" && "recover" in surface) throw invalid(`${owner}.recover is not a ${noun} option: a workflow task always re-attaches to its run.`);
|
|
22121
|
+
assertBoolean(`${owner}.recover`, surface.recover);
|
|
22122
|
+
resolved.set(exposedAs(key), {
|
|
22123
|
+
mode: surface.mode,
|
|
22124
|
+
syncWindowMs: surface.syncWindowMs ?? DEFAULT_SYNC_WINDOW_MS,
|
|
22125
|
+
ttlMs: surface.ttlMs === void 0 ? ttlMs : surface.ttlMs,
|
|
22126
|
+
recover: surfaceRecover(surface)
|
|
22127
|
+
});
|
|
22128
|
+
}
|
|
22129
|
+
return resolved;
|
|
22130
|
+
};
|
|
22131
|
+
const perTool = resolveSurface("tools", surfaces.tools, (key) => key, (tool) => tool.recover ?? recover);
|
|
22132
|
+
const perWorkflow = resolveSurface("workflows", surfaces.workflows ?? [], (key) => `run_${key}`, () => true);
|
|
22133
|
+
const perAgent = resolveSurface("agents", surfaces.agents ?? [], (key) => `ask_${key}`, (agent) => agent.recover ?? recover);
|
|
22134
|
+
const fallback = {
|
|
22135
|
+
mode: defaultMode,
|
|
22136
|
+
syncWindowMs: DEFAULT_SYNC_WINDOW_MS,
|
|
22137
|
+
ttlMs,
|
|
22138
|
+
recover
|
|
22139
|
+
};
|
|
22140
|
+
const modeFor = (name, toolType) => {
|
|
22141
|
+
if (toolType === "workflow") return perWorkflow.get(name) ?? {
|
|
22142
|
+
...fallback,
|
|
22143
|
+
recover: true
|
|
22144
|
+
};
|
|
22145
|
+
if (toolType === "agent") return perAgent.get(name) ?? fallback;
|
|
22146
|
+
return perTool.get(name) ?? fallback;
|
|
22147
|
+
};
|
|
22148
|
+
const exposed = new Map([
|
|
22149
|
+
...surfaces.tools.map((name) => [name, void 0]),
|
|
22150
|
+
...(surfaces.workflows ?? []).map((key) => [`run_${key}`, "workflow"]),
|
|
22151
|
+
...(surfaces.agents ?? []).map((key) => [`ask_${key}`, "agent"])
|
|
22152
|
+
]);
|
|
22153
|
+
const required = new Set(config.required ?? []);
|
|
22154
|
+
for (const name of required) {
|
|
22155
|
+
if (!exposed.has(name)) throw invalid(`tasks.required names "${name}", which is not one of this server's tools.`);
|
|
22156
|
+
if (modeFor(name, exposed.get(name)).mode === "off") throw invalid(`tasks.required names "${name}", which is not served as a task.`);
|
|
22157
|
+
}
|
|
22158
|
+
return {
|
|
22159
|
+
enabled: defaultMode !== "off" || [
|
|
22160
|
+
...perTool.values(),
|
|
22161
|
+
...perWorkflow.values(),
|
|
22162
|
+
...perAgent.values()
|
|
22163
|
+
].some((surface) => surface.mode !== "off"),
|
|
22164
|
+
store: config.store,
|
|
22165
|
+
limits,
|
|
22166
|
+
required,
|
|
22167
|
+
authorize: config.authorize,
|
|
22168
|
+
modeFor
|
|
22169
|
+
};
|
|
22170
|
+
}
|
|
22171
|
+
/**
|
|
22172
|
+
* Picks the store on first use, once the server knows whether it runs on a Mastra instance with
|
|
22173
|
+
* durable background tasks. `run` follows the `create` it pairs with, so the choice is made by then.
|
|
22174
|
+
*/
|
|
22175
|
+
var SelectingMCPTaskStore = class {
|
|
22176
|
+
#select;
|
|
22177
|
+
#selecting;
|
|
22178
|
+
#selected;
|
|
22179
|
+
/** In-memory stores chosen before a later registration on Mastra, still serving the tasks created on them. */
|
|
22180
|
+
#earlier = [];
|
|
22181
|
+
constructor(select) {
|
|
22182
|
+
this.#select = select;
|
|
22183
|
+
}
|
|
22184
|
+
/** The chosen store, choosing it if no task request has yet. */
|
|
22185
|
+
resolve() {
|
|
22186
|
+
return this.#store();
|
|
22187
|
+
}
|
|
22188
|
+
/**
|
|
22189
|
+
* Chooses again on next use, for a server registered on Mastra after it served tasks. An
|
|
22190
|
+
* in-memory store chosen before keeps the tasks created on it and goes on serving them.
|
|
22191
|
+
*/
|
|
22192
|
+
reselect() {
|
|
22193
|
+
const chosen = this.#selected;
|
|
22194
|
+
if (chosen) {
|
|
22195
|
+
if (!(chosen instanceof InMemoryMCPTaskStore)) return;
|
|
22196
|
+
this.#earlier.push(chosen);
|
|
22197
|
+
this.#selected = void 0;
|
|
22198
|
+
this.#selecting = void 0;
|
|
22199
|
+
return;
|
|
22200
|
+
}
|
|
22201
|
+
const choosing = this.#selecting;
|
|
22202
|
+
if (!choosing) return;
|
|
22203
|
+
const next = choosing.then(() => {
|
|
22204
|
+
this.reselect();
|
|
22205
|
+
return this.#store();
|
|
22206
|
+
});
|
|
22207
|
+
next.catch(() => {});
|
|
22208
|
+
this.#selecting = next;
|
|
22209
|
+
}
|
|
22210
|
+
/** Cancels the work of the in-memory stores it chose; a background-task store is closed by the server. */
|
|
22211
|
+
close() {
|
|
22212
|
+
for (const store of [...this.#earlier.splice(0), this.#selected]) if (store instanceof InMemoryMCPTaskStore) store.close();
|
|
22213
|
+
}
|
|
22214
|
+
/** The store that holds this task: an earlier in-memory store that created it, else the chosen one. */
|
|
22215
|
+
#storeOf(taskId) {
|
|
22216
|
+
const earlier = this.#earlier.find((store) => store.has(taskId));
|
|
22217
|
+
return earlier ? Promise.resolve(earlier) : this.#store();
|
|
22218
|
+
}
|
|
22219
|
+
#store() {
|
|
22220
|
+
this.#selecting ??= this.#select().then((store) => this.#selected = store, (error) => {
|
|
22221
|
+
this.#selecting = void 0;
|
|
22222
|
+
throw error;
|
|
22223
|
+
});
|
|
22224
|
+
return this.#selecting;
|
|
22225
|
+
}
|
|
22226
|
+
async create(params) {
|
|
22227
|
+
return (await this.#store()).create(params);
|
|
22228
|
+
}
|
|
22229
|
+
async get(taskId, access) {
|
|
22230
|
+
return (await this.#storeOf(taskId)).get(taskId, access);
|
|
22231
|
+
}
|
|
22232
|
+
async update(taskId, inputResponses, access) {
|
|
22233
|
+
return (await this.#storeOf(taskId)).update(taskId, inputResponses, access);
|
|
22234
|
+
}
|
|
22235
|
+
async cancel(taskId, access) {
|
|
22236
|
+
return (await this.#storeOf(taskId)).cancel(taskId, access);
|
|
22237
|
+
}
|
|
22238
|
+
async traceOf(taskId, access) {
|
|
22239
|
+
return (await this.#storeOf(taskId)).traceOf?.(taskId, access);
|
|
22240
|
+
}
|
|
22241
|
+
run(taskId, work, options) {
|
|
22242
|
+
const store = this.#earlier.find((earlier) => earlier.has(taskId)) ?? this.#selected;
|
|
22243
|
+
if (!store) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
22244
|
+
store.run(taskId, work, options);
|
|
22245
|
+
}
|
|
22246
|
+
/** The chosen store's admin view, or `undefined` when it has none. */
|
|
22247
|
+
async admin() {
|
|
22248
|
+
const store = await this.#store();
|
|
22249
|
+
return isMCPTaskStoreAdmin(store) ? store : void 0;
|
|
22250
|
+
}
|
|
22251
|
+
};
|
|
22252
|
+
// src/server/tasks/emit.ts
|
|
22253
|
+
/** `_meta` key a tool handler uses to hand a task to {@link emitTaskResults}. */
|
|
22254
|
+
const TASK_RESULT_META_KEY = "mastra.ai/mcp-task-result";
|
|
22255
|
+
/**
|
|
22256
|
+
* A `tools/call` result standing in for a task handle. The SDK only lets a tool handler return a
|
|
22257
|
+
* `CallToolResult`; {@link emitTaskResults} replaces this one with the `CreateTaskResult` on the
|
|
22258
|
+
* wire. `isError` keeps the SDK from applying the tool's output schema to the placeholder.
|
|
22259
|
+
*/
|
|
22260
|
+
function taskHandleResult(task) {
|
|
22261
|
+
return {
|
|
22262
|
+
isError: true,
|
|
22263
|
+
content: [{
|
|
22264
|
+
type: "text",
|
|
22265
|
+
text: "MCP task created"
|
|
22266
|
+
}],
|
|
22267
|
+
_meta: { [TASK_RESULT_META_KEY]: task }
|
|
22268
|
+
};
|
|
22269
|
+
}
|
|
22270
|
+
/** The task a result built by {@link taskHandleResult} stands for; `undefined` for any other result. */
|
|
22271
|
+
function taskOfHandleResult(result) {
|
|
22272
|
+
return (result?._meta)?.[TASK_RESULT_META_KEY];
|
|
22273
|
+
}
|
|
22274
|
+
/**
|
|
22275
|
+
* Sends results built by {@link taskHandleResult} as `CreateTaskResult`s. Only call handlers that
|
|
22276
|
+
* checked the caller declared the Tasks extension may build them.
|
|
22277
|
+
*/
|
|
22278
|
+
function emitTaskResults(transport) {
|
|
22279
|
+
const send = transport.send.bind(transport);
|
|
22280
|
+
transport.send = (message, options) => {
|
|
22281
|
+
const result = "result" in message ? message.result : void 0;
|
|
22282
|
+
const task = taskOfHandleResult(result);
|
|
22283
|
+
if (task) {
|
|
22284
|
+
const { [TASK_RESULT_META_KEY]: _task, ...rest } = result._meta;
|
|
22285
|
+
message = {
|
|
22286
|
+
...message,
|
|
22287
|
+
result: {
|
|
22288
|
+
...task,
|
|
22289
|
+
resultType: TASK_RESULT_TYPE,
|
|
22290
|
+
...Object.keys(rest).length > 0 ? { _meta: rest } : {}
|
|
22291
|
+
}
|
|
22292
|
+
};
|
|
22293
|
+
}
|
|
22294
|
+
return send(message, options);
|
|
22295
|
+
};
|
|
22296
|
+
}
|
|
22297
|
+
// src/server/tasks/handlers.ts
|
|
22298
|
+
function notFound() {
|
|
22299
|
+
return new ProtocolError$1(TASKS_ERROR_CODES.InvalidParams, "Task not found");
|
|
22300
|
+
}
|
|
22301
|
+
/**
|
|
22302
|
+
* Registers `tasks/get`, `tasks/update` and `tasks/cancel` on one protocol instance. They are
|
|
22303
|
+
* registered with explicit schemas, which the SDK requires to serve the Tasks extension's methods on
|
|
22304
|
+
* the 2026-07-28 revision.
|
|
22305
|
+
*/
|
|
22306
|
+
function registerTaskHandlers(server, deps) {
|
|
22307
|
+
const { store, authorize } = deps;
|
|
22308
|
+
const trace = deps.trace ?? ((_server, _method, _params, _ctx, fn) => fn(new RequestContext()));
|
|
22309
|
+
/** `tasks/update` and `tasks/cancel` check `authorize` against the task before acting on it. */
|
|
22310
|
+
const serve = (method, params, ctx, fn, metadata) => {
|
|
22311
|
+
const mcpTaskKey = taskKeyOf(params.taskId);
|
|
22312
|
+
return trace(server, method, { mcpTaskKey }, ctx, async (requestContext) => {
|
|
22313
|
+
const principal = await deps.principal(ctx, requestContext);
|
|
22314
|
+
if (authorize) {
|
|
22315
|
+
const task = await store.get(params.taskId, { principal });
|
|
22316
|
+
if (!task || !await authorize({
|
|
22317
|
+
principal,
|
|
22318
|
+
task,
|
|
22319
|
+
requestContext
|
|
22320
|
+
})) throw notFound();
|
|
22321
|
+
}
|
|
22322
|
+
return fn({ principal });
|
|
22323
|
+
}, {
|
|
22324
|
+
parentOf: store.traceOf ? async (requestContext) => store.traceOf(params.taskId, { principal: await deps.principal(ctx, requestContext) }) : void 0,
|
|
22325
|
+
metadata: {
|
|
22326
|
+
mcpTaskKey,
|
|
22327
|
+
...metadata
|
|
22328
|
+
}
|
|
22329
|
+
});
|
|
22330
|
+
};
|
|
22331
|
+
server.setRequestHandler(TASKS_METHODS.get, {
|
|
22332
|
+
params: GetTaskRequestParamsSchema,
|
|
22333
|
+
result: GetTaskResultSchema
|
|
22334
|
+
}, (params, ctx) => trace(server, TASKS_METHODS.get, {}, ctx, async (requestContext) => {
|
|
22335
|
+
const principal = await deps.principal(ctx, requestContext);
|
|
22336
|
+
const task = await store.get(params.taskId, { principal });
|
|
22337
|
+
if (!task || authorize && !await authorize({
|
|
22338
|
+
principal,
|
|
22339
|
+
task,
|
|
22340
|
+
requestContext
|
|
22341
|
+
})) throw notFound();
|
|
22342
|
+
return task;
|
|
22343
|
+
}, { untraced: true }));
|
|
22344
|
+
server.setRequestHandler(TASKS_METHODS.update, {
|
|
22345
|
+
params: UpdateTaskHandlerParamsSchema,
|
|
22346
|
+
result: TaskAckResultSchema
|
|
22347
|
+
}, (params, ctx) => serve(TASKS_METHODS.update, params, ctx, async (access) => {
|
|
22348
|
+
const inputResponses = ctx.mcpReq.inputResponses ?? {};
|
|
22349
|
+
if (!await store.update(params.taskId, inputResponses, access)) throw notFound();
|
|
22350
|
+
return {};
|
|
22351
|
+
}));
|
|
22352
|
+
server.setRequestHandler(TASKS_METHODS.cancel, {
|
|
22353
|
+
params: CancelTaskRequestParamsSchema,
|
|
22354
|
+
result: TaskAckResultSchema
|
|
22355
|
+
}, (params, ctx) => serve(TASKS_METHODS.cancel, params, ctx, async (access) => {
|
|
22356
|
+
if (!await store.cancel(params.taskId, access)) throw notFound();
|
|
22357
|
+
return {};
|
|
22358
|
+
}, { mcpTaskStatus: "cancelled" }));
|
|
22359
|
+
}
|
|
22360
|
+
// src/server/tasks/inspector.ts
|
|
22361
|
+
/** The tools served as tasks, as Studio shows them. */
|
|
22362
|
+
function listTaskSurfaces(tasks, tools) {
|
|
22363
|
+
const surfaces = [];
|
|
22364
|
+
for (const [name, toolType] of tools) {
|
|
22365
|
+
const { mode, recover, ttlMs } = tasks.modeFor(name, toolType);
|
|
22366
|
+
if (mode === "off") continue;
|
|
22367
|
+
surfaces.push({
|
|
22368
|
+
name,
|
|
22369
|
+
type: toolType === "workflow" ? "workflow" : toolType === "agent" ? "agent" : "tool",
|
|
22370
|
+
mode,
|
|
22371
|
+
recover,
|
|
22372
|
+
required: tasks.required.has(name),
|
|
22373
|
+
ttlMs
|
|
22374
|
+
});
|
|
22375
|
+
}
|
|
22376
|
+
return surfaces;
|
|
22377
|
+
}
|
|
22378
|
+
function tasksInfo(tasks, tools) {
|
|
22379
|
+
return {
|
|
22380
|
+
enabled: tasks.enabled,
|
|
22381
|
+
listable: !tasks.store || isMCPTaskStoreAdmin(tasks.store),
|
|
22382
|
+
surfaces: listTaskSurfaces(tasks, tools)
|
|
22383
|
+
};
|
|
22384
|
+
}
|
|
22385
|
+
/**
|
|
22386
|
+
* Studio's view of the task store. Cancel and answer go through the store's `cancelByKey` and
|
|
22387
|
+
* `updateByKey`, the same code as `tasks/cancel` and `tasks/update`.
|
|
22388
|
+
*/
|
|
22389
|
+
function createTasksInspector(admin) {
|
|
22390
|
+
const required = async () => {
|
|
22391
|
+
const store = await admin();
|
|
22392
|
+
if (!store) throw new Error("This MCP task store cannot be listed");
|
|
22393
|
+
return store;
|
|
22394
|
+
};
|
|
22395
|
+
return {
|
|
22396
|
+
async list(params = {}) {
|
|
22397
|
+
const { listings, nextCursor } = await (await required()).list(params);
|
|
22398
|
+
return {
|
|
22399
|
+
tasks: listings.map(toSummary),
|
|
22400
|
+
...nextCursor && { nextCursor }
|
|
22401
|
+
};
|
|
22402
|
+
},
|
|
22403
|
+
async find(taskId) {
|
|
22404
|
+
const { listings } = await (await required()).list({
|
|
22405
|
+
taskKey: taskKeyOf(taskId),
|
|
22406
|
+
limit: 1
|
|
22407
|
+
});
|
|
22408
|
+
return listings[0] && toSummary(listings[0]);
|
|
22409
|
+
},
|
|
22410
|
+
async cancel(taskKey) {
|
|
22411
|
+
return await (await required()).cancelByKey(taskKey) === "done";
|
|
22412
|
+
},
|
|
22413
|
+
async answer(taskKey, inputResponses) {
|
|
22414
|
+
return await (await required()).updateByKey(taskKey, inputResponses) === "done";
|
|
22415
|
+
}
|
|
22416
|
+
};
|
|
22417
|
+
}
|
|
22418
|
+
function toSummary(listing) {
|
|
22419
|
+
const toolName = listing.context?.toolName;
|
|
22420
|
+
return {
|
|
22421
|
+
taskKey: listing.taskKey,
|
|
22422
|
+
surface: typeof toolName === "string" ? toolName : "",
|
|
22423
|
+
status: listing.status,
|
|
22424
|
+
...listing.statusMessage !== void 0 && { statusMessage: listing.statusMessage },
|
|
22425
|
+
createdAt: listing.createdAt,
|
|
22426
|
+
lastUpdatedAt: listing.lastUpdatedAt,
|
|
22427
|
+
...listing.ttlMs !== null && { expiresAt: new Date(new Date(listing.createdAt).getTime() + listing.ttlMs).toISOString() },
|
|
22428
|
+
principalKey: listing.principalKey,
|
|
22429
|
+
...listing.clientLabel !== void 0 && { clientLabel: listing.clientLabel },
|
|
22430
|
+
...listing.inputRequests && { inputRequests: listing.inputRequests },
|
|
22431
|
+
...listing.result && { result: listing.result },
|
|
22432
|
+
...listing.error && { error: listing.error }
|
|
22433
|
+
};
|
|
22434
|
+
}
|
|
22435
|
+
// src/server/tasks/workflow-runs.ts
|
|
22436
|
+
/**
|
|
22437
|
+
* Whether a task's context is a workflow task's.
|
|
22438
|
+
*
|
|
22439
|
+
* @experimental SEP-2663 extension; API may change.
|
|
22440
|
+
*/
|
|
22441
|
+
function isWorkflowTaskContext(context) {
|
|
22442
|
+
return context?.kind === "workflow" && typeof context.runId === "string" && typeof context.workflowKey === "string";
|
|
22443
|
+
}
|
|
22444
|
+
/** Statuses a run never leaves. */
|
|
22445
|
+
const FINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
22446
|
+
"success",
|
|
22447
|
+
"failed",
|
|
22448
|
+
"tripwire",
|
|
22449
|
+
"canceled",
|
|
22450
|
+
"bailed"
|
|
22451
|
+
]);
|
|
22452
|
+
function isFinalRunStatus(status) {
|
|
22453
|
+
return FINAL_STATUSES.has(status);
|
|
22454
|
+
}
|
|
22455
|
+
/** The text a finished run answers with: the same fields the synchronous `run_` tool returns. */
|
|
22456
|
+
function runSummary(view) {
|
|
22457
|
+
return {
|
|
22458
|
+
status: view.status,
|
|
22459
|
+
input: view.input,
|
|
22460
|
+
steps: view.steps,
|
|
22461
|
+
result: view.result
|
|
22462
|
+
};
|
|
22463
|
+
}
|
|
22464
|
+
function runErrorMessage(view) {
|
|
22465
|
+
const error = view.error;
|
|
22466
|
+
if (typeof error === "string") return error;
|
|
22467
|
+
if (error && typeof error.message === "string") return error.message;
|
|
22468
|
+
return "unknown error";
|
|
22469
|
+
}
|
|
22470
|
+
/**
|
|
22471
|
+
* The first step the run is suspended on. A run with several (parallel branches) is resumed one at
|
|
22472
|
+
* a time: after each resume it suspends again on the next.
|
|
22473
|
+
*/
|
|
22474
|
+
function firstSuspendedStep(workflow, view) {
|
|
22475
|
+
for (const [stepId, raw] of Object.entries(view.steps ?? {})) {
|
|
22476
|
+
const result = raw;
|
|
22477
|
+
if (result?.status !== "suspended") continue;
|
|
22478
|
+
const { __workflow_meta: meta, ...payload } = result.suspendPayload ?? {};
|
|
22479
|
+
const nested = meta?.path;
|
|
22480
|
+
const path = [stepId, ...Array.isArray(nested) ? nested.filter((part) => typeof part === "string") : []];
|
|
22481
|
+
return {
|
|
22482
|
+
path,
|
|
22483
|
+
step: stepAt(workflow, path),
|
|
22484
|
+
payload,
|
|
22485
|
+
suspendedAt: result.suspendedAt
|
|
22486
|
+
};
|
|
22487
|
+
}
|
|
22488
|
+
}
|
|
22489
|
+
/**
|
|
22490
|
+
* Whether `answered` (recorded when the task asked) is still the run's current suspension. Another
|
|
22491
|
+
* resume, from Studio or the API, moves the run on; a step that suspends again gets a new `suspendedAt`.
|
|
22492
|
+
*/
|
|
22493
|
+
function isStillSuspended(view, answered) {
|
|
22494
|
+
const result = view.steps?.[answered.step[0]];
|
|
22495
|
+
if (view.status !== "suspended" || result?.status !== "suspended") return false;
|
|
22496
|
+
return answered.suspendedAt === void 0 || result.suspendedAt === answered.suspendedAt;
|
|
22497
|
+
}
|
|
22498
|
+
/** Walks the workflow's step definitions along `path`, descending into nested workflows. */
|
|
22499
|
+
function stepAt(workflow, path) {
|
|
22500
|
+
let steps = workflow.steps;
|
|
22501
|
+
let step;
|
|
22502
|
+
for (const id of path) {
|
|
22503
|
+
step = steps?.[id];
|
|
22504
|
+
if (!step) return void 0;
|
|
22505
|
+
steps = step.steps;
|
|
22506
|
+
}
|
|
22507
|
+
return step;
|
|
22508
|
+
}
|
|
22509
|
+
// src/server/server.ts
|
|
22510
|
+
const EMPTY_OBJECT_SCHEMA = {
|
|
22511
|
+
type: "object",
|
|
22512
|
+
properties: {}
|
|
22513
|
+
};
|
|
22514
|
+
/**
|
|
22515
|
+
* Exposes Mastra tools, agents, workflows, resources and prompts to Model Context
|
|
22516
|
+
* Protocol (MCP) clients using protocol revision 2026-07-28: self-contained requests
|
|
22517
|
+
* over Streamable HTTP or stdio, native `input_required` continuation and
|
|
22518
|
+
* per-request logging.
|
|
22519
|
+
*
|
|
22520
|
+
* @example
|
|
22521
|
+
* `yourTool` is a tool you have already configured.
|
|
22522
|
+
* ```typescript
|
|
22523
|
+
* import { MCPServer } from '@mastra/mcp';
|
|
22524
|
+
*
|
|
22525
|
+
* const server = new MCPServer({
|
|
22526
|
+
* id: 'my-server',
|
|
22527
|
+
* name: 'My Server',
|
|
22528
|
+
* version: '1.0.0',
|
|
22529
|
+
* tools: { yourTool },
|
|
22530
|
+
* });
|
|
22531
|
+
* await server.startStdio();
|
|
22532
|
+
* ```
|
|
22533
|
+
*
|
|
22534
|
+
* @see For documentation bundled with your installed package, locate
|
|
22535
|
+
* `@mastra/mcp/package.json` with your project's resolver or package-manager
|
|
22536
|
+
* tooling, then read `dist/docs/SKILL.md` from that package root and follow its
|
|
22537
|
+
* reference links. Use package-manager tools for virtual or archived packages.
|
|
22538
|
+
*
|
|
22539
|
+
* @see [MCP server documentation](https://mastra.ai/reference/tools/mcp-server)
|
|
22540
|
+
* if packaged docs are unavailable.
|
|
22541
|
+
*/
|
|
22542
|
+
var MCPServer = class extends MCPServerBase {
|
|
22543
|
+
mcpVersion = 2;
|
|
22544
|
+
resources;
|
|
22545
|
+
prompts;
|
|
22546
|
+
toolActions;
|
|
22547
|
+
/**
|
|
22548
|
+
* Publishes occurrences of the configured `events` to poll, push and (when
|
|
22549
|
+
* `eventsOptions.webhook` is set) webhook subscribers.
|
|
22550
|
+
*
|
|
22551
|
+
* @experimental SEP-3415 is a draft; this API may change.
|
|
22552
|
+
*/
|
|
22553
|
+
events;
|
|
22554
|
+
resourceOptions;
|
|
22555
|
+
promptOptions;
|
|
22556
|
+
appResourceList = [];
|
|
22557
|
+
appResourceHtml = /* @__PURE__ */ new Map();
|
|
22558
|
+
jsonSchemaValidator;
|
|
22559
|
+
cacheHints;
|
|
22560
|
+
mapAuthInfoToUser;
|
|
22561
|
+
fga;
|
|
22562
|
+
requestStateCodec;
|
|
22563
|
+
eventRegistry;
|
|
20256
22564
|
eventsOptions;
|
|
20257
22565
|
eventsInspector;
|
|
22566
|
+
tasksInspector;
|
|
20258
22567
|
webhooks;
|
|
20259
22568
|
legacy;
|
|
22569
|
+
tasks;
|
|
22570
|
+
/** Where tasks are kept: `tasks.store`, or the store picked on first use. */
|
|
22571
|
+
taskStore;
|
|
22572
|
+
/** Cancels for the workflow runs executing in this process, by run id, while they execute. */
|
|
22573
|
+
executingRuns = /* @__PURE__ */ new Map();
|
|
22574
|
+
/** Prepared on Mastra registration so its executors exist before the manager recovers rows. */
|
|
22575
|
+
backgroundTaskStore;
|
|
20260
22576
|
httpHandler;
|
|
20261
22577
|
nodeHandler;
|
|
20262
22578
|
stdioHandle;
|
|
@@ -20273,6 +22589,13 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20273
22589
|
this.requestStateCodec = this.createRequestStateCodec(config.requestState);
|
|
20274
22590
|
this.eventsOptions = config.eventsOptions ?? {};
|
|
20275
22591
|
this.legacy = config.legacy ?? "reject";
|
|
22592
|
+
this.tasks = resolveTasksConfig(config.tasks, {
|
|
22593
|
+
tools: Object.keys(config.tools ?? {}),
|
|
22594
|
+
workflows: Object.keys(config.workflows ?? {}),
|
|
22595
|
+
agents: Object.keys(config.agents ?? {})
|
|
22596
|
+
});
|
|
22597
|
+
this.hasStableId = Boolean(config.id);
|
|
22598
|
+
this.taskStore = this.tasks.store ?? new SelectingMCPTaskStore(() => this.selectTaskStore());
|
|
20276
22599
|
const webhook = this.eventsOptions.webhook;
|
|
20277
22600
|
this.eventRegistry = new ServerEventRegistry(config.events, { webhook: webhook !== void 0 });
|
|
20278
22601
|
if (webhook && this.eventRegistry.size > 0) this.webhooks = new WebhookDispatcher({
|
|
@@ -20401,11 +22724,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20401
22724
|
* so a tool cannot suspend into a request no client can answer.
|
|
20402
22725
|
*/
|
|
20403
22726
|
assertFormRepresentable(owner, resumeSchema) {
|
|
20404
|
-
const
|
|
20405
|
-
const validation = specTypeSchemas.ElicitRequestFormParams["~standard"].validate({
|
|
20406
|
-
message: owner,
|
|
20407
|
-
requestedSchema
|
|
20408
|
-
});
|
|
22727
|
+
const validation = formValidation(owner, this.formSchema(resumeSchema));
|
|
20409
22728
|
if (validation instanceof Promise || validation.issues) throw new MastraError({
|
|
20410
22729
|
id: "MCP_SERVER_RESUME_SCHEMA_NOT_REPRESENTABLE",
|
|
20411
22730
|
domain: ErrorDomain.MCP,
|
|
@@ -20417,6 +22736,15 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20417
22736
|
}
|
|
20418
22737
|
});
|
|
20419
22738
|
}
|
|
22739
|
+
/** The input-request form for `resumeSchema`, or `undefined` when the protocol can't present it. */
|
|
22740
|
+
representableForm(resumeSchema) {
|
|
22741
|
+
const requestedSchema = resumeSchema ? this.formSchema(resumeSchema) : {
|
|
22742
|
+
type: "object",
|
|
22743
|
+
properties: {}
|
|
22744
|
+
};
|
|
22745
|
+
const validation = formValidation("form", requestedSchema);
|
|
22746
|
+
return validation instanceof Promise || validation.issues ? void 0 : requestedSchema;
|
|
22747
|
+
}
|
|
20420
22748
|
/**
|
|
20421
22749
|
* Input-request forms are a restricted flat-object subset of JSON Schema, so
|
|
20422
22750
|
* the dialect declaration is left off the requested schema.
|
|
@@ -20546,6 +22874,10 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20546
22874
|
...capabilities.extensions,
|
|
20547
22875
|
[EVENTS_EXTENSION_ID]: this.eventsCapability()
|
|
20548
22876
|
};
|
|
22877
|
+
if (this.tasks.enabled) capabilities.extensions = {
|
|
22878
|
+
...capabilities.extensions,
|
|
22879
|
+
...tasksCapabilityEntry()
|
|
22880
|
+
};
|
|
20549
22881
|
return capabilities;
|
|
20550
22882
|
}
|
|
20551
22883
|
/**
|
|
@@ -20585,6 +22917,19 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20585
22917
|
jsonSchema: (schema, options) => this.jsonSchema(schema, options),
|
|
20586
22918
|
trace: (instance, method, params, ctx, fn) => this.traceCustomRequest(instance, method, params, ctx, fn)
|
|
20587
22919
|
});
|
|
22920
|
+
if (this.tasks.enabled) {
|
|
22921
|
+
registerTaskHandlers(server, {
|
|
22922
|
+
store: this.taskStore,
|
|
22923
|
+
authorize: this.tasks.authorize,
|
|
22924
|
+
principal: principalOf,
|
|
22925
|
+
trace: (instance, method, params, ctx, fn, tracing) => this.traceCustomRequest(instance, method, params, ctx, fn, tracing)
|
|
22926
|
+
});
|
|
22927
|
+
const connect = server.connect.bind(server);
|
|
22928
|
+
server.connect = async (transport) => {
|
|
22929
|
+
emitTaskResults(transport);
|
|
22930
|
+
return connect(transport);
|
|
22931
|
+
};
|
|
22932
|
+
}
|
|
20588
22933
|
return server;
|
|
20589
22934
|
}
|
|
20590
22935
|
/**
|
|
@@ -20592,8 +22937,10 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20592
22937
|
* extension): the request runs under an `MCP_SERVER_REQUEST` span with its resolved
|
|
20593
22938
|
* request context. A push stream's span lasts as long as the stream.
|
|
20594
22939
|
*/
|
|
20595
|
-
async traceCustomRequest(server, method, params, ctx, fn) {
|
|
22940
|
+
async traceCustomRequest(server, method, params, ctx, fn, tracing = {}) {
|
|
22941
|
+
if (tracing.untraced) return fn(await toRequestContext(ctx, this.mapAuthInfoToUser));
|
|
20596
22942
|
const caller = callerTrace(ctx);
|
|
22943
|
+
const { metadata } = tracing;
|
|
20597
22944
|
let requestContext;
|
|
20598
22945
|
try {
|
|
20599
22946
|
requestContext = await toRequestContext(ctx, this.mapAuthInfoToUser);
|
|
@@ -20601,15 +22948,19 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20601
22948
|
this.startRequestSpan(method, params, {
|
|
20602
22949
|
server,
|
|
20603
22950
|
ctx,
|
|
20604
|
-
caller
|
|
22951
|
+
caller,
|
|
22952
|
+
metadata
|
|
20605
22953
|
})?.error({ error });
|
|
20606
22954
|
throw error;
|
|
20607
22955
|
}
|
|
22956
|
+
const task = await tracing.parentOf?.(requestContext).catch(() => void 0);
|
|
20608
22957
|
const requestSpan = this.startRequestSpan(method, params, {
|
|
20609
22958
|
server,
|
|
20610
22959
|
ctx,
|
|
20611
22960
|
caller,
|
|
20612
|
-
requestContext
|
|
22961
|
+
requestContext,
|
|
22962
|
+
metadata,
|
|
22963
|
+
task
|
|
20613
22964
|
});
|
|
20614
22965
|
return this.traceRequest(requestSpan, () => fn(requestContext));
|
|
20615
22966
|
}
|
|
@@ -20681,6 +23032,11 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20681
23032
|
},
|
|
20682
23033
|
tracingContext: {},
|
|
20683
23034
|
links: caller && [caller.link],
|
|
23035
|
+
metadata: connection?.metadata,
|
|
23036
|
+
...connection?.task && {
|
|
23037
|
+
tracingOptions: { traceId: connection.task.traceId },
|
|
23038
|
+
resumedFromSpanId: connection.task.spanId
|
|
23039
|
+
},
|
|
20684
23040
|
requestContext: connection?.requestContext,
|
|
20685
23041
|
mastra: this.mastra
|
|
20686
23042
|
});
|
|
@@ -20693,6 +23049,22 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20693
23049
|
async traceRequest(requestSpan, fn, getReportedError) {
|
|
20694
23050
|
try {
|
|
20695
23051
|
const result = await fn();
|
|
23052
|
+
const task = taskOfHandleResult(result);
|
|
23053
|
+
if (task) {
|
|
23054
|
+
const mcpTaskKey = taskKeyOf(task.taskId);
|
|
23055
|
+
requestSpan?.end({
|
|
23056
|
+
output: { task: {
|
|
23057
|
+
status: task.status,
|
|
23058
|
+
mcpTaskKey
|
|
23059
|
+
} },
|
|
23060
|
+
metadata: {
|
|
23061
|
+
mcpTaskKey,
|
|
23062
|
+
mcpTaskStatus: task.status
|
|
23063
|
+
}
|
|
23064
|
+
});
|
|
23065
|
+
if (requestSpan) this.flushTaskRoot(requestSpan);
|
|
23066
|
+
return result;
|
|
23067
|
+
}
|
|
20696
23068
|
const errorResult = result;
|
|
20697
23069
|
if (errorResult?.isError) requestSpan?.error({ error: getReportedError?.() ?? new MastraError({
|
|
20698
23070
|
id: "MCP_SERVER_REQUEST_FAILED",
|
|
@@ -20710,19 +23082,19 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20710
23082
|
throw error;
|
|
20711
23083
|
}
|
|
20712
23084
|
}
|
|
23085
|
+
/**
|
|
23086
|
+
* Writes a task's root span out now rather than with the exporter's next batch: the task's later
|
|
23087
|
+
* spans may be recorded by another process, and if this one dies first, a trace without its root
|
|
23088
|
+
* drops out of trace lists.
|
|
23089
|
+
*/
|
|
23090
|
+
flushTaskRoot(span) {
|
|
23091
|
+
span.observabilityInstance.flush().catch((error) => this.logger.debug("Could not flush a task trace root", { error }));
|
|
23092
|
+
}
|
|
20713
23093
|
/** Answers a suspended round: one keyed form derived from the handler's `resumeSchema`. */
|
|
20714
23094
|
async inputRequired(ctx, requestContext, suspension) {
|
|
20715
|
-
const payload = suspension.suspendPayload;
|
|
20716
|
-
const message = typeof payload?.message === "string" ? payload.message : typeof suspension.resumeSchema?.description === "string" ? suspension.resumeSchema.description : `"${suspension.name}" needs more input`;
|
|
20717
23095
|
const { method, name, argsHash, round, suspendPayload } = suspension;
|
|
20718
23096
|
return inputRequired({
|
|
20719
|
-
inputRequests: { [INPUT_KEY]:
|
|
20720
|
-
message,
|
|
20721
|
-
requestedSchema: suspension.resumeSchema ?? {
|
|
20722
|
-
type: "object",
|
|
20723
|
-
properties: {}
|
|
20724
|
-
}
|
|
20725
|
-
}) },
|
|
23097
|
+
inputRequests: { [INPUT_KEY]: this.inputRequestForm(name, suspendPayload, suspension.resumeSchema) },
|
|
20726
23098
|
requestState: await this.requestStateCodec.mint({
|
|
20727
23099
|
method,
|
|
20728
23100
|
name,
|
|
@@ -20734,6 +23106,18 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20734
23106
|
})
|
|
20735
23107
|
});
|
|
20736
23108
|
}
|
|
23109
|
+
/** The form a suspended handler asks the client to fill in. */
|
|
23110
|
+
inputRequestForm(name, suspendPayload, resumeSchema) {
|
|
23111
|
+
const payload = suspendPayload;
|
|
23112
|
+
const message = typeof payload?.message === "string" ? payload.message : typeof resumeSchema?.description === "string" ? resumeSchema.description : `"${name}" needs more input`;
|
|
23113
|
+
return inputRequired.elicit({
|
|
23114
|
+
message,
|
|
23115
|
+
requestedSchema: resumeSchema ?? {
|
|
23116
|
+
type: "object",
|
|
23117
|
+
properties: {}
|
|
23118
|
+
}
|
|
23119
|
+
});
|
|
23120
|
+
}
|
|
20737
23121
|
async serverRequest(ctx, requestContext, method, name, args, resumeSchema) {
|
|
20738
23122
|
const argsHash = hashArguments(args);
|
|
20739
23123
|
const continuation = readContinuation(ctx, requestContext, {
|
|
@@ -20784,13 +23168,35 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20784
23168
|
try {
|
|
20785
23169
|
await this.enforceToolExecutionFGA(name, requestContext);
|
|
20786
23170
|
if (continuation && continuation.outcome !== "accept") return errorResult$1(`Tool '${name}' was ${continuation.outcome === "decline" ? "declined" : "cancelled"}`);
|
|
20787
|
-
const
|
|
23171
|
+
const resume = continuation && {
|
|
23172
|
+
resumeData: continuation.resumeData,
|
|
23173
|
+
suspendPayload: continuation.suspendPayload
|
|
23174
|
+
};
|
|
23175
|
+
const taskMode = this.tasks.modeFor(name, tool.mcp?.toolType);
|
|
23176
|
+
const asTask = taskMode.mode !== "off" && clientDeclaredTasks(ctx);
|
|
23177
|
+
if (taskMode.mode !== "off" && !asTask && this.tasks.required.has(name)) throw new MissingRequiredClientCapabilityError({ requiredCapabilities: { extensions: tasksCapabilityEntry() } }, `Tool '${name}' only runs as a task; declare the io.modelcontextprotocol/tasks extension`);
|
|
23178
|
+
if (asTask && tool.mcp?.toolType === "workflow") return await this.startWorkflowTask(ctx, requestContext, name, args, taskMode, { currentSpan: trace.requestSpan });
|
|
23179
|
+
if (asTask && taskMode.mode === "always") try {
|
|
23180
|
+
return await this.startToolTask(ctx, requestContext, name, args, taskMode, {
|
|
23181
|
+
resume,
|
|
23182
|
+
rootSpan: trace.requestSpan
|
|
23183
|
+
});
|
|
23184
|
+
} catch (error) {
|
|
23185
|
+
if (error instanceof MCPTaskLimitError) return errorResult$1(taskLimitMessage(error));
|
|
23186
|
+
throw error;
|
|
23187
|
+
}
|
|
23188
|
+
const options = {
|
|
20788
23189
|
requestContext,
|
|
20789
23190
|
mcp,
|
|
20790
23191
|
tracingContext: { currentSpan: trace.requestSpan },
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
23192
|
+
...resume
|
|
23193
|
+
};
|
|
23194
|
+
let execution;
|
|
23195
|
+
if (asTask) {
|
|
23196
|
+
const raced = await this.runWithinSyncWindow(ctx, requestContext, name, tool, args, taskMode, options);
|
|
23197
|
+
if ("task" in raced) return raced.task;
|
|
23198
|
+
execution = raced.execution;
|
|
23199
|
+
} else execution = await this.runTool(name, tool, args, options);
|
|
20794
23200
|
if (execution.status === "suspended") {
|
|
20795
23201
|
this.logger.debug(`Tool '${name}' requires client input.`);
|
|
20796
23202
|
return this.inputRequired(ctx, requestContext, {
|
|
@@ -20807,21 +23213,549 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20807
23213
|
return result;
|
|
20808
23214
|
} catch (error) {
|
|
20809
23215
|
if (error instanceof ProtocolError$1) throw error;
|
|
20810
|
-
this.
|
|
20811
|
-
tool: name,
|
|
20812
|
-
error
|
|
20813
|
-
});
|
|
20814
|
-
const mastraError = error instanceof MastraError ? error : new MastraError({
|
|
20815
|
-
id: "TOOL_EXECUTION_FAILED",
|
|
20816
|
-
domain: ErrorDomain.TOOL,
|
|
20817
|
-
category: ErrorCategory.USER,
|
|
20818
|
-
details: { toolName: name }
|
|
20819
|
-
}, error);
|
|
23216
|
+
const mastraError = this.toolError(name, error);
|
|
20820
23217
|
trace.reportError(mastraError);
|
|
20821
23218
|
return errorResult$1(JSON.stringify(mastraError.toJSON()));
|
|
20822
23219
|
}
|
|
20823
23220
|
});
|
|
20824
23221
|
}
|
|
23222
|
+
/** Logs a tool's failure and wraps it as the error a client receives. */
|
|
23223
|
+
toolError(name, error) {
|
|
23224
|
+
this.logger.error("Tool execution failed", {
|
|
23225
|
+
tool: name,
|
|
23226
|
+
error
|
|
23227
|
+
});
|
|
23228
|
+
return error instanceof MastraError ? error : new MastraError({
|
|
23229
|
+
id: "TOOL_EXECUTION_FAILED",
|
|
23230
|
+
domain: ErrorDomain.TOOL,
|
|
23231
|
+
category: ErrorCategory.USER,
|
|
23232
|
+
details: { toolName: name }
|
|
23233
|
+
}, error);
|
|
23234
|
+
}
|
|
23235
|
+
/**
|
|
23236
|
+
* `'auto'` mode: runs the tool under the request for up to `syncWindowMs`. A tool that settles in
|
|
23237
|
+
* time is answered like any synchronous call; otherwise the same execution carries on as a task,
|
|
23238
|
+
* no longer tied to the request's cancellation.
|
|
23239
|
+
*/
|
|
23240
|
+
async runWithinSyncWindow(ctx, requestContext, name, tool, args, taskMode, options) {
|
|
23241
|
+
const controller = new AbortController();
|
|
23242
|
+
const requestSignal = ctx.mcpReq.signal;
|
|
23243
|
+
const followRequest = () => controller.abort(requestSignal.reason);
|
|
23244
|
+
if (requestSignal.aborted) followRequest();
|
|
23245
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23246
|
+
const settled = this.runTool(name, tool, args, {
|
|
23247
|
+
...options,
|
|
23248
|
+
abortSignal: controller.signal,
|
|
23249
|
+
mcp: {
|
|
23250
|
+
...options.mcp,
|
|
23251
|
+
extra: {
|
|
23252
|
+
...options.mcp.extra,
|
|
23253
|
+
signal: controller.signal
|
|
23254
|
+
},
|
|
23255
|
+
log: (...params) => options.mcp.log(...params).catch(() => {}),
|
|
23256
|
+
progress: (params) => options.mcp.progress(params).catch(() => {})
|
|
23257
|
+
}
|
|
23258
|
+
}).then((execution) => ({ execution }), (error) => ({ error }));
|
|
23259
|
+
let timer;
|
|
23260
|
+
const deadline = new Promise((resolve) => {
|
|
23261
|
+
timer = setTimeout(() => resolve("timeout"), taskMode.syncWindowMs);
|
|
23262
|
+
});
|
|
23263
|
+
const winner = await Promise.race([settled, deadline]);
|
|
23264
|
+
clearTimeout(timer);
|
|
23265
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23266
|
+
if (winner !== "timeout") {
|
|
23267
|
+
if ("error" in winner) throw winner.error;
|
|
23268
|
+
return winner;
|
|
23269
|
+
}
|
|
23270
|
+
try {
|
|
23271
|
+
return { task: await this.startToolTask(ctx, requestContext, name, args, taskMode, {
|
|
23272
|
+
rootSpan: options.tracingContext?.currentSpan,
|
|
23273
|
+
resume: options.resumeData === void 0 ? void 0 : {
|
|
23274
|
+
resumeData: options.resumeData,
|
|
23275
|
+
suspendPayload: options.suspendPayload
|
|
23276
|
+
},
|
|
23277
|
+
initialAttempt: () => ({
|
|
23278
|
+
outcome: settled.then((outcome) => "error" in outcome ? this.toolErrorOutcome(name, outcome.error) : this.toTaskOutcome(name, tool, outcome.execution)),
|
|
23279
|
+
abort: (reason) => controller.abort(reason)
|
|
23280
|
+
})
|
|
23281
|
+
}) };
|
|
23282
|
+
} catch (error) {
|
|
23283
|
+
if (!(error instanceof MCPTaskLimitError)) {
|
|
23284
|
+
controller.abort(error);
|
|
23285
|
+
throw error;
|
|
23286
|
+
}
|
|
23287
|
+
}
|
|
23288
|
+
this.logger.debug(`Tool '${name}' outlived its sync window at a task cap; answering synchronously.`);
|
|
23289
|
+
if (requestSignal.aborted) followRequest();
|
|
23290
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23291
|
+
const outcome = await settled;
|
|
23292
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23293
|
+
if ("error" in outcome) throw outcome.error;
|
|
23294
|
+
return outcome;
|
|
23295
|
+
}
|
|
23296
|
+
/**
|
|
23297
|
+
* Creates the task (durably, before answering) and hands its work to the store. `initialAttempt`
|
|
23298
|
+
* builds the run of an execution that already started under the request; it stays in this
|
|
23299
|
+
* process. Rejects with {@link MCPTaskLimitError}, creating nothing, at a task cap.
|
|
23300
|
+
*/
|
|
23301
|
+
async startToolTask(ctx, requestContext, name, args, taskMode, { resume, initialAttempt, rootSpan }) {
|
|
23302
|
+
const context = {
|
|
23303
|
+
toolName: name,
|
|
23304
|
+
args,
|
|
23305
|
+
requestContext: this.persistTaskRequestContext(name, requestContext),
|
|
23306
|
+
...resume && { resume }
|
|
23307
|
+
};
|
|
23308
|
+
const params = {
|
|
23309
|
+
ttlMs: taskMode.ttlMs,
|
|
23310
|
+
principal: principalOf(ctx, requestContext),
|
|
23311
|
+
context,
|
|
23312
|
+
limits: this.tasks.limits,
|
|
23313
|
+
...rootSpan && { trace: {
|
|
23314
|
+
traceId: rootSpan.traceId,
|
|
23315
|
+
spanId: rootSpan.id
|
|
23316
|
+
} }
|
|
23317
|
+
};
|
|
23318
|
+
const { store, task } = await this.createTask(ctx, requestContext, params, initialAttempt !== void 0);
|
|
23319
|
+
store.run(task.taskId, this.toolTaskWork(context), { initialAttempt: initialAttempt && this.tracedInitialAttempt(name, initialAttempt(), params.trace, task.taskId, requestContext) });
|
|
23320
|
+
this.logger.debug(`Tool '${name}' continues as task.`);
|
|
23321
|
+
return taskHandleResult(task);
|
|
23322
|
+
}
|
|
23323
|
+
/** The request context a task keeps for its later attempts. */
|
|
23324
|
+
persistTaskRequestContext(name, requestContext) {
|
|
23325
|
+
const { persisted, dropped } = persistableRequestContext(requestContext);
|
|
23326
|
+
if (dropped.length > 0) this.logger.warn(`Tool task '${name}' can't persist request context keys; runs from the task won't see them`, {
|
|
23327
|
+
tool: name,
|
|
23328
|
+
keys: dropped
|
|
23329
|
+
});
|
|
23330
|
+
return persisted;
|
|
23331
|
+
}
|
|
23332
|
+
/** Creates a task durably. `pinned` keeps its first attempt on this worker, which already runs it. */
|
|
23333
|
+
async createTask(ctx, requestContext, params, pinned) {
|
|
23334
|
+
const store = this.taskStore instanceof SelectingMCPTaskStore ? await this.taskStore.resolve() : this.taskStore;
|
|
23335
|
+
return {
|
|
23336
|
+
store,
|
|
23337
|
+
task: store instanceof BackgroundTaskMCPTaskStore ? await store.create(params, {
|
|
23338
|
+
pinToWorker: pinned,
|
|
23339
|
+
clientLabel: clientLabelOf(ctx, requestContext)
|
|
23340
|
+
}) : store instanceof InMemoryMCPTaskStore ? await store.create(params, { clientLabel: clientLabelOf(ctx, requestContext) }) : await store.create(params)
|
|
23341
|
+
};
|
|
23342
|
+
}
|
|
23343
|
+
/**
|
|
23344
|
+
* The work of a tool task, rebuilt from nothing but what the task's `context` records, so a store
|
|
23345
|
+
* can dispatch it again after a restart. Each attempt runs the tool once; an answered suspension
|
|
23346
|
+
* runs it again with `resumeData`, as everywhere else in Mastra.
|
|
23347
|
+
*/
|
|
23348
|
+
toolTaskWork(context) {
|
|
23349
|
+
const name = context.toolName;
|
|
23350
|
+
return async (attempt) => {
|
|
23351
|
+
const tool = this.convertedTools[name];
|
|
23352
|
+
if (!tool) throw new Error(`Tool '${name}' is no longer served`);
|
|
23353
|
+
const requestContext = new RequestContext(Object.entries(context.requestContext));
|
|
23354
|
+
try {
|
|
23355
|
+
await this.enforceToolExecutionFGA(name, requestContext);
|
|
23356
|
+
} catch (error) {
|
|
23357
|
+
return this.toolErrorOutcome(name, error);
|
|
23358
|
+
}
|
|
23359
|
+
let { resumeData, suspendPayload } = context.resume ?? {};
|
|
23360
|
+
if (attempt.resume) {
|
|
23361
|
+
const [key] = Object.keys(attempt.resume.inputResponses);
|
|
23362
|
+
const answer = inputResponse(attempt.resume.inputResponses, key);
|
|
23363
|
+
if (answer.kind !== "elicit") return completed(errorResult$1(`Unsupported input response for tool '${name}'`));
|
|
23364
|
+
if (answer.action !== "accept") return completed(errorResult$1(`Tool '${name}' was ${answer.action === "decline" ? "declined" : "cancelled"}`));
|
|
23365
|
+
resumeData = answer.content ?? {};
|
|
23366
|
+
suspendPayload = attempt.resume.suspendPayload;
|
|
23367
|
+
}
|
|
23368
|
+
const span = attempt.trace && this.openTaskSpan(`task attempt ${name} #${attempt.trace.attempt}`, attempt.trace, {
|
|
23369
|
+
requestContext,
|
|
23370
|
+
input: context.args,
|
|
23371
|
+
mcpMethod: "tools/call",
|
|
23372
|
+
targetName: name,
|
|
23373
|
+
metadata: {
|
|
23374
|
+
mcpTaskKey: attempt.trace.taskKey,
|
|
23375
|
+
attempt: attempt.trace.attempt,
|
|
23376
|
+
resumed: attempt.trace.resumed,
|
|
23377
|
+
recovered: attempt.trace.recovered
|
|
23378
|
+
}
|
|
23379
|
+
});
|
|
23380
|
+
return this.endAttemptSpan(span, attempt.signal, (async () => {
|
|
23381
|
+
try {
|
|
23382
|
+
const execution = await this.runTool(name, tool, context.args, {
|
|
23383
|
+
requestContext,
|
|
23384
|
+
abortSignal: attempt.signal,
|
|
23385
|
+
resumeData,
|
|
23386
|
+
suspendPayload,
|
|
23387
|
+
tracingContext: span ? { currentSpan: span } : void 0
|
|
23388
|
+
});
|
|
23389
|
+
return this.toTaskOutcome(name, tool, execution);
|
|
23390
|
+
} catch (error) {
|
|
23391
|
+
return this.toolErrorOutcome(name, error);
|
|
23392
|
+
}
|
|
23393
|
+
})());
|
|
23394
|
+
};
|
|
23395
|
+
}
|
|
23396
|
+
/**
|
|
23397
|
+
* Opens a span in a task's trace, as a child of the task's root (the `tools/call` that created
|
|
23398
|
+
* it), from any process. Uses the resume parentage workflows and agents use: the root id becomes
|
|
23399
|
+
* the span's Mastra parent, so storage keeps one tree. `tracingOptions.parentSpanId` would not:
|
|
23400
|
+
* it only correlates with an external system, and storage would record another root.
|
|
23401
|
+
*/
|
|
23402
|
+
openTaskSpan(name, trace, options) {
|
|
23403
|
+
return getOrCreateSpan({
|
|
23404
|
+
type: SpanType.MCP_SERVER_REQUEST,
|
|
23405
|
+
name,
|
|
23406
|
+
entityType: EntityType.MCP_SERVER,
|
|
23407
|
+
entityId: this.id,
|
|
23408
|
+
entityName: this.name,
|
|
23409
|
+
input: options.input,
|
|
23410
|
+
attributes: {
|
|
23411
|
+
mcpMethod: options.mcpMethod,
|
|
23412
|
+
...options.targetName !== void 0 && { targetName: options.targetName },
|
|
23413
|
+
mcpServer: this.name,
|
|
23414
|
+
serverVersion: this.version
|
|
23415
|
+
},
|
|
23416
|
+
metadata: options.metadata,
|
|
23417
|
+
tracingOptions: { traceId: trace.traceId },
|
|
23418
|
+
resumedFromSpanId: trace.spanId,
|
|
23419
|
+
requestContext: options.requestContext,
|
|
23420
|
+
mastra: this.mastra
|
|
23421
|
+
});
|
|
23422
|
+
}
|
|
23423
|
+
/** Ends an attempt's span with how the attempt ended, and passes the outcome on. */
|
|
23424
|
+
async endAttemptSpan(span, signal, outcome) {
|
|
23425
|
+
if (!span) return outcome;
|
|
23426
|
+
try {
|
|
23427
|
+
const result = await outcome;
|
|
23428
|
+
if (signal?.aborted) span.end({ metadata: { mcpTaskStatus: "cancelled" } });
|
|
23429
|
+
else if (result.status === "input_required") span.end({ metadata: { mcpTaskStatus: "input_required" } });
|
|
23430
|
+
else if (result.result.isError) span.error({
|
|
23431
|
+
error: new MastraError({
|
|
23432
|
+
id: "MCP_SERVER_TASK_ATTEMPT_FAILED",
|
|
23433
|
+
domain: ErrorDomain.MCP,
|
|
23434
|
+
category: ErrorCategory.USER,
|
|
23435
|
+
text: result.result.content.map((c) => c.type === "text" ? c.text : "").filter(Boolean).join("\n")
|
|
23436
|
+
}),
|
|
23437
|
+
metadata: { mcpTaskStatus: "completed" }
|
|
23438
|
+
});
|
|
23439
|
+
else span.end({
|
|
23440
|
+
output: result.result,
|
|
23441
|
+
metadata: { mcpTaskStatus: "completed" }
|
|
23442
|
+
});
|
|
23443
|
+
return result;
|
|
23444
|
+
} catch (error) {
|
|
23445
|
+
if (signal?.aborted) span.end({ metadata: { mcpTaskStatus: "cancelled" } });
|
|
23446
|
+
else span.error({
|
|
23447
|
+
error,
|
|
23448
|
+
endTree: true,
|
|
23449
|
+
metadata: { mcpTaskStatus: "failed" }
|
|
23450
|
+
});
|
|
23451
|
+
throw error;
|
|
23452
|
+
}
|
|
23453
|
+
}
|
|
23454
|
+
/**
|
|
23455
|
+
* A first run already under way (an `'auto'` tool's or workflow's, under the request, or a
|
|
23456
|
+
* workflow run started under `opened`) as attempt 1 of its task: the attempt span ends with that run.
|
|
23457
|
+
*/
|
|
23458
|
+
tracedInitialAttempt(name, initialAttempt, trace, taskId, requestContext, opened) {
|
|
23459
|
+
if (!trace) return initialAttempt;
|
|
23460
|
+
const span = opened ?? this.openTaskSpan(`task attempt ${name} #1`, trace, {
|
|
23461
|
+
requestContext,
|
|
23462
|
+
...firstAttemptSpan(name, taskId)
|
|
23463
|
+
});
|
|
23464
|
+
const aborted = new AbortController();
|
|
23465
|
+
return {
|
|
23466
|
+
outcome: this.endAttemptSpan(span, aborted.signal, initialAttempt.outcome),
|
|
23467
|
+
abort: (reason) => {
|
|
23468
|
+
aborted.abort(reason);
|
|
23469
|
+
initialAttempt.abort(reason);
|
|
23470
|
+
}
|
|
23471
|
+
};
|
|
23472
|
+
}
|
|
23473
|
+
toTaskOutcome(name, tool, execution) {
|
|
23474
|
+
if (execution.status === "suspended") {
|
|
23475
|
+
this.logger.debug(`Tool task '${name}' requires client input.`);
|
|
23476
|
+
return {
|
|
23477
|
+
status: "input_required",
|
|
23478
|
+
inputRequests: { [`${INPUT_KEY}-${randomUUID()}`]: this.inputRequestForm(name, execution.suspendPayload, execution.resumeSchema) },
|
|
23479
|
+
suspendPayload: execution.suspendPayload
|
|
23480
|
+
};
|
|
23481
|
+
}
|
|
23482
|
+
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`));
|
|
23483
|
+
return completed(this.toCallToolResult(name, tool, execution.output));
|
|
23484
|
+
}
|
|
23485
|
+
/** A tool that throws inside a task completes it with `isError`, as a synchronous call would answer. */
|
|
23486
|
+
toolErrorOutcome(name, error) {
|
|
23487
|
+
return completed(errorResult$1(JSON.stringify(this.toolError(name, error).toJSON())));
|
|
23488
|
+
}
|
|
23489
|
+
/**
|
|
23490
|
+
* Starts the run behind a `run_<key>` task. `'auto'` answers synchronously when the run finishes
|
|
23491
|
+
* within `syncWindowMs`; a run that suspends or outlasts the window continues as a task.
|
|
23492
|
+
*/
|
|
23493
|
+
async startWorkflowTask(ctx, requestContext, name, args, taskMode, tracingContext) {
|
|
23494
|
+
const workflowKey = name.slice(4);
|
|
23495
|
+
const workflow = this.workflowFor(workflowKey);
|
|
23496
|
+
if (!workflow) throw new Error(`Workflow '${workflowKey}' is not served`);
|
|
23497
|
+
const input = validateToolInput(workflow.inputSchema, args, name);
|
|
23498
|
+
if (input.error) {
|
|
23499
|
+
this.logger.warn(`Tool '${name}' rejected its input.`, { error: input.error.message });
|
|
23500
|
+
return errorResult$1(input.error.message);
|
|
23501
|
+
}
|
|
23502
|
+
const context = {
|
|
23503
|
+
toolName: name,
|
|
23504
|
+
kind: "workflow",
|
|
23505
|
+
workflowKey,
|
|
23506
|
+
runId: randomUUID(),
|
|
23507
|
+
requestContext: this.persistTaskRequestContext(name, requestContext)
|
|
23508
|
+
};
|
|
23509
|
+
const rootSpan = tracingContext.currentSpan;
|
|
23510
|
+
const params = {
|
|
23511
|
+
ttlMs: taskMode.ttlMs,
|
|
23512
|
+
principal: principalOf(ctx, requestContext),
|
|
23513
|
+
context,
|
|
23514
|
+
limits: this.tasks.limits,
|
|
23515
|
+
...rootSpan && { trace: {
|
|
23516
|
+
traceId: rootSpan.traceId,
|
|
23517
|
+
spanId: rootSpan.id
|
|
23518
|
+
} }
|
|
23519
|
+
};
|
|
23520
|
+
const start = (runTracing) => {
|
|
23521
|
+
const run = workflow.createRun({ runId: context.runId });
|
|
23522
|
+
const result = run.then((started) => started.start({
|
|
23523
|
+
inputData: input.data,
|
|
23524
|
+
requestContext,
|
|
23525
|
+
tracingContext: runTracing
|
|
23526
|
+
}));
|
|
23527
|
+
const cancel = () => void run.then((started) => started.cancel()).catch((error) => this.logger.warn("Could not cancel workflow run", {
|
|
23528
|
+
tool: name,
|
|
23529
|
+
error
|
|
23530
|
+
}));
|
|
23531
|
+
this.executingRuns.set(context.runId, cancel);
|
|
23532
|
+
const done = () => this.executingRuns.delete(context.runId);
|
|
23533
|
+
result.then(done, done);
|
|
23534
|
+
return {
|
|
23535
|
+
result,
|
|
23536
|
+
cancel
|
|
23537
|
+
};
|
|
23538
|
+
};
|
|
23539
|
+
if (taskMode.mode === "always") {
|
|
23540
|
+
let created;
|
|
23541
|
+
try {
|
|
23542
|
+
created = await this.createTask(ctx, requestContext, params, true);
|
|
23543
|
+
} catch (error) {
|
|
23544
|
+
if (error instanceof MCPTaskLimitError) return errorResult$1(taskLimitMessage(error));
|
|
23545
|
+
throw error;
|
|
23546
|
+
}
|
|
23547
|
+
const { store, task } = created;
|
|
23548
|
+
const span = params.trace && this.openTaskSpan(`task attempt ${name} #1`, params.trace, firstAttemptSpan(name, task.taskId));
|
|
23549
|
+
const live = start(span ? { currentSpan: span } : tracingContext);
|
|
23550
|
+
store.run(task.taskId, this.workflowTaskWork(context), { initialAttempt: this.tracedInitialAttempt(name, this.liveWorkflowAttempt(workflow, context, live.result), params.trace, task.taskId, requestContext, span) });
|
|
23551
|
+
this.logger.debug(`Workflow tool '${name}' runs as task.`);
|
|
23552
|
+
return taskHandleResult(task);
|
|
23553
|
+
}
|
|
23554
|
+
const live = start(tracingContext);
|
|
23555
|
+
const requestSignal = ctx.mcpReq.signal;
|
|
23556
|
+
const followRequest = () => live.cancel();
|
|
23557
|
+
if (requestSignal.aborted) followRequest();
|
|
23558
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23559
|
+
const settled = live.result.then((view) => ({ view }), (error) => ({ error }));
|
|
23560
|
+
let timer;
|
|
23561
|
+
const deadline = new Promise((resolve) => {
|
|
23562
|
+
timer = setTimeout(() => resolve("timeout"), taskMode.syncWindowMs);
|
|
23563
|
+
});
|
|
23564
|
+
const winner = await Promise.race([settled, deadline]);
|
|
23565
|
+
clearTimeout(timer);
|
|
23566
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23567
|
+
if (winner !== "timeout") {
|
|
23568
|
+
if ("error" in winner) throw winner.error;
|
|
23569
|
+
const outcome = this.workflowRunOutcome(workflow, context, winner.view);
|
|
23570
|
+
if (outcome?.status === "completed") return outcome.result;
|
|
23571
|
+
}
|
|
23572
|
+
try {
|
|
23573
|
+
const { store, task } = await this.createTask(ctx, requestContext, params, true);
|
|
23574
|
+
store.run(task.taskId, this.workflowTaskWork(context), { initialAttempt: this.tracedInitialAttempt(name, this.liveWorkflowAttempt(workflow, context, live.result), params.trace, task.taskId, requestContext) });
|
|
23575
|
+
this.logger.debug(`Workflow tool '${name}' continues as task.`);
|
|
23576
|
+
return taskHandleResult(task);
|
|
23577
|
+
} catch (error) {
|
|
23578
|
+
if (!(error instanceof MCPTaskLimitError)) {
|
|
23579
|
+
live.cancel();
|
|
23580
|
+
throw error;
|
|
23581
|
+
}
|
|
23582
|
+
this.logger.debug(`Workflow tool '${name}' outlived its sync window at the open task cap.`);
|
|
23583
|
+
if (requestSignal.aborted) followRequest();
|
|
23584
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23585
|
+
const late = await settled;
|
|
23586
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23587
|
+
if ("error" in late) throw late.error;
|
|
23588
|
+
const outcome = this.workflowRunOutcome(workflow, context, late.view);
|
|
23589
|
+
if (outcome?.status === "completed") return outcome.result;
|
|
23590
|
+
live.cancel();
|
|
23591
|
+
return errorResult$1(taskLimitMessage(error));
|
|
23592
|
+
}
|
|
23593
|
+
}
|
|
23594
|
+
/** The first attempt of a workflow task: the run this request started, in this process. */
|
|
23595
|
+
liveWorkflowAttempt(workflow, context, result) {
|
|
23596
|
+
const controller = new AbortController();
|
|
23597
|
+
return {
|
|
23598
|
+
outcome: result.then((view) => this.workflowRunOutcome(workflow, context, view) ?? this.watchWorkflowRun(workflow, context, controller.signal), (error) => this.toolErrorOutcome(context.toolName, error)),
|
|
23599
|
+
abort: (reason) => controller.abort(reason)
|
|
23600
|
+
};
|
|
23601
|
+
}
|
|
23602
|
+
/**
|
|
23603
|
+
* The work of a workflow task, rebuilt from its context. It never starts the run: a first attempt
|
|
23604
|
+
* dispatched again (after a restart) re-attaches to it, and an answered suspension resumes it.
|
|
23605
|
+
*/
|
|
23606
|
+
workflowTaskWork(context) {
|
|
23607
|
+
const name = context.toolName;
|
|
23608
|
+
return async (attempt) => {
|
|
23609
|
+
const workflow = this.workflowFor(context.workflowKey);
|
|
23610
|
+
if (!workflow) throw new Error(`Workflow '${context.workflowKey}' is no longer served`);
|
|
23611
|
+
const requestContext = new RequestContext(Object.entries(context.requestContext));
|
|
23612
|
+
try {
|
|
23613
|
+
await this.enforceToolExecutionFGA(name, requestContext);
|
|
23614
|
+
} catch (error) {
|
|
23615
|
+
return this.toolErrorOutcome(name, error);
|
|
23616
|
+
}
|
|
23617
|
+
const span = attempt.trace && this.openTaskSpan(`task attempt ${name} #${attempt.trace.attempt}`, attempt.trace, {
|
|
23618
|
+
requestContext,
|
|
23619
|
+
mcpMethod: "tools/call",
|
|
23620
|
+
targetName: name,
|
|
23621
|
+
metadata: {
|
|
23622
|
+
mcpTaskKey: attempt.trace.taskKey,
|
|
23623
|
+
attempt: attempt.trace.attempt,
|
|
23624
|
+
resumed: attempt.trace.resumed,
|
|
23625
|
+
recovered: attempt.trace.recovered
|
|
23626
|
+
}
|
|
23627
|
+
});
|
|
23628
|
+
return this.endAttemptSpan(span, attempt.signal, attempt.resume ? this.resumeWorkflowRun(workflow, context, attempt, requestContext, span) : this.watchWorkflowRun(workflow, context, attempt.signal));
|
|
23629
|
+
};
|
|
23630
|
+
}
|
|
23631
|
+
async resumeWorkflowRun(workflow, context, attempt, requestContext, span) {
|
|
23632
|
+
const { inputResponses, suspendPayload } = attempt.resume;
|
|
23633
|
+
const [key] = Object.keys(inputResponses);
|
|
23634
|
+
const answer = inputResponse(inputResponses, key);
|
|
23635
|
+
const label = `Workflow '${context.workflowKey}'`;
|
|
23636
|
+
if (answer.kind !== "elicit") return completed(errorResult$1(`Unsupported input response for ${label}`));
|
|
23637
|
+
if (answer.action !== "accept") {
|
|
23638
|
+
await this.cancelWorkflowRun(context);
|
|
23639
|
+
return completed(errorResult$1(`${label} was ${answer.action === "decline" ? "declined" : "cancelled"}`));
|
|
23640
|
+
}
|
|
23641
|
+
const current = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23642
|
+
if (!current) throw new Error(`${label} run '${context.runId}' was lost`);
|
|
23643
|
+
const answered = suspendPayload;
|
|
23644
|
+
if (!isStillSuspended({
|
|
23645
|
+
status: current.status,
|
|
23646
|
+
steps: current.steps
|
|
23647
|
+
}, answered)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23648
|
+
const run = await workflow.createRun({ runId: context.runId });
|
|
23649
|
+
let view;
|
|
23650
|
+
this.executingRuns.set(context.runId, () => void run.cancel().catch((error) => this.logger.warn("Could not cancel workflow run", {
|
|
23651
|
+
tool: context.toolName,
|
|
23652
|
+
error
|
|
23653
|
+
})));
|
|
23654
|
+
try {
|
|
23655
|
+
view = await run.resume({
|
|
23656
|
+
step: answered.step,
|
|
23657
|
+
resumeData: answer.content ?? {},
|
|
23658
|
+
requestContext,
|
|
23659
|
+
...span && { tracingContext: { currentSpan: span } }
|
|
23660
|
+
});
|
|
23661
|
+
} catch (error) {
|
|
23662
|
+
if (isResumeClaimLost(error)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23663
|
+
const now = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23664
|
+
if (now && !isStillSuspended({
|
|
23665
|
+
status: now.status,
|
|
23666
|
+
steps: now.steps
|
|
23667
|
+
}, answered)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23668
|
+
return this.toolErrorOutcome(context.toolName, error);
|
|
23669
|
+
} finally {
|
|
23670
|
+
this.executingRuns.delete(context.runId);
|
|
23671
|
+
}
|
|
23672
|
+
return this.workflowRunOutcome(workflow, context, view) ?? this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23673
|
+
}
|
|
23674
|
+
/**
|
|
23675
|
+
* Polls the run until it finishes or suspends on a step the client can answer. The only way to
|
|
23676
|
+
* follow a run from any instance, whichever engine runs it.
|
|
23677
|
+
*/
|
|
23678
|
+
async watchWorkflowRun(workflow, context, signal) {
|
|
23679
|
+
let first = true;
|
|
23680
|
+
for (;;) {
|
|
23681
|
+
signal.throwIfAborted();
|
|
23682
|
+
const state = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23683
|
+
if (!state || first && state.status === "pending") throw new Error(`Workflow '${context.workflowKey}' run '${context.runId}' was lost`);
|
|
23684
|
+
first = false;
|
|
23685
|
+
const outcome = this.workflowRunOutcome(workflow, context, {
|
|
23686
|
+
status: state.status,
|
|
23687
|
+
input: state.payload,
|
|
23688
|
+
steps: state.steps,
|
|
23689
|
+
result: state.result,
|
|
23690
|
+
error: state.error
|
|
23691
|
+
});
|
|
23692
|
+
if (outcome) return outcome;
|
|
23693
|
+
await sleep(WORKFLOW_POLL_INTERVAL_MS, signal);
|
|
23694
|
+
}
|
|
23695
|
+
}
|
|
23696
|
+
/**
|
|
23697
|
+
* What a run's state means for its task, or `undefined` while there is nothing to report: the
|
|
23698
|
+
* run is still going, or waits on a step whose `resumeSchema` no input request can present (the
|
|
23699
|
+
* task stays `working` until the run is resumed some other way).
|
|
23700
|
+
*/
|
|
23701
|
+
workflowRunOutcome(workflow, context, view) {
|
|
23702
|
+
const label = `Workflow '${context.workflowKey}'`;
|
|
23703
|
+
switch (view.status) {
|
|
23704
|
+
case "success":
|
|
23705
|
+
case "bailed": return completed({
|
|
23706
|
+
isError: false,
|
|
23707
|
+
content: [{
|
|
23708
|
+
type: "text",
|
|
23709
|
+
text: JSON.stringify(runSummary(view))
|
|
23710
|
+
}]
|
|
23711
|
+
});
|
|
23712
|
+
case "failed": return completed(errorResult$1(`${label} failed: ${runErrorMessage(view)}`));
|
|
23713
|
+
case "tripwire": return completed(errorResult$1(`${label} was blocked: ${runErrorMessage(view)}`));
|
|
23714
|
+
case "canceled": return completed(errorResult$1(`${label} run was cancelled`));
|
|
23715
|
+
case "suspended": {
|
|
23716
|
+
const suspended = firstSuspendedStep(workflow, view);
|
|
23717
|
+
if (!suspended) return void 0;
|
|
23718
|
+
const requestedSchema = this.representableForm(suspended.step?.resumeSchema);
|
|
23719
|
+
if (!requestedSchema) return void 0;
|
|
23720
|
+
this.logger.debug(`Workflow task '${context.toolName}' requires client input.`);
|
|
23721
|
+
return {
|
|
23722
|
+
status: "input_required",
|
|
23723
|
+
inputRequests: { [`${INPUT_KEY}-${randomUUID()}`]: this.inputRequestForm(context.toolName, suspended.payload, requestedSchema) },
|
|
23724
|
+
suspendPayload: {
|
|
23725
|
+
step: suspended.path,
|
|
23726
|
+
suspendedAt: suspended.suspendedAt
|
|
23727
|
+
}
|
|
23728
|
+
};
|
|
23729
|
+
}
|
|
23730
|
+
default: return;
|
|
23731
|
+
}
|
|
23732
|
+
}
|
|
23733
|
+
/**
|
|
23734
|
+
* Cancels the task's run, for a declined input or the store's onCancel (`tasks/cancel`, TTL
|
|
23735
|
+
* expiry), on whichever instance that happens. A run executing in this process is cancelled here.
|
|
23736
|
+
* A run executing in another process is left alone: writing `canceled` under it doesn't stop it,
|
|
23737
|
+
* its next write overwrites the status, and its own cancel would then find it `canceled` and do
|
|
23738
|
+
* nothing. A run that waits (suspended, pending) is cancelled in storage. A finished run is left alone.
|
|
23739
|
+
*/
|
|
23740
|
+
async cancelWorkflowRun(context) {
|
|
23741
|
+
const local = this.executingRuns.get(context.runId);
|
|
23742
|
+
if (local) return local();
|
|
23743
|
+
const workflow = this.workflowFor(context.workflowKey);
|
|
23744
|
+
if (!workflow) return;
|
|
23745
|
+
try {
|
|
23746
|
+
const state = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23747
|
+
if (!state || isFinalRunStatus(state.status) || state.status === "running") return;
|
|
23748
|
+
await (await workflow.createRun({ runId: context.runId })).cancel();
|
|
23749
|
+
} catch (error) {
|
|
23750
|
+
this.logger.warn("Could not cancel workflow run", {
|
|
23751
|
+
tool: context.toolName,
|
|
23752
|
+
error
|
|
23753
|
+
});
|
|
23754
|
+
}
|
|
23755
|
+
}
|
|
23756
|
+
workflowFor(key) {
|
|
23757
|
+
return this.workflows?.[key];
|
|
23758
|
+
}
|
|
20825
23759
|
/**
|
|
20826
23760
|
* Runs one round of a tool. The tool sees the same `suspend` / `resumeData` /
|
|
20827
23761
|
* `suspendPayload` vocabulary as under an agent or workflow; nothing else is
|
|
@@ -20836,7 +23770,7 @@ var MCPServer = class extends MCPServerBase {
|
|
|
20836
23770
|
skipToolSpan: true,
|
|
20837
23771
|
requestContext: options.requestContext,
|
|
20838
23772
|
tracingContext: options.tracingContext,
|
|
20839
|
-
abortSignal: options.mcp?.extra.signal,
|
|
23773
|
+
abortSignal: options.abortSignal ?? options.mcp?.extra.signal,
|
|
20840
23774
|
...options.mcp ? { mcp: options.mcp } : {},
|
|
20841
23775
|
suspend: async (payload, suspendOptions) => {
|
|
20842
23776
|
suspension = {
|
|
@@ -21157,6 +24091,65 @@ var MCPServer = class extends MCPServerBase {
|
|
|
21157
24091
|
this.httpHandler = void 0;
|
|
21158
24092
|
this.nodeHandler = void 0;
|
|
21159
24093
|
}
|
|
24094
|
+
if (this.taskStore instanceof SelectingMCPTaskStore) this.taskStore.close();
|
|
24095
|
+
await this.backgroundTaskStore?.close();
|
|
24096
|
+
}
|
|
24097
|
+
/** Whether the id was given (in the config, or as a Mastra registration key) rather than generated per process. */
|
|
24098
|
+
hasStableId = false;
|
|
24099
|
+
setId(id) {
|
|
24100
|
+
super.setId(id);
|
|
24101
|
+
if (this.id === id) this.hasStableId = true;
|
|
24102
|
+
}
|
|
24103
|
+
/** @internal */
|
|
24104
|
+
__registerMastra(mastra) {
|
|
24105
|
+
super.__registerMastra(mastra);
|
|
24106
|
+
const prepared = this.backgroundTaskStore;
|
|
24107
|
+
if (!this.prepareBackgroundTaskStore() || !(this.taskStore instanceof SelectingMCPTaskStore)) return;
|
|
24108
|
+
if (!prepared) this.taskStore.reselect();
|
|
24109
|
+
this.taskStore.resolve().catch((error) => this.logger.debug(`MCP server '${this.name}' could not choose its task store yet`, { error }));
|
|
24110
|
+
}
|
|
24111
|
+
/**
|
|
24112
|
+
* Builds the background-task store and registers an executor for every task tool, when tasks
|
|
24113
|
+
* are on, no store was passed in, and Mastra has background tasks.
|
|
24114
|
+
*/
|
|
24115
|
+
prepareBackgroundTaskStore() {
|
|
24116
|
+
if (this.backgroundTaskStore) return this.backgroundTaskStore;
|
|
24117
|
+
if (!this.tasks.enabled || this.tasks.store) return void 0;
|
|
24118
|
+
const manager = this.mastra?.backgroundTaskManager;
|
|
24119
|
+
if (!manager) return void 0;
|
|
24120
|
+
const store = new BackgroundTaskMCPTaskStore({
|
|
24121
|
+
manager,
|
|
24122
|
+
serverId: this.id,
|
|
24123
|
+
workFor: (context) => isWorkflowTaskContext(context) ? this.workflowTaskWork(context) : this.toolTaskWork(context),
|
|
24124
|
+
onCancel: (context) => isWorkflowTaskContext(context) ? this.cancelWorkflowRun(context) : void 0,
|
|
24125
|
+
recoverFor: (name) => this.tasks.modeFor(name, this.convertedTools[name]?.mcp?.toolType).recover,
|
|
24126
|
+
logger: () => this.logger
|
|
24127
|
+
});
|
|
24128
|
+
for (const [name, tool] of Object.entries(this.convertedTools)) if (this.tasks.modeFor(name, tool.mcp?.toolType).mode !== "off") store.registerTool(name);
|
|
24129
|
+
this.backgroundTaskStore = store;
|
|
24130
|
+
return store;
|
|
24131
|
+
}
|
|
24132
|
+
/**
|
|
24133
|
+
* Mastra's background tasks when the storage keeps per-row expiry (so a task's TTL, not the
|
|
24134
|
+
* global cleanup, decides when it is deleted); an in-memory store otherwise.
|
|
24135
|
+
*/
|
|
24136
|
+
async selectTaskStore() {
|
|
24137
|
+
const manager = this.mastra?.backgroundTaskManager;
|
|
24138
|
+
if (manager) {
|
|
24139
|
+
const storage = await manager.getStorage();
|
|
24140
|
+
if (storage.supportsTaskExpiry === true) {
|
|
24141
|
+
const prepared = this.backgroundTaskStore;
|
|
24142
|
+
const store = this.prepareBackgroundTaskStore();
|
|
24143
|
+
if (!this.hasStableId) this.logger.warn(`MCP server '${this.name}' keeps tasks in Mastra storage under the generated id '${this.id}', which changes on every start: other instances and restarts won't find its tasks. Pass \`id\` to the MCPServer, or register it under a key in Mastra's \`mcpServers\`.`);
|
|
24144
|
+
store.startSweeping();
|
|
24145
|
+
if (!prepared) this.logger.warn(`MCP server '${this.name}' serves tasks from Mastra background tasks that started after it was registered; tasks created before a restart may not have been recovered`);
|
|
24146
|
+
return store;
|
|
24147
|
+
}
|
|
24148
|
+
this.logger.warn(`MCP server '${this.name}' keeps tasks in memory: the background tasks storage (${storage.constructor.name}) does not support task expiry. Upgrade the storage adapter to serve tasks across instances.`);
|
|
24149
|
+
await this.backgroundTaskStore?.close();
|
|
24150
|
+
this.backgroundTaskStore = void 0;
|
|
24151
|
+
}
|
|
24152
|
+
return new InMemoryMCPTaskStore({ onCancel: (context) => isWorkflowTaskContext(context) ? this.cancelWorkflowRun(context) : void 0 });
|
|
21160
24153
|
}
|
|
21161
24154
|
getServerInfo() {
|
|
21162
24155
|
return {
|
|
@@ -21282,6 +24275,32 @@ var MCPServer = class extends MCPServerBase {
|
|
|
21282
24275
|
this.eventsInspector ??= createEventsInspector(this.eventsDeps());
|
|
21283
24276
|
return this.eventsInspector;
|
|
21284
24277
|
}
|
|
24278
|
+
/**
|
|
24279
|
+
* Which tools run as MCP tasks and whether the task store can be listed, or `undefined` when
|
|
24280
|
+
* no tool runs as a task. Used by Studio.
|
|
24281
|
+
* @experimental
|
|
24282
|
+
*/
|
|
24283
|
+
getTasksInfo() {
|
|
24284
|
+
if (!this.tasks.enabled) return void 0;
|
|
24285
|
+
return tasksInfo(this.tasks, this.taskToolTypes());
|
|
24286
|
+
}
|
|
24287
|
+
/**
|
|
24288
|
+
* Lists, cancels and answers this server's tasks across principals, through the task store's
|
|
24289
|
+
* admin view, or `undefined` when no tool runs as a task or the store can't be listed. Used by
|
|
24290
|
+
* Studio; there is no protocol equivalent of the listing.
|
|
24291
|
+
* @experimental
|
|
24292
|
+
*/
|
|
24293
|
+
getTasksInspector() {
|
|
24294
|
+
if (!this.getTasksInfo()?.listable) return void 0;
|
|
24295
|
+
this.tasksInspector ??= createTasksInspector(async () => {
|
|
24296
|
+
if (this.taskStore instanceof SelectingMCPTaskStore) return this.taskStore.admin();
|
|
24297
|
+
return isMCPTaskStoreAdmin(this.taskStore) ? this.taskStore : void 0;
|
|
24298
|
+
});
|
|
24299
|
+
return this.tasksInspector;
|
|
24300
|
+
}
|
|
24301
|
+
taskToolTypes() {
|
|
24302
|
+
return Object.entries(this.convertedTools).map(([name, tool]) => [name, tool.mcp?.toolType]);
|
|
24303
|
+
}
|
|
21285
24304
|
eventsDeps() {
|
|
21286
24305
|
return {
|
|
21287
24306
|
registry: this.eventRegistry,
|
|
@@ -21296,6 +24315,10 @@ var MCPServer = class extends MCPServerBase {
|
|
|
21296
24315
|
function isRegistrableTool(tool) {
|
|
21297
24316
|
return !!tool && typeof tool === "object" && "id" in tool && typeof tool.id === "string";
|
|
21298
24317
|
}
|
|
24318
|
+
function taskLimitMessage({ scope, limit, kind }) {
|
|
24319
|
+
if (kind === "retained") return scope === "principal" ? `Too many recent tasks for this client (limit ${limit}). Try again once older tasks expire.` : `Too many recent tasks on this server (limit ${limit}). Try again later.`;
|
|
24320
|
+
return scope === "principal" ? `Too many open tasks for this client (limit ${limit}). Wait for one to finish or cancel one.` : `Too many open tasks on this server (limit ${limit}). Try again later.`;
|
|
24321
|
+
}
|
|
21299
24322
|
function errorResult$1(text) {
|
|
21300
24323
|
return {
|
|
21301
24324
|
content: [{
|
|
@@ -21305,6 +24328,75 @@ function errorResult$1(text) {
|
|
|
21305
24328
|
isError: true
|
|
21306
24329
|
};
|
|
21307
24330
|
}
|
|
24331
|
+
/** The span options of a task's first attempt, minus its request context. */
|
|
24332
|
+
function firstAttemptSpan(name, taskId) {
|
|
24333
|
+
return {
|
|
24334
|
+
mcpMethod: "tools/call",
|
|
24335
|
+
targetName: name,
|
|
24336
|
+
metadata: {
|
|
24337
|
+
mcpTaskKey: taskKeyOf(taskId),
|
|
24338
|
+
attempt: 1,
|
|
24339
|
+
resumed: false,
|
|
24340
|
+
recovered: false
|
|
24341
|
+
}
|
|
24342
|
+
};
|
|
24343
|
+
}
|
|
24344
|
+
function completed(result) {
|
|
24345
|
+
return {
|
|
24346
|
+
status: "completed",
|
|
24347
|
+
result
|
|
24348
|
+
};
|
|
24349
|
+
}
|
|
24350
|
+
/** How often a workflow task reads its run's state while it runs elsewhere. */
|
|
24351
|
+
const WORKFLOW_POLL_INTERVAL_MS = 1e3;
|
|
24352
|
+
function formValidation(message, requestedSchema) {
|
|
24353
|
+
return specTypeSchemas.ElicitRequestFormParams["~standard"].validate({
|
|
24354
|
+
message,
|
|
24355
|
+
requestedSchema
|
|
24356
|
+
});
|
|
24357
|
+
}
|
|
24358
|
+
/** `run.resume()` lost to another resume, or found the run no longer suspended. */
|
|
24359
|
+
function isResumeClaimLost(error) {
|
|
24360
|
+
return error instanceof MastraError && (error.id === "WORKFLOW_RESUME_ALREADY_CLAIMED" || error.id === "WORKFLOW_RUN_NOT_SUSPENDED");
|
|
24361
|
+
}
|
|
24362
|
+
function sleep(ms, signal) {
|
|
24363
|
+
return new Promise((resolve, reject) => {
|
|
24364
|
+
const onAbort = () => {
|
|
24365
|
+
clearTimeout(timer);
|
|
24366
|
+
reject(signal.reason);
|
|
24367
|
+
};
|
|
24368
|
+
const timer = setTimeout(() => {
|
|
24369
|
+
signal.removeEventListener("abort", onAbort);
|
|
24370
|
+
resolve();
|
|
24371
|
+
}, ms);
|
|
24372
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
24373
|
+
});
|
|
24374
|
+
}
|
|
24375
|
+
/** Whether this request's client declared the Tasks extension; only 2026-07-28 requests carry it. */
|
|
24376
|
+
function clientDeclaredTasks(ctx) {
|
|
24377
|
+
const envelope = ctx.mcpReq.envelope;
|
|
24378
|
+
return hasTasksCapability(envelope?.[CLIENT_CAPABILITIES_META_KEY]);
|
|
24379
|
+
}
|
|
24380
|
+
/** The request context a task keeps. Bearer tokens are dropped so they are never stored with the task. */
|
|
24381
|
+
function persistableRequestContext(requestContext) {
|
|
24382
|
+
const { authInfo, [MASTRA_AUTH_TOKEN_KEY]: _authToken, ...rest } = requestContext.toJSON();
|
|
24383
|
+
let stripped = rest;
|
|
24384
|
+
if (authInfo && typeof authInfo === "object") {
|
|
24385
|
+
const { token: _token, ...withoutToken } = authInfo;
|
|
24386
|
+
stripped = {
|
|
24387
|
+
...rest,
|
|
24388
|
+
authInfo: withoutToken
|
|
24389
|
+
};
|
|
24390
|
+
} else if (authInfo !== void 0) stripped = {
|
|
24391
|
+
...rest,
|
|
24392
|
+
authInfo
|
|
24393
|
+
};
|
|
24394
|
+
const persisted = JSON.parse(JSON.stringify(stripped));
|
|
24395
|
+
return {
|
|
24396
|
+
persisted,
|
|
24397
|
+
dropped: [...requestContext.keys()].filter((key) => typeof key === "string" && key !== MASTRA_AUTH_TOKEN_KEY && requestContext.get(key) !== void 0 && !(key in persisted))
|
|
24398
|
+
};
|
|
24399
|
+
}
|
|
21308
24400
|
/** The tool's MCP App URI; the nested key wins over the flat one, as in ext-apps' `getToolUiResourceUri`. */
|
|
21309
24401
|
function uiResourceUri(meta) {
|
|
21310
24402
|
return (meta?.ui)?.resourceUri || meta?.[RESOURCE_URI_META_KEY] || void 0;
|
|
@@ -22014,6 +25106,6 @@ var MCPEventsSignalProvider = class MCPEventsSignalProvider extends SignalProvid
|
|
|
22014
25106
|
else console.warn(`[${this.id}] ${info.key}:`, error);
|
|
22015
25107
|
}
|
|
22016
25108
|
};
|
|
22017
|
-
export { InMemoryMCPEventCursorStore, InMemoryOAuthStorage, InMemoryWebhookSubscriptionStore, InternalMastraMCPClient, MCPClient, MCPClientServerProxy, MCPEventsSignalProvider, MCPOAuthClientProvider, MCPServer, MCP_CALL_TOOL_CONTENT, MCP_CALL_TOOL_META, MCP_CLIENT_PROTOCOL_VERSION, MCP_PROTOCOL_VERSION, ServerEventActions, ServerPromptActions, ServerResourceActions, ServerToolActions, UnauthorizedError, auth, buildDiscoveryUrls, createIntrospectionValidator, createOAuthCallbackServer, createOAuthMiddleware, createSimpleTokenProvider, createStaticTokenValidator, defineEvent, discoverAuthorizationServerMetadata, discoverOAuthMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractBearerToken, extractResourceMetadataUrl, generateProtectedResourceMetadata, generateWWWAuthenticateHeader, getCallbackUrlCandidates, getMcpCallToolContent, getMcpCallToolMeta, parseErrorResponse, refreshAuthorization, selectResourceURL, startAuthorization, traceContextFromMeta, traceContextToMeta };
|
|
25109
|
+
export { InMemoryMCPEventCursorStore, InMemoryMCPTaskStore, InMemoryOAuthStorage, InMemoryWebhookSubscriptionStore, InternalMastraMCPClient, MCPClient, MCPClientServerProxy, MCPEventsSignalProvider, MCPOAuthClientProvider, MCPServer, MCPTaskLimitError, MCP_CALL_TOOL_CONTENT, MCP_CALL_TOOL_META, MCP_CLIENT_PROTOCOL_VERSION, MCP_PROTOCOL_VERSION, ServerEventActions, ServerPromptActions, ServerResourceActions, ServerToolActions, UnauthorizedError, auth, buildDiscoveryUrls, createIntrospectionValidator, createOAuthCallbackServer, createOAuthMiddleware, createSimpleTokenProvider, createStaticTokenValidator, defineEvent, discoverAuthorizationServerMetadata, discoverOAuthMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractBearerToken, extractResourceMetadataUrl, generateProtectedResourceMetadata, generateWWWAuthenticateHeader, getCallbackUrlCandidates, getMcpCallToolContent, getMcpCallToolMeta, isMCPTaskStoreAdmin, isWorkflowTaskContext, parseErrorResponse, principalKeyOf, refreshAuthorization, selectResourceURL, startAuthorization, taskKeyOf, traceContextFromMeta, traceContextToMeta };
|
|
22018
25110
|
|
|
22019
25111
|
//# sourceMappingURL=index.js.map
|