@mastra/mcp 2.6.0 → 2.7.0-alpha.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 +8 -7
package/dist/index.cjs
CHANGED
|
@@ -12534,8 +12534,8 @@ const WEBHOOK_HEADERS = {
|
|
|
12534
12534
|
*
|
|
12535
12535
|
* @experimental SEP-3415 is a draft Extensions Track proposal. Shapes may change.
|
|
12536
12536
|
*/
|
|
12537
|
-
const JsonObjectSchema = zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown());
|
|
12538
|
-
const MetaSchema = JsonObjectSchema.optional();
|
|
12537
|
+
const JsonObjectSchema$1 = zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown());
|
|
12538
|
+
const MetaSchema$1 = JsonObjectSchema$1.optional();
|
|
12539
12539
|
const CursorSchema = zod_v4.z.string().nullable();
|
|
12540
12540
|
const ResultTypeSchema = zod_v4.z.string().optional();
|
|
12541
12541
|
const RequestIdSchema = zod_v4.z.union([zod_v4.z.string(), zod_v4.z.number()]);
|
|
@@ -12546,17 +12546,17 @@ const EventDescriptorSchema = zod_v4.z.looseObject({
|
|
|
12546
12546
|
name: zod_v4.z.string(),
|
|
12547
12547
|
description: zod_v4.z.string().optional(),
|
|
12548
12548
|
delivery: zod_v4.z.array(zod_v4.z.string()).min(1),
|
|
12549
|
-
inputSchema: JsonObjectSchema.optional(),
|
|
12550
|
-
payloadSchema: JsonObjectSchema.optional(),
|
|
12551
|
-
_meta: MetaSchema
|
|
12549
|
+
inputSchema: JsonObjectSchema$1.optional(),
|
|
12550
|
+
payloadSchema: JsonObjectSchema$1.optional(),
|
|
12551
|
+
_meta: MetaSchema$1
|
|
12552
12552
|
});
|
|
12553
12553
|
const EventOccurrenceSchema = zod_v4.z.looseObject({
|
|
12554
12554
|
eventId: zod_v4.z.string(),
|
|
12555
12555
|
name: zod_v4.z.string(),
|
|
12556
12556
|
timestamp: zod_v4.z.string(),
|
|
12557
|
-
data: JsonObjectSchema,
|
|
12557
|
+
data: JsonObjectSchema$1,
|
|
12558
12558
|
cursor: CursorSchema.optional(),
|
|
12559
|
-
_meta: MetaSchema
|
|
12559
|
+
_meta: MetaSchema$1
|
|
12560
12560
|
});
|
|
12561
12561
|
const EventsErrorSchema = zod_v4.z.looseObject({
|
|
12562
12562
|
code: zod_v4.z.number().int(),
|
|
@@ -12565,7 +12565,7 @@ const EventsErrorSchema = zod_v4.z.looseObject({
|
|
|
12565
12565
|
});
|
|
12566
12566
|
const ListEventsRequestParamsSchema = zod_v4.z.looseObject({
|
|
12567
12567
|
cursor: zod_v4.z.string().optional(),
|
|
12568
|
-
_meta: MetaSchema
|
|
12568
|
+
_meta: MetaSchema$1
|
|
12569
12569
|
});
|
|
12570
12570
|
zod_v4.z.object({
|
|
12571
12571
|
method: zod_v4.z.literal(EVENTS_METHODS.list),
|
|
@@ -12575,14 +12575,14 @@ const ListEventsResultSchema = zod_v4.z.looseObject({
|
|
|
12575
12575
|
resultType: ResultTypeSchema,
|
|
12576
12576
|
events: zod_v4.z.array(EventDescriptorSchema),
|
|
12577
12577
|
nextCursor: zod_v4.z.string().optional(),
|
|
12578
|
-
_meta: MetaSchema
|
|
12578
|
+
_meta: MetaSchema$1
|
|
12579
12579
|
});
|
|
12580
12580
|
const SubscriptionParamsShape = {
|
|
12581
12581
|
name: zod_v4.z.string(),
|
|
12582
|
-
arguments: JsonObjectSchema.optional(),
|
|
12582
|
+
arguments: JsonObjectSchema$1.optional(),
|
|
12583
12583
|
cursor: CursorSchema.optional(),
|
|
12584
12584
|
maxAgeMs: zod_v4.z.number().int().nonnegative().optional(),
|
|
12585
|
-
_meta: MetaSchema
|
|
12585
|
+
_meta: MetaSchema$1
|
|
12586
12586
|
};
|
|
12587
12587
|
const PollEventsRequestParamsSchema = zod_v4.z.looseObject({
|
|
12588
12588
|
...SubscriptionParamsShape,
|
|
@@ -12599,7 +12599,7 @@ const PollEventsResultSchema = zod_v4.z.looseObject({
|
|
|
12599
12599
|
truncated: zod_v4.z.boolean().optional(),
|
|
12600
12600
|
hasMore: zod_v4.z.boolean().optional(),
|
|
12601
12601
|
nextPollMs: zod_v4.z.number().nonnegative().optional(),
|
|
12602
|
-
_meta: MetaSchema
|
|
12602
|
+
_meta: MetaSchema$1
|
|
12603
12603
|
});
|
|
12604
12604
|
const StreamEventsRequestParamsSchema = zod_v4.z.looseObject(SubscriptionParamsShape);
|
|
12605
12605
|
zod_v4.z.object({
|
|
@@ -12608,7 +12608,7 @@ zod_v4.z.object({
|
|
|
12608
12608
|
});
|
|
12609
12609
|
const StreamEventsResultSchema = zod_v4.z.looseObject({
|
|
12610
12610
|
resultType: ResultTypeSchema,
|
|
12611
|
-
_meta: MetaSchema
|
|
12611
|
+
_meta: MetaSchema$1
|
|
12612
12612
|
});
|
|
12613
12613
|
/** `_meta` on every `notifications/events/*` message delivered on a push stream. */
|
|
12614
12614
|
const StreamNotificationMetaSchema = zod_v4.z.looseObject({ [EVENTS_SUBSCRIPTION_ID_META_KEY]: RequestIdSchema });
|
|
@@ -12627,7 +12627,7 @@ const EventErrorNotificationParamsSchema = zod_v4.z.looseObject({
|
|
|
12627
12627
|
_meta: StreamNotificationMetaSchema
|
|
12628
12628
|
});
|
|
12629
12629
|
const EventTerminatedNotificationParamsSchema = EventErrorNotificationParamsSchema;
|
|
12630
|
-
const EventsListChangedNotificationParamsSchema = zod_v4.z.looseObject({ _meta: MetaSchema }).optional();
|
|
12630
|
+
const EventsListChangedNotificationParamsSchema = zod_v4.z.looseObject({ _meta: MetaSchema$1 }).optional();
|
|
12631
12631
|
zod_v4.z.object({
|
|
12632
12632
|
method: zod_v4.z.literal(EVENTS_NOTIFICATIONS.active),
|
|
12633
12633
|
params: EventActiveNotificationParamsSchema
|
|
@@ -12687,13 +12687,13 @@ const SubscribeEventsResultSchema = zod_v4.z.looseObject({
|
|
|
12687
12687
|
cursor: CursorSchema,
|
|
12688
12688
|
truncated: zod_v4.z.boolean().optional(),
|
|
12689
12689
|
deliveryStatus: WebhookDeliveryStatusSchema.optional(),
|
|
12690
|
-
_meta: MetaSchema
|
|
12690
|
+
_meta: MetaSchema$1
|
|
12691
12691
|
});
|
|
12692
12692
|
const UnsubscribeEventsRequestParamsSchema = zod_v4.z.looseObject({
|
|
12693
12693
|
name: zod_v4.z.string(),
|
|
12694
|
-
arguments: JsonObjectSchema.optional(),
|
|
12694
|
+
arguments: JsonObjectSchema$1.optional(),
|
|
12695
12695
|
delivery: zod_v4.z.looseObject({ url: zod_v4.z.string().url() }),
|
|
12696
|
-
_meta: MetaSchema
|
|
12696
|
+
_meta: MetaSchema$1
|
|
12697
12697
|
});
|
|
12698
12698
|
zod_v4.z.object({
|
|
12699
12699
|
method: zod_v4.z.literal(EVENTS_METHODS.unsubscribe),
|
|
@@ -12701,7 +12701,7 @@ zod_v4.z.object({
|
|
|
12701
12701
|
});
|
|
12702
12702
|
const UnsubscribeEventsResultSchema = zod_v4.z.looseObject({
|
|
12703
12703
|
resultType: ResultTypeSchema,
|
|
12704
|
-
_meta: MetaSchema
|
|
12704
|
+
_meta: MetaSchema$1
|
|
12705
12705
|
});
|
|
12706
12706
|
const WebhookGapBodySchema = zod_v4.z.looseObject({
|
|
12707
12707
|
type: zod_v4.z.literal("gap"),
|
|
@@ -13250,6 +13250,176 @@ function extractBearerToken(authHeader) {
|
|
|
13250
13250
|
if (authHeader.slice(0, 7).toLowerCase() !== prefix) return void 0;
|
|
13251
13251
|
return authHeader.slice(7).trim() || void 0;
|
|
13252
13252
|
}
|
|
13253
|
+
// src/shared/tasks/constants.ts
|
|
13254
|
+
/**
|
|
13255
|
+
* Wire constants for the MCP Tasks extension (SEP-2663, protocol revision 2026-07-28).
|
|
13256
|
+
*
|
|
13257
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13258
|
+
*/
|
|
13259
|
+
/** Extension identifier used as the key under `capabilities.extensions`. */
|
|
13260
|
+
const TASKS_EXTENSION_ID = "io.modelcontextprotocol/tasks";
|
|
13261
|
+
const TASKS_METHODS = {
|
|
13262
|
+
get: "tasks/get",
|
|
13263
|
+
update: "tasks/update",
|
|
13264
|
+
cancel: "tasks/cancel"
|
|
13265
|
+
};
|
|
13266
|
+
/** `resultType` of a `tools/call` result that is a task handle instead of a `CallToolResult`. */
|
|
13267
|
+
const TASK_RESULT_TYPE = "task";
|
|
13268
|
+
const TASK_STATUSES = [
|
|
13269
|
+
"working",
|
|
13270
|
+
"input_required",
|
|
13271
|
+
"completed",
|
|
13272
|
+
"failed",
|
|
13273
|
+
"cancelled"
|
|
13274
|
+
];
|
|
13275
|
+
const TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
13276
|
+
"completed",
|
|
13277
|
+
"failed",
|
|
13278
|
+
"cancelled"
|
|
13279
|
+
]);
|
|
13280
|
+
/** `completed`, `failed` and `cancelled` are terminal: the task never changes again. */
|
|
13281
|
+
function isTerminalTaskStatus(status) {
|
|
13282
|
+
return TERMINAL_STATUSES.has(status);
|
|
13283
|
+
}
|
|
13284
|
+
const TASKS_ERROR_CODES = {
|
|
13285
|
+
/** Unknown, expired or foreign task id. Also used for a principal mismatch, so existence is not revealed. */
|
|
13286
|
+
InvalidParams: -32602,
|
|
13287
|
+
InternalError: -32603,
|
|
13288
|
+
/** The request needs a task but the client did not declare `io.modelcontextprotocol/tasks`. */
|
|
13289
|
+
MissingRequiredClientCapability: -32021
|
|
13290
|
+
};
|
|
13291
|
+
/** How long a server keeps a task by default: 24 hours. */
|
|
13292
|
+
const DEFAULT_TASK_TTL_MS = 864e5;
|
|
13293
|
+
/** Suggested poll interval once a task is older than {@link DEFAULT_TASK_INITIAL_POLL_WINDOW_MS}. */
|
|
13294
|
+
const DEFAULT_TASK_POLL_INTERVAL_MS = 5e3;
|
|
13295
|
+
// src/shared/tasks/schemas.ts
|
|
13296
|
+
/**
|
|
13297
|
+
* Zod schemas for the MCP Tasks extension (SEP-2663) wire format.
|
|
13298
|
+
*
|
|
13299
|
+
* Objects are loose so fields added by later revisions pass through. `resultType` is required on
|
|
13300
|
+
* the raw `tools/call` task handle, as the 2026-07-28 revision requires it there.
|
|
13301
|
+
*
|
|
13302
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13303
|
+
*/
|
|
13304
|
+
const JsonObjectSchema = zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown());
|
|
13305
|
+
const MetaSchema = JsonObjectSchema.optional();
|
|
13306
|
+
const TaskStatusSchema = zod_v4.z.enum(TASK_STATUSES);
|
|
13307
|
+
/** An embedded elicitation, sampling or roots request (`{ method, params }`). */
|
|
13308
|
+
const TaskInputRequestSchema = zod_v4.z.looseObject({
|
|
13309
|
+
method: zod_v4.z.string(),
|
|
13310
|
+
params: JsonObjectSchema.optional()
|
|
13311
|
+
});
|
|
13312
|
+
/** Keyed by identifiers that are unique over the task's lifetime. */
|
|
13313
|
+
const TaskInputRequestsSchema = zod_v4.z.record(zod_v4.z.string(), TaskInputRequestSchema);
|
|
13314
|
+
/** The bare result object answering the input request with the same key. */
|
|
13315
|
+
const TaskInputResponsesSchema = zod_v4.z.record(zod_v4.z.string(), JsonObjectSchema);
|
|
13316
|
+
const TaskShape = {
|
|
13317
|
+
taskId: zod_v4.z.string().min(1),
|
|
13318
|
+
status: TaskStatusSchema,
|
|
13319
|
+
statusMessage: zod_v4.z.string().optional(),
|
|
13320
|
+
createdAt: zod_v4.z.string(),
|
|
13321
|
+
lastUpdatedAt: zod_v4.z.string(),
|
|
13322
|
+
ttlMs: zod_v4.z.number().int().nonnegative().nullable(),
|
|
13323
|
+
pollIntervalMs: zod_v4.z.number().int().nonnegative().optional()
|
|
13324
|
+
};
|
|
13325
|
+
zod_v4.z.looseObject(TaskShape);
|
|
13326
|
+
const DetailedTaskSchema = zod_v4.z.discriminatedUnion("status", [
|
|
13327
|
+
zod_v4.z.looseObject({
|
|
13328
|
+
...TaskShape,
|
|
13329
|
+
status: zod_v4.z.literal("working")
|
|
13330
|
+
}),
|
|
13331
|
+
zod_v4.z.looseObject({
|
|
13332
|
+
...TaskShape,
|
|
13333
|
+
status: zod_v4.z.literal("input_required"),
|
|
13334
|
+
inputRequests: TaskInputRequestsSchema
|
|
13335
|
+
}),
|
|
13336
|
+
zod_v4.z.looseObject({
|
|
13337
|
+
...TaskShape,
|
|
13338
|
+
status: zod_v4.z.literal("completed"),
|
|
13339
|
+
result: JsonObjectSchema
|
|
13340
|
+
}),
|
|
13341
|
+
zod_v4.z.looseObject({
|
|
13342
|
+
...TaskShape,
|
|
13343
|
+
status: zod_v4.z.literal("failed"),
|
|
13344
|
+
error: JsonObjectSchema
|
|
13345
|
+
}),
|
|
13346
|
+
zod_v4.z.looseObject({
|
|
13347
|
+
...TaskShape,
|
|
13348
|
+
status: zod_v4.z.literal("cancelled")
|
|
13349
|
+
})
|
|
13350
|
+
]);
|
|
13351
|
+
/** What a server returns from `tools/call` instead of a `CallToolResult` when it creates a task. */
|
|
13352
|
+
const CreateTaskResultSchema = zod_v4.z.looseObject({
|
|
13353
|
+
...TaskShape,
|
|
13354
|
+
resultType: zod_v4.z.literal(TASK_RESULT_TYPE),
|
|
13355
|
+
_meta: MetaSchema
|
|
13356
|
+
});
|
|
13357
|
+
const TaskIdParamsSchema = zod_v4.z.looseObject({
|
|
13358
|
+
taskId: zod_v4.z.string().min(1),
|
|
13359
|
+
_meta: MetaSchema
|
|
13360
|
+
});
|
|
13361
|
+
const GetTaskRequestParamsSchema = TaskIdParamsSchema;
|
|
13362
|
+
const CancelTaskRequestParamsSchema = TaskIdParamsSchema;
|
|
13363
|
+
TaskIdParamsSchema.extend({ inputResponses: TaskInputResponsesSchema });
|
|
13364
|
+
const UpdateTaskHandlerParamsSchema = TaskIdParamsSchema;
|
|
13365
|
+
const CompleteResultShape = {
|
|
13366
|
+
resultType: zod_v4.z.literal("complete").optional(),
|
|
13367
|
+
_meta: MetaSchema
|
|
13368
|
+
};
|
|
13369
|
+
/** `tasks/get` result: the full task. */
|
|
13370
|
+
const GetTaskResultSchema = zod_v4.z.intersection(zod_v4.z.looseObject(CompleteResultShape), DetailedTaskSchema);
|
|
13371
|
+
/** `tasks/update` and `tasks/cancel` only acknowledge. */
|
|
13372
|
+
const TaskAckResultSchema = zod_v4.z.looseObject(CompleteResultShape);
|
|
13373
|
+
// src/shared/tasks/capability.ts
|
|
13374
|
+
/**
|
|
13375
|
+
* Whether a capabilities object declares the Tasks extension. A non-object entry counts as absent,
|
|
13376
|
+
* and the legacy 2025-11-25 top-level `capabilities.tasks` key is ignored: it is not wire-compatible.
|
|
13377
|
+
*
|
|
13378
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13379
|
+
*/
|
|
13380
|
+
function hasTasksCapability(capabilities) {
|
|
13381
|
+
const entry = capabilities?.extensions?.[TASKS_EXTENSION_ID];
|
|
13382
|
+
return typeof entry === "object" && entry !== null && !Array.isArray(entry);
|
|
13383
|
+
}
|
|
13384
|
+
/**
|
|
13385
|
+
* The `extensions` entry a peer advertises to declare the Tasks extension. SEP-2663 defines no
|
|
13386
|
+
* settings, so the value is an empty object.
|
|
13387
|
+
*
|
|
13388
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13389
|
+
*/
|
|
13390
|
+
function tasksCapabilityEntry() {
|
|
13391
|
+
return { [TASKS_EXTENSION_ID]: {} };
|
|
13392
|
+
}
|
|
13393
|
+
// src/shared/tasks/task-id.ts
|
|
13394
|
+
/**
|
|
13395
|
+
* A new task id: 192 random bits, base64url. Task ids work as bearer tokens, so they must not be
|
|
13396
|
+
* guessable or derived from anything else (such as a workflow run id).
|
|
13397
|
+
*
|
|
13398
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13399
|
+
*/
|
|
13400
|
+
function createTaskId() {
|
|
13401
|
+
return (0, crypto$1.randomBytes)(24).toString("base64url");
|
|
13402
|
+
}
|
|
13403
|
+
/**
|
|
13404
|
+
* A stable key for a task that is safe to store and show: the SHA-256 of the task id, base64url.
|
|
13405
|
+
* Client and server compute the same key, so traces on both sides can be matched without
|
|
13406
|
+
* recording the task id itself.
|
|
13407
|
+
*
|
|
13408
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13409
|
+
*/
|
|
13410
|
+
function taskKeyOf(taskId) {
|
|
13411
|
+
return (0, crypto$1.createHash)("sha256").update(taskId).digest("base64url");
|
|
13412
|
+
}
|
|
13413
|
+
/**
|
|
13414
|
+
* The poll interval a server suggests for a task created at `createdAt`: frequent while the task is
|
|
13415
|
+
* young, then slower.
|
|
13416
|
+
*
|
|
13417
|
+
* @experimental SEP-2663 extension; API may change.
|
|
13418
|
+
*/
|
|
13419
|
+
function suggestedPollIntervalMs(createdAt, now, options = {}) {
|
|
13420
|
+
const window = options.initialWindowMs ?? 6e4;
|
|
13421
|
+
return now - createdAt < window ? options.initialMs ?? 2e3 : options.steadyMs ?? 5e3;
|
|
13422
|
+
}
|
|
13253
13423
|
// src/shared/trace-context.ts
|
|
13254
13424
|
/** `_meta` keys defined by the MCP specification for W3C trace propagation. */
|
|
13255
13425
|
const TRACEPARENT_META_KEY = "traceparent";
|
|
@@ -14241,6 +14411,26 @@ function isReconnectableMCPError(error) {
|
|
|
14241
14411
|
const errorMessage = error.message.toLowerCase();
|
|
14242
14412
|
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");
|
|
14243
14413
|
}
|
|
14414
|
+
const GATEWAY_UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
14415
|
+
502,
|
|
14416
|
+
503,
|
|
14417
|
+
504
|
|
14418
|
+
]);
|
|
14419
|
+
/**
|
|
14420
|
+
* Whether a failed `tasks/get` poll should be retried while waiting out an outage.
|
|
14421
|
+
*
|
|
14422
|
+
* Adds HTTP 502/503/504 (a proxy or load balancer answering while the server
|
|
14423
|
+
* restarts or deploys) to the reconnectable failures. Kept separate from
|
|
14424
|
+
* {@link isReconnectableMCPError}: `tasks/get` is idempotent, but a `tools/call`
|
|
14425
|
+
* retried after a gateway 5xx could run its work twice.
|
|
14426
|
+
*/
|
|
14427
|
+
function isRecoverableTaskPollError(error) {
|
|
14428
|
+
if (isReconnectableMCPError(error)) return true;
|
|
14429
|
+
if (!(error instanceof Error)) return false;
|
|
14430
|
+
const { httpStatus } = getMCPDiscoveryErrorDetails(error);
|
|
14431
|
+
if (httpStatus !== void 0) return GATEWAY_UNAVAILABLE_STATUSES.has(httpStatus);
|
|
14432
|
+
return /\bhttp 50[234]\b/i.test(error.message);
|
|
14433
|
+
}
|
|
14244
14434
|
const MAX_DISCOVERY_ERROR_CAUSE_DEPTH = 8;
|
|
14245
14435
|
function isHttpStatus(value) {
|
|
14246
14436
|
return typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599;
|
|
@@ -15122,6 +15312,270 @@ function getInputSchemaError(schema) {
|
|
|
15122
15312
|
}
|
|
15123
15313
|
return result.valid ? void 0 : result.errorMessage.split("; ").at(-1);
|
|
15124
15314
|
}
|
|
15315
|
+
// src/client/tasks/intercept.ts
|
|
15316
|
+
/**
|
|
15317
|
+
* `_meta` key carrying an intercepted task handle. The `mastra.ai/` prefix keeps it out of the
|
|
15318
|
+
* reserved MCP namespace.
|
|
15319
|
+
*/
|
|
15320
|
+
const TASK_HANDLE_META_KEY = "mastra.ai/mcp-task";
|
|
15321
|
+
/**
|
|
15322
|
+
* Lets task handles through on a connection that declared the Tasks extension.
|
|
15323
|
+
*
|
|
15324
|
+
* The SDK rejects `resultType: "task"` before any caller code runs. This rewrites such a response
|
|
15325
|
+
* into a complete `CallToolResult` with `isError: true` that carries the task under
|
|
15326
|
+
* {@link TASK_HANDLE_META_KEY}: `isError` keeps the SDK from applying the tool's output schema, and
|
|
15327
|
+
* if the marker is ever missed the call fails instead of returning a wrong result.
|
|
15328
|
+
* Call after `connect()`, which installs the protocol's own message handler.
|
|
15329
|
+
*/
|
|
15330
|
+
function interceptTaskResults(transport) {
|
|
15331
|
+
const deliver = transport.onmessage;
|
|
15332
|
+
if (!deliver) throw new Error("interceptTaskResults must run after the transport is connected");
|
|
15333
|
+
transport.onmessage = (message, extra) => {
|
|
15334
|
+
const result = "result" in message ? message.result : void 0;
|
|
15335
|
+
if (result?.resultType === "task") {
|
|
15336
|
+
const parsed = CreateTaskResultSchema.safeParse(result);
|
|
15337
|
+
if (parsed.success) {
|
|
15338
|
+
const { resultType: _resultType, _meta, ...task } = parsed.data;
|
|
15339
|
+
message = {
|
|
15340
|
+
...message,
|
|
15341
|
+
result: {
|
|
15342
|
+
resultType: "complete",
|
|
15343
|
+
isError: true,
|
|
15344
|
+
content: [{
|
|
15345
|
+
type: "text",
|
|
15346
|
+
text: `MCP task ${task.taskId} was not awaited`
|
|
15347
|
+
}],
|
|
15348
|
+
_meta: {
|
|
15349
|
+
..._meta ?? {},
|
|
15350
|
+
[TASK_HANDLE_META_KEY]: task
|
|
15351
|
+
}
|
|
15352
|
+
}
|
|
15353
|
+
};
|
|
15354
|
+
}
|
|
15355
|
+
}
|
|
15356
|
+
deliver.call(transport, message, extra);
|
|
15357
|
+
};
|
|
15358
|
+
}
|
|
15359
|
+
/** The task handle a rewritten result carries, if any. */
|
|
15360
|
+
function takeTaskHandle(result) {
|
|
15361
|
+
const handle = result._meta?.[TASK_HANDLE_META_KEY];
|
|
15362
|
+
return handle && typeof handle === "object" ? handle : void 0;
|
|
15363
|
+
}
|
|
15364
|
+
// src/client/tasks/options.ts
|
|
15365
|
+
/** `setTimeout` fires at once for longer delays. */
|
|
15366
|
+
const MAX_TIMER_DELAY_MS$1 = 2147483647;
|
|
15367
|
+
/** Normalises a server's `tasks` option; `undefined` means tasks are off. */
|
|
15368
|
+
function resolveTasksOptions(serverName, option) {
|
|
15369
|
+
if (!option) return void 0;
|
|
15370
|
+
if (option === true) return {};
|
|
15371
|
+
for (const key of ["pollIntervalMs", "maxWaitMs"]) {
|
|
15372
|
+
const value = option[key];
|
|
15373
|
+
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}`);
|
|
15374
|
+
}
|
|
15375
|
+
return option;
|
|
15376
|
+
}
|
|
15377
|
+
// src/client/tasks/requester.ts
|
|
15378
|
+
/**
|
|
15379
|
+
* Sends `tasks/get`, `tasks/update` and `tasks/cancel` to one server. Each is sent with an explicit
|
|
15380
|
+
* result schema, which the SDK requires for the Tasks extension's methods on the 2026-07-28 revision.
|
|
15381
|
+
* The SDK sets the `Mcp-Name: <taskId>` routing header on Streamable HTTP.
|
|
15382
|
+
*/
|
|
15383
|
+
var TaskRequester = class {
|
|
15384
|
+
client;
|
|
15385
|
+
options;
|
|
15386
|
+
constructor(client, options = {}) {
|
|
15387
|
+
this.client = client;
|
|
15388
|
+
this.options = options;
|
|
15389
|
+
}
|
|
15390
|
+
async get(taskId, options = {}) {
|
|
15391
|
+
const { resultType: _resultType, _meta, ...task } = await this.client.request({
|
|
15392
|
+
method: TASKS_METHODS.get,
|
|
15393
|
+
params: this.params({ taskId })
|
|
15394
|
+
}, GetTaskResultSchema, this.requestOptions(options));
|
|
15395
|
+
return task;
|
|
15396
|
+
}
|
|
15397
|
+
async update(taskId, inputResponses, options = {}) {
|
|
15398
|
+
await this.client.request({
|
|
15399
|
+
method: TASKS_METHODS.update,
|
|
15400
|
+
params: this.params({
|
|
15401
|
+
taskId,
|
|
15402
|
+
inputResponses
|
|
15403
|
+
})
|
|
15404
|
+
}, TaskAckResultSchema, this.requestOptions(options));
|
|
15405
|
+
}
|
|
15406
|
+
async cancel(taskId, options = {}) {
|
|
15407
|
+
await this.client.request({
|
|
15408
|
+
method: TASKS_METHODS.cancel,
|
|
15409
|
+
params: this.params({ taskId })
|
|
15410
|
+
}, TaskAckResultSchema, this.requestOptions(options));
|
|
15411
|
+
}
|
|
15412
|
+
params(params) {
|
|
15413
|
+
const meta = this.options.meta?.();
|
|
15414
|
+
return meta ? {
|
|
15415
|
+
...params,
|
|
15416
|
+
_meta: meta
|
|
15417
|
+
} : params;
|
|
15418
|
+
}
|
|
15419
|
+
requestOptions(options) {
|
|
15420
|
+
return {
|
|
15421
|
+
timeout: this.options.timeout,
|
|
15422
|
+
signal: options.signal
|
|
15423
|
+
};
|
|
15424
|
+
}
|
|
15425
|
+
};
|
|
15426
|
+
// src/client/tasks/wait.ts
|
|
15427
|
+
/** Why a task wait stopped without a terminal task. */
|
|
15428
|
+
var TaskWaitError = class extends Error {
|
|
15429
|
+
reason;
|
|
15430
|
+
task;
|
|
15431
|
+
constructor(reason, message, task, cause) {
|
|
15432
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
15433
|
+
this.reason = reason;
|
|
15434
|
+
this.task = task;
|
|
15435
|
+
this.name = "TaskWaitError";
|
|
15436
|
+
}
|
|
15437
|
+
};
|
|
15438
|
+
const DEFAULT_MAX_OUTAGE_MS = 3e5;
|
|
15439
|
+
const MAX_OUTAGE_BACKOFF_MS = 3e4;
|
|
15440
|
+
/** Polls at least this often, whatever the server suggests. */
|
|
15441
|
+
const MAX_TASK_POLL_INTERVAL_MS = 36e5;
|
|
15442
|
+
const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
15443
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
15444
|
+
const timer = setTimeout(() => {
|
|
15445
|
+
signal?.removeEventListener("abort", onAbort);
|
|
15446
|
+
resolve();
|
|
15447
|
+
}, ms);
|
|
15448
|
+
const onAbort = () => {
|
|
15449
|
+
clearTimeout(timer);
|
|
15450
|
+
reject(signal.reason);
|
|
15451
|
+
};
|
|
15452
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15453
|
+
});
|
|
15454
|
+
/**
|
|
15455
|
+
* Polls a remote task until it reaches a terminal status, answering its input requests on the way.
|
|
15456
|
+
*
|
|
15457
|
+
* Aborting the signal or exceeding `maxWaitMs` sends `tasks/cancel` (fire-and-forget) and rejects.
|
|
15458
|
+
* Each input request key is answered at most once, however many polls list it.
|
|
15459
|
+
*/
|
|
15460
|
+
async function waitForTask(port, created, options = {}) {
|
|
15461
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
15462
|
+
const now = options.now ?? Date.now;
|
|
15463
|
+
const maxOutageMs = options.maxOutageMs ?? DEFAULT_MAX_OUTAGE_MS;
|
|
15464
|
+
const deadline = options.maxWaitMs === void 0 ? void 0 : now() + options.maxWaitMs;
|
|
15465
|
+
const answered = /* @__PURE__ */ new Set();
|
|
15466
|
+
let task = created;
|
|
15467
|
+
const startedAt = now();
|
|
15468
|
+
const ttlEndOf = (current) => {
|
|
15469
|
+
if (current.ttlMs === null) return Infinity;
|
|
15470
|
+
const createdAt = Date.parse(current.createdAt);
|
|
15471
|
+
return (Number.isNaN(createdAt) ? startedAt : createdAt) + current.ttlMs;
|
|
15472
|
+
};
|
|
15473
|
+
let cancelSent = false;
|
|
15474
|
+
const cancel = () => {
|
|
15475
|
+
if (cancelSent) return;
|
|
15476
|
+
cancelSent = true;
|
|
15477
|
+
port.cancel(created.taskId).catch(() => {});
|
|
15478
|
+
};
|
|
15479
|
+
const timedOut = () => new TaskWaitError("timeout", `Gave up on MCP task ${task.taskId} after ${options.maxWaitMs}ms`, task);
|
|
15480
|
+
const stop = new AbortController();
|
|
15481
|
+
const stopped = new Promise((_, reject) => {
|
|
15482
|
+
stop.signal.addEventListener("abort", () => reject(stop.signal.reason), { once: true });
|
|
15483
|
+
});
|
|
15484
|
+
stopped.catch(() => {});
|
|
15485
|
+
const bounded = (operation) => Promise.race([operation, stopped]);
|
|
15486
|
+
const onAbort = () => {
|
|
15487
|
+
cancel();
|
|
15488
|
+
stop.abort(options.signal.reason);
|
|
15489
|
+
};
|
|
15490
|
+
if (options.signal?.aborted) onAbort();
|
|
15491
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15492
|
+
const deadlineTimer = options.maxWaitMs === void 0 ? void 0 : setTimeout(() => {
|
|
15493
|
+
cancel();
|
|
15494
|
+
stop.abort(timedOut());
|
|
15495
|
+
}, options.maxWaitMs);
|
|
15496
|
+
deadlineTimer?.unref?.();
|
|
15497
|
+
try {
|
|
15498
|
+
let reported;
|
|
15499
|
+
for (;;) {
|
|
15500
|
+
if (task.status !== reported) {
|
|
15501
|
+
reported = task.status;
|
|
15502
|
+
options.onStatus?.(task);
|
|
15503
|
+
}
|
|
15504
|
+
stop.signal.throwIfAborted();
|
|
15505
|
+
if (isTerminalTaskStatus(task.status)) return task;
|
|
15506
|
+
if (task.status === "input_required" && "inputRequests" in task) {
|
|
15507
|
+
const { inputRequests } = task;
|
|
15508
|
+
const pending = Object.entries(inputRequests).filter(([key]) => !answered.has(key));
|
|
15509
|
+
if (pending.length > 0) {
|
|
15510
|
+
const responses = await bounded(answerInputRequests(pending, task, options.inputRequests, stop.signal));
|
|
15511
|
+
for (const key of Object.keys(responses)) answered.add(key);
|
|
15512
|
+
await bounded(port.update(task.taskId, responses, { signal: stop.signal }));
|
|
15513
|
+
task = await poll();
|
|
15514
|
+
continue;
|
|
15515
|
+
}
|
|
15516
|
+
}
|
|
15517
|
+
const ttlLeft = ttlEndOf(task) - now();
|
|
15518
|
+
if (ttlLeft <= 0) throw new TaskWaitError("expired", `MCP task ${task.taskId} outlived its ${task.ttlMs}ms TTL`, task);
|
|
15519
|
+
let interval = Math.min(pollIntervalOf(task), ttlLeft);
|
|
15520
|
+
if (deadline !== void 0) {
|
|
15521
|
+
const remaining = deadline - now();
|
|
15522
|
+
if (remaining <= 0) {
|
|
15523
|
+
cancel();
|
|
15524
|
+
throw timedOut();
|
|
15525
|
+
}
|
|
15526
|
+
interval = Math.min(interval, remaining);
|
|
15527
|
+
}
|
|
15528
|
+
await bounded(sleep(interval, stop.signal));
|
|
15529
|
+
task = await poll();
|
|
15530
|
+
}
|
|
15531
|
+
function pollIntervalOf(current) {
|
|
15532
|
+
const suggested = current.pollIntervalMs ?? options.pollIntervalMs ?? 5e3;
|
|
15533
|
+
return Number.isFinite(suggested) ? Math.min(MAX_TASK_POLL_INTERVAL_MS, Math.max(100, suggested)) : DEFAULT_TASK_POLL_INTERVAL_MS;
|
|
15534
|
+
}
|
|
15535
|
+
async function poll() {
|
|
15536
|
+
let outageStartedAt;
|
|
15537
|
+
let backoff;
|
|
15538
|
+
for (;;) try {
|
|
15539
|
+
return await bounded(port.get(created.taskId, { signal: stop.signal }));
|
|
15540
|
+
} catch (error) {
|
|
15541
|
+
stop.signal.throwIfAborted();
|
|
15542
|
+
if (!port.isRecoverable(error)) throw error;
|
|
15543
|
+
const at = now();
|
|
15544
|
+
outageStartedAt ??= at;
|
|
15545
|
+
if (deadline !== void 0 && at >= deadline) {
|
|
15546
|
+
cancel();
|
|
15547
|
+
throw timedOut();
|
|
15548
|
+
}
|
|
15549
|
+
const giveUpAt = Math.min(outageStartedAt + maxOutageMs, ttlEndOf(task));
|
|
15550
|
+
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);
|
|
15551
|
+
try {
|
|
15552
|
+
await bounded(port.recover(error));
|
|
15553
|
+
} catch (recoverError) {
|
|
15554
|
+
stop.signal.throwIfAborted();
|
|
15555
|
+
if (!port.isRecoverable(recoverError)) throw recoverError;
|
|
15556
|
+
}
|
|
15557
|
+
backoff = Math.min(MAX_OUTAGE_BACKOFF_MS, backoff === void 0 ? pollIntervalOf(task) : backoff * 2);
|
|
15558
|
+
await bounded(sleep(Math.max(0, Math.min(backoff, giveUpAt - at, (deadline ?? Infinity) - at)), stop.signal));
|
|
15559
|
+
}
|
|
15560
|
+
}
|
|
15561
|
+
} finally {
|
|
15562
|
+
clearTimeout(deadlineTimer);
|
|
15563
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
15564
|
+
}
|
|
15565
|
+
}
|
|
15566
|
+
async function answerInputRequests(pending, task, handler, signal) {
|
|
15567
|
+
if (!handler) throw new TaskWaitError("input_unhandled", `MCP task ${task.taskId} needs input, but no inputRequests handler is configured`, task);
|
|
15568
|
+
const responses = {};
|
|
15569
|
+
for (const [key, request] of pending) {
|
|
15570
|
+
if (request.method !== "elicitation/create") throw new TaskWaitError("input_unsupported", `MCP task ${task.taskId} sent an unsupported input request '${request.method}'`, task);
|
|
15571
|
+
responses[key] = await handler({
|
|
15572
|
+
key,
|
|
15573
|
+
params: request.params,
|
|
15574
|
+
signal
|
|
15575
|
+
});
|
|
15576
|
+
}
|
|
15577
|
+
return responses;
|
|
15578
|
+
}
|
|
15125
15579
|
// src/client/url-policy.ts
|
|
15126
15580
|
/**
|
|
15127
15581
|
* Redirect status codes the manual redirect loop follows. Other 3xx statuses
|
|
@@ -15328,6 +15782,13 @@ function shouldDetachPersistentTransportRequest(init) {
|
|
|
15328
15782
|
return false;
|
|
15329
15783
|
}
|
|
15330
15784
|
}
|
|
15785
|
+
const TASK_WAIT_ERROR_IDS = {
|
|
15786
|
+
timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
|
|
15787
|
+
expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
|
|
15788
|
+
input_unhandled: "MCP_CLIENT_TOOL_TASK_INPUT_UNHANDLED",
|
|
15789
|
+
input_unsupported: "MCP_CLIENT_TOOL_TASK_INPUT_UNSUPPORTED",
|
|
15790
|
+
unreachable: "MCP_CLIENT_TOOL_TASK_POLL_FAILED"
|
|
15791
|
+
};
|
|
15331
15792
|
/**
|
|
15332
15793
|
* Reports the JSON-RPC id of each `events/stream` request as it goes out on `transport`. The
|
|
15333
15794
|
* server identifies a push stream by that id, and the SDK doesn't return it to the caller.
|
|
@@ -15514,6 +15975,8 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
15514
15975
|
priorDiscovery;
|
|
15515
15976
|
requireToolApproval;
|
|
15516
15977
|
onToolError;
|
|
15978
|
+
taskOptions;
|
|
15979
|
+
taskRequester;
|
|
15517
15980
|
jsonSchemaValidator;
|
|
15518
15981
|
jsonSchemaValidatorPromise;
|
|
15519
15982
|
/** Provides access to resource operations (list, read, notifications) */
|
|
@@ -15545,13 +16008,15 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
15545
16008
|
this.requireToolApproval = server.requireToolApproval;
|
|
15546
16009
|
this.onToolError = server.onToolError ?? "throw";
|
|
15547
16010
|
this.jsonSchemaValidator = server.jsonSchemaValidator;
|
|
16011
|
+
this.taskOptions = resolveTasksOptions(name, server.tasks);
|
|
15548
16012
|
const configured = server.capabilities ?? {};
|
|
15549
16013
|
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.`);
|
|
15550
16014
|
const clientCapabilities = {
|
|
15551
16015
|
...server.inputRequests ? { elicitation: configured.elicitation ?? { form: {} } } : {},
|
|
15552
16016
|
extensions: {
|
|
15553
16017
|
...configured.extensions ?? {},
|
|
15554
|
-
"io.modelcontextprotocol/ui": {}
|
|
16018
|
+
"io.modelcontextprotocol/ui": {},
|
|
16019
|
+
...this.taskOptions ? tasksCapabilityEntry() : {}
|
|
15555
16020
|
}
|
|
15556
16021
|
};
|
|
15557
16022
|
this.client = new _modelcontextprotocol_client.Client({
|
|
@@ -15562,6 +16027,10 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
15562
16027
|
...server.jsonSchemaValidator ? { jsonSchemaValidator: server.jsonSchemaValidator } : {},
|
|
15563
16028
|
versionNegotiation: { mode: negotiationMode(server.protocolVersion) }
|
|
15564
16029
|
});
|
|
16030
|
+
this.taskRequester = new TaskRequester(this.client, {
|
|
16031
|
+
timeout: this.timeout,
|
|
16032
|
+
meta: () => this.requestMeta()
|
|
16033
|
+
});
|
|
15565
16034
|
if (server.inputRequests) {
|
|
15566
16035
|
const handler = server.inputRequests;
|
|
15567
16036
|
this.client.setRequestHandler("elicitation/create", async (request, ctx) => {
|
|
@@ -15671,6 +16140,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
15671
16140
|
timeout: this.serverConfig.timeout ?? this.timeout,
|
|
15672
16141
|
prior: this.priorDiscovery
|
|
15673
16142
|
});
|
|
16143
|
+
if (this.taskOptions) interceptTaskResults(transport);
|
|
15674
16144
|
this.transport = transport;
|
|
15675
16145
|
this.log("debug", `Successfully connected to MCP server via Stdio`);
|
|
15676
16146
|
} catch (e) {
|
|
@@ -15704,6 +16174,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
15704
16174
|
timeout: connectTimeout ?? DEFAULT_SERVER_CONNECT_TIMEOUT_MSEC,
|
|
15705
16175
|
prior: this.priorDiscovery
|
|
15706
16176
|
});
|
|
16177
|
+
if (this.taskOptions) interceptTaskResults(transport);
|
|
15707
16178
|
this.transport = transport;
|
|
15708
16179
|
this.log("debug", "Successfully connected using Streamable HTTP transport.");
|
|
15709
16180
|
this.closePendingAuthTransport();
|
|
@@ -16500,6 +16971,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
16500
16971
|
strict: getMastraToolStrictMeta(toolMeta),
|
|
16501
16972
|
...mcpToolProps,
|
|
16502
16973
|
requireApproval,
|
|
16974
|
+
...this.taskOptions ? { background: this.taskToolBackgroundConfig() } : {},
|
|
16503
16975
|
mcpMetadata: {
|
|
16504
16976
|
serverName: this.name,
|
|
16505
16977
|
serverVersion: serverMeta.version,
|
|
@@ -16532,6 +17004,13 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
16532
17004
|
}));
|
|
16533
17005
|
const toolCallSpan = context?.tracingContext?.currentSpan;
|
|
16534
17006
|
if (toolCallSpan && serverSpan) toolCallSpan.links = [...toolCallSpan.links ?? [], serverSpan];
|
|
17007
|
+
const task = this.taskOptions ? takeTaskHandle(res) : void 0;
|
|
17008
|
+
if (!task) return finishToolCall(res);
|
|
17009
|
+
this.log("debug", `Tool ${tool.name} continues as MCP task ${task.taskId}`);
|
|
17010
|
+
toolCallSpan?.update({ metadata: { mcpTaskKey: taskKeyOf(task.taskId) } });
|
|
17011
|
+
return finishToolCall(await this.awaitToolTask(tool.name, task, context?.abortSignal, context?.writer));
|
|
17012
|
+
};
|
|
17013
|
+
const finishToolCall = async (res) => {
|
|
16535
17014
|
if (res.isError && this.onToolError === "throw") {
|
|
16536
17015
|
const errorText = extractToolErrorText(res.content);
|
|
16537
17016
|
this.log("debug", `Tool reported an error: ${tool.name}`, { error: errorText });
|
|
@@ -16566,7 +17045,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
16566
17045
|
try {
|
|
16567
17046
|
return await executeToolCall();
|
|
16568
17047
|
} catch (e) {
|
|
16569
|
-
if (!(e instanceof _mastra_core_error.MastraError && e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED") && isReconnectableMCPError(e)) {
|
|
17048
|
+
if (!(e instanceof _mastra_core_error.MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"))) && isReconnectableMCPError(e)) {
|
|
16570
17049
|
this.log("debug", `Transport error detected for tool ${tool.name}, attempting reconnection...`, { error: e instanceof Error ? e.message : String(e) });
|
|
16571
17050
|
try {
|
|
16572
17051
|
await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
|
|
@@ -16600,6 +17079,78 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
|
|
|
16600
17079
|
return;
|
|
16601
17080
|
}
|
|
16602
17081
|
}
|
|
17082
|
+
/** Reads, answers and cancels this server's tasks (SEP-2663). */
|
|
17083
|
+
get tasks() {
|
|
17084
|
+
return this.taskRequester;
|
|
17085
|
+
}
|
|
17086
|
+
/**
|
|
17087
|
+
* Background settings for tools of a tasks-enabled server: they may run for as long as the
|
|
17088
|
+
* remote task, and must never be retried, as a retry would start a second task.
|
|
17089
|
+
*/
|
|
17090
|
+
taskToolBackgroundConfig() {
|
|
17091
|
+
return {
|
|
17092
|
+
enabled: true,
|
|
17093
|
+
defaultDisposition: "deferred",
|
|
17094
|
+
timeoutMs: this.taskOptions?.maxWaitMs ?? 864e5,
|
|
17095
|
+
maxRetries: 0
|
|
17096
|
+
};
|
|
17097
|
+
}
|
|
17098
|
+
/** Waits for a remote task created by a tool call and returns its `CallToolResult`. */
|
|
17099
|
+
async awaitToolTask(toolName, task, signal, writer) {
|
|
17100
|
+
const details = {
|
|
17101
|
+
toolName,
|
|
17102
|
+
serverName: this.name,
|
|
17103
|
+
taskId: task.taskId
|
|
17104
|
+
};
|
|
17105
|
+
const onStatus = (current) => {
|
|
17106
|
+
const progress = {
|
|
17107
|
+
type: "mcp-task",
|
|
17108
|
+
taskId: current.taskId,
|
|
17109
|
+
status: current.status,
|
|
17110
|
+
createdAt: current.createdAt,
|
|
17111
|
+
serverName: this.name
|
|
17112
|
+
};
|
|
17113
|
+
writer?.write(progress).catch(() => {});
|
|
17114
|
+
};
|
|
17115
|
+
const fail = (id, text, cause) => new _mastra_core_error.MastraError({
|
|
17116
|
+
id,
|
|
17117
|
+
domain: _mastra_core_error.ErrorDomain.MCP,
|
|
17118
|
+
category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
|
|
17119
|
+
text,
|
|
17120
|
+
details
|
|
17121
|
+
}, cause);
|
|
17122
|
+
const lifecycleGeneration = this.lifecycleGeneration;
|
|
17123
|
+
let polledTransport;
|
|
17124
|
+
let terminal;
|
|
17125
|
+
try {
|
|
17126
|
+
terminal = await waitForTask({
|
|
17127
|
+
get: (taskId, options) => {
|
|
17128
|
+
polledTransport = this.transport;
|
|
17129
|
+
return this.taskRequester.get(taskId, options);
|
|
17130
|
+
},
|
|
17131
|
+
update: (taskId, responses, options) => this.taskRequester.update(taskId, responses, options),
|
|
17132
|
+
cancel: (taskId) => this.taskRequester.cancel(taskId),
|
|
17133
|
+
isRecoverable: isRecoverableTaskPollError,
|
|
17134
|
+
recover: () => this.reconnectAfterTransportFailure(polledTransport, lifecycleGeneration)
|
|
17135
|
+
}, task, {
|
|
17136
|
+
signal,
|
|
17137
|
+
inputRequests: this.serverConfig.inputRequests,
|
|
17138
|
+
pollIntervalMs: this.taskOptions?.pollIntervalMs,
|
|
17139
|
+
maxWaitMs: this.taskOptions?.maxWaitMs,
|
|
17140
|
+
onStatus
|
|
17141
|
+
});
|
|
17142
|
+
} catch (error) {
|
|
17143
|
+
if (signal?.aborted) throw error;
|
|
17144
|
+
if (error instanceof TaskWaitError) throw fail(TASK_WAIT_ERROR_IDS[error.reason], error.message, error.cause);
|
|
17145
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
17146
|
+
throw fail("MCP_CLIENT_TOOL_TASK_POLL_FAILED", `Polling MCP task ${task.taskId} failed: ${reason}`, error);
|
|
17147
|
+
}
|
|
17148
|
+
switch (terminal.status) {
|
|
17149
|
+
case "completed": return terminal.result;
|
|
17150
|
+
case "cancelled": throw fail("MCP_CLIENT_TOOL_TASK_CANCELLED", terminal.statusMessage ?? `MCP task ${task.taskId} was cancelled`);
|
|
17151
|
+
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`);
|
|
17152
|
+
}
|
|
17153
|
+
}
|
|
16603
17154
|
stampServerIdInMeta(meta) {
|
|
16604
17155
|
const ui = meta.ui;
|
|
16605
17156
|
if (!ui?.resourceUri) return meta;
|
|
@@ -17618,6 +18169,63 @@ To fix this you have three different options:
|
|
|
17618
18169
|
};
|
|
17619
18170
|
}
|
|
17620
18171
|
/**
|
|
18172
|
+
* Reads, answers and cancels MCP tasks (SEP-2663) on configured servers. Tool calls to a server
|
|
18173
|
+
* with `tasks` enabled already wait for their tasks; use this to inspect or steer one directly.
|
|
18174
|
+
*
|
|
18175
|
+
* @experimental SEP-2663 extension; this API may change.
|
|
18176
|
+
*
|
|
18177
|
+
* @example
|
|
18178
|
+
* ```typescript
|
|
18179
|
+
* const task = await mcp.tasks.get('reports', taskId);
|
|
18180
|
+
* if (task.status === 'working') await mcp.tasks.cancel('reports', taskId);
|
|
18181
|
+
* ```
|
|
18182
|
+
*/
|
|
18183
|
+
get tasks() {
|
|
18184
|
+
this.addToInstanceCache();
|
|
18185
|
+
const wrap = async (id, details, run) => {
|
|
18186
|
+
try {
|
|
18187
|
+
return await run();
|
|
18188
|
+
} catch (error) {
|
|
18189
|
+
throw new _mastra_core_error.MastraError({
|
|
18190
|
+
id,
|
|
18191
|
+
domain: _mastra_core_error.ErrorDomain.MCP,
|
|
18192
|
+
category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
|
|
18193
|
+
details
|
|
18194
|
+
}, error);
|
|
18195
|
+
}
|
|
18196
|
+
};
|
|
18197
|
+
return {
|
|
18198
|
+
/**
|
|
18199
|
+
* Returns a task's current state, including its result once completed.
|
|
18200
|
+
*
|
|
18201
|
+
* @throws {MastraError} `MCP_CLIENT_GET_TASK_FAILED`; the cause is a `-32602` protocol error
|
|
18202
|
+
* when the server does not know the task.
|
|
18203
|
+
*/
|
|
18204
|
+
get: (serverName, taskId, options) => wrap("MCP_CLIENT_GET_TASK_FAILED", {
|
|
18205
|
+
serverName,
|
|
18206
|
+
taskId
|
|
18207
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.get(taskId, options)),
|
|
18208
|
+
/**
|
|
18209
|
+
* Answers input requests of an `input_required` task, keyed as in its `inputRequests`.
|
|
18210
|
+
*
|
|
18211
|
+
* @throws {MastraError} `MCP_CLIENT_UPDATE_TASK_FAILED`
|
|
18212
|
+
*/
|
|
18213
|
+
update: (serverName, taskId, inputResponses, options) => wrap("MCP_CLIENT_UPDATE_TASK_FAILED", {
|
|
18214
|
+
serverName,
|
|
18215
|
+
taskId
|
|
18216
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.update(taskId, inputResponses, options)),
|
|
18217
|
+
/**
|
|
18218
|
+
* Asks the server to stop a task. Cancellation is cooperative: the task may still complete.
|
|
18219
|
+
*
|
|
18220
|
+
* @throws {MastraError} `MCP_CLIENT_CANCEL_TASK_FAILED`
|
|
18221
|
+
*/
|
|
18222
|
+
cancel: (serverName, taskId, options) => wrap("MCP_CLIENT_CANCEL_TASK_FAILED", {
|
|
18223
|
+
serverName,
|
|
18224
|
+
taskId
|
|
18225
|
+
}, async () => (await this.getConnectedClientForServer(serverName)).tasks.cancel(taskId, options))
|
|
18226
|
+
};
|
|
18227
|
+
}
|
|
18228
|
+
/**
|
|
17621
18229
|
* Provides access to resource-related operations across all configured servers.
|
|
17622
18230
|
*
|
|
17623
18231
|
* Resources represent data exposed by MCP servers (files, database records, API responses, etc.).
|
|
@@ -18828,14 +19436,45 @@ async function toRequestContext(ctx, mapAuthInfoToUser) {
|
|
|
18828
19436
|
* never share a principal. A token refreshed mid-round therefore starts over.
|
|
18829
19437
|
*/
|
|
18830
19438
|
function principalOf(ctx, requestContext) {
|
|
19439
|
+
return callerOf(ctx, requestContext).principal;
|
|
19440
|
+
}
|
|
19441
|
+
/**
|
|
19442
|
+
* A readable name for the caller, for operators debugging a server: the principal, except that a
|
|
19443
|
+
* token principal keeps only a short prefix of the token's hash. Never a credential, so it can be
|
|
19444
|
+
* stored and shown; it is not unique enough to authorize anything.
|
|
19445
|
+
*/
|
|
19446
|
+
function clientLabelOf(ctx, requestContext) {
|
|
19447
|
+
return callerOf(ctx, requestContext).label;
|
|
19448
|
+
}
|
|
19449
|
+
const TOKEN_LABEL_HASH_LENGTH = 8;
|
|
19450
|
+
function callerOf(ctx, requestContext) {
|
|
18831
19451
|
const authInfo = ctx.http?.authInfo;
|
|
18832
|
-
if (!authInfo) return
|
|
19452
|
+
if (!authInfo) return {
|
|
19453
|
+
principal: "anonymous",
|
|
19454
|
+
label: "anonymous"
|
|
19455
|
+
};
|
|
18833
19456
|
const subject = authInfo.extra?.sub ?? authInfo.extra?.subject;
|
|
18834
|
-
if (typeof subject === "string" && subject)
|
|
19457
|
+
if (typeof subject === "string" && subject) {
|
|
19458
|
+
const principal = `${authInfo.clientId}:sub:${subject}`;
|
|
19459
|
+
return {
|
|
19460
|
+
principal,
|
|
19461
|
+
label: principal
|
|
19462
|
+
};
|
|
19463
|
+
}
|
|
18835
19464
|
const user = requestContext.get("user");
|
|
18836
19465
|
const userId = user && typeof user === "object" ? user.id : void 0;
|
|
18837
|
-
if (typeof userId === "string" && userId)
|
|
18838
|
-
|
|
19466
|
+
if (typeof userId === "string" && userId) {
|
|
19467
|
+
const principal = `${authInfo.clientId}:user:${userId}`;
|
|
19468
|
+
return {
|
|
19469
|
+
principal,
|
|
19470
|
+
label: principal
|
|
19471
|
+
};
|
|
19472
|
+
}
|
|
19473
|
+
const tokenHash = (0, crypto$1.createHash)("sha256").update(authInfo.token).digest("base64url");
|
|
19474
|
+
return {
|
|
19475
|
+
principal: `${authInfo.clientId}:token:${tokenHash}`,
|
|
19476
|
+
label: `${authInfo.clientId}:token:${tokenHash.slice(0, TOKEN_LABEL_HASH_LENGTH)}`
|
|
19477
|
+
};
|
|
18839
19478
|
}
|
|
18840
19479
|
function hashArguments(value) {
|
|
18841
19480
|
return (0, crypto$1.createHash)("sha256").update(canonicalJSON(value)).digest("base64url");
|
|
@@ -20224,65 +20863,1742 @@ function echoes(body, challenge) {
|
|
|
20224
20863
|
const b = Buffer.from(challenge);
|
|
20225
20864
|
return a.length === b.length && (0, crypto$1.timingSafeEqual)(a, b);
|
|
20226
20865
|
}
|
|
20227
|
-
// src/server/
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
}
|
|
20866
|
+
// src/server/tasks/store.ts
|
|
20867
|
+
/** How a principal is stored and shown. Principals can carry user and subject ids, so only their hash is kept. */
|
|
20868
|
+
function principalKeyOf(principal) {
|
|
20869
|
+
return (0, crypto$1.createHash)("sha256").update(principal).digest("base64url");
|
|
20870
|
+
}
|
|
20232
20871
|
/**
|
|
20233
|
-
*
|
|
20234
|
-
* Protocol (MCP) clients using protocol revision 2026-07-28: self-contained requests
|
|
20235
|
-
* over Streamable HTTP or stdio, native `input_required` continuation and
|
|
20236
|
-
* per-request logging.
|
|
20237
|
-
*
|
|
20238
|
-
* @example
|
|
20239
|
-
* `yourTool` is a tool you have already configured.
|
|
20240
|
-
* ```typescript
|
|
20241
|
-
* import { MCPServer } from '@mastra/mcp';
|
|
20242
|
-
*
|
|
20243
|
-
* const server = new MCPServer({
|
|
20244
|
-
* id: 'my-server',
|
|
20245
|
-
* name: 'My Server',
|
|
20246
|
-
* version: '1.0.0',
|
|
20247
|
-
* tools: { yourTool },
|
|
20248
|
-
* });
|
|
20249
|
-
* await server.startStdio();
|
|
20250
|
-
* ```
|
|
20251
|
-
*
|
|
20252
|
-
* @see For documentation bundled with your installed package, locate
|
|
20253
|
-
* `@mastra/mcp/package.json` with your project's resolver or package-manager
|
|
20254
|
-
* tooling, then read `dist/docs/SKILL.md` from that package root and follow its
|
|
20255
|
-
* reference links. Use package-manager tools for virtual or archived packages.
|
|
20872
|
+
* Thrown by {@link MCPTaskStore.create} when the task would go over one of its `limits`.
|
|
20256
20873
|
*
|
|
20257
|
-
* @
|
|
20258
|
-
* if packaged docs are unavailable.
|
|
20874
|
+
* @experimental SEP-2663 extension; API may change.
|
|
20259
20875
|
*/
|
|
20260
|
-
var
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20876
|
+
var MCPTaskLimitError = class extends Error {
|
|
20877
|
+
scope;
|
|
20878
|
+
limit;
|
|
20879
|
+
kind;
|
|
20880
|
+
constructor(scope, limit, kind = "open") {
|
|
20881
|
+
super(`Too many ${kind} MCP tasks ${scope === "principal" ? "for this principal" : "in this store"} (limit ${limit})`);
|
|
20882
|
+
this.scope = scope;
|
|
20883
|
+
this.limit = limit;
|
|
20884
|
+
this.kind = kind;
|
|
20885
|
+
this.name = "MCPTaskLimitError";
|
|
20886
|
+
}
|
|
20887
|
+
};
|
|
20888
|
+
function decodeTaskListCursor(cursor) {
|
|
20889
|
+
if (!cursor) return void 0;
|
|
20890
|
+
try {
|
|
20891
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
20892
|
+
if (typeof parsed.before === "string" && !Number.isNaN(Date.parse(parsed.before)) && Array.isArray(parsed.seen)) return {
|
|
20893
|
+
before: parsed.before,
|
|
20894
|
+
seen: parsed.seen.filter((key) => typeof key === "string")
|
|
20895
|
+
};
|
|
20896
|
+
} catch {}
|
|
20897
|
+
}
|
|
20898
|
+
/** Whether a task comes after the cursor in newest-first order. */
|
|
20899
|
+
function isPastTaskListCursor(listing, cursor) {
|
|
20900
|
+
if (!cursor) return true;
|
|
20901
|
+
const created = Date.parse(listing.createdAt);
|
|
20902
|
+
const before = Date.parse(cursor.before);
|
|
20903
|
+
return created < before || created === before && !cursor.seen.includes(listing.taskKey);
|
|
20904
|
+
}
|
|
20905
|
+
/** The cursor for the page after `listings`, newest first. */
|
|
20906
|
+
function nextTaskListCursor(listings, previous) {
|
|
20907
|
+
const last = listings.at(-1);
|
|
20908
|
+
if (!last) return void 0;
|
|
20909
|
+
const before = Date.parse(last.createdAt);
|
|
20910
|
+
const seen = listings.filter((listing) => Date.parse(listing.createdAt) === before).map((listing) => listing.taskKey);
|
|
20911
|
+
if (previous && Date.parse(previous.before) === before) seen.push(...previous.seen);
|
|
20912
|
+
const cursor = {
|
|
20913
|
+
before: new Date(before).toISOString(),
|
|
20914
|
+
seen
|
|
20915
|
+
};
|
|
20916
|
+
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
20917
|
+
}
|
|
20918
|
+
/** Whether a listing matches the filters a store could not apply while reading. */
|
|
20919
|
+
function matchesTaskListOptions(listing, options) {
|
|
20920
|
+
if (options.status && listing.status !== options.status) return false;
|
|
20921
|
+
if (options.surface && listing.context?.toolName !== options.surface) return false;
|
|
20922
|
+
if (options.principalKey && listing.principalKey !== options.principalKey) return false;
|
|
20923
|
+
if (options.taskKey && listing.taskKey !== options.taskKey) return false;
|
|
20924
|
+
return true;
|
|
20925
|
+
}
|
|
20926
|
+
/** @experimental SEP-2663 extension; API may change. */
|
|
20927
|
+
function isMCPTaskStoreAdmin(store) {
|
|
20928
|
+
const admin = store;
|
|
20929
|
+
return typeof admin.list === "function" && typeof admin.updateByKey === "function" && typeof admin.cancelByKey === "function";
|
|
20930
|
+
}
|
|
20931
|
+
/** Projects a detailed task onto a listing. */
|
|
20932
|
+
function toListing(taskKey, principalKey, context, task, clientLabel) {
|
|
20933
|
+
return {
|
|
20934
|
+
taskKey,
|
|
20935
|
+
principalKey,
|
|
20936
|
+
...clientLabel !== void 0 && { clientLabel },
|
|
20937
|
+
...context && { context },
|
|
20938
|
+
status: task.status,
|
|
20939
|
+
...task.statusMessage !== void 0 && { statusMessage: task.statusMessage },
|
|
20940
|
+
createdAt: task.createdAt,
|
|
20941
|
+
lastUpdatedAt: task.lastUpdatedAt,
|
|
20942
|
+
ttlMs: task.ttlMs,
|
|
20943
|
+
...task.status === "input_required" && { inputRequests: task.inputRequests },
|
|
20944
|
+
...task.status === "completed" && { result: task.result },
|
|
20945
|
+
...task.status === "failed" && { error: task.error }
|
|
20946
|
+
};
|
|
20947
|
+
}
|
|
20948
|
+
// src/server/tasks/background-store.ts
|
|
20949
|
+
const ENVELOPE_VERSION = 1;
|
|
20950
|
+
/** For `ttlMs: null`: a deadline cleanup never reaches. */
|
|
20951
|
+
const NEVER = /* @__PURE__ */ new Date("9999-12-31T00:00:00.000Z");
|
|
20952
|
+
/** The longest delay `setTimeout` honours; larger values fire at once. */
|
|
20953
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
20954
|
+
/** How long an expired task still answers `failed` before it is forgotten. */
|
|
20955
|
+
const EXPIRED_RETENTION_MS = 6e4;
|
|
20956
|
+
/** What core writes when recovery reclaims a crashed attempt it won't run again. */
|
|
20957
|
+
const CORE_LOST_MESSAGE = "Worker process terminated before task completed";
|
|
20958
|
+
const TASK_LOST_MESSAGE = "task lost: the server restarted while it was running";
|
|
20959
|
+
const TASK_EXPIRED_MESSAGE = "Task expired before it finished";
|
|
20960
|
+
const TERMINAL_ROW_STATUSES = /* @__PURE__ */ new Set([
|
|
20961
|
+
"completed",
|
|
20962
|
+
"failed",
|
|
20963
|
+
"cancelled",
|
|
20964
|
+
"timed_out"
|
|
20965
|
+
]);
|
|
20966
|
+
const OPEN_ROW_STATUSES = [
|
|
20967
|
+
"pending",
|
|
20968
|
+
"running",
|
|
20969
|
+
"suspended"
|
|
20970
|
+
];
|
|
20971
|
+
const UPDATE_ATTEMPTS = 3;
|
|
20972
|
+
/**
|
|
20973
|
+
* The row statuses {@link projectTask} can turn into each task status: a suspended row whose
|
|
20974
|
+
* requests are all answered is working, and an open row past its TTL is failed.
|
|
20975
|
+
*/
|
|
20976
|
+
const ROW_STATUSES_FOR = {
|
|
20977
|
+
working: [
|
|
20978
|
+
"pending",
|
|
20979
|
+
"running",
|
|
20980
|
+
"suspended"
|
|
20981
|
+
],
|
|
20982
|
+
input_required: ["suspended"],
|
|
20983
|
+
completed: ["completed"],
|
|
20984
|
+
cancelled: ["cancelled"],
|
|
20985
|
+
failed: [
|
|
20986
|
+
"failed",
|
|
20987
|
+
"timed_out",
|
|
20988
|
+
...OPEN_ROW_STATUSES
|
|
20989
|
+
]
|
|
20990
|
+
};
|
|
20991
|
+
/** How long an aborted attempt's row is watched for core to record why: from 50ms apart, backing off to 1s. */
|
|
20992
|
+
const ABORT_SETTLE_FIRST_READ_MS = 50;
|
|
20993
|
+
const ABORT_SETTLE_MAX_READ_MS = 1e3;
|
|
20994
|
+
/** The most rows one sweep reads, in each of its two listings. */
|
|
20995
|
+
const SWEEP_BATCH = 100;
|
|
20996
|
+
/** Running, or pending but pinned (`pinToWorker`) to the worker that holds its attempt. */
|
|
20997
|
+
function isOwned(row) {
|
|
20998
|
+
return row.status === "running" || row.status === "pending" && Boolean(row.ownerId);
|
|
20999
|
+
}
|
|
21000
|
+
function isTerminalRow(row) {
|
|
21001
|
+
return TERMINAL_ROW_STATUSES.has(row.status);
|
|
21002
|
+
}
|
|
21003
|
+
function envelopeOf(row) {
|
|
21004
|
+
const metadata = row.metadata;
|
|
21005
|
+
return metadata?.v === ENVELOPE_VERSION && typeof metadata.principalKey === "string" ? metadata : void 0;
|
|
21006
|
+
}
|
|
21007
|
+
function openInputRequests(envelope) {
|
|
21008
|
+
const answered = envelope.inputResponses ?? {};
|
|
21009
|
+
return Object.fromEntries(Object.entries(envelope.inputRequests ?? {}).filter(([key]) => !(key in answered)));
|
|
21010
|
+
}
|
|
21011
|
+
function isFullyAnswered(row, envelope) {
|
|
21012
|
+
return row.status === "suspended" && Object.keys(envelope.inputRequests ?? {}).length > 0 && Object.keys(openInputRequests(envelope)).length === 0;
|
|
21013
|
+
}
|
|
21014
|
+
/**
|
|
21015
|
+
* Stopped by a client, by expiry (the sweep cancels the row, then records it as failed), or by core
|
|
21016
|
+
* timing the attempt out at the task's TTL.
|
|
21017
|
+
*/
|
|
21018
|
+
function wasStopped(row, now) {
|
|
21019
|
+
return row.status === "cancelled" || row.status === "timed_out" || row.status === "failed" && isExpired(row, now);
|
|
21020
|
+
}
|
|
21021
|
+
function isExpired(row, now) {
|
|
21022
|
+
return row.expiresAt !== void 0 && row.expiresAt.getTime() <= now;
|
|
21023
|
+
}
|
|
21024
|
+
/** Terminal past its deadline: gone, whether or not cleanup has deleted the row yet. */
|
|
21025
|
+
function isForgotten(row, now) {
|
|
21026
|
+
return isTerminalRow(row) && row.expiresAt !== void 0 && row.expiresAt.getTime() + EXPIRED_RETENTION_MS <= now;
|
|
21027
|
+
}
|
|
21028
|
+
function latest(...dates) {
|
|
21029
|
+
return new Date(Math.max(...dates.filter((date) => date !== void 0).map((date) => date.getTime())));
|
|
21030
|
+
}
|
|
21031
|
+
/**
|
|
21032
|
+
* Projects a background-task row onto the MCP task it backs, as `tasks/get` answers it.
|
|
21033
|
+
*
|
|
21034
|
+
* A row still running past its TTL reads as `failed` before the sweep gets to it.
|
|
21035
|
+
*/
|
|
21036
|
+
function projectTask(row, envelope, taskId, now) {
|
|
21037
|
+
const base = {
|
|
21038
|
+
taskId,
|
|
21039
|
+
status: "working",
|
|
21040
|
+
createdAt: row.createdAt.toISOString(),
|
|
21041
|
+
lastUpdatedAt: latest(row.createdAt, row.startedAt, row.suspendedAt, row.completedAt).toISOString(),
|
|
21042
|
+
ttlMs: envelope.ttlMs,
|
|
21043
|
+
pollIntervalMs: envelope.pollIntervalMs ?? suggestedPollIntervalMs(row.createdAt.getTime(), now)
|
|
21044
|
+
};
|
|
21045
|
+
const failed = (message) => ({
|
|
21046
|
+
...base,
|
|
21047
|
+
status: "failed",
|
|
21048
|
+
error: {
|
|
21049
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21050
|
+
message
|
|
21051
|
+
}
|
|
21052
|
+
});
|
|
21053
|
+
if (!isTerminalRow(row) && isExpired(row, now)) return failed(TASK_EXPIRED_MESSAGE);
|
|
21054
|
+
switch (row.status) {
|
|
21055
|
+
case "pending":
|
|
21056
|
+
case "running": return base;
|
|
21057
|
+
case "suspended": {
|
|
21058
|
+
const open = openInputRequests(envelope);
|
|
21059
|
+
return Object.keys(open).length === 0 ? base : {
|
|
21060
|
+
...base,
|
|
21061
|
+
status: "input_required",
|
|
21062
|
+
inputRequests: open
|
|
21063
|
+
};
|
|
21064
|
+
}
|
|
21065
|
+
case "completed": return {
|
|
21066
|
+
...base,
|
|
21067
|
+
status: "completed",
|
|
21068
|
+
result: { ...row.result }
|
|
21069
|
+
};
|
|
21070
|
+
case "cancelled": return {
|
|
21071
|
+
...base,
|
|
21072
|
+
status: "cancelled"
|
|
21073
|
+
};
|
|
21074
|
+
case "timed_out": return failed(isExpired(row, now) ? TASK_EXPIRED_MESSAGE : row.error?.message ?? "Task timed out");
|
|
21075
|
+
case "failed": {
|
|
21076
|
+
const message = row.error?.message ?? "Task failed";
|
|
21077
|
+
return failed(message === CORE_LOST_MESSAGE ? TASK_LOST_MESSAGE : message);
|
|
21078
|
+
}
|
|
21079
|
+
}
|
|
21080
|
+
}
|
|
21081
|
+
/**
|
|
21082
|
+
* An {@link MCPTaskStore} on Mastra's background tasks: every MCP task is a background-task row in
|
|
21083
|
+
* Mastra's storage, so any instance sharing that storage can read, answer and cancel it, and run it.
|
|
21084
|
+
*
|
|
21085
|
+
* Rows are owned by `mcp-server:<serverId>` and keyed by a hash of the task id. Each attempt runs
|
|
21086
|
+
* the tool's work rebuilt from the row (`workFor`), through a static executor registered for
|
|
21087
|
+
* every task tool, so a worker other than the creating one can run it.
|
|
21088
|
+
*/
|
|
21089
|
+
var BackgroundTaskMCPTaskStore = class {
|
|
21090
|
+
#manager;
|
|
21091
|
+
#owner;
|
|
21092
|
+
#workFor;
|
|
21093
|
+
#recoverFor;
|
|
21094
|
+
#getLogger;
|
|
21095
|
+
#gates = /* @__PURE__ */ new Map();
|
|
21096
|
+
/**
|
|
21097
|
+
* Aborts for first attempts already running under their request, by row id, until the row's
|
|
21098
|
+
* executor takes the attempt over. A row pinned here can wait for a slot while its attempt runs,
|
|
21099
|
+
* and the manager can't abort an attempt it never started.
|
|
21100
|
+
*/
|
|
21101
|
+
#firstAttempts = /* @__PURE__ */ new Map();
|
|
21102
|
+
#executors = /* @__PURE__ */ new Set();
|
|
21103
|
+
#onCancel;
|
|
21104
|
+
#sweepTimer;
|
|
21105
|
+
#closed = false;
|
|
21106
|
+
#sweeping;
|
|
21107
|
+
/** The page of open rows the next sweep checks. */
|
|
21108
|
+
#sweepPage = 0;
|
|
21109
|
+
constructor(options) {
|
|
21110
|
+
this.#manager = options.manager;
|
|
21111
|
+
this.#owner = `mcp-server:${options.serverId}`;
|
|
21112
|
+
this.#workFor = options.workFor;
|
|
21113
|
+
this.#recoverFor = options.recoverFor;
|
|
21114
|
+
this.#onCancel = options.onCancel;
|
|
21115
|
+
this.#getLogger = options.logger;
|
|
21116
|
+
}
|
|
21117
|
+
/** The `agentId` every row of this server carries. */
|
|
21118
|
+
get owner() {
|
|
21119
|
+
return this.#owner;
|
|
21120
|
+
}
|
|
21121
|
+
/**
|
|
21122
|
+
* The row's `toolName`: `mcp-server:<serverId>:<tool>`. Core looks an executor up as
|
|
21123
|
+
* `<agentId>:<toolName>`, then as the bare `toolName`; the bare lookup hits the executor
|
|
21124
|
+
* registered under this name, never a Mastra tool named like the MCP tool. Where no executor is
|
|
21125
|
+
* registered (the server closed, an older instance, a worker without the server), the attempt
|
|
21126
|
+
* fails instead of running a same-named tool with the task's arguments.
|
|
21127
|
+
*/
|
|
21128
|
+
#rowToolName(toolName) {
|
|
21129
|
+
return `${this.#owner}:${toolName}`;
|
|
21130
|
+
}
|
|
21131
|
+
/**
|
|
21132
|
+
* Registers the executor that runs `toolName`'s tasks on any worker. It must be registered on
|
|
21133
|
+
* every instance before the manager recovers rows, or recovered attempts find no executor.
|
|
21134
|
+
*/
|
|
21135
|
+
registerTool(toolName) {
|
|
21136
|
+
const key = this.#rowToolName(toolName);
|
|
21137
|
+
this.#manager.registerStaticExecutor(key, { execute: (args, options) => {
|
|
21138
|
+
const { context } = args;
|
|
21139
|
+
return this.#execute(this.#workFor(context), args, options);
|
|
21140
|
+
} });
|
|
21141
|
+
this.#executors.add(key);
|
|
21142
|
+
}
|
|
21143
|
+
async create(params, { pinToWorker = false, clientLabel } = {}) {
|
|
21144
|
+
const context = params.context;
|
|
21145
|
+
if (!context || typeof context.toolName !== "string") throw new Error("A background-task MCP task needs the tool it runs in its context");
|
|
21146
|
+
if (!this.#executors.has(this.#rowToolName(context.toolName))) this.registerTool(context.toolName);
|
|
21147
|
+
this.#startSweeping();
|
|
21148
|
+
const taskId = createTaskId();
|
|
21149
|
+
const taskKey = taskKeyOf(taskId);
|
|
21150
|
+
const recover = this.#recoverFor(context.toolName);
|
|
21151
|
+
const principalKey = principalKeyOf(params.principal);
|
|
21152
|
+
if (params.limits) await this.#checkLimits(params.limits, principalKey);
|
|
21153
|
+
const envelope = {
|
|
21154
|
+
v: ENVELOPE_VERSION,
|
|
21155
|
+
principalKey,
|
|
21156
|
+
...clientLabel !== void 0 && { clientLabel },
|
|
21157
|
+
...params.trace && { trace: {
|
|
21158
|
+
traceId: params.trace.traceId,
|
|
21159
|
+
spanId: params.trace.spanId
|
|
21160
|
+
} },
|
|
21161
|
+
ttlMs: params.ttlMs,
|
|
21162
|
+
...params.pollIntervalMs !== void 0 && { pollIntervalMs: params.pollIntervalMs },
|
|
21163
|
+
recover,
|
|
21164
|
+
usedInputKeys: []
|
|
21165
|
+
};
|
|
21166
|
+
const createdAt = Date.now();
|
|
21167
|
+
const gate = {};
|
|
21168
|
+
gate.promise = new Promise((resolve) => gate.resolve = resolve);
|
|
21169
|
+
this.#gates.set(taskId, gate);
|
|
21170
|
+
let first = true;
|
|
21171
|
+
const taskContext = { executor: { execute: async (args, options) => {
|
|
21172
|
+
const { work, options: runOptions } = await gate.promise;
|
|
21173
|
+
const initial = first ? runOptions.initialAttempt : void 0;
|
|
21174
|
+
first = false;
|
|
21175
|
+
if (initial && gate.rowId) this.#firstAttempts.delete(gate.rowId);
|
|
21176
|
+
return this.#execute(work, args, options, initial);
|
|
21177
|
+
} } };
|
|
21178
|
+
const args = {
|
|
21179
|
+
taskKey,
|
|
21180
|
+
context
|
|
21181
|
+
};
|
|
21182
|
+
try {
|
|
21183
|
+
const { task, fallbackToSync } = await this.#manager.enqueue({
|
|
21184
|
+
runId: taskKey,
|
|
21185
|
+
toolName: this.#rowToolName(context.toolName),
|
|
21186
|
+
toolCallId: taskKey,
|
|
21187
|
+
args,
|
|
21188
|
+
agentId: this.#owner,
|
|
21189
|
+
resourceId: `${this.#owner}:${principalKey}`,
|
|
21190
|
+
timeoutMs: params.ttlMs === null ? MAX_TIMEOUT_MS : Math.min(Math.max(params.ttlMs, 1), MAX_TIMEOUT_MS),
|
|
21191
|
+
maxRetries: recover ? 1 : 0,
|
|
21192
|
+
expiresAt: params.ttlMs === null ? NEVER : new Date(createdAt + params.ttlMs),
|
|
21193
|
+
metadata: envelope
|
|
21194
|
+
}, taskContext, { pinToWorker });
|
|
21195
|
+
if (fallbackToSync) throw new Error("Background tasks are at capacity and backpressure is 'fallback-sync'; MCP tasks need 'queue'");
|
|
21196
|
+
gate.rowId = task.id;
|
|
21197
|
+
} catch (error) {
|
|
21198
|
+
this.#gates.delete(taskId);
|
|
21199
|
+
throw error;
|
|
21200
|
+
}
|
|
21201
|
+
return {
|
|
21202
|
+
taskId,
|
|
21203
|
+
status: "working",
|
|
21204
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
21205
|
+
lastUpdatedAt: new Date(createdAt).toISOString(),
|
|
21206
|
+
ttlMs: params.ttlMs,
|
|
21207
|
+
pollIntervalMs: params.pollIntervalMs ?? suggestedPollIntervalMs(createdAt, createdAt)
|
|
21208
|
+
};
|
|
21209
|
+
}
|
|
21210
|
+
/**
|
|
21211
|
+
* Counts open rows (not finished and not past their TTL) in storage, so the limits hold across
|
|
21212
|
+
* every instance sharing it. Counting and inserting are separate writes, so creates racing on
|
|
21213
|
+
* different instances can each pass the count and overshoot a limit by up to their number.
|
|
21214
|
+
*/
|
|
21215
|
+
async #checkLimits(limits, principalKey) {
|
|
21216
|
+
const open = {
|
|
21217
|
+
agentId: this.#owner,
|
|
21218
|
+
status: OPEN_ROW_STATUSES,
|
|
21219
|
+
dateFilterBy: "expiresAt",
|
|
21220
|
+
fromDate: new Date(Date.now() + 1),
|
|
21221
|
+
perPage: 1
|
|
21222
|
+
};
|
|
21223
|
+
if ((await this.#manager.listTasks({
|
|
21224
|
+
...open,
|
|
21225
|
+
resourceId: `${this.#owner}:${principalKey}`
|
|
21226
|
+
})).total >= limits.perPrincipal) throw new MCPTaskLimitError("principal", limits.perPrincipal);
|
|
21227
|
+
if ((await this.#manager.listTasks(open)).total >= limits.total) throw new MCPTaskLimitError("total", limits.total);
|
|
21228
|
+
}
|
|
21229
|
+
run(taskId, work, options = {}) {
|
|
21230
|
+
const gate = this.#gates.get(taskId);
|
|
21231
|
+
if (!gate) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21232
|
+
this.#gates.delete(taskId);
|
|
21233
|
+
const { initialAttempt } = options;
|
|
21234
|
+
if (initialAttempt && gate.rowId) {
|
|
21235
|
+
const rowId = gate.rowId;
|
|
21236
|
+
this.#firstAttempts.set(rowId, initialAttempt.abort);
|
|
21237
|
+
const done = () => {
|
|
21238
|
+
if (this.#firstAttempts.get(rowId) === initialAttempt.abort) this.#firstAttempts.delete(rowId);
|
|
21239
|
+
};
|
|
21240
|
+
initialAttempt.outcome.then(done, done);
|
|
21241
|
+
}
|
|
21242
|
+
gate.resolve({
|
|
21243
|
+
work,
|
|
21244
|
+
options
|
|
21245
|
+
});
|
|
21246
|
+
}
|
|
21247
|
+
async get(taskId, access) {
|
|
21248
|
+
this.#startSweeping();
|
|
21249
|
+
const found = await this.#find(taskKeyOf(taskId), access);
|
|
21250
|
+
if (!found) return void 0;
|
|
21251
|
+
const { row, envelope } = found;
|
|
21252
|
+
if (isTerminalRow(row)) this.#manager.deregisterTaskContext(row.id);
|
|
21253
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21254
|
+
return projectTask(row, envelope, taskId, Date.now());
|
|
21255
|
+
}
|
|
21256
|
+
async update(taskId, inputResponses, access) {
|
|
21257
|
+
const taskKey = taskKeyOf(taskId);
|
|
21258
|
+
return await this.#update(taskKey, inputResponses, `with key "${taskKey}"`, access) !== "not_found";
|
|
21259
|
+
}
|
|
21260
|
+
async cancel(taskId, access) {
|
|
21261
|
+
return await this.#cancel(taskKeyOf(taskId), access) !== "not_found";
|
|
21262
|
+
}
|
|
21263
|
+
/** Lists this server's tasks across principals, most recently created first. */
|
|
21264
|
+
async list(options = {}) {
|
|
21265
|
+
this.#startSweeping();
|
|
21266
|
+
const limit = options.limit ?? 50;
|
|
21267
|
+
const cursor = decodeTaskListCursor(options.cursor);
|
|
21268
|
+
const now = Date.now();
|
|
21269
|
+
const matching = [];
|
|
21270
|
+
for (let page = 0; matching.length <= limit; page++) {
|
|
21271
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21272
|
+
agentId: this.#owner,
|
|
21273
|
+
...options.surface && { toolName: this.#rowToolName(options.surface) },
|
|
21274
|
+
...options.principalKey && { resourceId: `${this.#owner}:${options.principalKey}` },
|
|
21275
|
+
...options.taskKey && { toolCallId: options.taskKey },
|
|
21276
|
+
...options.status && { status: ROW_STATUSES_FOR[options.status] },
|
|
21277
|
+
...cursor && { toDate: new Date(Date.parse(cursor.before) + 1) },
|
|
21278
|
+
orderBy: "createdAt",
|
|
21279
|
+
orderDirection: "desc",
|
|
21280
|
+
page,
|
|
21281
|
+
perPage: limit + 1
|
|
21282
|
+
});
|
|
21283
|
+
for (const row of tasks) {
|
|
21284
|
+
const envelope = envelopeOf(row);
|
|
21285
|
+
if (!envelope || isForgotten(row, now)) continue;
|
|
21286
|
+
const { context } = row.args;
|
|
21287
|
+
const listing = toListing(row.toolCallId, envelope.principalKey, context, projectTask(row, envelope, "", now), envelope.clientLabel);
|
|
21288
|
+
if (!isPastTaskListCursor(listing, cursor) || !matchesTaskListOptions(listing, options)) continue;
|
|
21289
|
+
matching.push(listing);
|
|
21290
|
+
if (matching.length > limit) break;
|
|
21291
|
+
}
|
|
21292
|
+
if (tasks.length <= limit) break;
|
|
21293
|
+
}
|
|
21294
|
+
const listings = matching.slice(0, limit);
|
|
21295
|
+
return {
|
|
21296
|
+
listings,
|
|
21297
|
+
...matching.length > limit && { nextCursor: nextTaskListCursor(listings, cursor) }
|
|
21298
|
+
};
|
|
21299
|
+
}
|
|
21300
|
+
updateByKey(taskKey, inputResponses) {
|
|
21301
|
+
return this.#update(taskKey, inputResponses, `with key "${taskKey}"`);
|
|
21302
|
+
}
|
|
21303
|
+
cancelByKey(taskKey) {
|
|
21304
|
+
return this.#cancel(taskKey);
|
|
21305
|
+
}
|
|
21306
|
+
/** Without `access`, any principal's task (admin). `label` names the task in errors. */
|
|
21307
|
+
async #update(taskKey, inputResponses, label, access) {
|
|
21308
|
+
this.#startSweeping();
|
|
21309
|
+
const found = await this.#find(taskKey, access);
|
|
21310
|
+
if (!found) return "not_found";
|
|
21311
|
+
let { row, envelope } = found;
|
|
21312
|
+
let stored = false;
|
|
21313
|
+
const storage = await this.#manager.getStorage();
|
|
21314
|
+
for (let attempt = 0; attempt < UPDATE_ATTEMPTS && row.status === "suspended"; attempt++) {
|
|
21315
|
+
const open = openInputRequests(envelope);
|
|
21316
|
+
const accepted = Object.entries(inputResponses).filter(([key]) => key in open);
|
|
21317
|
+
if (accepted.length === 0) break;
|
|
21318
|
+
const next = {
|
|
21319
|
+
...envelope,
|
|
21320
|
+
inputResponses: {
|
|
21321
|
+
...envelope.inputResponses,
|
|
21322
|
+
...Object.fromEntries(accepted)
|
|
21323
|
+
}
|
|
21324
|
+
};
|
|
21325
|
+
const written = await storage.updateTask(row.id, { metadata: next }, {
|
|
21326
|
+
expectedStatus: "suspended",
|
|
21327
|
+
expectedSuspendedAt: row.suspendedAt ?? null
|
|
21328
|
+
});
|
|
21329
|
+
const reread = await storage.getTask(row.id);
|
|
21330
|
+
const rereadEnvelope = reread && envelopeOf(reread);
|
|
21331
|
+
if (!reread || !rereadEnvelope) return "not_found";
|
|
21332
|
+
row = reread;
|
|
21333
|
+
envelope = rereadEnvelope;
|
|
21334
|
+
if (written && accepted.every(([key]) => key in (envelope.inputResponses ?? {}))) {
|
|
21335
|
+
stored = true;
|
|
21336
|
+
break;
|
|
21337
|
+
}
|
|
21338
|
+
}
|
|
21339
|
+
if (!stored && row.status === "suspended") {
|
|
21340
|
+
const open = openInputRequests(envelope);
|
|
21341
|
+
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}`);
|
|
21342
|
+
}
|
|
21343
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21344
|
+
return stored ? "done" : "ignored";
|
|
21345
|
+
}
|
|
21346
|
+
async traceOf(taskId, access) {
|
|
21347
|
+
return (await this.#find(taskKeyOf(taskId), access))?.envelope.trace;
|
|
21348
|
+
}
|
|
21349
|
+
async #cancel(taskKey, access) {
|
|
21350
|
+
this.#startSweeping();
|
|
21351
|
+
const found = await this.#find(taskKey, access);
|
|
21352
|
+
if (!found) return "not_found";
|
|
21353
|
+
if (isTerminalRow(found.row)) return "ignored";
|
|
21354
|
+
try {
|
|
21355
|
+
await this.#manager.cancel(found.row.id);
|
|
21356
|
+
} catch (error) {
|
|
21357
|
+
this.#getLogger?.().warn("Could not cancel MCP task", { error });
|
|
21358
|
+
}
|
|
21359
|
+
this.#abortFirstAttempt(found.row.id, /* @__PURE__ */ new Error("Task cancelled"));
|
|
21360
|
+
this.#notifyCancel(found.row);
|
|
21361
|
+
return "done";
|
|
21362
|
+
}
|
|
21363
|
+
/** Fire and forget: a slow hook must hold up neither the `tasks/cancel` ack nor the sweep. */
|
|
21364
|
+
#notifyCancel(row) {
|
|
21365
|
+
const onCancel = this.#onCancel;
|
|
21366
|
+
if (!onCancel) return;
|
|
21367
|
+
const context = row.args.context;
|
|
21368
|
+
Promise.resolve().then(() => onCancel(context)).catch((error) => this.#getLogger?.().warn("Could not stop what a cancelled MCP task stands for", { error }));
|
|
21369
|
+
}
|
|
21370
|
+
/**
|
|
21371
|
+
* An attempt here was aborted. The abort says why only in core's message, so read the row: if
|
|
21372
|
+
* another instance cancelled or expired the task, or core timed the attempt out, run `onCancel`
|
|
21373
|
+
* here as well, where the work may be executing (a workflow run, say). Core records a timeout only
|
|
21374
|
+
* once the aborted attempt settles, so a row still running under this worker is read again, less
|
|
21375
|
+
* often each time and again after a failed read, until core records how the attempt ended. A shutdown releases the lease and a
|
|
21376
|
+
* lost lease changes the owner: either leaves the row running without this worker, and the work
|
|
21377
|
+
* alone.
|
|
21378
|
+
*/
|
|
21379
|
+
#notifyIfCancelled(taskKey) {
|
|
21380
|
+
const check = async () => {
|
|
21381
|
+
for (let wait = ABORT_SETTLE_FIRST_READ_MS; !this.#closed; wait = Math.min(wait * 2, ABORT_SETTLE_MAX_READ_MS)) {
|
|
21382
|
+
let row;
|
|
21383
|
+
try {
|
|
21384
|
+
row = (await this.#manager.listTasks({
|
|
21385
|
+
agentId: this.#owner,
|
|
21386
|
+
toolCallId: taskKey,
|
|
21387
|
+
perPage: 1
|
|
21388
|
+
})).tasks[0];
|
|
21389
|
+
} catch (error) {
|
|
21390
|
+
this.#getLogger?.().warn("Could not read an aborted MCP task attempt; reading it again", { error });
|
|
21391
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
21392
|
+
continue;
|
|
21393
|
+
}
|
|
21394
|
+
if (!row) return;
|
|
21395
|
+
if (wasStopped(row, Date.now())) return this.#notifyCancel(row);
|
|
21396
|
+
if (row.status !== "running" || row.ownerId !== this.#manager.ownerId) return;
|
|
21397
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
21398
|
+
}
|
|
21399
|
+
};
|
|
21400
|
+
check().catch((error) => this.#getLogger?.().warn("Could not check why an MCP task attempt was aborted", { error }));
|
|
21401
|
+
}
|
|
21402
|
+
#abortFirstAttempt(rowId, reason) {
|
|
21403
|
+
const abort = this.#firstAttempts.get(rowId);
|
|
21404
|
+
if (!abort) return;
|
|
21405
|
+
this.#firstAttempts.delete(rowId);
|
|
21406
|
+
abort(reason);
|
|
21407
|
+
}
|
|
21408
|
+
/**
|
|
21409
|
+
* Stops sweeping, waits for a sweep already running to stop at its next step, then unregisters
|
|
21410
|
+
* this store's executors. Rows stay in storage.
|
|
21411
|
+
*/
|
|
21412
|
+
async close() {
|
|
21413
|
+
this.#closed = true;
|
|
21414
|
+
clearInterval(this.#sweepTimer);
|
|
21415
|
+
this.#sweepTimer = void 0;
|
|
21416
|
+
await this.#sweeping?.catch(() => {});
|
|
21417
|
+
for (const key of this.#executors) this.#manager.unregisterStaticExecutor(key);
|
|
21418
|
+
this.#executors.clear();
|
|
21419
|
+
}
|
|
21420
|
+
/**
|
|
21421
|
+
* Repairs what no request would: expires rows past their TTL, resumes answered rows whose
|
|
21422
|
+
* process-local resume was lost, and settles rows whose worker died while this manager was
|
|
21423
|
+
* already running (the manager itself only recovers rows when it starts).
|
|
21424
|
+
*/
|
|
21425
|
+
sweep() {
|
|
21426
|
+
if (this.#closed) return Promise.resolve();
|
|
21427
|
+
this.#sweeping ??= this.#sweep().finally(() => this.#sweeping = void 0);
|
|
21428
|
+
return this.#sweeping;
|
|
21429
|
+
}
|
|
21430
|
+
/**
|
|
21431
|
+
* Starts the periodic sweep before any task request, so this instance settles rows a dead worker
|
|
21432
|
+
* left behind even if no client ever asks it about a task. Does nothing once the store is closed.
|
|
21433
|
+
*/
|
|
21434
|
+
startSweeping() {
|
|
21435
|
+
this.#startSweeping();
|
|
21436
|
+
}
|
|
21437
|
+
#startSweeping() {
|
|
21438
|
+
if (this.#sweepTimer || this.#closed) return;
|
|
21439
|
+
this.#sweepTimer = setInterval(() => {
|
|
21440
|
+
this.sweep().catch((error) => this.#getLogger?.().debug("MCP task sweep failed", { error }));
|
|
21441
|
+
}, this.#manager.config.leaseDurationMs);
|
|
21442
|
+
this.#sweepTimer.unref?.();
|
|
21443
|
+
}
|
|
21444
|
+
/**
|
|
21445
|
+
* Bounded per tick, however many rows are open: it expires at most {@link SWEEP_BATCH} rows past
|
|
21446
|
+
* their TTL, and checks one page of {@link SWEEP_BATCH} other open rows, moving to the next page
|
|
21447
|
+
* on the next tick. Rows that need a restart are restarted only while slots are free.
|
|
21448
|
+
*/
|
|
21449
|
+
async #sweep() {
|
|
21450
|
+
const storage = await this.#manager.getStorage();
|
|
21451
|
+
const now = Date.now();
|
|
21452
|
+
const lease = this.#manager.config.leaseDurationMs;
|
|
21453
|
+
for (const rowId of this.#firstAttempts.keys()) {
|
|
21454
|
+
if (this.#closed) return;
|
|
21455
|
+
const current = await storage.getTask(rowId).catch(() => void 0);
|
|
21456
|
+
if (current && isTerminalRow(current) && !this.#closed) this.#abortFirstAttempt(rowId, /* @__PURE__ */ new Error("Task cancelled"));
|
|
21457
|
+
}
|
|
21458
|
+
if (this.#closed) return;
|
|
21459
|
+
if (storage.supportsTaskExpiry) await storage.deleteTasks({
|
|
21460
|
+
agentId: this.#owner,
|
|
21461
|
+
status: [...TERMINAL_ROW_STATUSES],
|
|
21462
|
+
dateFilterBy: "expiresAt",
|
|
21463
|
+
toDate: /* @__PURE__ */ new Date(now - EXPIRED_RETENTION_MS)
|
|
21464
|
+
}).catch((error) => this.#getLogger?.().debug("Could not delete expired MCP tasks", { error }));
|
|
21465
|
+
if (this.#closed) return;
|
|
21466
|
+
const { tasks: expired } = await this.#manager.listTasks({
|
|
21467
|
+
agentId: this.#owner,
|
|
21468
|
+
status: OPEN_ROW_STATUSES,
|
|
21469
|
+
dateFilterBy: "expiresAt",
|
|
21470
|
+
toDate: new Date(now + 1),
|
|
21471
|
+
perPage: SWEEP_BATCH
|
|
21472
|
+
});
|
|
21473
|
+
for (const row of expired) {
|
|
21474
|
+
if (this.#closed) return;
|
|
21475
|
+
if (!envelopeOf(row)) continue;
|
|
21476
|
+
await this.#expire(storage, row).catch((error) => this.#getLogger?.().debug("Could not expire MCP task", { error }));
|
|
21477
|
+
}
|
|
21478
|
+
if (this.#closed) return;
|
|
21479
|
+
const { tasks: rows } = await this.#manager.listTasks({
|
|
21480
|
+
agentId: this.#owner,
|
|
21481
|
+
status: OPEN_ROW_STATUSES,
|
|
21482
|
+
orderBy: "createdAt",
|
|
21483
|
+
orderDirection: "asc",
|
|
21484
|
+
page: this.#sweepPage,
|
|
21485
|
+
perPage: SWEEP_BATCH
|
|
21486
|
+
});
|
|
21487
|
+
this.#sweepPage = rows.length < SWEEP_BATCH ? 0 : this.#sweepPage + 1;
|
|
21488
|
+
const restarts = [];
|
|
21489
|
+
for (const row of rows) {
|
|
21490
|
+
if (this.#closed) return;
|
|
21491
|
+
const envelope = envelopeOf(row);
|
|
21492
|
+
if (!envelope || isExpired(row, now)) continue;
|
|
21493
|
+
try {
|
|
21494
|
+
if (isFullyAnswered(row, envelope)) await this.#resume(row, envelope);
|
|
21495
|
+
else if (isOwned(row) && (!row.leaseExpiresAt || row.leaseExpiresAt.getTime() <= now)) {
|
|
21496
|
+
if (await this.#reclaim(storage, row, envelope) && !this.#closed) restarts.push(row.id);
|
|
21497
|
+
} else if (row.status === "pending" && !row.ownerId && now - row.createdAt.getTime() >= lease) restarts.push(row.id);
|
|
21498
|
+
} catch (error) {
|
|
21499
|
+
this.#getLogger?.().debug("Could not sweep MCP task", { error });
|
|
21500
|
+
}
|
|
21501
|
+
}
|
|
21502
|
+
if (restarts.length === 0 || this.#closed) return;
|
|
21503
|
+
const free = await this.#freeSlots(storage);
|
|
21504
|
+
for (const rowId of restarts.slice(0, free)) {
|
|
21505
|
+
if (this.#closed) return;
|
|
21506
|
+
await this.#restart(rowId);
|
|
21507
|
+
}
|
|
21508
|
+
}
|
|
21509
|
+
/** Slots this server's rows can take now, by the manager's global and per-owner limits. */
|
|
21510
|
+
async #freeSlots(storage) {
|
|
21511
|
+
const { globalConcurrency, perAgentConcurrency, perOwnerConcurrency } = this.#manager.config;
|
|
21512
|
+
const ownerLimit = perOwnerConcurrency && Object.hasOwn(perOwnerConcurrency, this.#owner) ? perOwnerConcurrency[this.#owner] : perAgentConcurrency;
|
|
21513
|
+
const [running, mine] = await Promise.all([storage.getRunningCount(), storage.getRunningCountByAgent(this.#owner)]);
|
|
21514
|
+
return Math.max(0, Math.min(globalConcurrency - running, ownerLimit - mine));
|
|
21515
|
+
}
|
|
21516
|
+
/** Aborts the work, then records the task as failed until its retention ends. */
|
|
21517
|
+
async #expire(storage, row) {
|
|
21518
|
+
await this.#manager.cancel(row.id);
|
|
21519
|
+
this.#abortFirstAttempt(row.id, /* @__PURE__ */ new Error(TASK_EXPIRED_MESSAGE));
|
|
21520
|
+
this.#notifyCancel(row);
|
|
21521
|
+
await storage.updateTask(row.id, {
|
|
21522
|
+
status: "failed",
|
|
21523
|
+
error: { message: TASK_EXPIRED_MESSAGE },
|
|
21524
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
21525
|
+
expiresAt: new Date(Date.now() + EXPIRED_RETENTION_MS)
|
|
21526
|
+
}, { expectedStatus: "cancelled" });
|
|
21527
|
+
}
|
|
21528
|
+
/**
|
|
21529
|
+
* A running row, or a pending row pinned to the worker running its first attempt, whose lease
|
|
21530
|
+
* lapsed or was released: its worker died, or shut down, with the attempt in flight. Fenced like
|
|
21531
|
+
* the manager's own recovery, so of all instances (and managers) noticing it, exactly one
|
|
21532
|
+
* settles it. Returns whether it won the row back to pending, for the sweep to restart.
|
|
21533
|
+
*/
|
|
21534
|
+
async #reclaim(storage, row, envelope) {
|
|
21535
|
+
const fence = {
|
|
21536
|
+
expectedStatus: row.status,
|
|
21537
|
+
expectedOwnerId: row.ownerId ?? null,
|
|
21538
|
+
expectedLeaseExpiresAt: row.leaseExpiresAt ?? null
|
|
21539
|
+
};
|
|
21540
|
+
if (envelope.recover && row.retryCount < row.maxRetries) return await storage.updateTask(row.id, {
|
|
21541
|
+
status: "pending",
|
|
21542
|
+
retryCount: row.retryCount + 1,
|
|
21543
|
+
ownerId: void 0,
|
|
21544
|
+
leaseExpiresAt: void 0
|
|
21545
|
+
}, fence);
|
|
21546
|
+
await storage.updateTask(row.id, {
|
|
21547
|
+
status: "failed",
|
|
21548
|
+
error: { message: TASK_LOST_MESSAGE },
|
|
21549
|
+
completedAt: /* @__PURE__ */ new Date(),
|
|
21550
|
+
ownerId: void 0,
|
|
21551
|
+
leaseExpiresAt: void 0
|
|
21552
|
+
}, fence);
|
|
21553
|
+
return false;
|
|
21554
|
+
}
|
|
21555
|
+
/** Dispatches a reclaimed row; the dispatch claim is fenced, so it runs once however often this is called. */
|
|
21556
|
+
async #restart(rowId) {
|
|
21557
|
+
try {
|
|
21558
|
+
await this.#manager.restart(rowId);
|
|
21559
|
+
} catch (error) {
|
|
21560
|
+
this.#getLogger?.().debug("Could not restart MCP task", { error });
|
|
21561
|
+
}
|
|
21562
|
+
}
|
|
21563
|
+
/** Without `access`, any principal's task (admin). */
|
|
21564
|
+
async #find(taskKey, access) {
|
|
21565
|
+
const principalKey = access && principalKeyOf(access.principal);
|
|
21566
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21567
|
+
agentId: this.#owner,
|
|
21568
|
+
...principalKey !== void 0 && { resourceId: `${this.#owner}:${principalKey}` },
|
|
21569
|
+
toolCallId: taskKey,
|
|
21570
|
+
perPage: 1
|
|
21571
|
+
});
|
|
21572
|
+
const row = tasks[0];
|
|
21573
|
+
const envelope = row && envelopeOf(row);
|
|
21574
|
+
if (!row || !envelope || principalKey !== void 0 && envelope.principalKey !== principalKey) return void 0;
|
|
21575
|
+
if (isForgotten(row, Date.now())) return void 0;
|
|
21576
|
+
return {
|
|
21577
|
+
row,
|
|
21578
|
+
envelope
|
|
21579
|
+
};
|
|
21580
|
+
}
|
|
21581
|
+
async #resume(row, envelope) {
|
|
21582
|
+
const resumeData = {
|
|
21583
|
+
inputResponses: envelope.inputResponses ?? {},
|
|
21584
|
+
suspendPayload: row.suspendPayload
|
|
21585
|
+
};
|
|
21586
|
+
try {
|
|
21587
|
+
await this.#manager.resume(row.id, resumeData, { onNoSlot: "queue" });
|
|
21588
|
+
} catch (error) {
|
|
21589
|
+
this.#getLogger?.().debug("Could not resume MCP task", { error });
|
|
21590
|
+
}
|
|
21591
|
+
}
|
|
21592
|
+
/**
|
|
21593
|
+
* Counts the attempt this worker just claimed, for a traced task, and describes it for its span.
|
|
21594
|
+
* The write is fenced on the claim; losing it only costs the span its number.
|
|
21595
|
+
*/
|
|
21596
|
+
async #countAttempt(taskKey, resumed) {
|
|
21597
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21598
|
+
agentId: this.#owner,
|
|
21599
|
+
toolCallId: taskKey,
|
|
21600
|
+
perPage: 1
|
|
21601
|
+
});
|
|
21602
|
+
const row = tasks[0];
|
|
21603
|
+
const envelope = row && envelopeOf(row);
|
|
21604
|
+
if (!row || !envelope?.trace) return void 0;
|
|
21605
|
+
const attempt = (envelope.attempts ?? 0) + 1;
|
|
21606
|
+
await (await this.#manager.getStorage()).updateTask(row.id, { metadata: {
|
|
21607
|
+
...envelope,
|
|
21608
|
+
attempts: attempt
|
|
21609
|
+
} }, {
|
|
21610
|
+
expectedStatus: "running",
|
|
21611
|
+
expectedOwnerId: row.ownerId ?? null
|
|
21612
|
+
}).catch(() => void 0);
|
|
21613
|
+
return {
|
|
21614
|
+
...envelope.trace,
|
|
21615
|
+
taskKey,
|
|
21616
|
+
attempt,
|
|
21617
|
+
resumed,
|
|
21618
|
+
recovered: !resumed && attempt > 1
|
|
21619
|
+
};
|
|
21620
|
+
}
|
|
21621
|
+
/** Runs one attempt and turns its outcome into what the background-task workflow records. */
|
|
21622
|
+
async #execute(work, args, options, initial) {
|
|
21623
|
+
const signal = options?.abortSignal ?? new AbortController().signal;
|
|
21624
|
+
const resumeData = options?.resumeData;
|
|
21625
|
+
const trace = await this.#countAttempt(args.taskKey, resumeData !== void 0);
|
|
21626
|
+
let outcome;
|
|
21627
|
+
if (initial) {
|
|
21628
|
+
const abort = () => initial.abort(signal.reason);
|
|
21629
|
+
if (signal.aborted) abort();
|
|
21630
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
21631
|
+
outcome = initial.outcome.finally(() => signal.removeEventListener("abort", abort));
|
|
21632
|
+
} else {
|
|
21633
|
+
const attempt = {
|
|
21634
|
+
signal,
|
|
21635
|
+
...trace && { trace },
|
|
21636
|
+
...resumeData && { resume: {
|
|
21637
|
+
inputResponses: resumeData.inputResponses,
|
|
21638
|
+
suspendPayload: resumeData.suspendPayload
|
|
21639
|
+
} }
|
|
21640
|
+
};
|
|
21641
|
+
outcome = Promise.resolve().then(() => work(attempt));
|
|
21642
|
+
}
|
|
21643
|
+
const result = await new Promise((resolve, reject) => {
|
|
21644
|
+
const onAbort = () => {
|
|
21645
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("Task cancelled"));
|
|
21646
|
+
this.#notifyIfCancelled(args.taskKey);
|
|
21647
|
+
};
|
|
21648
|
+
if (signal.aborted) onAbort();
|
|
21649
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
21650
|
+
outcome.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
21651
|
+
});
|
|
21652
|
+
if (result.status === "completed") return result.result;
|
|
21653
|
+
const storage = await this.#manager.getStorage();
|
|
21654
|
+
const { tasks } = await this.#manager.listTasks({
|
|
21655
|
+
agentId: this.#owner,
|
|
21656
|
+
toolCallId: args.taskKey,
|
|
21657
|
+
perPage: 1
|
|
21658
|
+
});
|
|
21659
|
+
const row = tasks[0];
|
|
21660
|
+
const envelope = row && envelopeOf(row);
|
|
21661
|
+
if (!row || !envelope) throw new Error("MCP task row is gone");
|
|
21662
|
+
const keys = Object.keys(result.inputRequests);
|
|
21663
|
+
if (keys.length === 0) throw new Error("Task asked for input without any input request");
|
|
21664
|
+
const reused = keys.find((key) => envelope.usedInputKeys.includes(key));
|
|
21665
|
+
if (reused !== void 0) throw new Error(`Input request key "${reused}" was already used by this task`);
|
|
21666
|
+
const next = {
|
|
21667
|
+
...envelope,
|
|
21668
|
+
inputRequests: result.inputRequests,
|
|
21669
|
+
inputResponses: {},
|
|
21670
|
+
usedInputKeys: [...envelope.usedInputKeys, ...keys]
|
|
21671
|
+
};
|
|
21672
|
+
if (!await storage.updateTask(row.id, { metadata: next }, {
|
|
21673
|
+
expectedStatus: "running",
|
|
21674
|
+
expectedOwnerId: this.#manager.ownerId
|
|
21675
|
+
})) throw new Error("MCP task is no longer running here");
|
|
21676
|
+
if (!options?.suspend) throw new Error("Background task runtime cannot suspend this task");
|
|
21677
|
+
await options.suspend(result.suspendPayload);
|
|
21678
|
+
}
|
|
21679
|
+
};
|
|
21680
|
+
// src/server/tasks/in-memory-store.ts
|
|
21681
|
+
const DEFAULT_EXPIRED_RETENTION_MS = 6e4;
|
|
21682
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
21683
|
+
/**
|
|
21684
|
+
* Default {@link MCPTaskStore}: tasks live in this process. A task created here can only be read on
|
|
21685
|
+
* the same instance and does not survive a restart, so use it for single-instance servers.
|
|
21686
|
+
*
|
|
21687
|
+
* When a task's TTL elapses its work is aborted and it turns `failed`; it is forgotten after
|
|
21688
|
+
* `expiredRetentionMs`. Finished tasks stay in memory until then, so `limits.retainedPerPrincipal`
|
|
21689
|
+
* and `limits.retainedTotal` bound how many tasks it holds; a task with a `null` TTL counts until
|
|
21690
|
+
* the store closes.
|
|
21691
|
+
*
|
|
21692
|
+
* @experimental SEP-2663 extension; API may change.
|
|
21693
|
+
*/
|
|
21694
|
+
var InMemoryMCPTaskStore = class {
|
|
21695
|
+
#tasks = /* @__PURE__ */ new Map();
|
|
21696
|
+
/** The same records, by task key. */
|
|
21697
|
+
#byKey = /* @__PURE__ */ new Map();
|
|
21698
|
+
/** Open (non-terminal) tasks per principal, and in total. */
|
|
21699
|
+
#openByPrincipal = /* @__PURE__ */ new Map();
|
|
21700
|
+
#open = 0;
|
|
21701
|
+
/** Every record held (open or finished, until purged) per principal, and in total. */
|
|
21702
|
+
#retainedByPrincipal = /* @__PURE__ */ new Map();
|
|
21703
|
+
#now;
|
|
21704
|
+
#expiredRetentionMs;
|
|
21705
|
+
#onCancel;
|
|
21706
|
+
constructor(options = {}) {
|
|
21707
|
+
this.#now = options.now ?? Date.now;
|
|
21708
|
+
this.#expiredRetentionMs = options.expiredRetentionMs ?? DEFAULT_EXPIRED_RETENTION_MS;
|
|
21709
|
+
this.#onCancel = options.onCancel;
|
|
21710
|
+
}
|
|
21711
|
+
async create(params, { clientLabel } = {}) {
|
|
21712
|
+
assertNonNegativeInteger("ttlMs", params.ttlMs);
|
|
21713
|
+
assertNonNegativeInteger("pollIntervalMs", params.pollIntervalMs);
|
|
21714
|
+
const openForPrincipal = this.#openByPrincipal.get(params.principal) ?? 0;
|
|
21715
|
+
const retainedForPrincipal = this.#retainedByPrincipal.get(params.principal) ?? 0;
|
|
21716
|
+
const { limits } = params;
|
|
21717
|
+
if (limits) {
|
|
21718
|
+
if (openForPrincipal >= limits.perPrincipal) throw new MCPTaskLimitError("principal", limits.perPrincipal);
|
|
21719
|
+
if (this.#open >= limits.total) throw new MCPTaskLimitError("total", limits.total);
|
|
21720
|
+
if (limits.retainedPerPrincipal !== void 0 && retainedForPrincipal >= limits.retainedPerPrincipal) throw new MCPTaskLimitError("principal", limits.retainedPerPrincipal, "retained");
|
|
21721
|
+
if (limits.retainedTotal !== void 0 && this.#tasks.size >= limits.retainedTotal) throw new MCPTaskLimitError("total", limits.retainedTotal, "retained");
|
|
21722
|
+
}
|
|
21723
|
+
const now = this.#now();
|
|
21724
|
+
const taskId = createTaskId();
|
|
21725
|
+
const record = {
|
|
21726
|
+
taskId,
|
|
21727
|
+
taskKey: taskKeyOf(taskId),
|
|
21728
|
+
principal: params.principal,
|
|
21729
|
+
clientLabel,
|
|
21730
|
+
context: params.context,
|
|
21731
|
+
...params.trace && { trace: params.trace },
|
|
21732
|
+
attempts: 0,
|
|
21733
|
+
status: "working",
|
|
21734
|
+
createdAt: now,
|
|
21735
|
+
lastUpdatedAt: now,
|
|
21736
|
+
ttlMs: params.ttlMs,
|
|
21737
|
+
pollIntervalMs: params.pollIntervalMs,
|
|
21738
|
+
usedInputKeys: /* @__PURE__ */ new Set(),
|
|
21739
|
+
abort: new AbortController()
|
|
21740
|
+
};
|
|
21741
|
+
if (record.ttlMs !== null) this.#arm(record, now + record.ttlMs, () => this.#expire(record));
|
|
21742
|
+
this.#tasks.set(record.taskId, record);
|
|
21743
|
+
this.#byKey.set(record.taskKey, record);
|
|
21744
|
+
this.#openByPrincipal.set(record.principal, openForPrincipal + 1);
|
|
21745
|
+
this.#open++;
|
|
21746
|
+
this.#retainedByPrincipal.set(record.principal, retainedForPrincipal + 1);
|
|
21747
|
+
return this.#snapshot(record);
|
|
21748
|
+
}
|
|
21749
|
+
async get(taskId, access) {
|
|
21750
|
+
const record = this.#lookup(taskKeyOf(taskId), access);
|
|
21751
|
+
return record && this.#detailed(record);
|
|
21752
|
+
}
|
|
21753
|
+
async update(taskId, inputResponses, access) {
|
|
21754
|
+
return this.#update(taskKeyOf(taskId), inputResponses, access) !== "not_found";
|
|
21755
|
+
}
|
|
21756
|
+
async cancel(taskId, access) {
|
|
21757
|
+
return this.#cancel(taskKeyOf(taskId), access) !== "not_found";
|
|
21758
|
+
}
|
|
21759
|
+
async list(options = {}) {
|
|
21760
|
+
const limit = options.limit ?? 50;
|
|
21761
|
+
const cursor = decodeTaskListCursor(options.cursor);
|
|
21762
|
+
const matching = [];
|
|
21763
|
+
const now = this.#now();
|
|
21764
|
+
for (const record of [...this.#tasks.values()].reverse()) {
|
|
21765
|
+
if (record.purgeAt !== void 0 && now >= record.purgeAt) {
|
|
21766
|
+
this.#purge(record);
|
|
21767
|
+
continue;
|
|
21768
|
+
}
|
|
21769
|
+
if (options.taskKey && record.taskKey !== options.taskKey) continue;
|
|
21770
|
+
const listing = toListing(record.taskKey, principalKeyOf(record.principal), record.context, this.#detailed(record), record.clientLabel);
|
|
21771
|
+
if (!isPastTaskListCursor(listing, cursor) || !matchesTaskListOptions(listing, options)) continue;
|
|
21772
|
+
matching.push(listing);
|
|
21773
|
+
if (matching.length > limit) break;
|
|
21774
|
+
}
|
|
21775
|
+
const listings = matching.slice(0, limit);
|
|
21776
|
+
return {
|
|
21777
|
+
listings,
|
|
21778
|
+
...matching.length > limit && { nextCursor: nextTaskListCursor(listings, cursor) }
|
|
21779
|
+
};
|
|
21780
|
+
}
|
|
21781
|
+
async updateByKey(taskKey, inputResponses) {
|
|
21782
|
+
return this.#update(taskKey, inputResponses);
|
|
21783
|
+
}
|
|
21784
|
+
async cancelByKey(taskKey) {
|
|
21785
|
+
return this.#cancel(taskKey);
|
|
21786
|
+
}
|
|
21787
|
+
#update(taskKey, inputResponses, access) {
|
|
21788
|
+
const record = this.#lookup(taskKey, access);
|
|
21789
|
+
if (!record) return "not_found";
|
|
21790
|
+
const pending = record.pendingInput;
|
|
21791
|
+
if (!pending) return "ignored";
|
|
21792
|
+
let accepted = false;
|
|
21793
|
+
for (const [key, response] of Object.entries(inputResponses)) if (Object.hasOwn(pending.requests, key) && !pending.responses.has(key)) {
|
|
21794
|
+
pending.responses.set(key, response);
|
|
21795
|
+
accepted = true;
|
|
21796
|
+
}
|
|
21797
|
+
if (Object.keys(pending.requests).every((key) => pending.responses.has(key))) {
|
|
21798
|
+
record.pendingInput = void 0;
|
|
21799
|
+
this.#transition(record, "working");
|
|
21800
|
+
pending.resolve(Object.fromEntries(pending.responses));
|
|
21801
|
+
}
|
|
21802
|
+
return accepted ? "done" : "ignored";
|
|
21803
|
+
}
|
|
21804
|
+
#cancel(taskKey, access) {
|
|
21805
|
+
const record = this.#lookup(taskKey, access);
|
|
21806
|
+
if (!record) return "not_found";
|
|
21807
|
+
if (isTerminalTaskStatus(record.status)) return "ignored";
|
|
21808
|
+
this.#finish(record, "cancelled", /* @__PURE__ */ new Error("Task was cancelled"));
|
|
21809
|
+
this.#notifyCancel(record);
|
|
21810
|
+
return "done";
|
|
21811
|
+
}
|
|
21812
|
+
async traceOf(taskId, access) {
|
|
21813
|
+
return this.#lookup(taskKeyOf(taskId), access)?.trace;
|
|
21814
|
+
}
|
|
21815
|
+
/** Whether this store holds the task, for any caller. */
|
|
21816
|
+
has(taskId) {
|
|
21817
|
+
return this.#tasks.has(taskId);
|
|
21818
|
+
}
|
|
21819
|
+
/** The context recorded at creation. */
|
|
21820
|
+
context(taskId) {
|
|
21821
|
+
return this.#tasks.get(taskId)?.context;
|
|
21822
|
+
}
|
|
21823
|
+
run(taskId, work, options = {}) {
|
|
21824
|
+
const record = this.#tasks.get(taskId);
|
|
21825
|
+
if (!record) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21826
|
+
const initial = options.initialAttempt;
|
|
21827
|
+
if (!initial) {
|
|
21828
|
+
this.#attempt(record, work);
|
|
21829
|
+
return;
|
|
21830
|
+
}
|
|
21831
|
+
record.attempts++;
|
|
21832
|
+
const signal = record.abort.signal;
|
|
21833
|
+
const abort = () => initial.abort(signal.reason);
|
|
21834
|
+
if (signal.aborted) abort();
|
|
21835
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
21836
|
+
this.#settle(record, work, initial.outcome.finally(() => signal.removeEventListener("abort", abort)));
|
|
21837
|
+
}
|
|
21838
|
+
#attempt(record, work, resume) {
|
|
21839
|
+
record.attempts++;
|
|
21840
|
+
const attempt = {
|
|
21841
|
+
signal: record.abort.signal,
|
|
21842
|
+
...resume && { resume },
|
|
21843
|
+
...record.trace && { trace: {
|
|
21844
|
+
...record.trace,
|
|
21845
|
+
taskKey: taskKeyOf(record.taskId),
|
|
21846
|
+
attempt: record.attempts,
|
|
21847
|
+
resumed: resume !== void 0,
|
|
21848
|
+
recovered: false
|
|
21849
|
+
} }
|
|
21850
|
+
};
|
|
21851
|
+
this.#settle(record, work, Promise.resolve().then(() => work(attempt)));
|
|
21852
|
+
}
|
|
21853
|
+
#settle(record, work, outcome) {
|
|
21854
|
+
outcome.then((result) => this.#record(record, work, result), (error) => this.#fail(record, {
|
|
21855
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21856
|
+
message: error instanceof Error ? error.message : String(error)
|
|
21857
|
+
}));
|
|
21858
|
+
}
|
|
21859
|
+
#record(record, work, outcome) {
|
|
21860
|
+
if (isTerminalTaskStatus(record.status)) return;
|
|
21861
|
+
if (outcome.status === "completed") {
|
|
21862
|
+
record.result = outcome.result;
|
|
21863
|
+
this.#finish(record, "completed");
|
|
21864
|
+
return;
|
|
21865
|
+
}
|
|
21866
|
+
const keys = Object.keys(outcome.inputRequests);
|
|
21867
|
+
const reused = keys.find((key) => record.usedInputKeys.has(key));
|
|
21868
|
+
if (keys.length === 0 || reused !== void 0) {
|
|
21869
|
+
this.#fail(record, {
|
|
21870
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21871
|
+
message: reused === void 0 ? "Task asked for input without any input request" : `Input request key "${reused}" was already used by this task`
|
|
21872
|
+
});
|
|
21873
|
+
return;
|
|
21874
|
+
}
|
|
21875
|
+
for (const key of keys) record.usedInputKeys.add(key);
|
|
21876
|
+
const { suspendPayload } = outcome;
|
|
21877
|
+
record.pendingInput = {
|
|
21878
|
+
requests: outcome.inputRequests,
|
|
21879
|
+
responses: /* @__PURE__ */ new Map(),
|
|
21880
|
+
resolve: (inputResponses) => this.#attempt(record, work, {
|
|
21881
|
+
inputResponses,
|
|
21882
|
+
suspendPayload
|
|
21883
|
+
}),
|
|
21884
|
+
reject: () => {}
|
|
21885
|
+
};
|
|
21886
|
+
this.#transition(record, "input_required");
|
|
21887
|
+
}
|
|
21888
|
+
#fail(record, error) {
|
|
21889
|
+
if (isTerminalTaskStatus(record.status)) return;
|
|
21890
|
+
record.error = error;
|
|
21891
|
+
this.#finish(record, "failed", new Error(error.message));
|
|
21892
|
+
}
|
|
21893
|
+
/** The writer for a task this store holds. Writes after the task is terminal are ignored. */
|
|
21894
|
+
handle(taskId) {
|
|
21895
|
+
const record = this.#tasks.get(taskId);
|
|
21896
|
+
if (!record) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
21897
|
+
const live = () => !isTerminalTaskStatus(record.status);
|
|
21898
|
+
return {
|
|
21899
|
+
taskId,
|
|
21900
|
+
signal: record.abort.signal,
|
|
21901
|
+
status: (message) => {
|
|
21902
|
+
if (!live()) return;
|
|
21903
|
+
record.statusMessage = message;
|
|
21904
|
+
record.lastUpdatedAt = this.#now();
|
|
21905
|
+
},
|
|
21906
|
+
requireInput: (requests) => {
|
|
21907
|
+
if (!live()) return Promise.reject(/* @__PURE__ */ new Error(`MCP task ${taskKeyOf(taskId)} is no longer running`));
|
|
21908
|
+
if (record.pendingInput) return Promise.reject(/* @__PURE__ */ new Error(`MCP task ${taskKeyOf(taskId)} is already waiting for input`));
|
|
21909
|
+
const keys = Object.keys(requests);
|
|
21910
|
+
if (keys.length === 0) return Promise.reject(/* @__PURE__ */ new Error("requireInput needs at least one input request"));
|
|
21911
|
+
const reused = keys.find((key) => record.usedInputKeys.has(key));
|
|
21912
|
+
if (reused !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`Input request key "${reused}" was already used by MCP task ${taskKeyOf(taskId)}`));
|
|
21913
|
+
for (const key of keys) record.usedInputKeys.add(key);
|
|
21914
|
+
return new Promise((resolve, reject) => {
|
|
21915
|
+
record.pendingInput = {
|
|
21916
|
+
requests,
|
|
21917
|
+
responses: /* @__PURE__ */ new Map(),
|
|
21918
|
+
resolve,
|
|
21919
|
+
reject
|
|
21920
|
+
};
|
|
21921
|
+
this.#transition(record, "input_required");
|
|
21922
|
+
});
|
|
21923
|
+
},
|
|
21924
|
+
complete: (result) => {
|
|
21925
|
+
if (!live()) return;
|
|
21926
|
+
record.result = result;
|
|
21927
|
+
this.#finish(record, "completed");
|
|
21928
|
+
},
|
|
21929
|
+
fail: (error) => {
|
|
21930
|
+
if (!live()) return;
|
|
21931
|
+
record.error = error;
|
|
21932
|
+
this.#finish(record, "failed");
|
|
21933
|
+
}
|
|
21934
|
+
};
|
|
21935
|
+
}
|
|
21936
|
+
/** Cancels all running work, invalidates every handle and forgets every task. */
|
|
21937
|
+
close() {
|
|
21938
|
+
for (const record of this.#tasks.values()) {
|
|
21939
|
+
clearTimeout(record.expiryTimer);
|
|
21940
|
+
if (!isTerminalTaskStatus(record.status)) this.#finish(record, "cancelled", /* @__PURE__ */ new Error("MCP task store closed"));
|
|
21941
|
+
}
|
|
21942
|
+
this.#tasks.clear();
|
|
21943
|
+
this.#byKey.clear();
|
|
21944
|
+
this.#openByPrincipal.clear();
|
|
21945
|
+
this.#open = 0;
|
|
21946
|
+
this.#retainedByPrincipal.clear();
|
|
21947
|
+
}
|
|
21948
|
+
/** Without `access`, any principal's task (admin). */
|
|
21949
|
+
#lookup(taskKey, access) {
|
|
21950
|
+
const record = this.#byKey.get(taskKey);
|
|
21951
|
+
if (!record || access && record.principal !== access.principal) return void 0;
|
|
21952
|
+
if (record.purgeAt !== void 0 && this.#now() >= record.purgeAt) {
|
|
21953
|
+
this.#purge(record);
|
|
21954
|
+
return;
|
|
21955
|
+
}
|
|
21956
|
+
return record;
|
|
21957
|
+
}
|
|
21958
|
+
/** Forgets an expired task, freeing its retained slot. */
|
|
21959
|
+
#purge(record) {
|
|
21960
|
+
if (this.#tasks.get(record.taskId) !== record) return;
|
|
21961
|
+
clearTimeout(record.expiryTimer);
|
|
21962
|
+
this.#tasks.delete(record.taskId);
|
|
21963
|
+
this.#byKey.delete(record.taskKey);
|
|
21964
|
+
const retained = this.#retainedByPrincipal.get(record.principal) - 1;
|
|
21965
|
+
if (retained === 0) this.#retainedByPrincipal.delete(record.principal);
|
|
21966
|
+
else this.#retainedByPrincipal.set(record.principal, retained);
|
|
21967
|
+
}
|
|
21968
|
+
/** setTimeout fires after 1 ms past 2^31-1 ms (~24.8 days), so a longer wait goes in steps. */
|
|
21969
|
+
#arm(record, at, fire) {
|
|
21970
|
+
const delay = at - this.#now();
|
|
21971
|
+
record.expiryTimer = delay > MAX_TIMER_DELAY_MS ? setTimeout(() => this.#arm(record, at, fire), MAX_TIMER_DELAY_MS) : setTimeout(fire, delay);
|
|
21972
|
+
record.expiryTimer.unref?.();
|
|
21973
|
+
}
|
|
21974
|
+
#expire(record) {
|
|
21975
|
+
if (!isTerminalTaskStatus(record.status)) {
|
|
21976
|
+
record.error = {
|
|
21977
|
+
code: TASKS_ERROR_CODES.InternalError,
|
|
21978
|
+
message: "Task expired before it finished"
|
|
21979
|
+
};
|
|
21980
|
+
this.#finish(record, "failed", /* @__PURE__ */ new Error("Task expired"));
|
|
21981
|
+
this.#notifyCancel(record);
|
|
21982
|
+
}
|
|
21983
|
+
record.purgeAt = this.#now() + this.#expiredRetentionMs;
|
|
21984
|
+
this.#arm(record, record.purgeAt, () => this.#purge(record));
|
|
21985
|
+
}
|
|
21986
|
+
/** Fire and forget: a slow hook must hold up neither the `tasks/cancel` ack nor expiry. */
|
|
21987
|
+
#notifyCancel(record) {
|
|
21988
|
+
const onCancel = this.#onCancel;
|
|
21989
|
+
const context = record.context;
|
|
21990
|
+
if (!onCancel || !context) return;
|
|
21991
|
+
Promise.resolve().then(() => onCancel(context)).catch(() => {});
|
|
21992
|
+
}
|
|
21993
|
+
#transition(record, status) {
|
|
21994
|
+
record.status = status;
|
|
21995
|
+
record.lastUpdatedAt = this.#now();
|
|
21996
|
+
}
|
|
21997
|
+
/** The only way a task turns terminal, so it is also where it stops counting as open. */
|
|
21998
|
+
#finish(record, status, reason) {
|
|
21999
|
+
const open = this.#openByPrincipal.get(record.principal) - 1;
|
|
22000
|
+
if (open === 0) this.#openByPrincipal.delete(record.principal);
|
|
22001
|
+
else this.#openByPrincipal.set(record.principal, open);
|
|
22002
|
+
this.#open--;
|
|
22003
|
+
this.#transition(record, status);
|
|
22004
|
+
const pending = record.pendingInput;
|
|
22005
|
+
record.pendingInput = void 0;
|
|
22006
|
+
pending?.reject(reason ?? /* @__PURE__ */ new Error(`MCP task "${taskKeyOf(record.taskId)}" ${status} while waiting for input`));
|
|
22007
|
+
if (reason) record.abort.abort(reason);
|
|
22008
|
+
}
|
|
22009
|
+
#snapshot(record) {
|
|
22010
|
+
return {
|
|
22011
|
+
taskId: record.taskId,
|
|
22012
|
+
status: record.status,
|
|
22013
|
+
...record.statusMessage !== void 0 && { statusMessage: record.statusMessage },
|
|
22014
|
+
createdAt: new Date(record.createdAt).toISOString(),
|
|
22015
|
+
lastUpdatedAt: new Date(record.lastUpdatedAt).toISOString(),
|
|
22016
|
+
ttlMs: record.ttlMs,
|
|
22017
|
+
pollIntervalMs: record.pollIntervalMs ?? suggestedPollIntervalMs(record.createdAt, this.#now())
|
|
22018
|
+
};
|
|
22019
|
+
}
|
|
22020
|
+
#detailed(record) {
|
|
22021
|
+
const base = this.#snapshot(record);
|
|
22022
|
+
switch (record.status) {
|
|
22023
|
+
case "input_required": {
|
|
22024
|
+
const { requests, responses } = record.pendingInput;
|
|
22025
|
+
const open = Object.fromEntries(Object.entries(requests).filter(([key]) => !responses.has(key)));
|
|
22026
|
+
return {
|
|
22027
|
+
...base,
|
|
22028
|
+
status: "input_required",
|
|
22029
|
+
inputRequests: open
|
|
22030
|
+
};
|
|
22031
|
+
}
|
|
22032
|
+
case "completed": return {
|
|
22033
|
+
...base,
|
|
22034
|
+
status: "completed",
|
|
22035
|
+
result: { ...record.result }
|
|
22036
|
+
};
|
|
22037
|
+
case "failed": return {
|
|
22038
|
+
...base,
|
|
22039
|
+
status: "failed",
|
|
22040
|
+
error: { ...record.error }
|
|
22041
|
+
};
|
|
22042
|
+
case "working":
|
|
22043
|
+
case "cancelled": return {
|
|
22044
|
+
...base,
|
|
22045
|
+
status: record.status
|
|
22046
|
+
};
|
|
22047
|
+
}
|
|
22048
|
+
}
|
|
22049
|
+
};
|
|
22050
|
+
function assertNonNegativeInteger(name, value) {
|
|
22051
|
+
if (value === null || value === void 0) return;
|
|
22052
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative integer, got ${value}`);
|
|
22053
|
+
}
|
|
22054
|
+
// src/server/tasks/config.ts
|
|
22055
|
+
const MODES = [
|
|
22056
|
+
"off",
|
|
22057
|
+
"auto",
|
|
22058
|
+
"always"
|
|
22059
|
+
];
|
|
22060
|
+
const DEFAULT_SYNC_WINDOW_MS = 2e3;
|
|
22061
|
+
const DEFAULT_MAX_OPEN_TASKS_PER_CLIENT = 100;
|
|
22062
|
+
const DEFAULT_MAX_OPEN_TASKS = 1e3;
|
|
22063
|
+
const DEFAULT_MAX_RETAINED_TASKS_PER_CLIENT = 1e3;
|
|
22064
|
+
const DEFAULT_MAX_RETAINED_TASKS = 1e4;
|
|
22065
|
+
function invalid(text) {
|
|
22066
|
+
return new _mastra_core_error.MastraError({
|
|
22067
|
+
id: "MCP_SERVER_TASKS_INVALID_CONFIG",
|
|
22068
|
+
domain: _mastra_core_error.ErrorDomain.MCP,
|
|
22069
|
+
category: _mastra_core_error.ErrorCategory.USER,
|
|
22070
|
+
text
|
|
22071
|
+
});
|
|
22072
|
+
}
|
|
22073
|
+
function assertMode(owner, mode) {
|
|
22074
|
+
if (!MODES.includes(mode)) throw invalid(`${owner} has an unknown task mode "${String(mode)}"; use 'off', 'auto' or 'always'.`);
|
|
22075
|
+
}
|
|
22076
|
+
function assertDuration(owner, value) {
|
|
22077
|
+
if (value === null || value === void 0) return;
|
|
22078
|
+
if (!Number.isSafeInteger(value) || value < 0) throw invalid(`${owner} must be a non-negative integer number of milliseconds, got ${value}.`);
|
|
22079
|
+
}
|
|
22080
|
+
function assertBoolean(owner, value) {
|
|
22081
|
+
if (value !== void 0 && typeof value !== "boolean") throw invalid(`${owner} must be a boolean, got ${String(value)}.`);
|
|
22082
|
+
}
|
|
22083
|
+
function positiveInteger(owner, value) {
|
|
22084
|
+
if (value === void 0) return void 0;
|
|
22085
|
+
if (!Number.isSafeInteger(value) || value < 1) throw invalid(`${owner} must be a positive integer, got ${value}.`);
|
|
22086
|
+
return value;
|
|
22087
|
+
}
|
|
22088
|
+
/**
|
|
22089
|
+
* Resolves an open cap and the retained cap that contains it. Every open task is also retained, so
|
|
22090
|
+
* an unset cap follows the one that was set, and setting both with retained below open is refused.
|
|
22091
|
+
*/
|
|
22092
|
+
function caps([openName, open, defaultOpen], [retainedName, retained, defaultRetained]) {
|
|
22093
|
+
const openCap = positiveInteger(openName, open);
|
|
22094
|
+
const retainedCap = positiveInteger(retainedName, retained);
|
|
22095
|
+
if (openCap !== void 0 && retainedCap !== void 0 && retainedCap < openCap) throw invalid(`${retainedName} (${retainedCap}) must be at least ${openName} (${openCap}).`);
|
|
22096
|
+
return [openCap ?? Math.min(defaultOpen, retainedCap ?? defaultOpen), retainedCap ?? Math.max(defaultRetained, openCap ?? 0)];
|
|
22097
|
+
}
|
|
22098
|
+
/** Validates `tasks` against the configured tools, workflows and agents. Throws on anything a server could not serve. */
|
|
22099
|
+
function resolveTasksConfig(options, surfaces) {
|
|
22100
|
+
const config = options ?? {};
|
|
22101
|
+
const defaultMode = config.default ?? "off";
|
|
22102
|
+
assertMode("tasks.default", defaultMode);
|
|
22103
|
+
assertDuration("tasks.ttlMs", config.ttlMs);
|
|
22104
|
+
const ttlMs = config.ttlMs === void 0 ? DEFAULT_TASK_TTL_MS : config.ttlMs;
|
|
22105
|
+
assertBoolean("tasks.recover", config.recover);
|
|
22106
|
+
const recover = config.recover ?? false;
|
|
22107
|
+
const [perPrincipal, retainedPerPrincipal] = caps([
|
|
22108
|
+
"tasks.maxOpenTasksPerClient",
|
|
22109
|
+
config.maxOpenTasksPerClient,
|
|
22110
|
+
DEFAULT_MAX_OPEN_TASKS_PER_CLIENT
|
|
22111
|
+
], [
|
|
22112
|
+
"tasks.maxRetainedTasksPerClient",
|
|
22113
|
+
config.maxRetainedTasksPerClient,
|
|
22114
|
+
DEFAULT_MAX_RETAINED_TASKS_PER_CLIENT
|
|
22115
|
+
]);
|
|
22116
|
+
const [total, retainedTotal] = caps([
|
|
22117
|
+
"tasks.maxOpenTasks",
|
|
22118
|
+
config.maxOpenTasks,
|
|
22119
|
+
DEFAULT_MAX_OPEN_TASKS
|
|
22120
|
+
], [
|
|
22121
|
+
"tasks.maxRetainedTasks",
|
|
22122
|
+
config.maxRetainedTasks,
|
|
22123
|
+
DEFAULT_MAX_RETAINED_TASKS
|
|
22124
|
+
]);
|
|
22125
|
+
const limits = {
|
|
22126
|
+
perPrincipal,
|
|
22127
|
+
total,
|
|
22128
|
+
retainedPerPrincipal,
|
|
22129
|
+
retainedTotal
|
|
22130
|
+
};
|
|
22131
|
+
/** Resolves one surface's per-key entries, keyed by the tool name each key is exposed as. */
|
|
22132
|
+
const resolveSurface = (option, keys, exposedAs, surfaceRecover) => {
|
|
22133
|
+
const known = new Set(keys);
|
|
22134
|
+
const tools = new Set(surfaces.tools);
|
|
22135
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
22136
|
+
const entries = config[option] ?? {};
|
|
22137
|
+
const noun = option.slice(0, -1);
|
|
22138
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
22139
|
+
if (!known.has(key)) throw invalid(`tasks.${option} names "${key}", which is not one of this server's ${option}.`);
|
|
22140
|
+
const owner = `tasks.${option}.${key}`;
|
|
22141
|
+
if (option !== "tools" && tools.has(exposedAs(key))) throw invalid(`${owner} names a ${noun} hidden by the tool "${exposedAs(key)}", so it is never served.`);
|
|
22142
|
+
const surface = typeof entry === "string" ? { mode: entry } : entry;
|
|
22143
|
+
assertMode(owner, surface.mode);
|
|
22144
|
+
assertDuration(`${owner}.syncWindowMs`, surface.syncWindowMs);
|
|
22145
|
+
assertDuration(`${owner}.ttlMs`, surface.ttlMs);
|
|
22146
|
+
if (option === "workflows" && "recover" in surface) throw invalid(`${owner}.recover is not a ${noun} option: a workflow task always re-attaches to its run.`);
|
|
22147
|
+
assertBoolean(`${owner}.recover`, surface.recover);
|
|
22148
|
+
resolved.set(exposedAs(key), {
|
|
22149
|
+
mode: surface.mode,
|
|
22150
|
+
syncWindowMs: surface.syncWindowMs ?? DEFAULT_SYNC_WINDOW_MS,
|
|
22151
|
+
ttlMs: surface.ttlMs === void 0 ? ttlMs : surface.ttlMs,
|
|
22152
|
+
recover: surfaceRecover(surface)
|
|
22153
|
+
});
|
|
22154
|
+
}
|
|
22155
|
+
return resolved;
|
|
22156
|
+
};
|
|
22157
|
+
const perTool = resolveSurface("tools", surfaces.tools, (key) => key, (tool) => tool.recover ?? recover);
|
|
22158
|
+
const perWorkflow = resolveSurface("workflows", surfaces.workflows ?? [], (key) => `run_${key}`, () => true);
|
|
22159
|
+
const perAgent = resolveSurface("agents", surfaces.agents ?? [], (key) => `ask_${key}`, (agent) => agent.recover ?? recover);
|
|
22160
|
+
const fallback = {
|
|
22161
|
+
mode: defaultMode,
|
|
22162
|
+
syncWindowMs: DEFAULT_SYNC_WINDOW_MS,
|
|
22163
|
+
ttlMs,
|
|
22164
|
+
recover
|
|
22165
|
+
};
|
|
22166
|
+
const modeFor = (name, toolType) => {
|
|
22167
|
+
if (toolType === "workflow") return perWorkflow.get(name) ?? {
|
|
22168
|
+
...fallback,
|
|
22169
|
+
recover: true
|
|
22170
|
+
};
|
|
22171
|
+
if (toolType === "agent") return perAgent.get(name) ?? fallback;
|
|
22172
|
+
return perTool.get(name) ?? fallback;
|
|
22173
|
+
};
|
|
22174
|
+
const exposed = new Map([
|
|
22175
|
+
...surfaces.tools.map((name) => [name, void 0]),
|
|
22176
|
+
...(surfaces.workflows ?? []).map((key) => [`run_${key}`, "workflow"]),
|
|
22177
|
+
...(surfaces.agents ?? []).map((key) => [`ask_${key}`, "agent"])
|
|
22178
|
+
]);
|
|
22179
|
+
const required = new Set(config.required ?? []);
|
|
22180
|
+
for (const name of required) {
|
|
22181
|
+
if (!exposed.has(name)) throw invalid(`tasks.required names "${name}", which is not one of this server's tools.`);
|
|
22182
|
+
if (modeFor(name, exposed.get(name)).mode === "off") throw invalid(`tasks.required names "${name}", which is not served as a task.`);
|
|
22183
|
+
}
|
|
22184
|
+
return {
|
|
22185
|
+
enabled: defaultMode !== "off" || [
|
|
22186
|
+
...perTool.values(),
|
|
22187
|
+
...perWorkflow.values(),
|
|
22188
|
+
...perAgent.values()
|
|
22189
|
+
].some((surface) => surface.mode !== "off"),
|
|
22190
|
+
store: config.store,
|
|
22191
|
+
limits,
|
|
22192
|
+
required,
|
|
22193
|
+
authorize: config.authorize,
|
|
22194
|
+
modeFor
|
|
22195
|
+
};
|
|
22196
|
+
}
|
|
22197
|
+
/**
|
|
22198
|
+
* Picks the store on first use, once the server knows whether it runs on a Mastra instance with
|
|
22199
|
+
* durable background tasks. `run` follows the `create` it pairs with, so the choice is made by then.
|
|
22200
|
+
*/
|
|
22201
|
+
var SelectingMCPTaskStore = class {
|
|
22202
|
+
#select;
|
|
22203
|
+
#selecting;
|
|
22204
|
+
#selected;
|
|
22205
|
+
/** In-memory stores chosen before a later registration on Mastra, still serving the tasks created on them. */
|
|
22206
|
+
#earlier = [];
|
|
22207
|
+
constructor(select) {
|
|
22208
|
+
this.#select = select;
|
|
22209
|
+
}
|
|
22210
|
+
/** The chosen store, choosing it if no task request has yet. */
|
|
22211
|
+
resolve() {
|
|
22212
|
+
return this.#store();
|
|
22213
|
+
}
|
|
22214
|
+
/**
|
|
22215
|
+
* Chooses again on next use, for a server registered on Mastra after it served tasks. An
|
|
22216
|
+
* in-memory store chosen before keeps the tasks created on it and goes on serving them.
|
|
22217
|
+
*/
|
|
22218
|
+
reselect() {
|
|
22219
|
+
const chosen = this.#selected;
|
|
22220
|
+
if (chosen) {
|
|
22221
|
+
if (!(chosen instanceof InMemoryMCPTaskStore)) return;
|
|
22222
|
+
this.#earlier.push(chosen);
|
|
22223
|
+
this.#selected = void 0;
|
|
22224
|
+
this.#selecting = void 0;
|
|
22225
|
+
return;
|
|
22226
|
+
}
|
|
22227
|
+
const choosing = this.#selecting;
|
|
22228
|
+
if (!choosing) return;
|
|
22229
|
+
const next = choosing.then(() => {
|
|
22230
|
+
this.reselect();
|
|
22231
|
+
return this.#store();
|
|
22232
|
+
});
|
|
22233
|
+
next.catch(() => {});
|
|
22234
|
+
this.#selecting = next;
|
|
22235
|
+
}
|
|
22236
|
+
/** Cancels the work of the in-memory stores it chose; a background-task store is closed by the server. */
|
|
22237
|
+
close() {
|
|
22238
|
+
for (const store of [...this.#earlier.splice(0), this.#selected]) if (store instanceof InMemoryMCPTaskStore) store.close();
|
|
22239
|
+
}
|
|
22240
|
+
/** The store that holds this task: an earlier in-memory store that created it, else the chosen one. */
|
|
22241
|
+
#storeOf(taskId) {
|
|
22242
|
+
const earlier = this.#earlier.find((store) => store.has(taskId));
|
|
22243
|
+
return earlier ? Promise.resolve(earlier) : this.#store();
|
|
22244
|
+
}
|
|
22245
|
+
#store() {
|
|
22246
|
+
this.#selecting ??= this.#select().then((store) => this.#selected = store, (error) => {
|
|
22247
|
+
this.#selecting = void 0;
|
|
22248
|
+
throw error;
|
|
22249
|
+
});
|
|
22250
|
+
return this.#selecting;
|
|
22251
|
+
}
|
|
22252
|
+
async create(params) {
|
|
22253
|
+
return (await this.#store()).create(params);
|
|
22254
|
+
}
|
|
22255
|
+
async get(taskId, access) {
|
|
22256
|
+
return (await this.#storeOf(taskId)).get(taskId, access);
|
|
22257
|
+
}
|
|
22258
|
+
async update(taskId, inputResponses, access) {
|
|
22259
|
+
return (await this.#storeOf(taskId)).update(taskId, inputResponses, access);
|
|
22260
|
+
}
|
|
22261
|
+
async cancel(taskId, access) {
|
|
22262
|
+
return (await this.#storeOf(taskId)).cancel(taskId, access);
|
|
22263
|
+
}
|
|
22264
|
+
async traceOf(taskId, access) {
|
|
22265
|
+
return (await this.#storeOf(taskId)).traceOf?.(taskId, access);
|
|
22266
|
+
}
|
|
22267
|
+
run(taskId, work, options) {
|
|
22268
|
+
const store = this.#earlier.find((earlier) => earlier.has(taskId)) ?? this.#selected;
|
|
22269
|
+
if (!store) throw new Error(`MCP task ${taskKeyOf(taskId)} not found`);
|
|
22270
|
+
store.run(taskId, work, options);
|
|
22271
|
+
}
|
|
22272
|
+
/** The chosen store's admin view, or `undefined` when it has none. */
|
|
22273
|
+
async admin() {
|
|
22274
|
+
const store = await this.#store();
|
|
22275
|
+
return isMCPTaskStoreAdmin(store) ? store : void 0;
|
|
22276
|
+
}
|
|
22277
|
+
};
|
|
22278
|
+
// src/server/tasks/emit.ts
|
|
22279
|
+
/** `_meta` key a tool handler uses to hand a task to {@link emitTaskResults}. */
|
|
22280
|
+
const TASK_RESULT_META_KEY = "mastra.ai/mcp-task-result";
|
|
22281
|
+
/**
|
|
22282
|
+
* A `tools/call` result standing in for a task handle. The SDK only lets a tool handler return a
|
|
22283
|
+
* `CallToolResult`; {@link emitTaskResults} replaces this one with the `CreateTaskResult` on the
|
|
22284
|
+
* wire. `isError` keeps the SDK from applying the tool's output schema to the placeholder.
|
|
22285
|
+
*/
|
|
22286
|
+
function taskHandleResult(task) {
|
|
22287
|
+
return {
|
|
22288
|
+
isError: true,
|
|
22289
|
+
content: [{
|
|
22290
|
+
type: "text",
|
|
22291
|
+
text: "MCP task created"
|
|
22292
|
+
}],
|
|
22293
|
+
_meta: { [TASK_RESULT_META_KEY]: task }
|
|
22294
|
+
};
|
|
22295
|
+
}
|
|
22296
|
+
/** The task a result built by {@link taskHandleResult} stands for; `undefined` for any other result. */
|
|
22297
|
+
function taskOfHandleResult(result) {
|
|
22298
|
+
return (result?._meta)?.[TASK_RESULT_META_KEY];
|
|
22299
|
+
}
|
|
22300
|
+
/**
|
|
22301
|
+
* Sends results built by {@link taskHandleResult} as `CreateTaskResult`s. Only call handlers that
|
|
22302
|
+
* checked the caller declared the Tasks extension may build them.
|
|
22303
|
+
*/
|
|
22304
|
+
function emitTaskResults(transport) {
|
|
22305
|
+
const send = transport.send.bind(transport);
|
|
22306
|
+
transport.send = (message, options) => {
|
|
22307
|
+
const result = "result" in message ? message.result : void 0;
|
|
22308
|
+
const task = taskOfHandleResult(result);
|
|
22309
|
+
if (task) {
|
|
22310
|
+
const { [TASK_RESULT_META_KEY]: _task, ...rest } = result._meta;
|
|
22311
|
+
message = {
|
|
22312
|
+
...message,
|
|
22313
|
+
result: {
|
|
22314
|
+
...task,
|
|
22315
|
+
resultType: TASK_RESULT_TYPE,
|
|
22316
|
+
...Object.keys(rest).length > 0 ? { _meta: rest } : {}
|
|
22317
|
+
}
|
|
22318
|
+
};
|
|
22319
|
+
}
|
|
22320
|
+
return send(message, options);
|
|
22321
|
+
};
|
|
22322
|
+
}
|
|
22323
|
+
// src/server/tasks/handlers.ts
|
|
22324
|
+
function notFound() {
|
|
22325
|
+
return new _modelcontextprotocol_server.ProtocolError(TASKS_ERROR_CODES.InvalidParams, "Task not found");
|
|
22326
|
+
}
|
|
22327
|
+
/**
|
|
22328
|
+
* Registers `tasks/get`, `tasks/update` and `tasks/cancel` on one protocol instance. They are
|
|
22329
|
+
* registered with explicit schemas, which the SDK requires to serve the Tasks extension's methods on
|
|
22330
|
+
* the 2026-07-28 revision.
|
|
22331
|
+
*/
|
|
22332
|
+
function registerTaskHandlers(server, deps) {
|
|
22333
|
+
const { store, authorize } = deps;
|
|
22334
|
+
const trace = deps.trace ?? ((_server, _method, _params, _ctx, fn) => fn(new _mastra_core_request_context.RequestContext()));
|
|
22335
|
+
/** `tasks/update` and `tasks/cancel` check `authorize` against the task before acting on it. */
|
|
22336
|
+
const serve = (method, params, ctx, fn, metadata) => {
|
|
22337
|
+
const mcpTaskKey = taskKeyOf(params.taskId);
|
|
22338
|
+
return trace(server, method, { mcpTaskKey }, ctx, async (requestContext) => {
|
|
22339
|
+
const principal = await deps.principal(ctx, requestContext);
|
|
22340
|
+
if (authorize) {
|
|
22341
|
+
const task = await store.get(params.taskId, { principal });
|
|
22342
|
+
if (!task || !await authorize({
|
|
22343
|
+
principal,
|
|
22344
|
+
task,
|
|
22345
|
+
requestContext
|
|
22346
|
+
})) throw notFound();
|
|
22347
|
+
}
|
|
22348
|
+
return fn({ principal });
|
|
22349
|
+
}, {
|
|
22350
|
+
parentOf: store.traceOf ? async (requestContext) => store.traceOf(params.taskId, { principal: await deps.principal(ctx, requestContext) }) : void 0,
|
|
22351
|
+
metadata: {
|
|
22352
|
+
mcpTaskKey,
|
|
22353
|
+
...metadata
|
|
22354
|
+
}
|
|
22355
|
+
});
|
|
22356
|
+
};
|
|
22357
|
+
server.setRequestHandler(TASKS_METHODS.get, {
|
|
22358
|
+
params: GetTaskRequestParamsSchema,
|
|
22359
|
+
result: GetTaskResultSchema
|
|
22360
|
+
}, (params, ctx) => trace(server, TASKS_METHODS.get, {}, ctx, async (requestContext) => {
|
|
22361
|
+
const principal = await deps.principal(ctx, requestContext);
|
|
22362
|
+
const task = await store.get(params.taskId, { principal });
|
|
22363
|
+
if (!task || authorize && !await authorize({
|
|
22364
|
+
principal,
|
|
22365
|
+
task,
|
|
22366
|
+
requestContext
|
|
22367
|
+
})) throw notFound();
|
|
22368
|
+
return task;
|
|
22369
|
+
}, { untraced: true }));
|
|
22370
|
+
server.setRequestHandler(TASKS_METHODS.update, {
|
|
22371
|
+
params: UpdateTaskHandlerParamsSchema,
|
|
22372
|
+
result: TaskAckResultSchema
|
|
22373
|
+
}, (params, ctx) => serve(TASKS_METHODS.update, params, ctx, async (access) => {
|
|
22374
|
+
const inputResponses = ctx.mcpReq.inputResponses ?? {};
|
|
22375
|
+
if (!await store.update(params.taskId, inputResponses, access)) throw notFound();
|
|
22376
|
+
return {};
|
|
22377
|
+
}));
|
|
22378
|
+
server.setRequestHandler(TASKS_METHODS.cancel, {
|
|
22379
|
+
params: CancelTaskRequestParamsSchema,
|
|
22380
|
+
result: TaskAckResultSchema
|
|
22381
|
+
}, (params, ctx) => serve(TASKS_METHODS.cancel, params, ctx, async (access) => {
|
|
22382
|
+
if (!await store.cancel(params.taskId, access)) throw notFound();
|
|
22383
|
+
return {};
|
|
22384
|
+
}, { mcpTaskStatus: "cancelled" }));
|
|
22385
|
+
}
|
|
22386
|
+
// src/server/tasks/inspector.ts
|
|
22387
|
+
/** The tools served as tasks, as Studio shows them. */
|
|
22388
|
+
function listTaskSurfaces(tasks, tools) {
|
|
22389
|
+
const surfaces = [];
|
|
22390
|
+
for (const [name, toolType] of tools) {
|
|
22391
|
+
const { mode, recover, ttlMs } = tasks.modeFor(name, toolType);
|
|
22392
|
+
if (mode === "off") continue;
|
|
22393
|
+
surfaces.push({
|
|
22394
|
+
name,
|
|
22395
|
+
type: toolType === "workflow" ? "workflow" : toolType === "agent" ? "agent" : "tool",
|
|
22396
|
+
mode,
|
|
22397
|
+
recover,
|
|
22398
|
+
required: tasks.required.has(name),
|
|
22399
|
+
ttlMs
|
|
22400
|
+
});
|
|
22401
|
+
}
|
|
22402
|
+
return surfaces;
|
|
22403
|
+
}
|
|
22404
|
+
function tasksInfo(tasks, tools) {
|
|
22405
|
+
return {
|
|
22406
|
+
enabled: tasks.enabled,
|
|
22407
|
+
listable: !tasks.store || isMCPTaskStoreAdmin(tasks.store),
|
|
22408
|
+
surfaces: listTaskSurfaces(tasks, tools)
|
|
22409
|
+
};
|
|
22410
|
+
}
|
|
22411
|
+
/**
|
|
22412
|
+
* Studio's view of the task store. Cancel and answer go through the store's `cancelByKey` and
|
|
22413
|
+
* `updateByKey`, the same code as `tasks/cancel` and `tasks/update`.
|
|
22414
|
+
*/
|
|
22415
|
+
function createTasksInspector(admin) {
|
|
22416
|
+
const required = async () => {
|
|
22417
|
+
const store = await admin();
|
|
22418
|
+
if (!store) throw new Error("This MCP task store cannot be listed");
|
|
22419
|
+
return store;
|
|
22420
|
+
};
|
|
22421
|
+
return {
|
|
22422
|
+
async list(params = {}) {
|
|
22423
|
+
const { listings, nextCursor } = await (await required()).list(params);
|
|
22424
|
+
return {
|
|
22425
|
+
tasks: listings.map(toSummary),
|
|
22426
|
+
...nextCursor && { nextCursor }
|
|
22427
|
+
};
|
|
22428
|
+
},
|
|
22429
|
+
async find(taskId) {
|
|
22430
|
+
const { listings } = await (await required()).list({
|
|
22431
|
+
taskKey: taskKeyOf(taskId),
|
|
22432
|
+
limit: 1
|
|
22433
|
+
});
|
|
22434
|
+
return listings[0] && toSummary(listings[0]);
|
|
22435
|
+
},
|
|
22436
|
+
async cancel(taskKey) {
|
|
22437
|
+
return await (await required()).cancelByKey(taskKey) === "done";
|
|
22438
|
+
},
|
|
22439
|
+
async answer(taskKey, inputResponses) {
|
|
22440
|
+
return await (await required()).updateByKey(taskKey, inputResponses) === "done";
|
|
22441
|
+
}
|
|
22442
|
+
};
|
|
22443
|
+
}
|
|
22444
|
+
function toSummary(listing) {
|
|
22445
|
+
const toolName = listing.context?.toolName;
|
|
22446
|
+
return {
|
|
22447
|
+
taskKey: listing.taskKey,
|
|
22448
|
+
surface: typeof toolName === "string" ? toolName : "",
|
|
22449
|
+
status: listing.status,
|
|
22450
|
+
...listing.statusMessage !== void 0 && { statusMessage: listing.statusMessage },
|
|
22451
|
+
createdAt: listing.createdAt,
|
|
22452
|
+
lastUpdatedAt: listing.lastUpdatedAt,
|
|
22453
|
+
...listing.ttlMs !== null && { expiresAt: new Date(new Date(listing.createdAt).getTime() + listing.ttlMs).toISOString() },
|
|
22454
|
+
principalKey: listing.principalKey,
|
|
22455
|
+
...listing.clientLabel !== void 0 && { clientLabel: listing.clientLabel },
|
|
22456
|
+
...listing.inputRequests && { inputRequests: listing.inputRequests },
|
|
22457
|
+
...listing.result && { result: listing.result },
|
|
22458
|
+
...listing.error && { error: listing.error }
|
|
22459
|
+
};
|
|
22460
|
+
}
|
|
22461
|
+
// src/server/tasks/workflow-runs.ts
|
|
22462
|
+
/**
|
|
22463
|
+
* Whether a task's context is a workflow task's.
|
|
22464
|
+
*
|
|
22465
|
+
* @experimental SEP-2663 extension; API may change.
|
|
22466
|
+
*/
|
|
22467
|
+
function isWorkflowTaskContext(context) {
|
|
22468
|
+
return context?.kind === "workflow" && typeof context.runId === "string" && typeof context.workflowKey === "string";
|
|
22469
|
+
}
|
|
22470
|
+
/** Statuses a run never leaves. */
|
|
22471
|
+
const FINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
22472
|
+
"success",
|
|
22473
|
+
"failed",
|
|
22474
|
+
"tripwire",
|
|
22475
|
+
"canceled",
|
|
22476
|
+
"bailed"
|
|
22477
|
+
]);
|
|
22478
|
+
function isFinalRunStatus(status) {
|
|
22479
|
+
return FINAL_STATUSES.has(status);
|
|
22480
|
+
}
|
|
22481
|
+
/** The text a finished run answers with: the same fields the synchronous `run_` tool returns. */
|
|
22482
|
+
function runSummary(view) {
|
|
22483
|
+
return {
|
|
22484
|
+
status: view.status,
|
|
22485
|
+
input: view.input,
|
|
22486
|
+
steps: view.steps,
|
|
22487
|
+
result: view.result
|
|
22488
|
+
};
|
|
22489
|
+
}
|
|
22490
|
+
function runErrorMessage(view) {
|
|
22491
|
+
const error = view.error;
|
|
22492
|
+
if (typeof error === "string") return error;
|
|
22493
|
+
if (error && typeof error.message === "string") return error.message;
|
|
22494
|
+
return "unknown error";
|
|
22495
|
+
}
|
|
22496
|
+
/**
|
|
22497
|
+
* The first step the run is suspended on. A run with several (parallel branches) is resumed one at
|
|
22498
|
+
* a time: after each resume it suspends again on the next.
|
|
22499
|
+
*/
|
|
22500
|
+
function firstSuspendedStep(workflow, view) {
|
|
22501
|
+
for (const [stepId, raw] of Object.entries(view.steps ?? {})) {
|
|
22502
|
+
const result = raw;
|
|
22503
|
+
if (result?.status !== "suspended") continue;
|
|
22504
|
+
const { __workflow_meta: meta, ...payload } = result.suspendPayload ?? {};
|
|
22505
|
+
const nested = meta?.path;
|
|
22506
|
+
const path = [stepId, ...Array.isArray(nested) ? nested.filter((part) => typeof part === "string") : []];
|
|
22507
|
+
return {
|
|
22508
|
+
path,
|
|
22509
|
+
step: stepAt(workflow, path),
|
|
22510
|
+
payload,
|
|
22511
|
+
suspendedAt: result.suspendedAt
|
|
22512
|
+
};
|
|
22513
|
+
}
|
|
22514
|
+
}
|
|
22515
|
+
/**
|
|
22516
|
+
* Whether `answered` (recorded when the task asked) is still the run's current suspension. Another
|
|
22517
|
+
* resume, from Studio or the API, moves the run on; a step that suspends again gets a new `suspendedAt`.
|
|
22518
|
+
*/
|
|
22519
|
+
function isStillSuspended(view, answered) {
|
|
22520
|
+
const result = view.steps?.[answered.step[0]];
|
|
22521
|
+
if (view.status !== "suspended" || result?.status !== "suspended") return false;
|
|
22522
|
+
return answered.suspendedAt === void 0 || result.suspendedAt === answered.suspendedAt;
|
|
22523
|
+
}
|
|
22524
|
+
/** Walks the workflow's step definitions along `path`, descending into nested workflows. */
|
|
22525
|
+
function stepAt(workflow, path) {
|
|
22526
|
+
let steps = workflow.steps;
|
|
22527
|
+
let step;
|
|
22528
|
+
for (const id of path) {
|
|
22529
|
+
step = steps?.[id];
|
|
22530
|
+
if (!step) return void 0;
|
|
22531
|
+
steps = step.steps;
|
|
22532
|
+
}
|
|
22533
|
+
return step;
|
|
22534
|
+
}
|
|
22535
|
+
// src/server/server.ts
|
|
22536
|
+
const EMPTY_OBJECT_SCHEMA = {
|
|
22537
|
+
type: "object",
|
|
22538
|
+
properties: {}
|
|
22539
|
+
};
|
|
22540
|
+
/**
|
|
22541
|
+
* Exposes Mastra tools, agents, workflows, resources and prompts to Model Context
|
|
22542
|
+
* Protocol (MCP) clients using protocol revision 2026-07-28: self-contained requests
|
|
22543
|
+
* over Streamable HTTP or stdio, native `input_required` continuation and
|
|
22544
|
+
* per-request logging.
|
|
22545
|
+
*
|
|
22546
|
+
* @example
|
|
22547
|
+
* `yourTool` is a tool you have already configured.
|
|
22548
|
+
* ```typescript
|
|
22549
|
+
* import { MCPServer } from '@mastra/mcp';
|
|
22550
|
+
*
|
|
22551
|
+
* const server = new MCPServer({
|
|
22552
|
+
* id: 'my-server',
|
|
22553
|
+
* name: 'My Server',
|
|
22554
|
+
* version: '1.0.0',
|
|
22555
|
+
* tools: { yourTool },
|
|
22556
|
+
* });
|
|
22557
|
+
* await server.startStdio();
|
|
22558
|
+
* ```
|
|
22559
|
+
*
|
|
22560
|
+
* @see For documentation bundled with your installed package, locate
|
|
22561
|
+
* `@mastra/mcp/package.json` with your project's resolver or package-manager
|
|
22562
|
+
* tooling, then read `dist/docs/SKILL.md` from that package root and follow its
|
|
22563
|
+
* reference links. Use package-manager tools for virtual or archived packages.
|
|
22564
|
+
*
|
|
22565
|
+
* @see [MCP server documentation](https://mastra.ai/reference/tools/mcp-server)
|
|
22566
|
+
* if packaged docs are unavailable.
|
|
22567
|
+
*/
|
|
22568
|
+
var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
22569
|
+
mcpVersion = 2;
|
|
22570
|
+
resources;
|
|
22571
|
+
prompts;
|
|
22572
|
+
toolActions;
|
|
22573
|
+
/**
|
|
22574
|
+
* Publishes occurrences of the configured `events` to poll, push and (when
|
|
22575
|
+
* `eventsOptions.webhook` is set) webhook subscribers.
|
|
22576
|
+
*
|
|
22577
|
+
* @experimental SEP-3415 is a draft; this API may change.
|
|
22578
|
+
*/
|
|
22579
|
+
events;
|
|
22580
|
+
resourceOptions;
|
|
22581
|
+
promptOptions;
|
|
22582
|
+
appResourceList = [];
|
|
22583
|
+
appResourceHtml = /* @__PURE__ */ new Map();
|
|
22584
|
+
jsonSchemaValidator;
|
|
22585
|
+
cacheHints;
|
|
22586
|
+
mapAuthInfoToUser;
|
|
22587
|
+
fga;
|
|
22588
|
+
requestStateCodec;
|
|
22589
|
+
eventRegistry;
|
|
20282
22590
|
eventsOptions;
|
|
20283
22591
|
eventsInspector;
|
|
22592
|
+
tasksInspector;
|
|
20284
22593
|
webhooks;
|
|
20285
22594
|
legacy;
|
|
22595
|
+
tasks;
|
|
22596
|
+
/** Where tasks are kept: `tasks.store`, or the store picked on first use. */
|
|
22597
|
+
taskStore;
|
|
22598
|
+
/** Cancels for the workflow runs executing in this process, by run id, while they execute. */
|
|
22599
|
+
executingRuns = /* @__PURE__ */ new Map();
|
|
22600
|
+
/** Prepared on Mastra registration so its executors exist before the manager recovers rows. */
|
|
22601
|
+
backgroundTaskStore;
|
|
20286
22602
|
httpHandler;
|
|
20287
22603
|
nodeHandler;
|
|
20288
22604
|
stdioHandle;
|
|
@@ -20299,6 +22615,13 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20299
22615
|
this.requestStateCodec = this.createRequestStateCodec(config.requestState);
|
|
20300
22616
|
this.eventsOptions = config.eventsOptions ?? {};
|
|
20301
22617
|
this.legacy = config.legacy ?? "reject";
|
|
22618
|
+
this.tasks = resolveTasksConfig(config.tasks, {
|
|
22619
|
+
tools: Object.keys(config.tools ?? {}),
|
|
22620
|
+
workflows: Object.keys(config.workflows ?? {}),
|
|
22621
|
+
agents: Object.keys(config.agents ?? {})
|
|
22622
|
+
});
|
|
22623
|
+
this.hasStableId = Boolean(config.id);
|
|
22624
|
+
this.taskStore = this.tasks.store ?? new SelectingMCPTaskStore(() => this.selectTaskStore());
|
|
20302
22625
|
const webhook = this.eventsOptions.webhook;
|
|
20303
22626
|
this.eventRegistry = new ServerEventRegistry(config.events, { webhook: webhook !== void 0 });
|
|
20304
22627
|
if (webhook && this.eventRegistry.size > 0) this.webhooks = new WebhookDispatcher({
|
|
@@ -20427,11 +22750,7 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20427
22750
|
* so a tool cannot suspend into a request no client can answer.
|
|
20428
22751
|
*/
|
|
20429
22752
|
assertFormRepresentable(owner, resumeSchema) {
|
|
20430
|
-
const
|
|
20431
|
-
const validation = _modelcontextprotocol_server.specTypeSchemas.ElicitRequestFormParams["~standard"].validate({
|
|
20432
|
-
message: owner,
|
|
20433
|
-
requestedSchema
|
|
20434
|
-
});
|
|
22753
|
+
const validation = formValidation(owner, this.formSchema(resumeSchema));
|
|
20435
22754
|
if (validation instanceof Promise || validation.issues) throw new _mastra_core_error.MastraError({
|
|
20436
22755
|
id: "MCP_SERVER_RESUME_SCHEMA_NOT_REPRESENTABLE",
|
|
20437
22756
|
domain: _mastra_core_error.ErrorDomain.MCP,
|
|
@@ -20443,6 +22762,15 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20443
22762
|
}
|
|
20444
22763
|
});
|
|
20445
22764
|
}
|
|
22765
|
+
/** The input-request form for `resumeSchema`, or `undefined` when the protocol can't present it. */
|
|
22766
|
+
representableForm(resumeSchema) {
|
|
22767
|
+
const requestedSchema = resumeSchema ? this.formSchema(resumeSchema) : {
|
|
22768
|
+
type: "object",
|
|
22769
|
+
properties: {}
|
|
22770
|
+
};
|
|
22771
|
+
const validation = formValidation("form", requestedSchema);
|
|
22772
|
+
return validation instanceof Promise || validation.issues ? void 0 : requestedSchema;
|
|
22773
|
+
}
|
|
20446
22774
|
/**
|
|
20447
22775
|
* Input-request forms are a restricted flat-object subset of JSON Schema, so
|
|
20448
22776
|
* the dialect declaration is left off the requested schema.
|
|
@@ -20572,6 +22900,10 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20572
22900
|
...capabilities.extensions,
|
|
20573
22901
|
[EVENTS_EXTENSION_ID]: this.eventsCapability()
|
|
20574
22902
|
};
|
|
22903
|
+
if (this.tasks.enabled) capabilities.extensions = {
|
|
22904
|
+
...capabilities.extensions,
|
|
22905
|
+
...tasksCapabilityEntry()
|
|
22906
|
+
};
|
|
20575
22907
|
return capabilities;
|
|
20576
22908
|
}
|
|
20577
22909
|
/**
|
|
@@ -20611,6 +22943,19 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20611
22943
|
jsonSchema: (schema, options) => this.jsonSchema(schema, options),
|
|
20612
22944
|
trace: (instance, method, params, ctx, fn) => this.traceCustomRequest(instance, method, params, ctx, fn)
|
|
20613
22945
|
});
|
|
22946
|
+
if (this.tasks.enabled) {
|
|
22947
|
+
registerTaskHandlers(server, {
|
|
22948
|
+
store: this.taskStore,
|
|
22949
|
+
authorize: this.tasks.authorize,
|
|
22950
|
+
principal: principalOf,
|
|
22951
|
+
trace: (instance, method, params, ctx, fn, tracing) => this.traceCustomRequest(instance, method, params, ctx, fn, tracing)
|
|
22952
|
+
});
|
|
22953
|
+
const connect = server.connect.bind(server);
|
|
22954
|
+
server.connect = async (transport) => {
|
|
22955
|
+
emitTaskResults(transport);
|
|
22956
|
+
return connect(transport);
|
|
22957
|
+
};
|
|
22958
|
+
}
|
|
20614
22959
|
return server;
|
|
20615
22960
|
}
|
|
20616
22961
|
/**
|
|
@@ -20618,8 +22963,10 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20618
22963
|
* extension): the request runs under an `MCP_SERVER_REQUEST` span with its resolved
|
|
20619
22964
|
* request context. A push stream's span lasts as long as the stream.
|
|
20620
22965
|
*/
|
|
20621
|
-
async traceCustomRequest(server, method, params, ctx, fn) {
|
|
22966
|
+
async traceCustomRequest(server, method, params, ctx, fn, tracing = {}) {
|
|
22967
|
+
if (tracing.untraced) return fn(await toRequestContext(ctx, this.mapAuthInfoToUser));
|
|
20622
22968
|
const caller = callerTrace(ctx);
|
|
22969
|
+
const { metadata } = tracing;
|
|
20623
22970
|
let requestContext;
|
|
20624
22971
|
try {
|
|
20625
22972
|
requestContext = await toRequestContext(ctx, this.mapAuthInfoToUser);
|
|
@@ -20627,15 +22974,19 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20627
22974
|
this.startRequestSpan(method, params, {
|
|
20628
22975
|
server,
|
|
20629
22976
|
ctx,
|
|
20630
|
-
caller
|
|
22977
|
+
caller,
|
|
22978
|
+
metadata
|
|
20631
22979
|
})?.error({ error });
|
|
20632
22980
|
throw error;
|
|
20633
22981
|
}
|
|
22982
|
+
const task = await tracing.parentOf?.(requestContext).catch(() => void 0);
|
|
20634
22983
|
const requestSpan = this.startRequestSpan(method, params, {
|
|
20635
22984
|
server,
|
|
20636
22985
|
ctx,
|
|
20637
22986
|
caller,
|
|
20638
|
-
requestContext
|
|
22987
|
+
requestContext,
|
|
22988
|
+
metadata,
|
|
22989
|
+
task
|
|
20639
22990
|
});
|
|
20640
22991
|
return this.traceRequest(requestSpan, () => fn(requestContext));
|
|
20641
22992
|
}
|
|
@@ -20707,6 +23058,11 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20707
23058
|
},
|
|
20708
23059
|
tracingContext: {},
|
|
20709
23060
|
links: caller && [caller.link],
|
|
23061
|
+
metadata: connection?.metadata,
|
|
23062
|
+
...connection?.task && {
|
|
23063
|
+
tracingOptions: { traceId: connection.task.traceId },
|
|
23064
|
+
resumedFromSpanId: connection.task.spanId
|
|
23065
|
+
},
|
|
20710
23066
|
requestContext: connection?.requestContext,
|
|
20711
23067
|
mastra: this.mastra
|
|
20712
23068
|
});
|
|
@@ -20719,6 +23075,22 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20719
23075
|
async traceRequest(requestSpan, fn, getReportedError) {
|
|
20720
23076
|
try {
|
|
20721
23077
|
const result = await fn();
|
|
23078
|
+
const task = taskOfHandleResult(result);
|
|
23079
|
+
if (task) {
|
|
23080
|
+
const mcpTaskKey = taskKeyOf(task.taskId);
|
|
23081
|
+
requestSpan?.end({
|
|
23082
|
+
output: { task: {
|
|
23083
|
+
status: task.status,
|
|
23084
|
+
mcpTaskKey
|
|
23085
|
+
} },
|
|
23086
|
+
metadata: {
|
|
23087
|
+
mcpTaskKey,
|
|
23088
|
+
mcpTaskStatus: task.status
|
|
23089
|
+
}
|
|
23090
|
+
});
|
|
23091
|
+
if (requestSpan) this.flushTaskRoot(requestSpan);
|
|
23092
|
+
return result;
|
|
23093
|
+
}
|
|
20722
23094
|
const errorResult = result;
|
|
20723
23095
|
if (errorResult?.isError) requestSpan?.error({ error: getReportedError?.() ?? new _mastra_core_error.MastraError({
|
|
20724
23096
|
id: "MCP_SERVER_REQUEST_FAILED",
|
|
@@ -20736,19 +23108,19 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20736
23108
|
throw error;
|
|
20737
23109
|
}
|
|
20738
23110
|
}
|
|
23111
|
+
/**
|
|
23112
|
+
* Writes a task's root span out now rather than with the exporter's next batch: the task's later
|
|
23113
|
+
* spans may be recorded by another process, and if this one dies first, a trace without its root
|
|
23114
|
+
* drops out of trace lists.
|
|
23115
|
+
*/
|
|
23116
|
+
flushTaskRoot(span) {
|
|
23117
|
+
span.observabilityInstance.flush().catch((error) => this.logger.debug("Could not flush a task trace root", { error }));
|
|
23118
|
+
}
|
|
20739
23119
|
/** Answers a suspended round: one keyed form derived from the handler's `resumeSchema`. */
|
|
20740
23120
|
async inputRequired(ctx, requestContext, suspension) {
|
|
20741
|
-
const payload = suspension.suspendPayload;
|
|
20742
|
-
const message = typeof payload?.message === "string" ? payload.message : typeof suspension.resumeSchema?.description === "string" ? suspension.resumeSchema.description : `"${suspension.name}" needs more input`;
|
|
20743
23121
|
const { method, name, argsHash, round, suspendPayload } = suspension;
|
|
20744
23122
|
return (0, _modelcontextprotocol_server.inputRequired)({
|
|
20745
|
-
inputRequests: { [INPUT_KEY]:
|
|
20746
|
-
message,
|
|
20747
|
-
requestedSchema: suspension.resumeSchema ?? {
|
|
20748
|
-
type: "object",
|
|
20749
|
-
properties: {}
|
|
20750
|
-
}
|
|
20751
|
-
}) },
|
|
23123
|
+
inputRequests: { [INPUT_KEY]: this.inputRequestForm(name, suspendPayload, suspension.resumeSchema) },
|
|
20752
23124
|
requestState: await this.requestStateCodec.mint({
|
|
20753
23125
|
method,
|
|
20754
23126
|
name,
|
|
@@ -20760,6 +23132,18 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20760
23132
|
})
|
|
20761
23133
|
});
|
|
20762
23134
|
}
|
|
23135
|
+
/** The form a suspended handler asks the client to fill in. */
|
|
23136
|
+
inputRequestForm(name, suspendPayload, resumeSchema) {
|
|
23137
|
+
const payload = suspendPayload;
|
|
23138
|
+
const message = typeof payload?.message === "string" ? payload.message : typeof resumeSchema?.description === "string" ? resumeSchema.description : `"${name}" needs more input`;
|
|
23139
|
+
return _modelcontextprotocol_server.inputRequired.elicit({
|
|
23140
|
+
message,
|
|
23141
|
+
requestedSchema: resumeSchema ?? {
|
|
23142
|
+
type: "object",
|
|
23143
|
+
properties: {}
|
|
23144
|
+
}
|
|
23145
|
+
});
|
|
23146
|
+
}
|
|
20763
23147
|
async serverRequest(ctx, requestContext, method, name, args, resumeSchema) {
|
|
20764
23148
|
const argsHash = hashArguments(args);
|
|
20765
23149
|
const continuation = readContinuation(ctx, requestContext, {
|
|
@@ -20810,13 +23194,35 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20810
23194
|
try {
|
|
20811
23195
|
await this.enforceToolExecutionFGA(name, requestContext);
|
|
20812
23196
|
if (continuation && continuation.outcome !== "accept") return errorResult$1(`Tool '${name}' was ${continuation.outcome === "decline" ? "declined" : "cancelled"}`);
|
|
20813
|
-
const
|
|
23197
|
+
const resume = continuation && {
|
|
23198
|
+
resumeData: continuation.resumeData,
|
|
23199
|
+
suspendPayload: continuation.suspendPayload
|
|
23200
|
+
};
|
|
23201
|
+
const taskMode = this.tasks.modeFor(name, tool.mcp?.toolType);
|
|
23202
|
+
const asTask = taskMode.mode !== "off" && clientDeclaredTasks(ctx);
|
|
23203
|
+
if (taskMode.mode !== "off" && !asTask && this.tasks.required.has(name)) throw new _modelcontextprotocol_server.MissingRequiredClientCapabilityError({ requiredCapabilities: { extensions: tasksCapabilityEntry() } }, `Tool '${name}' only runs as a task; declare the io.modelcontextprotocol/tasks extension`);
|
|
23204
|
+
if (asTask && tool.mcp?.toolType === "workflow") return await this.startWorkflowTask(ctx, requestContext, name, args, taskMode, { currentSpan: trace.requestSpan });
|
|
23205
|
+
if (asTask && taskMode.mode === "always") try {
|
|
23206
|
+
return await this.startToolTask(ctx, requestContext, name, args, taskMode, {
|
|
23207
|
+
resume,
|
|
23208
|
+
rootSpan: trace.requestSpan
|
|
23209
|
+
});
|
|
23210
|
+
} catch (error) {
|
|
23211
|
+
if (error instanceof MCPTaskLimitError) return errorResult$1(taskLimitMessage(error));
|
|
23212
|
+
throw error;
|
|
23213
|
+
}
|
|
23214
|
+
const options = {
|
|
20814
23215
|
requestContext,
|
|
20815
23216
|
mcp,
|
|
20816
23217
|
tracingContext: { currentSpan: trace.requestSpan },
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
23218
|
+
...resume
|
|
23219
|
+
};
|
|
23220
|
+
let execution;
|
|
23221
|
+
if (asTask) {
|
|
23222
|
+
const raced = await this.runWithinSyncWindow(ctx, requestContext, name, tool, args, taskMode, options);
|
|
23223
|
+
if ("task" in raced) return raced.task;
|
|
23224
|
+
execution = raced.execution;
|
|
23225
|
+
} else execution = await this.runTool(name, tool, args, options);
|
|
20820
23226
|
if (execution.status === "suspended") {
|
|
20821
23227
|
this.logger.debug(`Tool '${name}' requires client input.`);
|
|
20822
23228
|
return this.inputRequired(ctx, requestContext, {
|
|
@@ -20833,21 +23239,549 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20833
23239
|
return result;
|
|
20834
23240
|
} catch (error) {
|
|
20835
23241
|
if (error instanceof _modelcontextprotocol_server.ProtocolError) throw error;
|
|
20836
|
-
this.
|
|
20837
|
-
tool: name,
|
|
20838
|
-
error
|
|
20839
|
-
});
|
|
20840
|
-
const mastraError = error instanceof _mastra_core_error.MastraError ? error : new _mastra_core_error.MastraError({
|
|
20841
|
-
id: "TOOL_EXECUTION_FAILED",
|
|
20842
|
-
domain: _mastra_core_error.ErrorDomain.TOOL,
|
|
20843
|
-
category: _mastra_core_error.ErrorCategory.USER,
|
|
20844
|
-
details: { toolName: name }
|
|
20845
|
-
}, error);
|
|
23242
|
+
const mastraError = this.toolError(name, error);
|
|
20846
23243
|
trace.reportError(mastraError);
|
|
20847
23244
|
return errorResult$1(JSON.stringify(mastraError.toJSON()));
|
|
20848
23245
|
}
|
|
20849
23246
|
});
|
|
20850
23247
|
}
|
|
23248
|
+
/** Logs a tool's failure and wraps it as the error a client receives. */
|
|
23249
|
+
toolError(name, error) {
|
|
23250
|
+
this.logger.error("Tool execution failed", {
|
|
23251
|
+
tool: name,
|
|
23252
|
+
error
|
|
23253
|
+
});
|
|
23254
|
+
return error instanceof _mastra_core_error.MastraError ? error : new _mastra_core_error.MastraError({
|
|
23255
|
+
id: "TOOL_EXECUTION_FAILED",
|
|
23256
|
+
domain: _mastra_core_error.ErrorDomain.TOOL,
|
|
23257
|
+
category: _mastra_core_error.ErrorCategory.USER,
|
|
23258
|
+
details: { toolName: name }
|
|
23259
|
+
}, error);
|
|
23260
|
+
}
|
|
23261
|
+
/**
|
|
23262
|
+
* `'auto'` mode: runs the tool under the request for up to `syncWindowMs`. A tool that settles in
|
|
23263
|
+
* time is answered like any synchronous call; otherwise the same execution carries on as a task,
|
|
23264
|
+
* no longer tied to the request's cancellation.
|
|
23265
|
+
*/
|
|
23266
|
+
async runWithinSyncWindow(ctx, requestContext, name, tool, args, taskMode, options) {
|
|
23267
|
+
const controller = new AbortController();
|
|
23268
|
+
const requestSignal = ctx.mcpReq.signal;
|
|
23269
|
+
const followRequest = () => controller.abort(requestSignal.reason);
|
|
23270
|
+
if (requestSignal.aborted) followRequest();
|
|
23271
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23272
|
+
const settled = this.runTool(name, tool, args, {
|
|
23273
|
+
...options,
|
|
23274
|
+
abortSignal: controller.signal,
|
|
23275
|
+
mcp: {
|
|
23276
|
+
...options.mcp,
|
|
23277
|
+
extra: {
|
|
23278
|
+
...options.mcp.extra,
|
|
23279
|
+
signal: controller.signal
|
|
23280
|
+
},
|
|
23281
|
+
log: (...params) => options.mcp.log(...params).catch(() => {}),
|
|
23282
|
+
progress: (params) => options.mcp.progress(params).catch(() => {})
|
|
23283
|
+
}
|
|
23284
|
+
}).then((execution) => ({ execution }), (error) => ({ error }));
|
|
23285
|
+
let timer;
|
|
23286
|
+
const deadline = new Promise((resolve) => {
|
|
23287
|
+
timer = setTimeout(() => resolve("timeout"), taskMode.syncWindowMs);
|
|
23288
|
+
});
|
|
23289
|
+
const winner = await Promise.race([settled, deadline]);
|
|
23290
|
+
clearTimeout(timer);
|
|
23291
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23292
|
+
if (winner !== "timeout") {
|
|
23293
|
+
if ("error" in winner) throw winner.error;
|
|
23294
|
+
return winner;
|
|
23295
|
+
}
|
|
23296
|
+
try {
|
|
23297
|
+
return { task: await this.startToolTask(ctx, requestContext, name, args, taskMode, {
|
|
23298
|
+
rootSpan: options.tracingContext?.currentSpan,
|
|
23299
|
+
resume: options.resumeData === void 0 ? void 0 : {
|
|
23300
|
+
resumeData: options.resumeData,
|
|
23301
|
+
suspendPayload: options.suspendPayload
|
|
23302
|
+
},
|
|
23303
|
+
initialAttempt: () => ({
|
|
23304
|
+
outcome: settled.then((outcome) => "error" in outcome ? this.toolErrorOutcome(name, outcome.error) : this.toTaskOutcome(name, tool, outcome.execution)),
|
|
23305
|
+
abort: (reason) => controller.abort(reason)
|
|
23306
|
+
})
|
|
23307
|
+
}) };
|
|
23308
|
+
} catch (error) {
|
|
23309
|
+
if (!(error instanceof MCPTaskLimitError)) {
|
|
23310
|
+
controller.abort(error);
|
|
23311
|
+
throw error;
|
|
23312
|
+
}
|
|
23313
|
+
}
|
|
23314
|
+
this.logger.debug(`Tool '${name}' outlived its sync window at a task cap; answering synchronously.`);
|
|
23315
|
+
if (requestSignal.aborted) followRequest();
|
|
23316
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23317
|
+
const outcome = await settled;
|
|
23318
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23319
|
+
if ("error" in outcome) throw outcome.error;
|
|
23320
|
+
return outcome;
|
|
23321
|
+
}
|
|
23322
|
+
/**
|
|
23323
|
+
* Creates the task (durably, before answering) and hands its work to the store. `initialAttempt`
|
|
23324
|
+
* builds the run of an execution that already started under the request; it stays in this
|
|
23325
|
+
* process. Rejects with {@link MCPTaskLimitError}, creating nothing, at a task cap.
|
|
23326
|
+
*/
|
|
23327
|
+
async startToolTask(ctx, requestContext, name, args, taskMode, { resume, initialAttempt, rootSpan }) {
|
|
23328
|
+
const context = {
|
|
23329
|
+
toolName: name,
|
|
23330
|
+
args,
|
|
23331
|
+
requestContext: this.persistTaskRequestContext(name, requestContext),
|
|
23332
|
+
...resume && { resume }
|
|
23333
|
+
};
|
|
23334
|
+
const params = {
|
|
23335
|
+
ttlMs: taskMode.ttlMs,
|
|
23336
|
+
principal: principalOf(ctx, requestContext),
|
|
23337
|
+
context,
|
|
23338
|
+
limits: this.tasks.limits,
|
|
23339
|
+
...rootSpan && { trace: {
|
|
23340
|
+
traceId: rootSpan.traceId,
|
|
23341
|
+
spanId: rootSpan.id
|
|
23342
|
+
} }
|
|
23343
|
+
};
|
|
23344
|
+
const { store, task } = await this.createTask(ctx, requestContext, params, initialAttempt !== void 0);
|
|
23345
|
+
store.run(task.taskId, this.toolTaskWork(context), { initialAttempt: initialAttempt && this.tracedInitialAttempt(name, initialAttempt(), params.trace, task.taskId, requestContext) });
|
|
23346
|
+
this.logger.debug(`Tool '${name}' continues as task.`);
|
|
23347
|
+
return taskHandleResult(task);
|
|
23348
|
+
}
|
|
23349
|
+
/** The request context a task keeps for its later attempts. */
|
|
23350
|
+
persistTaskRequestContext(name, requestContext) {
|
|
23351
|
+
const { persisted, dropped } = persistableRequestContext(requestContext);
|
|
23352
|
+
if (dropped.length > 0) this.logger.warn(`Tool task '${name}' can't persist request context keys; runs from the task won't see them`, {
|
|
23353
|
+
tool: name,
|
|
23354
|
+
keys: dropped
|
|
23355
|
+
});
|
|
23356
|
+
return persisted;
|
|
23357
|
+
}
|
|
23358
|
+
/** Creates a task durably. `pinned` keeps its first attempt on this worker, which already runs it. */
|
|
23359
|
+
async createTask(ctx, requestContext, params, pinned) {
|
|
23360
|
+
const store = this.taskStore instanceof SelectingMCPTaskStore ? await this.taskStore.resolve() : this.taskStore;
|
|
23361
|
+
return {
|
|
23362
|
+
store,
|
|
23363
|
+
task: store instanceof BackgroundTaskMCPTaskStore ? await store.create(params, {
|
|
23364
|
+
pinToWorker: pinned,
|
|
23365
|
+
clientLabel: clientLabelOf(ctx, requestContext)
|
|
23366
|
+
}) : store instanceof InMemoryMCPTaskStore ? await store.create(params, { clientLabel: clientLabelOf(ctx, requestContext) }) : await store.create(params)
|
|
23367
|
+
};
|
|
23368
|
+
}
|
|
23369
|
+
/**
|
|
23370
|
+
* The work of a tool task, rebuilt from nothing but what the task's `context` records, so a store
|
|
23371
|
+
* can dispatch it again after a restart. Each attempt runs the tool once; an answered suspension
|
|
23372
|
+
* runs it again with `resumeData`, as everywhere else in Mastra.
|
|
23373
|
+
*/
|
|
23374
|
+
toolTaskWork(context) {
|
|
23375
|
+
const name = context.toolName;
|
|
23376
|
+
return async (attempt) => {
|
|
23377
|
+
const tool = this.convertedTools[name];
|
|
23378
|
+
if (!tool) throw new Error(`Tool '${name}' is no longer served`);
|
|
23379
|
+
const requestContext = new _mastra_core_request_context.RequestContext(Object.entries(context.requestContext));
|
|
23380
|
+
try {
|
|
23381
|
+
await this.enforceToolExecutionFGA(name, requestContext);
|
|
23382
|
+
} catch (error) {
|
|
23383
|
+
return this.toolErrorOutcome(name, error);
|
|
23384
|
+
}
|
|
23385
|
+
let { resumeData, suspendPayload } = context.resume ?? {};
|
|
23386
|
+
if (attempt.resume) {
|
|
23387
|
+
const [key] = Object.keys(attempt.resume.inputResponses);
|
|
23388
|
+
const answer = (0, _modelcontextprotocol_server.inputResponse)(attempt.resume.inputResponses, key);
|
|
23389
|
+
if (answer.kind !== "elicit") return completed(errorResult$1(`Unsupported input response for tool '${name}'`));
|
|
23390
|
+
if (answer.action !== "accept") return completed(errorResult$1(`Tool '${name}' was ${answer.action === "decline" ? "declined" : "cancelled"}`));
|
|
23391
|
+
resumeData = answer.content ?? {};
|
|
23392
|
+
suspendPayload = attempt.resume.suspendPayload;
|
|
23393
|
+
}
|
|
23394
|
+
const span = attempt.trace && this.openTaskSpan(`task attempt ${name} #${attempt.trace.attempt}`, attempt.trace, {
|
|
23395
|
+
requestContext,
|
|
23396
|
+
input: context.args,
|
|
23397
|
+
mcpMethod: "tools/call",
|
|
23398
|
+
targetName: name,
|
|
23399
|
+
metadata: {
|
|
23400
|
+
mcpTaskKey: attempt.trace.taskKey,
|
|
23401
|
+
attempt: attempt.trace.attempt,
|
|
23402
|
+
resumed: attempt.trace.resumed,
|
|
23403
|
+
recovered: attempt.trace.recovered
|
|
23404
|
+
}
|
|
23405
|
+
});
|
|
23406
|
+
return this.endAttemptSpan(span, attempt.signal, (async () => {
|
|
23407
|
+
try {
|
|
23408
|
+
const execution = await this.runTool(name, tool, context.args, {
|
|
23409
|
+
requestContext,
|
|
23410
|
+
abortSignal: attempt.signal,
|
|
23411
|
+
resumeData,
|
|
23412
|
+
suspendPayload,
|
|
23413
|
+
tracingContext: span ? { currentSpan: span } : void 0
|
|
23414
|
+
});
|
|
23415
|
+
return this.toTaskOutcome(name, tool, execution);
|
|
23416
|
+
} catch (error) {
|
|
23417
|
+
return this.toolErrorOutcome(name, error);
|
|
23418
|
+
}
|
|
23419
|
+
})());
|
|
23420
|
+
};
|
|
23421
|
+
}
|
|
23422
|
+
/**
|
|
23423
|
+
* Opens a span in a task's trace, as a child of the task's root (the `tools/call` that created
|
|
23424
|
+
* it), from any process. Uses the resume parentage workflows and agents use: the root id becomes
|
|
23425
|
+
* the span's Mastra parent, so storage keeps one tree. `tracingOptions.parentSpanId` would not:
|
|
23426
|
+
* it only correlates with an external system, and storage would record another root.
|
|
23427
|
+
*/
|
|
23428
|
+
openTaskSpan(name, trace, options) {
|
|
23429
|
+
return (0, _mastra_core_observability.getOrCreateSpan)({
|
|
23430
|
+
type: _mastra_core_observability.SpanType.MCP_SERVER_REQUEST,
|
|
23431
|
+
name,
|
|
23432
|
+
entityType: _mastra_core_observability.EntityType.MCP_SERVER,
|
|
23433
|
+
entityId: this.id,
|
|
23434
|
+
entityName: this.name,
|
|
23435
|
+
input: options.input,
|
|
23436
|
+
attributes: {
|
|
23437
|
+
mcpMethod: options.mcpMethod,
|
|
23438
|
+
...options.targetName !== void 0 && { targetName: options.targetName },
|
|
23439
|
+
mcpServer: this.name,
|
|
23440
|
+
serverVersion: this.version
|
|
23441
|
+
},
|
|
23442
|
+
metadata: options.metadata,
|
|
23443
|
+
tracingOptions: { traceId: trace.traceId },
|
|
23444
|
+
resumedFromSpanId: trace.spanId,
|
|
23445
|
+
requestContext: options.requestContext,
|
|
23446
|
+
mastra: this.mastra
|
|
23447
|
+
});
|
|
23448
|
+
}
|
|
23449
|
+
/** Ends an attempt's span with how the attempt ended, and passes the outcome on. */
|
|
23450
|
+
async endAttemptSpan(span, signal, outcome) {
|
|
23451
|
+
if (!span) return outcome;
|
|
23452
|
+
try {
|
|
23453
|
+
const result = await outcome;
|
|
23454
|
+
if (signal?.aborted) span.end({ metadata: { mcpTaskStatus: "cancelled" } });
|
|
23455
|
+
else if (result.status === "input_required") span.end({ metadata: { mcpTaskStatus: "input_required" } });
|
|
23456
|
+
else if (result.result.isError) span.error({
|
|
23457
|
+
error: new _mastra_core_error.MastraError({
|
|
23458
|
+
id: "MCP_SERVER_TASK_ATTEMPT_FAILED",
|
|
23459
|
+
domain: _mastra_core_error.ErrorDomain.MCP,
|
|
23460
|
+
category: _mastra_core_error.ErrorCategory.USER,
|
|
23461
|
+
text: result.result.content.map((c) => c.type === "text" ? c.text : "").filter(Boolean).join("\n")
|
|
23462
|
+
}),
|
|
23463
|
+
metadata: { mcpTaskStatus: "completed" }
|
|
23464
|
+
});
|
|
23465
|
+
else span.end({
|
|
23466
|
+
output: result.result,
|
|
23467
|
+
metadata: { mcpTaskStatus: "completed" }
|
|
23468
|
+
});
|
|
23469
|
+
return result;
|
|
23470
|
+
} catch (error) {
|
|
23471
|
+
if (signal?.aborted) span.end({ metadata: { mcpTaskStatus: "cancelled" } });
|
|
23472
|
+
else span.error({
|
|
23473
|
+
error,
|
|
23474
|
+
endTree: true,
|
|
23475
|
+
metadata: { mcpTaskStatus: "failed" }
|
|
23476
|
+
});
|
|
23477
|
+
throw error;
|
|
23478
|
+
}
|
|
23479
|
+
}
|
|
23480
|
+
/**
|
|
23481
|
+
* A first run already under way (an `'auto'` tool's or workflow's, under the request, or a
|
|
23482
|
+
* workflow run started under `opened`) as attempt 1 of its task: the attempt span ends with that run.
|
|
23483
|
+
*/
|
|
23484
|
+
tracedInitialAttempt(name, initialAttempt, trace, taskId, requestContext, opened) {
|
|
23485
|
+
if (!trace) return initialAttempt;
|
|
23486
|
+
const span = opened ?? this.openTaskSpan(`task attempt ${name} #1`, trace, {
|
|
23487
|
+
requestContext,
|
|
23488
|
+
...firstAttemptSpan(name, taskId)
|
|
23489
|
+
});
|
|
23490
|
+
const aborted = new AbortController();
|
|
23491
|
+
return {
|
|
23492
|
+
outcome: this.endAttemptSpan(span, aborted.signal, initialAttempt.outcome),
|
|
23493
|
+
abort: (reason) => {
|
|
23494
|
+
aborted.abort(reason);
|
|
23495
|
+
initialAttempt.abort(reason);
|
|
23496
|
+
}
|
|
23497
|
+
};
|
|
23498
|
+
}
|
|
23499
|
+
toTaskOutcome(name, tool, execution) {
|
|
23500
|
+
if (execution.status === "suspended") {
|
|
23501
|
+
this.logger.debug(`Tool task '${name}' requires client input.`);
|
|
23502
|
+
return {
|
|
23503
|
+
status: "input_required",
|
|
23504
|
+
inputRequests: { [`${INPUT_KEY}-${(0, crypto$1.randomUUID)()}`]: this.inputRequestForm(name, execution.suspendPayload, execution.resumeSchema) },
|
|
23505
|
+
suspendPayload: execution.suspendPayload
|
|
23506
|
+
};
|
|
23507
|
+
}
|
|
23508
|
+
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`));
|
|
23509
|
+
return completed(this.toCallToolResult(name, tool, execution.output));
|
|
23510
|
+
}
|
|
23511
|
+
/** A tool that throws inside a task completes it with `isError`, as a synchronous call would answer. */
|
|
23512
|
+
toolErrorOutcome(name, error) {
|
|
23513
|
+
return completed(errorResult$1(JSON.stringify(this.toolError(name, error).toJSON())));
|
|
23514
|
+
}
|
|
23515
|
+
/**
|
|
23516
|
+
* Starts the run behind a `run_<key>` task. `'auto'` answers synchronously when the run finishes
|
|
23517
|
+
* within `syncWindowMs`; a run that suspends or outlasts the window continues as a task.
|
|
23518
|
+
*/
|
|
23519
|
+
async startWorkflowTask(ctx, requestContext, name, args, taskMode, tracingContext) {
|
|
23520
|
+
const workflowKey = name.slice(4);
|
|
23521
|
+
const workflow = this.workflowFor(workflowKey);
|
|
23522
|
+
if (!workflow) throw new Error(`Workflow '${workflowKey}' is not served`);
|
|
23523
|
+
const input = (0, _mastra_core_tools.validateToolInput)(workflow.inputSchema, args, name);
|
|
23524
|
+
if (input.error) {
|
|
23525
|
+
this.logger.warn(`Tool '${name}' rejected its input.`, { error: input.error.message });
|
|
23526
|
+
return errorResult$1(input.error.message);
|
|
23527
|
+
}
|
|
23528
|
+
const context = {
|
|
23529
|
+
toolName: name,
|
|
23530
|
+
kind: "workflow",
|
|
23531
|
+
workflowKey,
|
|
23532
|
+
runId: (0, crypto$1.randomUUID)(),
|
|
23533
|
+
requestContext: this.persistTaskRequestContext(name, requestContext)
|
|
23534
|
+
};
|
|
23535
|
+
const rootSpan = tracingContext.currentSpan;
|
|
23536
|
+
const params = {
|
|
23537
|
+
ttlMs: taskMode.ttlMs,
|
|
23538
|
+
principal: principalOf(ctx, requestContext),
|
|
23539
|
+
context,
|
|
23540
|
+
limits: this.tasks.limits,
|
|
23541
|
+
...rootSpan && { trace: {
|
|
23542
|
+
traceId: rootSpan.traceId,
|
|
23543
|
+
spanId: rootSpan.id
|
|
23544
|
+
} }
|
|
23545
|
+
};
|
|
23546
|
+
const start = (runTracing) => {
|
|
23547
|
+
const run = workflow.createRun({ runId: context.runId });
|
|
23548
|
+
const result = run.then((started) => started.start({
|
|
23549
|
+
inputData: input.data,
|
|
23550
|
+
requestContext,
|
|
23551
|
+
tracingContext: runTracing
|
|
23552
|
+
}));
|
|
23553
|
+
const cancel = () => void run.then((started) => started.cancel()).catch((error) => this.logger.warn("Could not cancel workflow run", {
|
|
23554
|
+
tool: name,
|
|
23555
|
+
error
|
|
23556
|
+
}));
|
|
23557
|
+
this.executingRuns.set(context.runId, cancel);
|
|
23558
|
+
const done = () => this.executingRuns.delete(context.runId);
|
|
23559
|
+
result.then(done, done);
|
|
23560
|
+
return {
|
|
23561
|
+
result,
|
|
23562
|
+
cancel
|
|
23563
|
+
};
|
|
23564
|
+
};
|
|
23565
|
+
if (taskMode.mode === "always") {
|
|
23566
|
+
let created;
|
|
23567
|
+
try {
|
|
23568
|
+
created = await this.createTask(ctx, requestContext, params, true);
|
|
23569
|
+
} catch (error) {
|
|
23570
|
+
if (error instanceof MCPTaskLimitError) return errorResult$1(taskLimitMessage(error));
|
|
23571
|
+
throw error;
|
|
23572
|
+
}
|
|
23573
|
+
const { store, task } = created;
|
|
23574
|
+
const span = params.trace && this.openTaskSpan(`task attempt ${name} #1`, params.trace, firstAttemptSpan(name, task.taskId));
|
|
23575
|
+
const live = start(span ? { currentSpan: span } : tracingContext);
|
|
23576
|
+
store.run(task.taskId, this.workflowTaskWork(context), { initialAttempt: this.tracedInitialAttempt(name, this.liveWorkflowAttempt(workflow, context, live.result), params.trace, task.taskId, requestContext, span) });
|
|
23577
|
+
this.logger.debug(`Workflow tool '${name}' runs as task.`);
|
|
23578
|
+
return taskHandleResult(task);
|
|
23579
|
+
}
|
|
23580
|
+
const live = start(tracingContext);
|
|
23581
|
+
const requestSignal = ctx.mcpReq.signal;
|
|
23582
|
+
const followRequest = () => live.cancel();
|
|
23583
|
+
if (requestSignal.aborted) followRequest();
|
|
23584
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23585
|
+
const settled = live.result.then((view) => ({ view }), (error) => ({ error }));
|
|
23586
|
+
let timer;
|
|
23587
|
+
const deadline = new Promise((resolve) => {
|
|
23588
|
+
timer = setTimeout(() => resolve("timeout"), taskMode.syncWindowMs);
|
|
23589
|
+
});
|
|
23590
|
+
const winner = await Promise.race([settled, deadline]);
|
|
23591
|
+
clearTimeout(timer);
|
|
23592
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23593
|
+
if (winner !== "timeout") {
|
|
23594
|
+
if ("error" in winner) throw winner.error;
|
|
23595
|
+
const outcome = this.workflowRunOutcome(workflow, context, winner.view);
|
|
23596
|
+
if (outcome?.status === "completed") return outcome.result;
|
|
23597
|
+
}
|
|
23598
|
+
try {
|
|
23599
|
+
const { store, task } = await this.createTask(ctx, requestContext, params, true);
|
|
23600
|
+
store.run(task.taskId, this.workflowTaskWork(context), { initialAttempt: this.tracedInitialAttempt(name, this.liveWorkflowAttempt(workflow, context, live.result), params.trace, task.taskId, requestContext) });
|
|
23601
|
+
this.logger.debug(`Workflow tool '${name}' continues as task.`);
|
|
23602
|
+
return taskHandleResult(task);
|
|
23603
|
+
} catch (error) {
|
|
23604
|
+
if (!(error instanceof MCPTaskLimitError)) {
|
|
23605
|
+
live.cancel();
|
|
23606
|
+
throw error;
|
|
23607
|
+
}
|
|
23608
|
+
this.logger.debug(`Workflow tool '${name}' outlived its sync window at the open task cap.`);
|
|
23609
|
+
if (requestSignal.aborted) followRequest();
|
|
23610
|
+
else requestSignal.addEventListener("abort", followRequest, { once: true });
|
|
23611
|
+
const late = await settled;
|
|
23612
|
+
requestSignal.removeEventListener("abort", followRequest);
|
|
23613
|
+
if ("error" in late) throw late.error;
|
|
23614
|
+
const outcome = this.workflowRunOutcome(workflow, context, late.view);
|
|
23615
|
+
if (outcome?.status === "completed") return outcome.result;
|
|
23616
|
+
live.cancel();
|
|
23617
|
+
return errorResult$1(taskLimitMessage(error));
|
|
23618
|
+
}
|
|
23619
|
+
}
|
|
23620
|
+
/** The first attempt of a workflow task: the run this request started, in this process. */
|
|
23621
|
+
liveWorkflowAttempt(workflow, context, result) {
|
|
23622
|
+
const controller = new AbortController();
|
|
23623
|
+
return {
|
|
23624
|
+
outcome: result.then((view) => this.workflowRunOutcome(workflow, context, view) ?? this.watchWorkflowRun(workflow, context, controller.signal), (error) => this.toolErrorOutcome(context.toolName, error)),
|
|
23625
|
+
abort: (reason) => controller.abort(reason)
|
|
23626
|
+
};
|
|
23627
|
+
}
|
|
23628
|
+
/**
|
|
23629
|
+
* The work of a workflow task, rebuilt from its context. It never starts the run: a first attempt
|
|
23630
|
+
* dispatched again (after a restart) re-attaches to it, and an answered suspension resumes it.
|
|
23631
|
+
*/
|
|
23632
|
+
workflowTaskWork(context) {
|
|
23633
|
+
const name = context.toolName;
|
|
23634
|
+
return async (attempt) => {
|
|
23635
|
+
const workflow = this.workflowFor(context.workflowKey);
|
|
23636
|
+
if (!workflow) throw new Error(`Workflow '${context.workflowKey}' is no longer served`);
|
|
23637
|
+
const requestContext = new _mastra_core_request_context.RequestContext(Object.entries(context.requestContext));
|
|
23638
|
+
try {
|
|
23639
|
+
await this.enforceToolExecutionFGA(name, requestContext);
|
|
23640
|
+
} catch (error) {
|
|
23641
|
+
return this.toolErrorOutcome(name, error);
|
|
23642
|
+
}
|
|
23643
|
+
const span = attempt.trace && this.openTaskSpan(`task attempt ${name} #${attempt.trace.attempt}`, attempt.trace, {
|
|
23644
|
+
requestContext,
|
|
23645
|
+
mcpMethod: "tools/call",
|
|
23646
|
+
targetName: name,
|
|
23647
|
+
metadata: {
|
|
23648
|
+
mcpTaskKey: attempt.trace.taskKey,
|
|
23649
|
+
attempt: attempt.trace.attempt,
|
|
23650
|
+
resumed: attempt.trace.resumed,
|
|
23651
|
+
recovered: attempt.trace.recovered
|
|
23652
|
+
}
|
|
23653
|
+
});
|
|
23654
|
+
return this.endAttemptSpan(span, attempt.signal, attempt.resume ? this.resumeWorkflowRun(workflow, context, attempt, requestContext, span) : this.watchWorkflowRun(workflow, context, attempt.signal));
|
|
23655
|
+
};
|
|
23656
|
+
}
|
|
23657
|
+
async resumeWorkflowRun(workflow, context, attempt, requestContext, span) {
|
|
23658
|
+
const { inputResponses, suspendPayload } = attempt.resume;
|
|
23659
|
+
const [key] = Object.keys(inputResponses);
|
|
23660
|
+
const answer = (0, _modelcontextprotocol_server.inputResponse)(inputResponses, key);
|
|
23661
|
+
const label = `Workflow '${context.workflowKey}'`;
|
|
23662
|
+
if (answer.kind !== "elicit") return completed(errorResult$1(`Unsupported input response for ${label}`));
|
|
23663
|
+
if (answer.action !== "accept") {
|
|
23664
|
+
await this.cancelWorkflowRun(context);
|
|
23665
|
+
return completed(errorResult$1(`${label} was ${answer.action === "decline" ? "declined" : "cancelled"}`));
|
|
23666
|
+
}
|
|
23667
|
+
const current = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23668
|
+
if (!current) throw new Error(`${label} run '${context.runId}' was lost`);
|
|
23669
|
+
const answered = suspendPayload;
|
|
23670
|
+
if (!isStillSuspended({
|
|
23671
|
+
status: current.status,
|
|
23672
|
+
steps: current.steps
|
|
23673
|
+
}, answered)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23674
|
+
const run = await workflow.createRun({ runId: context.runId });
|
|
23675
|
+
let view;
|
|
23676
|
+
this.executingRuns.set(context.runId, () => void run.cancel().catch((error) => this.logger.warn("Could not cancel workflow run", {
|
|
23677
|
+
tool: context.toolName,
|
|
23678
|
+
error
|
|
23679
|
+
})));
|
|
23680
|
+
try {
|
|
23681
|
+
view = await run.resume({
|
|
23682
|
+
step: answered.step,
|
|
23683
|
+
resumeData: answer.content ?? {},
|
|
23684
|
+
requestContext,
|
|
23685
|
+
...span && { tracingContext: { currentSpan: span } }
|
|
23686
|
+
});
|
|
23687
|
+
} catch (error) {
|
|
23688
|
+
if (isResumeClaimLost(error)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23689
|
+
const now = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23690
|
+
if (now && !isStillSuspended({
|
|
23691
|
+
status: now.status,
|
|
23692
|
+
steps: now.steps
|
|
23693
|
+
}, answered)) return this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23694
|
+
return this.toolErrorOutcome(context.toolName, error);
|
|
23695
|
+
} finally {
|
|
23696
|
+
this.executingRuns.delete(context.runId);
|
|
23697
|
+
}
|
|
23698
|
+
return this.workflowRunOutcome(workflow, context, view) ?? this.watchWorkflowRun(workflow, context, attempt.signal);
|
|
23699
|
+
}
|
|
23700
|
+
/**
|
|
23701
|
+
* Polls the run until it finishes or suspends on a step the client can answer. The only way to
|
|
23702
|
+
* follow a run from any instance, whichever engine runs it.
|
|
23703
|
+
*/
|
|
23704
|
+
async watchWorkflowRun(workflow, context, signal) {
|
|
23705
|
+
let first = true;
|
|
23706
|
+
for (;;) {
|
|
23707
|
+
signal.throwIfAborted();
|
|
23708
|
+
const state = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23709
|
+
if (!state || first && state.status === "pending") throw new Error(`Workflow '${context.workflowKey}' run '${context.runId}' was lost`);
|
|
23710
|
+
first = false;
|
|
23711
|
+
const outcome = this.workflowRunOutcome(workflow, context, {
|
|
23712
|
+
status: state.status,
|
|
23713
|
+
input: state.payload,
|
|
23714
|
+
steps: state.steps,
|
|
23715
|
+
result: state.result,
|
|
23716
|
+
error: state.error
|
|
23717
|
+
});
|
|
23718
|
+
if (outcome) return outcome;
|
|
23719
|
+
await sleep(WORKFLOW_POLL_INTERVAL_MS, signal);
|
|
23720
|
+
}
|
|
23721
|
+
}
|
|
23722
|
+
/**
|
|
23723
|
+
* What a run's state means for its task, or `undefined` while there is nothing to report: the
|
|
23724
|
+
* run is still going, or waits on a step whose `resumeSchema` no input request can present (the
|
|
23725
|
+
* task stays `working` until the run is resumed some other way).
|
|
23726
|
+
*/
|
|
23727
|
+
workflowRunOutcome(workflow, context, view) {
|
|
23728
|
+
const label = `Workflow '${context.workflowKey}'`;
|
|
23729
|
+
switch (view.status) {
|
|
23730
|
+
case "success":
|
|
23731
|
+
case "bailed": return completed({
|
|
23732
|
+
isError: false,
|
|
23733
|
+
content: [{
|
|
23734
|
+
type: "text",
|
|
23735
|
+
text: JSON.stringify(runSummary(view))
|
|
23736
|
+
}]
|
|
23737
|
+
});
|
|
23738
|
+
case "failed": return completed(errorResult$1(`${label} failed: ${runErrorMessage(view)}`));
|
|
23739
|
+
case "tripwire": return completed(errorResult$1(`${label} was blocked: ${runErrorMessage(view)}`));
|
|
23740
|
+
case "canceled": return completed(errorResult$1(`${label} run was cancelled`));
|
|
23741
|
+
case "suspended": {
|
|
23742
|
+
const suspended = firstSuspendedStep(workflow, view);
|
|
23743
|
+
if (!suspended) return void 0;
|
|
23744
|
+
const requestedSchema = this.representableForm(suspended.step?.resumeSchema);
|
|
23745
|
+
if (!requestedSchema) return void 0;
|
|
23746
|
+
this.logger.debug(`Workflow task '${context.toolName}' requires client input.`);
|
|
23747
|
+
return {
|
|
23748
|
+
status: "input_required",
|
|
23749
|
+
inputRequests: { [`${INPUT_KEY}-${(0, crypto$1.randomUUID)()}`]: this.inputRequestForm(context.toolName, suspended.payload, requestedSchema) },
|
|
23750
|
+
suspendPayload: {
|
|
23751
|
+
step: suspended.path,
|
|
23752
|
+
suspendedAt: suspended.suspendedAt
|
|
23753
|
+
}
|
|
23754
|
+
};
|
|
23755
|
+
}
|
|
23756
|
+
default: return;
|
|
23757
|
+
}
|
|
23758
|
+
}
|
|
23759
|
+
/**
|
|
23760
|
+
* Cancels the task's run, for a declined input or the store's onCancel (`tasks/cancel`, TTL
|
|
23761
|
+
* expiry), on whichever instance that happens. A run executing in this process is cancelled here.
|
|
23762
|
+
* A run executing in another process is left alone: writing `canceled` under it doesn't stop it,
|
|
23763
|
+
* its next write overwrites the status, and its own cancel would then find it `canceled` and do
|
|
23764
|
+
* nothing. A run that waits (suspended, pending) is cancelled in storage. A finished run is left alone.
|
|
23765
|
+
*/
|
|
23766
|
+
async cancelWorkflowRun(context) {
|
|
23767
|
+
const local = this.executingRuns.get(context.runId);
|
|
23768
|
+
if (local) return local();
|
|
23769
|
+
const workflow = this.workflowFor(context.workflowKey);
|
|
23770
|
+
if (!workflow) return;
|
|
23771
|
+
try {
|
|
23772
|
+
const state = await workflow.getWorkflowRunById(context.runId, { withNestedWorkflows: false });
|
|
23773
|
+
if (!state || isFinalRunStatus(state.status) || state.status === "running") return;
|
|
23774
|
+
await (await workflow.createRun({ runId: context.runId })).cancel();
|
|
23775
|
+
} catch (error) {
|
|
23776
|
+
this.logger.warn("Could not cancel workflow run", {
|
|
23777
|
+
tool: context.toolName,
|
|
23778
|
+
error
|
|
23779
|
+
});
|
|
23780
|
+
}
|
|
23781
|
+
}
|
|
23782
|
+
workflowFor(key) {
|
|
23783
|
+
return this.workflows?.[key];
|
|
23784
|
+
}
|
|
20851
23785
|
/**
|
|
20852
23786
|
* Runs one round of a tool. The tool sees the same `suspend` / `resumeData` /
|
|
20853
23787
|
* `suspendPayload` vocabulary as under an agent or workflow; nothing else is
|
|
@@ -20862,7 +23796,7 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
20862
23796
|
skipToolSpan: true,
|
|
20863
23797
|
requestContext: options.requestContext,
|
|
20864
23798
|
tracingContext: options.tracingContext,
|
|
20865
|
-
abortSignal: options.mcp?.extra.signal,
|
|
23799
|
+
abortSignal: options.abortSignal ?? options.mcp?.extra.signal,
|
|
20866
23800
|
...options.mcp ? { mcp: options.mcp } : {},
|
|
20867
23801
|
suspend: async (payload, suspendOptions) => {
|
|
20868
23802
|
suspension = {
|
|
@@ -21183,6 +24117,65 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
21183
24117
|
this.httpHandler = void 0;
|
|
21184
24118
|
this.nodeHandler = void 0;
|
|
21185
24119
|
}
|
|
24120
|
+
if (this.taskStore instanceof SelectingMCPTaskStore) this.taskStore.close();
|
|
24121
|
+
await this.backgroundTaskStore?.close();
|
|
24122
|
+
}
|
|
24123
|
+
/** Whether the id was given (in the config, or as a Mastra registration key) rather than generated per process. */
|
|
24124
|
+
hasStableId = false;
|
|
24125
|
+
setId(id) {
|
|
24126
|
+
super.setId(id);
|
|
24127
|
+
if (this.id === id) this.hasStableId = true;
|
|
24128
|
+
}
|
|
24129
|
+
/** @internal */
|
|
24130
|
+
__registerMastra(mastra) {
|
|
24131
|
+
super.__registerMastra(mastra);
|
|
24132
|
+
const prepared = this.backgroundTaskStore;
|
|
24133
|
+
if (!this.prepareBackgroundTaskStore() || !(this.taskStore instanceof SelectingMCPTaskStore)) return;
|
|
24134
|
+
if (!prepared) this.taskStore.reselect();
|
|
24135
|
+
this.taskStore.resolve().catch((error) => this.logger.debug(`MCP server '${this.name}' could not choose its task store yet`, { error }));
|
|
24136
|
+
}
|
|
24137
|
+
/**
|
|
24138
|
+
* Builds the background-task store and registers an executor for every task tool, when tasks
|
|
24139
|
+
* are on, no store was passed in, and Mastra has background tasks.
|
|
24140
|
+
*/
|
|
24141
|
+
prepareBackgroundTaskStore() {
|
|
24142
|
+
if (this.backgroundTaskStore) return this.backgroundTaskStore;
|
|
24143
|
+
if (!this.tasks.enabled || this.tasks.store) return void 0;
|
|
24144
|
+
const manager = this.mastra?.backgroundTaskManager;
|
|
24145
|
+
if (!manager) return void 0;
|
|
24146
|
+
const store = new BackgroundTaskMCPTaskStore({
|
|
24147
|
+
manager,
|
|
24148
|
+
serverId: this.id,
|
|
24149
|
+
workFor: (context) => isWorkflowTaskContext(context) ? this.workflowTaskWork(context) : this.toolTaskWork(context),
|
|
24150
|
+
onCancel: (context) => isWorkflowTaskContext(context) ? this.cancelWorkflowRun(context) : void 0,
|
|
24151
|
+
recoverFor: (name) => this.tasks.modeFor(name, this.convertedTools[name]?.mcp?.toolType).recover,
|
|
24152
|
+
logger: () => this.logger
|
|
24153
|
+
});
|
|
24154
|
+
for (const [name, tool] of Object.entries(this.convertedTools)) if (this.tasks.modeFor(name, tool.mcp?.toolType).mode !== "off") store.registerTool(name);
|
|
24155
|
+
this.backgroundTaskStore = store;
|
|
24156
|
+
return store;
|
|
24157
|
+
}
|
|
24158
|
+
/**
|
|
24159
|
+
* Mastra's background tasks when the storage keeps per-row expiry (so a task's TTL, not the
|
|
24160
|
+
* global cleanup, decides when it is deleted); an in-memory store otherwise.
|
|
24161
|
+
*/
|
|
24162
|
+
async selectTaskStore() {
|
|
24163
|
+
const manager = this.mastra?.backgroundTaskManager;
|
|
24164
|
+
if (manager) {
|
|
24165
|
+
const storage = await manager.getStorage();
|
|
24166
|
+
if (storage.supportsTaskExpiry === true) {
|
|
24167
|
+
const prepared = this.backgroundTaskStore;
|
|
24168
|
+
const store = this.prepareBackgroundTaskStore();
|
|
24169
|
+
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\`.`);
|
|
24170
|
+
store.startSweeping();
|
|
24171
|
+
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`);
|
|
24172
|
+
return store;
|
|
24173
|
+
}
|
|
24174
|
+
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.`);
|
|
24175
|
+
await this.backgroundTaskStore?.close();
|
|
24176
|
+
this.backgroundTaskStore = void 0;
|
|
24177
|
+
}
|
|
24178
|
+
return new InMemoryMCPTaskStore({ onCancel: (context) => isWorkflowTaskContext(context) ? this.cancelWorkflowRun(context) : void 0 });
|
|
21186
24179
|
}
|
|
21187
24180
|
getServerInfo() {
|
|
21188
24181
|
return {
|
|
@@ -21308,6 +24301,32 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
21308
24301
|
this.eventsInspector ??= createEventsInspector(this.eventsDeps());
|
|
21309
24302
|
return this.eventsInspector;
|
|
21310
24303
|
}
|
|
24304
|
+
/**
|
|
24305
|
+
* Which tools run as MCP tasks and whether the task store can be listed, or `undefined` when
|
|
24306
|
+
* no tool runs as a task. Used by Studio.
|
|
24307
|
+
* @experimental
|
|
24308
|
+
*/
|
|
24309
|
+
getTasksInfo() {
|
|
24310
|
+
if (!this.tasks.enabled) return void 0;
|
|
24311
|
+
return tasksInfo(this.tasks, this.taskToolTypes());
|
|
24312
|
+
}
|
|
24313
|
+
/**
|
|
24314
|
+
* Lists, cancels and answers this server's tasks across principals, through the task store's
|
|
24315
|
+
* admin view, or `undefined` when no tool runs as a task or the store can't be listed. Used by
|
|
24316
|
+
* Studio; there is no protocol equivalent of the listing.
|
|
24317
|
+
* @experimental
|
|
24318
|
+
*/
|
|
24319
|
+
getTasksInspector() {
|
|
24320
|
+
if (!this.getTasksInfo()?.listable) return void 0;
|
|
24321
|
+
this.tasksInspector ??= createTasksInspector(async () => {
|
|
24322
|
+
if (this.taskStore instanceof SelectingMCPTaskStore) return this.taskStore.admin();
|
|
24323
|
+
return isMCPTaskStoreAdmin(this.taskStore) ? this.taskStore : void 0;
|
|
24324
|
+
});
|
|
24325
|
+
return this.tasksInspector;
|
|
24326
|
+
}
|
|
24327
|
+
taskToolTypes() {
|
|
24328
|
+
return Object.entries(this.convertedTools).map(([name, tool]) => [name, tool.mcp?.toolType]);
|
|
24329
|
+
}
|
|
21311
24330
|
eventsDeps() {
|
|
21312
24331
|
return {
|
|
21313
24332
|
registry: this.eventRegistry,
|
|
@@ -21322,6 +24341,10 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
|
|
|
21322
24341
|
function isRegistrableTool(tool) {
|
|
21323
24342
|
return !!tool && typeof tool === "object" && "id" in tool && typeof tool.id === "string";
|
|
21324
24343
|
}
|
|
24344
|
+
function taskLimitMessage({ scope, limit, kind }) {
|
|
24345
|
+
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.`;
|
|
24346
|
+
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.`;
|
|
24347
|
+
}
|
|
21325
24348
|
function errorResult$1(text) {
|
|
21326
24349
|
return {
|
|
21327
24350
|
content: [{
|
|
@@ -21331,6 +24354,75 @@ function errorResult$1(text) {
|
|
|
21331
24354
|
isError: true
|
|
21332
24355
|
};
|
|
21333
24356
|
}
|
|
24357
|
+
/** The span options of a task's first attempt, minus its request context. */
|
|
24358
|
+
function firstAttemptSpan(name, taskId) {
|
|
24359
|
+
return {
|
|
24360
|
+
mcpMethod: "tools/call",
|
|
24361
|
+
targetName: name,
|
|
24362
|
+
metadata: {
|
|
24363
|
+
mcpTaskKey: taskKeyOf(taskId),
|
|
24364
|
+
attempt: 1,
|
|
24365
|
+
resumed: false,
|
|
24366
|
+
recovered: false
|
|
24367
|
+
}
|
|
24368
|
+
};
|
|
24369
|
+
}
|
|
24370
|
+
function completed(result) {
|
|
24371
|
+
return {
|
|
24372
|
+
status: "completed",
|
|
24373
|
+
result
|
|
24374
|
+
};
|
|
24375
|
+
}
|
|
24376
|
+
/** How often a workflow task reads its run's state while it runs elsewhere. */
|
|
24377
|
+
const WORKFLOW_POLL_INTERVAL_MS = 1e3;
|
|
24378
|
+
function formValidation(message, requestedSchema) {
|
|
24379
|
+
return _modelcontextprotocol_server.specTypeSchemas.ElicitRequestFormParams["~standard"].validate({
|
|
24380
|
+
message,
|
|
24381
|
+
requestedSchema
|
|
24382
|
+
});
|
|
24383
|
+
}
|
|
24384
|
+
/** `run.resume()` lost to another resume, or found the run no longer suspended. */
|
|
24385
|
+
function isResumeClaimLost(error) {
|
|
24386
|
+
return error instanceof _mastra_core_error.MastraError && (error.id === "WORKFLOW_RESUME_ALREADY_CLAIMED" || error.id === "WORKFLOW_RUN_NOT_SUSPENDED");
|
|
24387
|
+
}
|
|
24388
|
+
function sleep(ms, signal) {
|
|
24389
|
+
return new Promise((resolve, reject) => {
|
|
24390
|
+
const onAbort = () => {
|
|
24391
|
+
clearTimeout(timer);
|
|
24392
|
+
reject(signal.reason);
|
|
24393
|
+
};
|
|
24394
|
+
const timer = setTimeout(() => {
|
|
24395
|
+
signal.removeEventListener("abort", onAbort);
|
|
24396
|
+
resolve();
|
|
24397
|
+
}, ms);
|
|
24398
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
24399
|
+
});
|
|
24400
|
+
}
|
|
24401
|
+
/** Whether this request's client declared the Tasks extension; only 2026-07-28 requests carry it. */
|
|
24402
|
+
function clientDeclaredTasks(ctx) {
|
|
24403
|
+
const envelope = ctx.mcpReq.envelope;
|
|
24404
|
+
return hasTasksCapability(envelope?.[_modelcontextprotocol_server.CLIENT_CAPABILITIES_META_KEY]);
|
|
24405
|
+
}
|
|
24406
|
+
/** The request context a task keeps. Bearer tokens are dropped so they are never stored with the task. */
|
|
24407
|
+
function persistableRequestContext(requestContext) {
|
|
24408
|
+
const { authInfo, [_mastra_core_request_context.MASTRA_AUTH_TOKEN_KEY]: _authToken, ...rest } = requestContext.toJSON();
|
|
24409
|
+
let stripped = rest;
|
|
24410
|
+
if (authInfo && typeof authInfo === "object") {
|
|
24411
|
+
const { token: _token, ...withoutToken } = authInfo;
|
|
24412
|
+
stripped = {
|
|
24413
|
+
...rest,
|
|
24414
|
+
authInfo: withoutToken
|
|
24415
|
+
};
|
|
24416
|
+
} else if (authInfo !== void 0) stripped = {
|
|
24417
|
+
...rest,
|
|
24418
|
+
authInfo
|
|
24419
|
+
};
|
|
24420
|
+
const persisted = JSON.parse(JSON.stringify(stripped));
|
|
24421
|
+
return {
|
|
24422
|
+
persisted,
|
|
24423
|
+
dropped: [...requestContext.keys()].filter((key) => typeof key === "string" && key !== _mastra_core_request_context.MASTRA_AUTH_TOKEN_KEY && requestContext.get(key) !== void 0 && !(key in persisted))
|
|
24424
|
+
};
|
|
24425
|
+
}
|
|
21334
24426
|
/** The tool's MCP App URI; the nested key wins over the flat one, as in ext-apps' `getToolUiResourceUri`. */
|
|
21335
24427
|
function uiResourceUri(meta) {
|
|
21336
24428
|
return (meta?.ui)?.resourceUri || meta?.[_modelcontextprotocol_ext_apps.RESOURCE_URI_META_KEY] || void 0;
|
|
@@ -22041,6 +25133,7 @@ var MCPEventsSignalProvider = class MCPEventsSignalProvider extends _mastra_core
|
|
|
22041
25133
|
}
|
|
22042
25134
|
};
|
|
22043
25135
|
exports.InMemoryMCPEventCursorStore = InMemoryMCPEventCursorStore;
|
|
25136
|
+
exports.InMemoryMCPTaskStore = InMemoryMCPTaskStore;
|
|
22044
25137
|
exports.InMemoryOAuthStorage = InMemoryOAuthStorage;
|
|
22045
25138
|
exports.InMemoryWebhookSubscriptionStore = InMemoryWebhookSubscriptionStore;
|
|
22046
25139
|
exports.InternalMastraMCPClient = InternalMastraMCPClient;
|
|
@@ -22049,6 +25142,7 @@ exports.MCPClientServerProxy = MCPClientServerProxy;
|
|
|
22049
25142
|
exports.MCPEventsSignalProvider = MCPEventsSignalProvider;
|
|
22050
25143
|
exports.MCPOAuthClientProvider = MCPOAuthClientProvider;
|
|
22051
25144
|
exports.MCPServer = MCPServer;
|
|
25145
|
+
exports.MCPTaskLimitError = MCPTaskLimitError;
|
|
22052
25146
|
exports.MCP_CALL_TOOL_CONTENT = MCP_CALL_TOOL_CONTENT;
|
|
22053
25147
|
exports.MCP_CALL_TOOL_META = MCP_CALL_TOOL_META;
|
|
22054
25148
|
exports.MCP_CLIENT_PROTOCOL_VERSION = MCP_CLIENT_PROTOCOL_VERSION;
|
|
@@ -22117,12 +25211,15 @@ exports.generateWWWAuthenticateHeader = generateWWWAuthenticateHeader;
|
|
|
22117
25211
|
exports.getCallbackUrlCandidates = getCallbackUrlCandidates;
|
|
22118
25212
|
exports.getMcpCallToolContent = getMcpCallToolContent;
|
|
22119
25213
|
exports.getMcpCallToolMeta = getMcpCallToolMeta;
|
|
25214
|
+
exports.isMCPTaskStoreAdmin = isMCPTaskStoreAdmin;
|
|
25215
|
+
exports.isWorkflowTaskContext = isWorkflowTaskContext;
|
|
22120
25216
|
Object.defineProperty(exports, "parseErrorResponse", {
|
|
22121
25217
|
enumerable: true,
|
|
22122
25218
|
get: function() {
|
|
22123
25219
|
return _modelcontextprotocol_client.parseErrorResponse;
|
|
22124
25220
|
}
|
|
22125
25221
|
});
|
|
25222
|
+
exports.principalKeyOf = principalKeyOf;
|
|
22126
25223
|
Object.defineProperty(exports, "refreshAuthorization", {
|
|
22127
25224
|
enumerable: true,
|
|
22128
25225
|
get: function() {
|
|
@@ -22141,6 +25238,7 @@ Object.defineProperty(exports, "startAuthorization", {
|
|
|
22141
25238
|
return _modelcontextprotocol_client.startAuthorization;
|
|
22142
25239
|
}
|
|
22143
25240
|
});
|
|
25241
|
+
exports.taskKeyOf = taskKeyOf;
|
|
22144
25242
|
exports.traceContextFromMeta = traceContextFromMeta;
|
|
22145
25243
|
exports.traceContextToMeta = traceContextToMeta;
|
|
22146
25244
|
|