@mastra/mcp 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/client/client.d.ts +12 -0
  2. package/dist/client/client.d.ts.map +1 -1
  3. package/dist/client/configuration.d.ts +40 -0
  4. package/dist/client/configuration.d.ts.map +1 -1
  5. package/dist/client/error-utils.d.ts +9 -0
  6. package/dist/client/error-utils.d.ts.map +1 -1
  7. package/dist/client/index.d.ts +2 -1
  8. package/dist/client/index.d.ts.map +1 -1
  9. package/dist/client/tasks/intercept.d.ts +22 -0
  10. package/dist/client/tasks/intercept.d.ts.map +1 -0
  11. package/dist/client/tasks/options.d.ts +4 -0
  12. package/dist/client/tasks/options.d.ts.map +1 -0
  13. package/dist/client/tasks/requester.d.ts +26 -0
  14. package/dist/client/tasks/requester.d.ts.map +1 -0
  15. package/dist/client/tasks/wait.d.ts +60 -0
  16. package/dist/client/tasks/wait.d.ts.map +1 -0
  17. package/dist/client/types.d.ts +47 -0
  18. package/dist/client/types.d.ts.map +1 -1
  19. package/dist/docs/SKILL.md +1 -1
  20. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  21. package/dist/docs/references/docs-auth-fga.md +1 -1
  22. package/dist/docs/references/reference-tools-mcp-client.md +48 -0
  23. package/dist/docs/references/reference-tools-mcp-server.md +216 -1
  24. package/dist/index.cjs +3206 -108
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +3204 -112
  27. package/dist/index.js.map +1 -1
  28. package/dist/server/index.d.ts +1 -0
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/request.d.ts +6 -0
  31. package/dist/server/request.d.ts.map +1 -1
  32. package/dist/server/server.d.ts +131 -1
  33. package/dist/server/server.d.ts.map +1 -1
  34. package/dist/server/tasks/background-store.d.ts +107 -0
  35. package/dist/server/tasks/background-store.d.ts.map +1 -0
  36. package/dist/server/tasks/config.d.ts +226 -0
  37. package/dist/server/tasks/config.d.ts.map +1 -0
  38. package/dist/server/tasks/emit.d.ts +16 -0
  39. package/dist/server/tasks/emit.d.ts.map +1 -0
  40. package/dist/server/tasks/handlers.d.ts +33 -0
  41. package/dist/server/tasks/handlers.d.ts.map +1 -0
  42. package/dist/server/tasks/in-memory-store.d.ts +71 -0
  43. package/dist/server/tasks/in-memory-store.d.ts.map +1 -0
  44. package/dist/server/tasks/index.d.ts +9 -0
  45. package/dist/server/tasks/index.d.ts.map +1 -0
  46. package/dist/server/tasks/inspector.d.ts +13 -0
  47. package/dist/server/tasks/inspector.d.ts.map +1 -0
  48. package/dist/server/tasks/store.d.ts +260 -0
  49. package/dist/server/tasks/store.d.ts.map +1 -0
  50. package/dist/server/tasks/workflow-runs.d.ts +63 -0
  51. package/dist/server/tasks/workflow-runs.d.ts.map +1 -0
  52. package/dist/shared/tasks/capability.d.ts +18 -0
  53. package/dist/shared/tasks/capability.d.ts.map +1 -0
  54. package/dist/shared/tasks/constants.d.ts +34 -0
  55. package/dist/shared/tasks/constants.d.ts.map +1 -0
  56. package/dist/shared/tasks/index.d.ts +6 -0
  57. package/dist/shared/tasks/index.d.ts.map +1 -0
  58. package/dist/shared/tasks/schemas.d.ts +187 -0
  59. package/dist/shared/tasks/schemas.d.ts.map +1 -0
  60. package/dist/shared/tasks/task-id.d.ts +27 -0
  61. package/dist/shared/tasks/task-id.d.ts.map +1 -0
  62. package/dist/shared/tasks/types.d.ts +21 -0
  63. package/dist/shared/tasks/types.d.ts.map +1 -0
  64. package/package.json +11 -10
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 "anonymous";
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) return `${authInfo.clientId}:sub:${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) return `${authInfo.clientId}:user:${userId}`;
18838
- return `${authInfo.clientId}:token:${(0, crypto$1.createHash)("sha256").update(authInfo.token).digest("base64url")}`;
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/server.ts
20228
- const EMPTY_OBJECT_SCHEMA = {
20229
- type: "object",
20230
- properties: {}
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
- * Exposes Mastra tools, agents, workflows, resources and prompts to Model Context
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
- * @see [MCP server documentation](https://mastra.ai/reference/tools/mcp-server)
20258
- * if packaged docs are unavailable.
20874
+ * @experimental SEP-2663 extension; API may change.
20259
20875
  */
20260
- var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
20261
- mcpVersion = 2;
20262
- resources;
20263
- prompts;
20264
- toolActions;
20265
- /**
20266
- * Publishes occurrences of the configured `events` to poll, push and (when
20267
- * `eventsOptions.webhook` is set) webhook subscribers.
20268
- *
20269
- * @experimental SEP-3415 is a draft; this API may change.
20270
- */
20271
- events;
20272
- resourceOptions;
20273
- promptOptions;
20274
- appResourceList = [];
20275
- appResourceHtml = /* @__PURE__ */ new Map();
20276
- jsonSchemaValidator;
20277
- cacheHints;
20278
- mapAuthInfoToUser;
20279
- fga;
20280
- requestStateCodec;
20281
- eventRegistry;
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 requestedSchema = this.formSchema(resumeSchema);
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]: _modelcontextprotocol_server.inputRequired.elicit({
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 execution = await this.runTool(name, tool, args, {
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
- resumeData: continuation?.resumeData,
20818
- suspendPayload: continuation?.suspendPayload
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.logger.error("Tool execution failed", {
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