@mastra/mcp 2.6.0-alpha.3 → 2.7.0-alpha.0

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