@frontmcp/sdk 1.8.3 → 1.8.5

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 (158) hide show
  1. package/agent/agent-execution-loop.d.ts.map +1 -1
  2. package/agent/agent.instance.d.ts +15 -4
  3. package/agent/agent.instance.d.ts.map +1 -1
  4. package/agent/flows/call-agent.flow.d.ts +19 -1
  5. package/agent/flows/call-agent.flow.d.ts.map +1 -1
  6. package/agent/hooks/agent.hooks.d.ts +4 -4
  7. package/agent/hooks/agent.hooks.d.ts.map +1 -1
  8. package/auth/flows/auth.verify.flow.d.ts.map +1 -1
  9. package/auth/flows/oauth.auth-ui.flow.d.ts +1 -1
  10. package/auth/flows/oauth.authorize.flow.d.ts +23 -14
  11. package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
  12. package/auth/flows/oauth.callback.flow.d.ts +5 -2
  13. package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
  14. package/auth/flows/oauth.connect.flow.d.ts +1 -1
  15. package/auth/flows/oauth.provider-callback.flow.d.ts +2 -2
  16. package/auth/flows/oauth.provider-callback.flow.d.ts.map +1 -1
  17. package/auth/flows/oauth.register.flow.d.ts +1 -1
  18. package/auth/flows/oauth.token.flow.d.ts +1 -1
  19. package/auth/flows/oauth.token.flow.d.ts.map +1 -1
  20. package/auth/flows/oauth.userinfo.flow.d.ts +1 -1
  21. package/auth/flows/oauth.userinfo.flow.d.ts.map +1 -1
  22. package/auth/flows/session.verify.flow.d.ts +1 -0
  23. package/auth/flows/session.verify.flow.d.ts.map +1 -1
  24. package/auth/flows/well-known.jwks.flow.d.ts +3 -3
  25. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts +3 -2
  26. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts.map +1 -1
  27. package/auth/flows/well-known.prm.flow.d.ts +4 -3
  28. package/auth/flows/well-known.prm.flow.d.ts.map +1 -1
  29. package/auth/instances/instance.local-primary-auth.d.ts +48 -6
  30. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  31. package/auth/secure-store/secure-store.providers.d.ts +8 -5
  32. package/auth/secure-store/secure-store.providers.d.ts.map +1 -1
  33. package/auth/session-scope-identity.d.ts +18 -0
  34. package/auth/session-scope-identity.d.ts.map +1 -0
  35. package/auth/vault/auth-providers.providers.d.ts +2 -4
  36. package/auth/vault/auth-providers.providers.d.ts.map +1 -1
  37. package/channel/channel-scope.helper.d.ts +13 -1
  38. package/channel/channel-scope.helper.d.ts.map +1 -1
  39. package/channel/sources/completion-events.d.ts +24 -0
  40. package/channel/sources/completion-events.d.ts.map +1 -0
  41. package/channel/sources/job-completion.source.d.ts +1 -1
  42. package/channel/sources/job-completion.source.d.ts.map +1 -1
  43. package/channel/sources/webhook.source.d.ts +1 -1
  44. package/channel/sources/webhook.source.d.ts.map +1 -1
  45. package/common/availability/availability.utils.d.ts +4 -2
  46. package/common/availability/availability.utils.d.ts.map +1 -1
  47. package/common/decorators/hook.decorator.d.ts +2 -0
  48. package/common/decorators/hook.decorator.d.ts.map +1 -1
  49. package/common/entries/agent.entry.d.ts +6 -0
  50. package/common/entries/agent.entry.d.ts.map +1 -1
  51. package/common/entries/scope.entry.d.ts +2 -3
  52. package/common/entries/scope.entry.d.ts.map +1 -1
  53. package/common/interfaces/agent.interface.d.ts +8 -0
  54. package/common/interfaces/agent.interface.d.ts.map +1 -1
  55. package/common/interfaces/channel.interface.d.ts +6 -0
  56. package/common/interfaces/channel.interface.d.ts.map +1 -1
  57. package/common/interfaces/execution-context.interface.d.ts +12 -0
  58. package/common/interfaces/execution-context.interface.d.ts.map +1 -1
  59. package/common/interfaces/internal/primary-auth-provider.interface.d.ts +5 -1
  60. package/common/interfaces/internal/primary-auth-provider.interface.d.ts.map +1 -1
  61. package/common/interfaces/job.interface.d.ts +3 -0
  62. package/common/interfaces/job.interface.d.ts.map +1 -1
  63. package/common/interfaces/server.interface.d.ts +2 -0
  64. package/common/interfaces/server.interface.d.ts.map +1 -1
  65. package/common/interfaces/tool.interface.d.ts.map +1 -1
  66. package/common/metadata/plugin.metadata.d.ts +9 -0
  67. package/common/metadata/plugin.metadata.d.ts.map +1 -1
  68. package/common/metadata/tool.metadata.d.ts +2 -2
  69. package/common/metadata/workflow.metadata.d.ts +1 -1
  70. package/common/schemas/http-output.schema.d.ts +24 -24
  71. package/common/tokens/plugin.tokens.d.ts +1 -0
  72. package/common/tokens/plugin.tokens.d.ts.map +1 -1
  73. package/common/utils/auth-info.utils.d.ts +26 -1
  74. package/common/utils/auth-info.utils.d.ts.map +1 -1
  75. package/common/utils/guard.utils.d.ts +2 -0
  76. package/common/utils/guard.utils.d.ts.map +1 -1
  77. package/common/utils/path.utils.d.ts +2 -0
  78. package/common/utils/path.utils.d.ts.map +1 -1
  79. package/context/call-surface.d.ts +6 -4
  80. package/context/call-surface.d.ts.map +1 -1
  81. package/context/frontmcp-context.d.ts +13 -1
  82. package/context/frontmcp-context.d.ts.map +1 -1
  83. package/direct/direct-server.d.ts.map +1 -1
  84. package/elicitation/helpers/elicit.helper.d.ts.map +1 -1
  85. package/elicitation/send-elicitation-result.tool.d.ts.map +1 -1
  86. package/errors/error-handler.d.ts.map +1 -1
  87. package/errors/index.d.ts +1 -0
  88. package/errors/index.d.ts.map +1 -1
  89. package/errors/mcp.error.d.ts.map +1 -1
  90. package/errors/session-identity-required.error.d.ts +20 -0
  91. package/errors/session-identity-required.error.d.ts.map +1 -0
  92. package/esm/index.mjs +1728 -488
  93. package/flows/flow.http-path.d.ts +21 -0
  94. package/flows/flow.http-path.d.ts.map +1 -0
  95. package/flows/flow.instance.d.ts.map +1 -1
  96. package/front-mcp/front-mcp.d.ts +5 -0
  97. package/front-mcp/front-mcp.d.ts.map +1 -1
  98. package/front-mcp/static-startup.check.d.ts +12 -0
  99. package/front-mcp/static-startup.check.d.ts.map +1 -0
  100. package/hooks/hook-coverage.d.ts +29 -0
  101. package/hooks/hook-coverage.d.ts.map +1 -0
  102. package/index.d.ts +15 -11
  103. package/index.d.ts.map +1 -1
  104. package/index.js +2222 -979
  105. package/job/execution/job-execution.manager.d.ts.map +1 -1
  106. package/job/job-scope.helper.d.ts.map +1 -1
  107. package/notification/notification.service.d.ts.map +1 -1
  108. package/package.json +10 -10
  109. package/prompt/flows/get-prompt.flow.d.ts.map +1 -1
  110. package/scope/flows/http.request.flow.d.ts +25 -1
  111. package/scope/flows/http.request.flow.d.ts.map +1 -1
  112. package/scope/scope.instance.d.ts +3 -3
  113. package/scope/scope.instance.d.ts.map +1 -1
  114. package/skill/auth/skill-http-auth.d.ts.map +1 -1
  115. package/skill/flows/http/llm-full-txt.flow.d.ts +1 -1
  116. package/skill/flows/http/llm-txt.flow.d.ts +1 -1
  117. package/skill/flows/http/skills-api.flow.d.ts +1 -1
  118. package/skill/flows/http/skills-api.flow.d.ts.map +1 -1
  119. package/skill/flows/load-skill.flow.d.ts.map +1 -1
  120. package/skill/index.d.ts +1 -1
  121. package/skill/index.d.ts.map +1 -1
  122. package/skill/sep-2640/sep-2640.per-skill.d.ts.map +1 -1
  123. package/skill/sep-2640/sep-2640.resource-helpers.d.ts +6 -0
  124. package/skill/sep-2640/sep-2640.resource-helpers.d.ts.map +1 -1
  125. package/skill/skill-filter.helper.d.ts +31 -2
  126. package/skill/skill-filter.helper.d.ts.map +1 -1
  127. package/skill/skill-http.utils.d.ts.map +1 -1
  128. package/skill/skill-scope.helper.d.ts +11 -1
  129. package/skill/skill-scope.helper.d.ts.map +1 -1
  130. package/skill/skill.events.d.ts.map +1 -1
  131. package/skill/skill.registry.d.ts +121 -17
  132. package/skill/skill.registry.d.ts.map +1 -1
  133. package/task/helpers/task-runner.d.ts.map +1 -1
  134. package/task/runtime/execute-task.d.ts.map +1 -1
  135. package/task/task.types.d.ts +7 -0
  136. package/task/task.types.d.ts.map +1 -1
  137. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  138. package/transport/flows/handle.mcp-20260728.flow.d.ts +1 -0
  139. package/transport/flows/handle.mcp-20260728.flow.d.ts.map +1 -1
  140. package/transport/flows/handle.streamable-http.flow.d.ts +1 -1
  141. package/transport/flows/handle.streamable-http.flow.d.ts.map +1 -1
  142. package/transport/in-memory-server.d.ts.map +1 -1
  143. package/transport/index.d.ts +2 -1
  144. package/transport/index.d.ts.map +1 -1
  145. package/transport/mcp-20260728/dispatcher.d.ts +5 -0
  146. package/transport/mcp-20260728/dispatcher.d.ts.map +1 -1
  147. package/transport/mcp-20260728/mrtr.d.ts +8 -0
  148. package/transport/mcp-20260728/mrtr.d.ts.map +1 -1
  149. package/transport/mcp-handlers/index.d.ts.map +1 -1
  150. package/transport/mcp-handlers/skills-load-request.handler.d.ts.map +1 -1
  151. package/transport/persistent-session-owner.d.ts +41 -0
  152. package/transport/persistent-session-owner.d.ts.map +1 -0
  153. package/transport/web-fetch-handler.d.ts +33 -0
  154. package/transport/web-fetch-handler.d.ts.map +1 -1
  155. package/transport/web-standard-mcp.d.ts +4 -0
  156. package/transport/web-standard-mcp.d.ts.map +1 -1
  157. package/workflow/engine/workflow-step.executor.d.ts.map +1 -1
  158. package/workflow/engine/workflow.engine.d.ts.map +1 -1
package/esm/index.mjs CHANGED
@@ -110,7 +110,7 @@ function extractPublicMessageImpl(error, visited) {
110
110
  }
111
111
  return error == null ? "Unknown error" : String(error);
112
112
  }
113
- var MCP_ERROR_CODES, McpError, PublicMcpError, InternalMcpError, ToolNotFoundError, ToolNotConsentedError, EntryUnavailableError, ResourceNotFoundError, ResourceReadError, InvalidResourceUriError, InvalidInputError, InvalidOutputError, InvalidMethodError, PayloadTooLargeError, ToolExecutionError, RateLimitError, QuotaExceededError, UnauthorizedError, GenericServerError, DependencyNotFoundError, InvalidHookFlowError, InvalidPluginScopeError, RequestContextNotAvailableError, AuthConfigurationError, PromptNotFoundError, PromptExecutionError, SessionMissingError, UnsupportedClientVersionError, GlobalConfigNotFoundError, AuthorityDeniedMcpError, GuardLimitMcpError;
113
+ var MCP_ERROR_CODES, sequentialErrorIds, McpError, PublicMcpError, InternalMcpError, ToolNotFoundError, ToolNotConsentedError, EntryUnavailableError, ResourceNotFoundError, ResourceReadError, InvalidResourceUriError, InvalidInputError, InvalidOutputError, InvalidMethodError, PayloadTooLargeError, ToolExecutionError, RateLimitError, QuotaExceededError, UnauthorizedError, GenericServerError, DependencyNotFoundError, InvalidHookFlowError, InvalidPluginScopeError, RequestContextNotAvailableError, AuthConfigurationError, PromptNotFoundError, PromptExecutionError, SessionMissingError, UnsupportedClientVersionError, GlobalConfigNotFoundError, AuthorityDeniedMcpError, GuardLimitMcpError;
114
114
  var init_mcp_error = __esm({
115
115
  "libs/sdk/src/errors/mcp.error.ts"() {
116
116
  "use strict";
@@ -132,6 +132,7 @@ var init_mcp_error = __esm({
132
132
  /** Parse error (-32700) */
133
133
  PARSE_ERROR: -32700
134
134
  };
135
+ sequentialErrorIds = 0;
135
136
  McpError = class extends Error {
136
137
  /**
137
138
  * Unique error ID for tracking in logs
@@ -144,7 +145,12 @@ var init_mcp_error = __esm({
144
145
  Error.captureStackTrace(this, this.constructor);
145
146
  }
146
147
  generateErrorId() {
147
- return `err_${bytesToHex(randomBytes(8))}`;
148
+ try {
149
+ return `err_${bytesToHex(randomBytes(8))}`;
150
+ } catch {
151
+ sequentialErrorIds += 1;
152
+ return `err_${sequentialErrorIds.toString(16).padStart(16, "0")}`;
153
+ }
148
154
  }
149
155
  /**
150
156
  * Get the internal error message (for logging)
@@ -865,7 +871,8 @@ var init_plugin_tokens = __esm({
865
871
  skills: tokenFactory.meta("skills"),
866
872
  scope: tokenFactory.meta("scope"),
867
873
  contextExtensions: tokenFactory.meta("contextExtensions"),
868
- enforcesMetadata: tokenFactory.meta("enforcesMetadata")
874
+ enforcesMetadata: tokenFactory.meta("enforcesMetadata"),
875
+ dynamicSkills: tokenFactory.meta("dynamicSkills")
869
876
  };
870
877
  }
871
878
  });
@@ -3436,7 +3443,8 @@ var init_plugin_metadata = __esm({
3436
3443
  skills: z30.array(annotatedFrontMcpSkillsSchema).optional(),
3437
3444
  scope: z30.enum(["app", "server"]).optional().default("app"),
3438
3445
  contextExtensions: z30.array(contextExtensionSchema).optional(),
3439
- enforcesMetadata: z30.array(z30.string().min(1)).optional()
3446
+ enforcesMetadata: z30.array(z30.string().min(1)).optional(),
3447
+ dynamicSkills: z30.boolean().optional()
3440
3448
  }).passthrough();
3441
3449
  }
3442
3450
  });
@@ -4571,6 +4579,25 @@ var init_tool_credentials_required_error = __esm({
4571
4579
  }
4572
4580
  });
4573
4581
 
4582
+ // libs/sdk/src/errors/session-identity-required.error.ts
4583
+ var SessionIdentityRequiredError;
4584
+ var init_session_identity_required_error = __esm({
4585
+ "libs/sdk/src/errors/session-identity-required.error.ts"() {
4586
+ "use strict";
4587
+ init_mcp_error();
4588
+ SessionIdentityRequiredError = class extends PublicMcpError {
4589
+ mcpErrorCode = MCP_ERROR_CODES.FORBIDDEN;
4590
+ constructor(what = "Session-scoped storage") {
4591
+ super(
4592
+ `${what} needs a verified session or a signed-in caller. Authenticate the request, or use a session transport (an MCP revision before 2026-07-28).`,
4593
+ "SESSION_IDENTITY_REQUIRED",
4594
+ 403
4595
+ );
4596
+ }
4597
+ };
4598
+ }
4599
+ });
4600
+
4574
4601
  // libs/sdk/src/errors/error-handler.ts
4575
4602
  import { isProduction as isProduction2 } from "@frontmcp/utils";
4576
4603
  function createErrorHandler(options = {}) {
@@ -5932,6 +5959,7 @@ __export(errors_exports, {
5932
5959
  ServerlessHandlerNotInitializedError: () => ServerlessHandlerNotInitializedError,
5933
5960
  SessionClaimConflictError: () => SessionClaimConflictError,
5934
5961
  SessionIdEmptyError: () => SessionIdEmptyError,
5962
+ SessionIdentityRequiredError: () => SessionIdentityRequiredError,
5935
5963
  SessionMissingError: () => SessionMissingError,
5936
5964
  SessionSecretRequiredError: () => SessionSecretRequiredError,
5937
5965
  SessionVerificationFailedError: () => SessionVerificationFailedError,
@@ -5986,6 +6014,7 @@ var init_errors = __esm({
5986
6014
  init_mcp_error();
5987
6015
  init_authorization_required_error();
5988
6016
  init_tool_credentials_required_error();
6017
+ init_session_identity_required_error();
5989
6018
  init_error_handler();
5990
6019
  init_agent_errors();
5991
6020
  init_task_error();
@@ -6468,30 +6497,30 @@ var require_dist = __commonJS({
6468
6497
  sqliteStorageOptionsSchema: () => sqliteStorageOptionsSchema
6469
6498
  });
6470
6499
  module.exports = __toCommonJS2(index_exports);
6471
- var import_utils108 = __require("@frontmcp/utils");
6500
+ var import_utils114 = __require("@frontmcp/utils");
6472
6501
  var HKDF_SALT = new TextEncoder().encode("frontmcp-sqlite-storage-v1");
6473
6502
  var HKDF_INFO = new TextEncoder().encode("aes-256-gcm-value-encryption");
6474
6503
  var KEY_LENGTH = 32;
6475
6504
  function deriveEncryptionKey(secret) {
6476
6505
  const ikm = new TextEncoder().encode(secret);
6477
- return (0, import_utils108.hkdfSha256)(ikm, HKDF_SALT, HKDF_INFO, KEY_LENGTH);
6506
+ return (0, import_utils114.hkdfSha256)(ikm, HKDF_SALT, HKDF_INFO, KEY_LENGTH);
6478
6507
  }
6479
6508
  var SEPARATOR = ":";
6480
6509
  function encryptValue(key, plaintext) {
6481
- const iv = (0, import_utils108.randomBytes)(12);
6510
+ const iv = (0, import_utils114.randomBytes)(12);
6482
6511
  const plaintextBytes = new TextEncoder().encode(plaintext);
6483
- const { ciphertext, tag } = (0, import_utils108.encryptAesGcm)(key, plaintextBytes, iv);
6484
- return [(0, import_utils108.base64urlEncode)(iv), (0, import_utils108.base64urlEncode)(tag), (0, import_utils108.base64urlEncode)(ciphertext)].join(SEPARATOR);
6512
+ const { ciphertext, tag } = (0, import_utils114.encryptAesGcm)(key, plaintextBytes, iv);
6513
+ return [(0, import_utils114.base64urlEncode)(iv), (0, import_utils114.base64urlEncode)(tag), (0, import_utils114.base64urlEncode)(ciphertext)].join(SEPARATOR);
6485
6514
  }
6486
6515
  function decryptValue(key, encrypted) {
6487
6516
  const parts = encrypted.split(SEPARATOR);
6488
6517
  if (parts.length !== 3) {
6489
6518
  throw new Error("Invalid encrypted value format");
6490
6519
  }
6491
- const iv = (0, import_utils108.base64urlDecode)(parts[0]);
6492
- const tag = (0, import_utils108.base64urlDecode)(parts[1]);
6493
- const ciphertext = (0, import_utils108.base64urlDecode)(parts[2]);
6494
- const plaintext = (0, import_utils108.decryptAesGcm)(key, ciphertext, iv, tag);
6520
+ const iv = (0, import_utils114.base64urlDecode)(parts[0]);
6521
+ const tag = (0, import_utils114.base64urlDecode)(parts[1]);
6522
+ const ciphertext = (0, import_utils114.base64urlDecode)(parts[2]);
6523
+ const plaintext = (0, import_utils114.decryptAesGcm)(key, ciphertext, iv, tag);
6495
6524
  return new TextDecoder().decode(plaintext);
6496
6525
  }
6497
6526
  var import_utils210 = __require("@frontmcp/utils");
@@ -7405,8 +7434,8 @@ function detectStorageType(storage, configuredType) {
7405
7434
  return "memory";
7406
7435
  }
7407
7436
  async function createTaskStore(options = {}) {
7408
- const { storage: storageConfig, redis, keyPrefix = "mcp:task:", logger, isEdgeRuntime: isEdgeRuntime2 = false, sqlite } = options;
7409
- if (isEdgeRuntime2 && sqlite) {
7437
+ const { storage: storageConfig, redis, keyPrefix = "mcp:task:", logger, isEdgeRuntime: isEdgeRuntime3 = false, sqlite } = options;
7438
+ if (isEdgeRuntime3 && sqlite) {
7410
7439
  throw new TaskStoreNotSupportedError(
7411
7440
  "SQLite task store is not supported on Edge runtime (better-sqlite3 is a native module). Use Redis or Upstash."
7412
7441
  );
@@ -7461,7 +7490,7 @@ async function createTaskStore(options = {}) {
7461
7490
  "Vercel KV is not supported for task stores (pub/sub required). Use Redis or Upstash."
7462
7491
  );
7463
7492
  }
7464
- if (isEdgeRuntime2 && (!finalStorageConfig || finalStorageConfig.type === "memory")) {
7493
+ if (isEdgeRuntime3 && (!finalStorageConfig || finalStorageConfig.type === "memory")) {
7465
7494
  throw new TaskStoreNotSupportedError(
7466
7495
  "Tasks require distributed storage on Edge runtime. Configure Redis or Upstash."
7467
7496
  );
@@ -7469,7 +7498,7 @@ async function createTaskStore(options = {}) {
7469
7498
  const storage = await createStorage({
7470
7499
  ...finalStorageConfig,
7471
7500
  prefix: keyPrefix,
7472
- fallback: isEdgeRuntime2 ? "error" : "memory"
7501
+ fallback: isEdgeRuntime3 ? "error" : "memory"
7473
7502
  });
7474
7503
  const store = new StorageTaskStore(storage, logger);
7475
7504
  const type = detectStorageType(storage, finalStorageConfig?.type);
@@ -7651,8 +7680,9 @@ var init_task_id = __esm({
7651
7680
  });
7652
7681
 
7653
7682
  // libs/sdk/src/task/helpers/task-runner.ts
7683
+ import { runRequestExclusive } from "@frontmcp/utils";
7654
7684
  function runTaskInBackground(params) {
7655
- return Promise.resolve().then(() => executeTask(params));
7685
+ return Promise.resolve().then(() => runRequestExclusive(() => executeTask(params)));
7656
7686
  }
7657
7687
  async function executeTask(params) {
7658
7688
  const { record, cleanedRequestParams, ctx, scope, store, registry, notifier, logger } = params;
@@ -8694,6 +8724,7 @@ var init_get_prompt_request_handler = __esm({
8694
8724
  import {
8695
8725
  ListRootsResultSchema
8696
8726
  } from "@frontmcp/protocol";
8727
+ import { awaitOutsideRequest } from "@frontmcp/utils";
8697
8728
  function hasMcpAppsExtension(capabilities) {
8698
8729
  if (!capabilities?.experimental) {
8699
8730
  return false;
@@ -9410,9 +9441,11 @@ var init_notification_service = __esm({
9410
9441
  }
9411
9442
  try {
9412
9443
  this.logger.verbose(`Requesting roots from client for session ${sessionId.slice(0, 20)}...`);
9413
- const result = await registered.server.request({ method: "roots/list" }, ListRootsResultSchema, {
9414
- timeout: options?.timeout ?? 3e4
9415
- });
9444
+ const result = await awaitOutsideRequest(
9445
+ registered.server.request({ method: "roots/list" }, ListRootsResultSchema, {
9446
+ timeout: options?.timeout ?? 3e4
9447
+ })
9448
+ );
9416
9449
  registered.cachedRoots = result.roots;
9417
9450
  registered.rootsFetchedAt = Date.now();
9418
9451
  this.logger.verbose(
@@ -10221,7 +10254,7 @@ function callerSurface(ctx, surface) {
10221
10254
  }
10222
10255
  async function filterServableSkills(scope, skills, ctx, surface) {
10223
10256
  const caller = callerSurface(ctx, surface);
10224
- const offered = skills.filter((skill) => isOfferedOnSurface(skill.metadata.availableWhen, caller));
10257
+ const offered = skills.filter((skill) => availabilityForCall(skill.metadata.availableWhen, caller) === "available");
10225
10258
  if (offered.length === 0) return [];
10226
10259
  const { skills: servable } = await scope.runFlowForOutput("skills:filter", { skills: [...offered], ctx });
10227
10260
  const servableSkills = new Set(servable);
@@ -10242,7 +10275,33 @@ async function filterDiscoverableSkillResults(scope, registry, results, caller =
10242
10275
  return entry === void 0 || servable.has(entry);
10243
10276
  });
10244
10277
  }
10245
- var SKILLS_HTTP_SURFACE;
10278
+ function skillToolsForCaller(loaded, tools, ctx, surface) {
10279
+ const caller = callerSurface(ctx, surface);
10280
+ if (!tools || caller === void 0 || loaded.availableTools.length === 0) return loaded;
10281
+ const availability = new Map(tools.getTools(true).map((tool) => [tool.name, tool.metadata?.availableWhen]));
10282
+ const notOffered = loaded.availableTools.filter((name44) => !isOfferedOnSurface(availability.get(name44), caller));
10283
+ if (notOffered.length === 0) return loaded;
10284
+ return {
10285
+ ...loaded,
10286
+ availableTools: loaded.availableTools.filter((name44) => !notOffered.includes(name44)),
10287
+ missingTools: [...loaded.missingTools, ...notOffered],
10288
+ isComplete: false,
10289
+ warning: warningWithMissingTools(loaded.warning, loaded.skill?.name, notOffered)
10290
+ };
10291
+ }
10292
+ function warningWithMissingTools(warning, skillName, names) {
10293
+ const prefix = warning?.match(WARNING_PREFIX);
10294
+ if (warning && prefix && warning.endsWith(WARNING_SUFFIX)) {
10295
+ const parts = warning.slice(prefix[0].length, -WARNING_SUFFIX.length).split("; ");
10296
+ const missing = parts.findIndex((part) => part.startsWith(MISSING_PART));
10297
+ if (missing === -1) parts.unshift(`${MISSING_PART}${names.join(", ")}`);
10298
+ else parts[missing] = `${parts[missing]}, ${names.join(", ")}`;
10299
+ return `${prefix[0]}${parts.join("; ")}${WARNING_SUFFIX}`;
10300
+ }
10301
+ const sentence = `Skill "${skillName ?? "skill"}" references ${MISSING_PART}${names.join(", ")}${WARNING_SUFFIX}`;
10302
+ return warning ? `${warning} ${sentence}` : sentence;
10303
+ }
10304
+ var SKILLS_HTTP_SURFACE, WARNING_PREFIX, WARNING_SUFFIX, MISSING_PART;
10246
10305
  var init_skill_filter_helper = __esm({
10247
10306
  "libs/sdk/src/skill/skill-filter.helper.ts"() {
10248
10307
  "use strict";
@@ -10251,6 +10310,9 @@ var init_skill_filter_helper = __esm({
10251
10310
  init_skill_authorities_helper();
10252
10311
  init_skill_entry_resolver();
10253
10312
  SKILLS_HTTP_SURFACE = "mcp";
10313
+ WARNING_PREFIX = /^Skill "(.*)" references /;
10314
+ WARNING_SUFFIX = ". Some functionality may be limited.";
10315
+ MISSING_PART = "missing tools: ";
10254
10316
  }
10255
10317
  });
10256
10318
 
@@ -10351,7 +10413,13 @@ async function formatSkillsForLlmFull(registry, toolRegistry, visibility = "both
10351
10413
  }
10352
10414
  const loaded = await registry.loadSkill(skill.name);
10353
10415
  if (loaded) {
10354
- parts.push(formatSkillForLLMWithSchemas(loaded.skill, loaded.availableTools, loaded.missingTools, toolRegistry));
10416
+ const { availableTools, missingTools } = skillToolsForCaller(
10417
+ loaded,
10418
+ toolRegistry,
10419
+ void 0,
10420
+ SKILLS_HTTP_SURFACE
10421
+ );
10422
+ parts.push(formatSkillForLLMWithSchemas(loaded.skill, availableTools, missingTools, toolRegistry));
10355
10423
  }
10356
10424
  }
10357
10425
  return parts.join("\n\n---\n\n");
@@ -10471,6 +10539,7 @@ function skillToApiResponse(skill, loadResult) {
10471
10539
  var init_skill_http_utils = __esm({
10472
10540
  "libs/sdk/src/skill/skill-http.utils.ts"() {
10473
10541
  "use strict";
10542
+ init_skill_filter_helper();
10474
10543
  }
10475
10544
  });
10476
10545
 
@@ -11032,7 +11101,11 @@ function skillsLoadRequestHandler({
11032
11101
  throw toSdkMcpError(err);
11033
11102
  }
11034
11103
  }
11035
- const { skill, availableTools, missingTools, isComplete, warning } = loadResult;
11104
+ const { skill, availableTools, missingTools, isComplete, warning } = skillToolsForCaller(
11105
+ loadResult,
11106
+ toolRegistry,
11107
+ withMcpSurface(scope, ctx)
11108
+ );
11036
11109
  if (warning) {
11037
11110
  warnings.push(warning);
11038
11111
  }
@@ -11889,7 +11962,8 @@ function createMcpHandlers(options) {
11889
11962
  const hasPromptsOrResources = options.scope?.prompts?.hasAny() || options.scope?.resources?.hasAny();
11890
11963
  const completionHandler = hasPromptsOrResources && options.serverOptions?.capabilities?.completions ? [completeRequestHandler(options)] : [];
11891
11964
  const loggingHandler = options.serverOptions?.capabilities?.logging ? [LoggingSetLevelRequestHandler(options)] : [];
11892
- const skillsHandler = options.scope?.skills?.hasAny() ? [skillsSearchRequestHandler(options), skillsLoadRequestHandler(options), skillsListRequestHandler(options)] : [];
11965
+ const skillRegistry = options.scope?.skills;
11966
+ const skillsHandler = skillRegistry?.servesSkills?.() ?? skillRegistry?.hasAny() ? [skillsSearchRequestHandler(options), skillsLoadRequestHandler(options), skillsListRequestHandler(options)] : [];
11893
11967
  const tasksHandler = options.serverOptions?.capabilities?.["tasks"] ? [
11894
11968
  tasksGetRequestHandler(options),
11895
11969
  tasksResultRequestHandler(options),
@@ -11944,7 +12018,7 @@ var in_memory_server_exports = {};
11944
12018
  __export(in_memory_server_exports, {
11945
12019
  createInMemoryServer: () => createInMemoryServer
11946
12020
  });
11947
- import { randomUUID as randomUUID4 } from "@frontmcp/utils";
12021
+ import { randomUUID as randomUUID4, runRequestExclusive as runRequestExclusive2 } from "@frontmcp/utils";
11948
12022
  async function createInMemoryServer(scope, options) {
11949
12023
  const { InMemoryTransport, McpServer: McpServer2 } = await import("@frontmcp/protocol");
11950
12024
  const { createMcpHandlers: createMcpHandlers2 } = await Promise.resolve().then(() => (init_mcp_handlers(), mcp_handlers_exports));
@@ -11964,7 +12038,7 @@ async function createInMemoryServer(scope, options) {
11964
12038
  sessionId
11965
12039
  }
11966
12040
  };
11967
- return originalHandler(request, enrichedCtx);
12041
+ return runRequestExclusive2(() => originalHandler(request, enrichedCtx));
11968
12042
  };
11969
12043
  mcpServer.setRequestHandler(handler.requestSchema, wrappedHandler);
11970
12044
  }
@@ -12612,7 +12686,7 @@ var init_connect = __esm({
12612
12686
  });
12613
12687
 
12614
12688
  // libs/sdk/src/direct/direct-server.ts
12615
- import { randomUUID as randomUUID6, sha256Hex } from "@frontmcp/utils";
12689
+ import { randomUUID as randomUUID6, runRequestExclusive as runRequestExclusive3, sha256Hex } from "@frontmcp/utils";
12616
12690
  function buildAuthInfo(authContext, defaultSessionId) {
12617
12691
  if (!authContext && !defaultSessionId) {
12618
12692
  return void 0;
@@ -12687,7 +12761,7 @@ var init_direct_server = __esm({
12687
12761
  }
12688
12762
  const ctx = this.buildHandlerContext(options);
12689
12763
  try {
12690
- return await this.scope.runFlowForOutput(flowName, { request, ctx });
12764
+ return await runRequestExclusive3(() => this.scope.runFlowForOutput(flowName, { request, ctx }));
12691
12765
  } catch (e) {
12692
12766
  if (e instanceof FlowControl && e.type === "respond") {
12693
12767
  return e.output;
@@ -14116,11 +14190,19 @@ var init_frontmcp_context = __esm({
14116
14190
  updateAuthInfo(authInfo) {
14117
14191
  this._authInfo = { ...this._authInfo, ...authInfo };
14118
14192
  }
14119
- /** `sessionId` when the server verified it; undefined for stateless and per-request placeholder ids. */
14193
+ /**
14194
+ * The session the server verified for this request; undefined for a request without one (MCP
14195
+ * 2026-07-28, the stateless transports, an `mcp-session-id` the server did not accept).
14196
+ *
14197
+ * Over HTTP that is the session session verification recorded (`authInfo.extra.sessionId`),
14198
+ * which it records only for a session the request presented: its `mcp-session-id` header, or the
14199
+ * `?sessionId=` a legacy SSE client posts its messages with. Over an in-process transport (stdio,
14200
+ * `connect()`, `createDirect()`) it is the session the transport runs the request in.
14201
+ */
14120
14202
  get verifiedSessionId() {
14121
- if (this.sessionId === STATELESS_SESSION_ID) return void 0;
14122
- const verified = this._authInfo.sessionId ?? this._authInfo.extra?.["sessionId"];
14123
- return verified === this.sessionId ? this.sessionId : void 0;
14203
+ const recorded = this._authInfo.extra?.["sessionId"];
14204
+ const verified = typeof recorded === "string" && recorded.length > 0 ? recorded : this._authInfo.sessionId === this.sessionId ? this.sessionId : void 0;
14205
+ return verified === STATELESS_SESSION_ID ? void 0 : verified;
14124
14206
  }
14125
14207
  /**
14126
14208
  * The client's self-reported identity, sent with this request (MCP 2026-07-28 `_meta`).
@@ -14572,6 +14654,9 @@ function readPublicUrlPin() {
14572
14654
  function isPublicUrlPinned() {
14573
14655
  return readPublicUrlPin() !== void 0;
14574
14656
  }
14657
+ function getPinnedPublicUrl() {
14658
+ return readPublicUrlPin();
14659
+ }
14575
14660
  function isProxyTrusted() {
14576
14661
  try {
14577
14662
  if (typeof process !== "undefined" && process.env) {
@@ -15863,6 +15948,9 @@ function registerHookOnClass(ctor, meta) {
15863
15948
  function resolvePendingTC39HooksForClass(ctor) {
15864
15949
  return pendingHookRegistry.resolveForClass(ctor);
15865
15950
  }
15951
+ function peekPendingTC39HooksForClass(ctor) {
15952
+ return pendingHookRegistry.resolveForClass(ctor, false);
15953
+ }
15866
15954
  function registerPendingTC39Hook(method, meta) {
15867
15955
  pendingHookRegistry.store(method, meta);
15868
15956
  }
@@ -16008,7 +16096,7 @@ var init_elicitation_types = __esm({
16008
16096
 
16009
16097
  // libs/sdk/src/elicitation/helpers/elicit.helper.ts
16010
16098
  import { toJSONSchema, z as z45 } from "@frontmcp/lazy-zod";
16011
- import { randomUUID as randomUUID10 } from "@frontmcp/utils";
16099
+ import { awaitOutsideRequest as awaitOutsideRequest2, randomUUID as randomUUID10 } from "@frontmcp/utils";
16012
16100
  async function performElicit(deps, message, requestedSchema, options) {
16013
16101
  const { sessionId, getClientCapabilities, tryGetContext, entryName, entryInput, elicitationEnabled } = deps;
16014
16102
  if (elicitationEnabled !== true) {
@@ -16016,6 +16104,11 @@ async function performElicit(deps, message, requestedSchema, options) {
16016
16104
  }
16017
16105
  const ctx = tryGetContext();
16018
16106
  const mrtr = ctx?.getMrtrExchange?.();
16107
+ if (mrtr && mrtr.clientDeclaredRevision === false && mrtr.supportsElicitation?.(options?.mode ?? "form") !== true) {
16108
+ throw new ElicitationNotSupportedError(
16109
+ "This client does not support elicitation: it declared no elicitation capability, and without a session the elicitation fallback has no one to answer it"
16110
+ );
16111
+ }
16019
16112
  if (mrtr) {
16020
16113
  const zodSchema = requestedSchema instanceof z45.ZodType ? requestedSchema : z45.object(requestedSchema);
16021
16114
  const answer = mrtr.resolveElicitation({
@@ -16056,7 +16149,7 @@ async function performElicit(deps, message, requestedSchema, options) {
16056
16149
  if (!transport) {
16057
16150
  throw new ElicitationNotSupportedError("Transport not available for elicitation");
16058
16151
  }
16059
- return transport.elicit(message, requestedSchema, options);
16152
+ return awaitOutsideRequest2(transport.elicit(message, requestedSchema, options));
16060
16153
  }
16061
16154
  function generateElicitationId() {
16062
16155
  return `elicit-${randomUUID10()}`;
@@ -18224,12 +18317,12 @@ function qualifiedNameOf(lineage, name44) {
18224
18317
  return `${ownerKeyOf(lineage)}:${name44}`;
18225
18318
  }
18226
18319
  function hookOwnerIdOf(lineage, owner) {
18227
- const owners = owner ? [...lineage, owner] : lineage;
18228
- return appOwnerIdOf(owners) ?? owners[owners.length - 1]?.id;
18320
+ const owners2 = owner ? [...lineage, owner] : lineage;
18321
+ return appOwnerIdOf(owners2) ?? owners2[owners2.length - 1]?.id;
18229
18322
  }
18230
18323
  function appOwnerIdOf(lineage, owner) {
18231
- const owners = owner ? [...lineage, owner] : lineage;
18232
- return owners.find((candidate) => candidate.kind === "app")?.id;
18324
+ const owners2 = owner ? [...lineage, owner] : lineage;
18325
+ return owners2.find((candidate) => candidate.kind === "app")?.id;
18233
18326
  }
18234
18327
  var EntryLineageIndex;
18235
18328
  var init_lineage_utils = __esm({
@@ -18327,6 +18420,7 @@ var init_get_prompt_flow = __esm({
18327
18420
  init_remote_capabilities_utils();
18328
18421
  init_common();
18329
18422
  init_availability();
18423
+ init_call_surface();
18330
18424
  init_errors();
18331
18425
  init_resolved_entries();
18332
18426
  init_hooks_utils();
@@ -18530,7 +18624,10 @@ var init_get_prompt_flow = __esm({
18530
18624
  }
18531
18625
  promptContext.mark("execute");
18532
18626
  try {
18533
- promptContext.output = await promptContext.execute(parsedArgs);
18627
+ promptContext.output = await runOnSurface(
18628
+ callSurfaceOf(this.input.ctx),
18629
+ async () => promptContext.execute(parsedArgs)
18630
+ );
18534
18631
  this.logger.verbose("execute:done");
18535
18632
  } catch (error) {
18536
18633
  if (error instanceof FlowControl || isClientFacingError(error)) throw error;
@@ -21615,6 +21712,7 @@ var init_send_elicitation_result_tool = __esm({
21615
21712
  "libs/sdk/src/elicitation/send-elicitation-result.tool.ts"() {
21616
21713
  "use strict";
21617
21714
  init_common();
21715
+ init_call_surface();
21618
21716
  init_errors();
21619
21717
  init_fallback_helper();
21620
21718
  inputSchema12 = {
@@ -21695,8 +21793,15 @@ var init_send_elicitation_result_tool = __esm({
21695
21793
  arguments: pending.toolInput
21696
21794
  }
21697
21795
  },
21698
- // Pass the context for auth info and request ID (used for elicitation routing)
21699
- ctx: this.tryGetContext() ?? { authInfo: this.getAuthInfo() }
21796
+ // The call context of this call, whose caller is the owner: its auth info as its transport
21797
+ // gave it (the tool reads it as `this.auth`), on the surface it was made from. Not the
21798
+ // request context object, whose auth info is a getter a call context does not carry over.
21799
+ ctx: {
21800
+ authInfo: this.authInfo,
21801
+ requestId: this.tryGetContext()?.requestId ?? this.runId,
21802
+ surface: getCallSurface(),
21803
+ ...this.signal ? { signal: this.signal } : {}
21804
+ }
21700
21805
  });
21701
21806
  this.logger.info("sendElicitationResult: original tool completed", {
21702
21807
  elicitId,
@@ -21874,7 +21979,14 @@ import {
21874
21979
  MCP_APPS_MIME_TYPE as MCP_APPS_MIME_TYPE2,
21875
21980
  resolveServingMode
21876
21981
  } from "@frontmcp/uipack/adapters";
21877
- import { findNonFiniteNumber, getRuntimeContext as getRuntimeContext7, isDebug, isDevelopment as isDevelopment2, randomUUID as randomUUID11 } from "@frontmcp/utils";
21982
+ import {
21983
+ awaitOutsideRequest as awaitOutsideRequest3,
21984
+ findNonFiniteNumber,
21985
+ getRuntimeContext as getRuntimeContext7,
21986
+ isDebug,
21987
+ isDevelopment as isDevelopment2,
21988
+ randomUUID as randomUUID11
21989
+ } from "@frontmcp/utils";
21878
21990
  function toolNameCandidates(name44) {
21879
21991
  if (!/[-_]/.test(name44)) return [name44];
21880
21992
  return [name44, name44.includes("_") ? name44.replace(/_/g, "-") : name44.replace(/-/g, "_")];
@@ -22138,6 +22250,8 @@ var init_call_tool_flow = __esm({
22138
22250
  request: { method: "tools/call", params: cleanedParams },
22139
22251
  scopeId: this.scope.id
22140
22252
  };
22253
+ const callSurface = callSurfaceOf(this.input.ctx);
22254
+ if (callSurface !== void 0) record.surface = callSurface;
22141
22255
  if (this.state.progressToken !== void 0) {
22142
22256
  record.progressToken = this.state.progressToken;
22143
22257
  }
@@ -22362,6 +22476,7 @@ var init_call_tool_flow = __esm({
22362
22476
  context.mark("createToolCallContext");
22363
22477
  context._toolNameInternal = tool.metadata.id ?? tool.metadata.name;
22364
22478
  context._toolInputInternal = input.arguments;
22479
+ context._jsonRpcRequestIdInternal = this.state.jsonRpcRequestId;
22365
22480
  const frontmcpContext = context.tryGetContext();
22366
22481
  const authInfoWithTransport = authInfo;
22367
22482
  if (frontmcpContext && authInfoWithTransport?.transport?.sendElicitRequest) {
@@ -22543,8 +22658,11 @@ var init_call_tool_flow = __esm({
22543
22658
  elicitId: error.elicitId,
22544
22659
  sessionId,
22545
22660
  owner,
22546
- toolName: error.toolName,
22547
- toolInput: error.toolInput,
22661
+ // The answer re-runs the call this flow ran: this tool, with these arguments. What asked
22662
+ // may be an entry the tool runs, like the agent behind `invoke_<agent>`, whose own name
22663
+ // no `tools/call` resolves.
22664
+ toolName: tool.fullName,
22665
+ toolInput: this.state.required.input.arguments,
22548
22666
  elicitMessage: error.elicitMessage,
22549
22667
  elicitSchema: error.schema,
22550
22668
  createdAt: Date.now(),
@@ -22556,7 +22674,7 @@ var init_call_tool_flow = __esm({
22556
22674
  elicitId: error.elicitId
22557
22675
  });
22558
22676
  const deps = { scope: this.scope, sessionId, logger: this.logger };
22559
- const result = await handleWaitingFallback(deps, error);
22677
+ const result = await awaitOutsideRequest3(handleWaitingFallback(deps, error));
22560
22678
  toolContext.output = result;
22561
22679
  this.logger.verbose("execute:done (elicitation waiting fallback)");
22562
22680
  return;
@@ -24430,6 +24548,39 @@ var init_server_validation = __esm({
24430
24548
  }
24431
24549
  });
24432
24550
 
24551
+ // libs/sdk/src/flows/flow.http-path.ts
24552
+ function matchMountedPath(pattern, path3) {
24553
+ if (!pattern.includes("/:")) {
24554
+ const prefix = pattern.endsWith("/") ? pattern : `${pattern}/`;
24555
+ return path3 === pattern || path3.startsWith(prefix) ? {} : void 0;
24556
+ }
24557
+ const mount = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
24558
+ const expected = mount.split("/");
24559
+ const actual = path3.split("/");
24560
+ if (actual.length < expected.length) return void 0;
24561
+ const params = {};
24562
+ for (let i = 0; i < expected.length; i++) {
24563
+ const segment = expected[i];
24564
+ const value = actual[i];
24565
+ if (segment.length > 1 && segment.startsWith(":")) {
24566
+ if (value.length === 0) return void 0;
24567
+ try {
24568
+ params[segment.slice(1)] = decodeURIComponent(value);
24569
+ } catch {
24570
+ return void 0;
24571
+ }
24572
+ } else if (segment !== value) {
24573
+ return void 0;
24574
+ }
24575
+ }
24576
+ return params;
24577
+ }
24578
+ var init_flow_http_path = __esm({
24579
+ "libs/sdk/src/flows/flow.http-path.ts"() {
24580
+ "use strict";
24581
+ }
24582
+ });
24583
+
24433
24584
  // libs/sdk/src/flows/flow.stages.ts
24434
24585
  function sortStageMap(table) {
24435
24586
  for (const key of Object.keys(table)) {
@@ -24537,6 +24688,7 @@ var init_flow_instance = __esm({
24537
24688
  init_context();
24538
24689
  init_errors();
24539
24690
  init_server_validation();
24691
+ init_flow_http_path();
24540
24692
  init_flow_stages();
24541
24693
  FAILED_OUTCOMES = /* @__PURE__ */ new Set(["fail", "abort", "unknown_error"]);
24542
24694
  CAP = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
@@ -24656,12 +24808,16 @@ var init_flow_instance = __esm({
24656
24808
  const hasMwCanActivate = (mw.canActivate?.length ?? 0) > 0;
24657
24809
  if (!path3 && !hasStaticCanActivate && !hasMwCanActivate) return false;
24658
24810
  if (this.method && request.method !== this.method) return false;
24659
- if (path3) {
24660
- const reqPath = request.path;
24661
- const prefix = path3.endsWith("/") ? path3 : `${path3}/`;
24662
- if (reqPath !== path3 && !reqPath.startsWith(prefix)) return false;
24811
+ const captured = path3 ? matchMountedPath(path3, request.path) : {};
24812
+ if (!captured) return false;
24813
+ const previous = request.params;
24814
+ if (Object.keys(captured).length > 0) request.params = { ...captured, ...previous };
24815
+ const activated = await this.canActivate(request);
24816
+ if (!activated) {
24817
+ if (previous === void 0) delete request.params;
24818
+ else request.params = previous;
24663
24819
  }
24664
- return this.canActivate(request);
24820
+ return activated;
24665
24821
  }
24666
24822
  /**
24667
24823
  * Get FrontMcpContextStorage from providers (with fallback).
@@ -25656,6 +25812,11 @@ var init_execution_context_interface = __esm({
25656
25812
  * Authority/authorization checks still run — an internal call carries the
25657
25813
  * same `authInfo` as the surrounding request, so ABAC/RBAC guards apply.
25658
25814
  *
25815
+ * `availableWhen.surface`: the call carries {@link callToolSurface}. From a tool, resource or
25816
+ * prompt that is none (in-process dispatch, which no surface restricts); a job's call carries
25817
+ * `'job'`, an agent's `'agent'`, and an HTTP trigger's `'http-trigger'`, so a tool whose `surface`
25818
+ * leaves that caller out answers as an unknown tool.
25819
+ *
25659
25820
  * @param name Tool name (or fully-qualified `owner.name`).
25660
25821
  * @param args Tool arguments — validated by the tool's input schema.
25661
25822
  * @param opts Optional progress token / abort signal forwarded into `_meta`.
@@ -25673,10 +25834,12 @@ var init_execution_context_interface = __esm({
25673
25834
  ...Object.keys(requestMeta).length > 0 && { _meta: requestMeta }
25674
25835
  }
25675
25836
  };
25837
+ const surface = this.callToolSurface();
25676
25838
  const ctx = {
25677
25839
  authInfo: this.getAuthInfo(),
25678
25840
  requestId: this.tryGetContext()?.requestId ?? this.runId,
25679
25841
  internalCall: true,
25842
+ ...surface && { surface },
25680
25843
  ...opts?.signal && { signal: opts.signal }
25681
25844
  };
25682
25845
  const result = await scope.runFlow("tools:call-tool", { request, ctx });
@@ -25685,6 +25848,14 @@ var init_execution_context_interface = __esm({
25685
25848
  }
25686
25849
  return result;
25687
25850
  }
25851
+ /**
25852
+ * The surface (`availableWhen.surface`) a {@link callTool} from this context calls on. None by
25853
+ * default: a tool, resource or prompt composing with other tools is in-process dispatch. Contexts
25854
+ * that are callers of their own (jobs, agents, HTTP triggers) name their surface.
25855
+ */
25856
+ callToolSurface() {
25857
+ return void 0;
25858
+ }
25688
25859
  /**
25689
25860
  * Fail the execution and trigger error handling.
25690
25861
  */
@@ -27175,7 +27346,11 @@ var init_skill_events = __esm({
27175
27346
  */
27176
27347
  emit(event) {
27177
27348
  for (const listener of [...this.listeners]) {
27178
- listener(event);
27349
+ try {
27350
+ listener(event);
27351
+ } catch (err) {
27352
+ console.error("SkillEmitter listener error:", err instanceof Error ? err.message : "Unknown error");
27353
+ }
27179
27354
  }
27180
27355
  }
27181
27356
  /**
@@ -27456,6 +27631,24 @@ var init_skill_instance = __esm({
27456
27631
  // libs/sdk/src/skill/skill.registry.ts
27457
27632
  import { tokenName as tokenName9 } from "@frontmcp/di";
27458
27633
  import { getRuntimeContext as getRuntimeContext11, isEntryAvailable as isEntryAvailable5 } from "@frontmcp/utils";
27634
+ function findIdPathCollision(skills) {
27635
+ const byPath = /* @__PURE__ */ new Map();
27636
+ for (const skill of skills) {
27637
+ const path3 = skill.getSkillPath();
27638
+ if (!byPath.has(path3)) byPath.set(path3, skill);
27639
+ }
27640
+ for (const idOwner of skills) {
27641
+ const id = idOwner.metadata.id;
27642
+ if (!id || id === idOwner.getSkillPath()) continue;
27643
+ const pathOwner = byPath.get(id);
27644
+ if (pathOwner) return { idOwner, pathOwner };
27645
+ }
27646
+ return void 0;
27647
+ }
27648
+ function describeIdPathCollision({ idOwner, pathOwner }) {
27649
+ const id = idOwner.metadata.id;
27650
+ return `id "${id}" of skill "${idOwner.metadata.name}" is the skill:// path of skill "${pathOwner.metadata.name}", so skill://${id}/SKILL.md would serve one for the other. Give one of them another id or name.`;
27651
+ }
27459
27652
  var DEFAULT_LIST_LIMIT, SkillRegistry;
27460
27653
  var init_skill_registry = __esm({
27461
27654
  "libs/sdk/src/skill/skill.registry.ts"() {
@@ -27487,16 +27680,34 @@ var init_skill_registry = __esm({
27487
27680
  dynamicContents = /* @__PURE__ */ new Map();
27488
27681
  /**
27489
27682
  * Per-id generation counter for dynamic registrations. Bumped on every
27490
- * registerSkillContent call so a stale unregister handle held by an earlier
27491
- * caller cannot remove a replacement registration.
27683
+ * registerSkillContent call, and on every removal, so a stale unregister
27684
+ * handle held by an earlier caller cannot remove a later registration. Never
27685
+ * reset: a counter restarted at 1 would match an old handle's generation.
27492
27686
  */
27493
27687
  dynamicGenerations = /* @__PURE__ */ new Map();
27688
+ /**
27689
+ * Ids of skills that left the registry (its own dynamic skills, or an adopted child's) whose copy
27690
+ * in the storage provider is not removed yet: the removal is pending, or the provider failed it.
27691
+ * Provider-backed reads skip them until the removal succeeds or the id is served again. Each maps
27692
+ * to the number of its withdrawal.
27693
+ */
27694
+ withdrawn = /* @__PURE__ */ new Map();
27695
+ /** Numbers each withdrawal, so a removal that completes late clears only its own. */
27696
+ withdrawals = 0;
27697
+ /** The last storage-provider write queued per skill id ({@link syncProvider}). */
27698
+ providerWrites = /* @__PURE__ */ new Map();
27699
+ /** Whether a plugin registers skills at runtime ({@link expectDynamicSkills}). */
27700
+ dynamicSkillsExpected = false;
27494
27701
  /** Adopted skills from child registries */
27495
27702
  adopted = /* @__PURE__ */ new Map();
27496
27703
  /** Children registries */
27497
27704
  children = /* @__PURE__ */ new Set();
27705
+ /** The registries that adopted this one: a skill registered here is served through them too. */
27706
+ adopters = /* @__PURE__ */ new Set();
27498
27707
  /** O(1) indexes */
27499
27708
  byQualifiedId = /* @__PURE__ */ new Map();
27709
+ /** Owner lineage per skill instance, rebuilt with the index so `lineageOf` is O(1). */
27710
+ lineages = new EntryLineageIndex();
27500
27711
  byName = /* @__PURE__ */ new Map();
27501
27712
  byOwnerAndName = /* @__PURE__ */ new Map();
27502
27713
  /** Version and emitter for change tracking */
@@ -27577,8 +27788,10 @@ var init_skill_registry = __esm({
27577
27788
  const row = this.makeRow(token, instance, lineage, this);
27578
27789
  this.localRows.push(row);
27579
27790
  try {
27580
- const content = await instance.load();
27581
- await this.storageProvider.add(content);
27791
+ await this.syncProvider(instance.metadata.id ?? instance.metadata.name, async () => {
27792
+ const content = await instance.load();
27793
+ await this.storageProvider.add(content);
27794
+ });
27582
27795
  } catch (error) {
27583
27796
  this.scope.logger.warn(`Failed to load skill ${instance.name}: ${error.message}`);
27584
27797
  }
@@ -27601,6 +27814,10 @@ var init_skill_registry = __esm({
27601
27814
  await this.adoptFromChild(child, child.owner);
27602
27815
  }
27603
27816
  }
27817
+ const collision = findIdPathCollision(this.listAllInstances());
27818
+ if (collision) {
27819
+ throw new InvalidSkillError(collision.idOwner.metadata.name, describeIdPathCollision(collision));
27820
+ }
27604
27821
  this.reindex();
27605
27822
  logAvailabilityFiltering(
27606
27823
  "SkillRegistry",
@@ -27620,24 +27837,31 @@ var init_skill_registry = __esm({
27620
27837
  const adoptedRows = childRows.map((r) => this.relineage(r, prepend));
27621
27838
  this.adopted.set(child, adoptedRows);
27622
27839
  this.children.add(child);
27840
+ child.adopters.add(this);
27623
27841
  child.subscribe({ immediate: false }, () => {
27842
+ const previous = this.adopted.get(child) ?? [];
27624
27843
  const latest = child.listAllIndexed().map((r) => this.relineage(r, prepend));
27625
27844
  this.adopted.set(child, latest);
27626
27845
  this.reindex();
27627
- this.bump("reset");
27846
+ this.withdrawGoneAdoptedSkills(previous);
27628
27847
  for (const row of latest) {
27629
- row.instance.load().then((content) => {
27630
- this.storageProvider.add(content);
27848
+ const skillId = row.instance.metadata.id ?? row.instance.metadata.name;
27849
+ this.syncProvider(skillId, async () => {
27850
+ const content = await row.instance.load();
27851
+ await this.storageProvider.add(content);
27631
27852
  }).catch((error) => {
27632
27853
  this.scope.logger.warn(`Failed to reload adopted skill ${row.baseName}: ${error.message}`);
27633
27854
  });
27634
27855
  }
27856
+ this.bump("reset");
27635
27857
  });
27636
27858
  await Promise.all(
27637
27859
  adoptedRows.map(async (row) => {
27638
27860
  try {
27639
- const content = await row.instance.load();
27640
- await this.storageProvider.add(content);
27861
+ await this.syncProvider(row.instance.metadata.id ?? row.instance.metadata.name, async () => {
27862
+ const content = await row.instance.load();
27863
+ await this.storageProvider.add(content);
27864
+ });
27641
27865
  } catch (error) {
27642
27866
  this.scope.logger.warn(`Failed to load adopted skill ${row.baseName}: ${error.message}`);
27643
27867
  }
@@ -27646,6 +27870,37 @@ var init_skill_registry = __esm({
27646
27870
  this.reindex();
27647
27871
  this.bump("reset");
27648
27872
  }
27873
+ /**
27874
+ * The `skill://` namespaces this registry's skills are served in: its own, and that of every
27875
+ * registry that adopts it, directly or through another, sibling registries included.
27876
+ */
27877
+ namespacesServingThis() {
27878
+ const registries = /* @__PURE__ */ new Set();
27879
+ const climb = (registry) => {
27880
+ if (registries.has(registry)) return;
27881
+ registries.add(registry);
27882
+ for (const adopter of registry.adopters) climb(adopter);
27883
+ };
27884
+ climb(this);
27885
+ return [...registries].map((registry) => registry.liveSkills());
27886
+ }
27887
+ /**
27888
+ * This registry's skills and those of every registry it adopted, read from each registry's own rows.
27889
+ * An adopter's copy of a child's rows is refreshed only on the child's change event, which a
27890
+ * registration emits after it has committed its row and awaited the storage provider.
27891
+ */
27892
+ liveSkills() {
27893
+ const registries = /* @__PURE__ */ new Set();
27894
+ const skills = /* @__PURE__ */ new Set();
27895
+ const descend = (registry) => {
27896
+ if (registries.has(registry)) return;
27897
+ registries.add(registry);
27898
+ for (const row of [...registry.localRows, ...registry.dynamicRows]) skills.add(row.instance);
27899
+ for (const child of registry.children) descend(child);
27900
+ };
27901
+ descend(this);
27902
+ return [...skills];
27903
+ }
27649
27904
  /* -------------------- Public API -------------------- */
27650
27905
  /**
27651
27906
  * Get all skills in the registry.
@@ -27763,7 +28018,14 @@ var init_skill_registry = __esm({
27763
28018
  }
27764
28019
  }
27765
28020
  async search(query, options) {
27766
- const baseResults = await this.storageProvider.search(query, options);
28021
+ const topK = options?.topK;
28022
+ const hidden = this.hiddenFromProviderRead();
28023
+ const providerOptions = hidden.size > 0 && typeof topK === "number" && topK >= 0 ? { ...options, topK: topK + hidden.size } : options;
28024
+ let baseResults = await this.storageProvider.search(query, providerOptions);
28025
+ if (hidden.size > 0 || this.withdrawn.size > 0) {
28026
+ baseResults = baseResults.filter((r) => !hidden.has(r.metadata));
28027
+ if (typeof topK === "number" && topK >= 0) baseResults = baseResults.slice(0, topK);
28028
+ }
27767
28029
  if (this.dynamicContents.size === 0) return baseResults;
27768
28030
  const baseSeen = /* @__PURE__ */ new Set();
27769
28031
  let rewrites = 0;
@@ -27787,7 +28049,6 @@ var init_skill_registry = __esm({
27787
28049
  const overlayOnly = this.searchDynamicOverlay(query, options, baseSeen);
27788
28050
  if (overlayOnly.length === 0 && rewrites === 0) return baseResults;
27789
28051
  const merged = [...rewrittenBase, ...overlayOnly].sort((a, b) => b.score - a.score);
27790
- const topK = options?.topK;
27791
28052
  return typeof topK === "number" && topK >= 0 ? merged.slice(0, topK) : merged;
27792
28053
  }
27793
28054
  /**
@@ -27842,7 +28103,10 @@ var init_skill_registry = __esm({
27842
28103
  warning
27843
28104
  };
27844
28105
  }
28106
+ const hidden = this.hiddenFromProviderRead();
28107
+ if (hidden.has({ name: skillId })) return void 0;
27845
28108
  const result = await this.storageProvider.load(skillId);
28109
+ if (result && hidden.has(result.skill)) return void 0;
27846
28110
  return result ?? void 0;
27847
28111
  }
27848
28112
  /**
@@ -27878,8 +28142,12 @@ var init_skill_registry = __esm({
27878
28142
  * and `hasMore` stay correct.
27879
28143
  */
27880
28144
  async listSkills(options) {
27881
- if (this.dynamicContents.size === 0) return this.storageProvider.list(options);
27882
- const base = await this.listAllFromProvider(options);
28145
+ const hidden = this.hiddenFromProviderRead();
28146
+ if (this.dynamicContents.size === 0 && hidden.size === 0) {
28147
+ const page = await this.storageProvider.list(options);
28148
+ if (this.withdrawn.size === 0) return page;
28149
+ }
28150
+ const base = (await this.listAllFromProvider(options)).filter((meta) => !hidden.has(meta));
27883
28151
  const baseIds = /* @__PURE__ */ new Set();
27884
28152
  const merged = base.map((meta) => {
27885
28153
  const id = meta.id ?? meta.name;
@@ -27911,30 +28179,48 @@ var init_skill_registry = __esm({
27911
28179
  if (!page.hasMore || page.skills.length === 0) return all;
27912
28180
  }
27913
28181
  }
28182
+ /**
28183
+ * Owner lineage (root → leaf) of a skill, or undefined when no row holds it. A skill a plugin
28184
+ * contributes is owned by the plugin; its lineage names the app the plugin is installed on.
28185
+ */
28186
+ lineageOf(entry) {
28187
+ return this.lineages.lineageOf(entry);
28188
+ }
27914
28189
  /**
27915
28190
  * Check if any skills exist.
27916
28191
  */
27917
28192
  hasAny() {
27918
28193
  return this.listAllIndexed().length > 0;
27919
28194
  }
28195
+ /**
28196
+ * Record that a plugin registers skills at runtime ({@link registerSkillContent}), such as a
28197
+ * skill bundle loaded after the server starts. The scope then serves the skills capability and
28198
+ * methods from startup, answering with the skills registered so far.
28199
+ */
28200
+ expectDynamicSkills() {
28201
+ this.dynamicSkillsExpected = true;
28202
+ }
28203
+ /** Whether the scope serves skills: it has some, or a plugin registers them at runtime. */
28204
+ servesSkills() {
28205
+ return this.dynamicSkillsExpected || this.hasAny();
28206
+ }
27920
28207
  /**
27921
28208
  * Get total skill count.
27922
28209
  *
27923
- * Sums the provider count with overlay rows the provider does not know
28210
+ * Counts the provider's rows with overlay rows the provider does not know
27924
28211
  * about so callers see the same surface as `search` / `listSkills`.
27925
28212
  */
27926
28213
  async count(options) {
27927
- const baseCount = await this.storageProvider.count(options);
27928
- if (this.dynamicContents.size === 0) return baseCount;
27929
- const baseList = await this.storageProvider.list({
27930
- tags: options?.tags,
27931
- includeHidden: options?.includeHidden,
27932
- limit: Number.MAX_SAFE_INTEGER
27933
- });
28214
+ const hidden = this.hiddenFromProviderRead();
28215
+ if (this.dynamicContents.size === 0 && hidden.size === 0) {
28216
+ const counted = await this.storageProvider.count(options);
28217
+ if (this.withdrawn.size === 0) return counted;
28218
+ }
28219
+ const baseList = (await this.listAllFromProvider({ tags: options?.tags, includeHidden: options?.includeHidden })).filter((m) => !hidden.has(m));
27934
28220
  const baseIds = /* @__PURE__ */ new Set();
27935
- for (const m of baseList.skills) baseIds.add(m.id ?? m.name);
28221
+ for (const m of baseList) baseIds.add(m.id ?? m.name);
27936
28222
  const overlay = this.collectDynamicMetadata(options).filter((m) => !baseIds.has(m.id ?? m.name));
27937
- return baseCount + overlay.length;
28223
+ return baseList.length + overlay.length;
27938
28224
  }
27939
28225
  /**
27940
28226
  * Project a SkillContent into the SkillMetadata shape callers expect from
@@ -28060,7 +28346,7 @@ var init_skill_registry = __esm({
28060
28346
  * of the schema cutover see the declaration.
28061
28347
  */
28062
28348
  getCapabilities() {
28063
- if (!this.hasAny()) return {};
28349
+ if (!this.servesSkills()) return {};
28064
28350
  return {
28065
28351
  experimental: {
28066
28352
  [SEP_2640_EXTENSION_ID]: {}
@@ -28224,10 +28510,6 @@ var init_skill_registry = __esm({
28224
28510
  throw new PublicMcpError("registerSkillContent: SkillContent.name is required", "INVALID_PARAMS");
28225
28511
  }
28226
28512
  const id = content.id;
28227
- const existingIdx = this.dynamicRows.findIndex((r) => r.instance.name === id);
28228
- if (existingIdx !== -1) {
28229
- this.dynamicRows.splice(existingIdx, 1);
28230
- }
28231
28513
  const metadata = {
28232
28514
  id,
28233
28515
  name: content.name,
@@ -28263,28 +28545,49 @@ var init_skill_registry = __esm({
28263
28545
  `[SkillRegistry] Failed to pre-load dynamic skill ${id} from source ${opts?.source ?? "unknown"}: ${error.message}`
28264
28546
  );
28265
28547
  }
28548
+ const existingIdx = this.dynamicRows.findIndex((r) => r.instance.name === id);
28549
+ const superseded = new Set(opts?.supersedes);
28550
+ const leaving = new Set(
28551
+ this.dynamicRows.filter((r) => r.instance.name === id || superseded.has(r.instance.name)).map((r) => r.instance)
28552
+ );
28553
+ for (const skills of this.namespacesServingThis()) {
28554
+ const collision = findIdPathCollision([...skills.filter((other) => !leaving.has(other)), instance]);
28555
+ if (collision) {
28556
+ throw new PublicMcpError(`registerSkillContent: ${describeIdPathCollision(collision)}`, "INVALID_PARAMS");
28557
+ }
28558
+ }
28559
+ if (existingIdx !== -1) {
28560
+ this.dynamicRows.splice(existingIdx, 1);
28561
+ }
28562
+ const mutable = this.asMutableProvider(this.storageProvider);
28563
+ const removed = this.dynamicRows.filter((r) => superseded.has(r.instance.name) && findIdPathCollision([r.instance, instance])).map((r) => r.instance.name);
28564
+ for (const removedId of removed) {
28565
+ this.withdrawDynamicSkill(removedId, mutable);
28566
+ }
28266
28567
  const lineage = this.owner ? [this.owner] : [];
28267
28568
  const row = this.makeRow(provideToken, instance, lineage, this);
28268
28569
  this.dynamicRows.push(row);
28269
28570
  this.dynamicContents.set(id, structuredClone(content));
28571
+ this.withdrawn.delete(id);
28270
28572
  const generation = (this.dynamicGenerations.get(id) ?? 0) + 1;
28271
28573
  this.dynamicGenerations.set(id, generation);
28272
- const mutable = this.asMutableProvider(this.storageProvider);
28273
- if (mutable) {
28274
- try {
28574
+ this.reindex();
28575
+ const providerWrites = mutable ? [
28576
+ ...removed.map((removedId) => this.removeFromProvider(mutable, removedId)),
28577
+ this.syncProvider(id, async () => {
28275
28578
  if (await mutable.exists(id)) {
28276
28579
  await mutable.update(id, content);
28277
28580
  } else {
28278
28581
  await mutable.add(content);
28279
28582
  }
28280
- } catch (error) {
28583
+ }).catch((error) => {
28281
28584
  this.scope.logger.warn(
28282
28585
  `[SkillRegistry] Failed to index dynamic skill ${id} in storage provider: ${error.message}`
28283
28586
  );
28284
- }
28285
- }
28286
- this.reindex();
28587
+ })
28588
+ ] : [];
28287
28589
  this.bump("reset");
28590
+ await Promise.all(providerWrites);
28288
28591
  let unregistered = false;
28289
28592
  const unregister = async () => {
28290
28593
  if (unregistered) return;
@@ -28297,32 +28600,99 @@ var init_skill_registry = __esm({
28297
28600
  this.scope.logger.debug(
28298
28601
  `[SkillRegistry] Registered dynamic skill ${id}${opts?.source ? ` (source: ${opts.source})` : ""}`
28299
28602
  );
28300
- return { id, unregister };
28603
+ return { id, unregister, removed };
28301
28604
  }
28302
28605
  /**
28303
28606
  * Remove a previously-registered dynamic skill by id.
28304
28607
  */
28305
28608
  async unregisterSkill(id) {
28306
- const idx = this.dynamicRows.findIndex((r) => r.instance.name === id);
28307
- if (idx === -1) return false;
28308
- this.dynamicRows.splice(idx, 1);
28309
- this.dynamicContents.delete(id);
28310
- this.dynamicGenerations.delete(id);
28609
+ if (!this.dynamicRows.some((r) => r.instance.name === id)) return false;
28311
28610
  const mutable = this.asMutableProvider(this.storageProvider);
28312
- if (mutable) {
28313
- try {
28314
- await mutable.remove(id);
28315
- } catch (error) {
28316
- this.scope.logger.warn(
28317
- `[SkillRegistry] Failed to remove dynamic skill ${id} from storage provider: ${error.message}`
28318
- );
28319
- }
28320
- }
28611
+ this.withdrawDynamicSkill(id, mutable);
28321
28612
  this.reindex();
28613
+ const removal = mutable ? this.removeFromProvider(mutable, id) : void 0;
28322
28614
  this.bump("reset");
28615
+ await removal;
28323
28616
  this.scope.logger.debug(`[SkillRegistry] Unregistered dynamic skill ${id}`);
28324
28617
  return true;
28325
28618
  }
28619
+ /**
28620
+ * Take a dynamic skill out of the registry, synchronously. Its generation advances so a handle
28621
+ * from an earlier registration stays a no-op, and while a mutable provider still holds its copy it
28622
+ * is withdrawn from provider-backed reads.
28623
+ */
28624
+ withdrawDynamicSkill(id, mutable) {
28625
+ this.dynamicRows.splice(
28626
+ this.dynamicRows.findIndex((r) => r.instance.name === id),
28627
+ 1
28628
+ );
28629
+ this.dynamicContents.delete(id);
28630
+ this.dynamicGenerations.set(id, (this.dynamicGenerations.get(id) ?? 0) + 1);
28631
+ if (mutable) this.withdrawn.set(id, ++this.withdrawals);
28632
+ }
28633
+ /** Remove a withdrawn skill's copy from the provider. If that fails it stays withdrawn from reads. */
28634
+ async removeFromProvider(mutable, id) {
28635
+ const withdrawal = this.withdrawn.get(id);
28636
+ try {
28637
+ await this.syncProvider(id, () => mutable.remove(id));
28638
+ if (this.withdrawn.get(id) === withdrawal) this.withdrawn.delete(id);
28639
+ } catch (error) {
28640
+ this.scope.logger.warn(
28641
+ `[SkillRegistry] Failed to remove skill ${id} from storage provider; it stays hidden from provider-backed reads: ${error.message}`
28642
+ );
28643
+ }
28644
+ }
28645
+ /**
28646
+ * Run a storage-provider write for one skill id after the ones queued before it for that id, so a
28647
+ * removal that completes late cannot delete the copy a later registration added. The returned
28648
+ * promise settles with the write; a failure does not hold back the next one.
28649
+ */
28650
+ syncProvider(id, write) {
28651
+ const run = (this.providerWrites.get(id) ?? Promise.resolve()).then(write);
28652
+ const settled = run.then(
28653
+ () => void 0,
28654
+ () => void 0
28655
+ );
28656
+ this.providerWrites.set(id, settled);
28657
+ void settled.then(() => {
28658
+ if (this.providerWrites.get(id) === settled) this.providerWrites.delete(id);
28659
+ });
28660
+ return run;
28661
+ }
28662
+ /**
28663
+ * After a child's change, take the skills that left it (and are not served here some other way)
28664
+ * out of this registry's storage provider too. The re-add on change only adds, so without this an
28665
+ * adopter's provider kept serving a skill the child had removed. Until the removal completes, and
28666
+ * if it fails, provider-backed reads skip it.
28667
+ */
28668
+ withdrawGoneAdoptedSkills(previous) {
28669
+ const mutable = this.asMutableProvider(this.storageProvider);
28670
+ if (!mutable) return;
28671
+ const skillIdOf = (entry) => entry.metadata.id ?? entry.metadata.name;
28672
+ const live = new Set(this.listAllIndexed().map((r) => skillIdOf(r.instance)));
28673
+ for (const id of live) this.withdrawn.delete(id);
28674
+ for (const row of previous) {
28675
+ const id = skillIdOf(row.instance);
28676
+ if (live.has(id) || this.withdrawn.has(id)) continue;
28677
+ this.withdrawn.set(id, ++this.withdrawals);
28678
+ void this.removeFromProvider(mutable, id);
28679
+ }
28680
+ }
28681
+ /**
28682
+ * Which provider-backed rows a read that starts now must skip: a skill withdrawn when the read
28683
+ * starts (its removal may complete while the provider answers, clearing the withdrawal, yet the
28684
+ * answer can still hold it), or withdrawn by the time the answer arrives.
28685
+ */
28686
+ hiddenFromProviderRead() {
28687
+ const atStart = new Set(this.withdrawn.keys());
28688
+ return {
28689
+ size: atStart.size,
28690
+ has: (meta) => {
28691
+ const id = meta.id ?? meta.name;
28692
+ return atStart.has(id) || this.withdrawn.has(id);
28693
+ }
28694
+ };
28695
+ }
28326
28696
  /* -------------------- Internal helpers -------------------- */
28327
28697
  asMutableProvider(provider) {
28328
28698
  const candidate = provider;
@@ -28339,6 +28709,7 @@ var init_skill_registry = __esm({
28339
28709
  }
28340
28710
  reindex() {
28341
28711
  const effective = this.listAllIndexed();
28712
+ this.lineages.rebuild(effective);
28342
28713
  this.byQualifiedId.clear();
28343
28714
  this.byName.clear();
28344
28715
  this.byOwnerAndName.clear();
@@ -29263,13 +29634,15 @@ var init_agent_utils = __esm({
29263
29634
  // libs/sdk/src/agent/agent.instance.ts
29264
29635
  import { AuthorityDeniedError as AuthorityDeniedError6 } from "@frontmcp/auth";
29265
29636
  import { toJSONSchema as toJSONSchema2, z as z59 } from "@frontmcp/lazy-zod";
29266
- var VALID_AGENT_HOOK_FLOWS, AGENT_ONLY_METADATA_KEYS, AgentInstance, FunctionAgentContext;
29637
+ var VALID_AGENT_HOOK_FLOWS, AGENT_SURFACE, AGENT_ONLY_METADATA_KEYS, AgentInstance, FunctionAgentContext;
29267
29638
  var init_agent_instance = __esm({
29268
29639
  "libs/sdk/src/agent/agent.instance.ts"() {
29269
29640
  "use strict";
29270
29641
  init_config();
29271
29642
  init_common();
29643
+ init_availability();
29272
29644
  init_tool_decorator();
29645
+ init_call_surface();
29273
29646
  init_running_tool();
29274
29647
  init_errors();
29275
29648
  init_hooks_utils();
@@ -29279,6 +29652,7 @@ var init_agent_instance = __esm({
29279
29652
  init_agent_scope();
29280
29653
  init_agent_utils();
29281
29654
  VALID_AGENT_HOOK_FLOWS = ["agents:call-agent", "agents:list-agents"];
29655
+ AGENT_SURFACE = "agent";
29282
29656
  AGENT_ONLY_METADATA_KEYS = /* @__PURE__ */ new Set([
29283
29657
  "id",
29284
29658
  "name",
@@ -29301,7 +29675,7 @@ var init_agent_instance = __esm({
29301
29675
  "hideFromDiscovery"
29302
29676
  ]);
29303
29677
  AgentInstance = class extends AgentEntry {
29304
- providers;
29678
+ providerRegistry;
29305
29679
  scope;
29306
29680
  hooks;
29307
29681
  /** The LLM adapter for this agent */
@@ -29319,11 +29693,11 @@ var init_agent_instance = __esm({
29319
29693
  constructor(record, providers, owner) {
29320
29694
  super(record);
29321
29695
  this.owner = owner;
29322
- this.providers = providers;
29696
+ this.providerRegistry = providers;
29323
29697
  this.name = record.metadata.id ?? record.metadata.name;
29324
29698
  this.id = record.metadata.id ?? record.metadata.name;
29325
29699
  this.fullName = this.owner.id + ":" + this.name;
29326
- this.scope = this.providers.getActiveScope();
29700
+ this.scope = this.providerRegistry.getActiveScope();
29327
29701
  this.hooks = this.scope.hooks;
29328
29702
  this.inputSchema = record.metadata.inputSchema ?? {};
29329
29703
  this.outputSchema = record.metadata.outputSchema;
@@ -29382,11 +29756,11 @@ var init_agent_instance = __esm({
29382
29756
  const adapterOptions = {
29383
29757
  providerResolver: {
29384
29758
  get: (token) => {
29385
- return this.providers.get(token);
29759
+ return this.providerRegistry.get(token);
29386
29760
  },
29387
29761
  tryGet: (token) => {
29388
29762
  try {
29389
- return this.providers.get(token);
29763
+ return this.providerRegistry.get(token);
29390
29764
  } catch {
29391
29765
  return void 0;
29392
29766
  }
@@ -29399,7 +29773,7 @@ var init_agent_instance = __esm({
29399
29773
  }
29400
29774
  };
29401
29775
  try {
29402
- const configService = this.providers.get(ConfigService);
29776
+ const configService = this.providerRegistry.get(ConfigService);
29403
29777
  if (configService) {
29404
29778
  adapterOptions.configResolver = this.createConfigResolver(configService);
29405
29779
  }
@@ -29507,7 +29881,7 @@ var init_agent_instance = __esm({
29507
29881
  const toolMetadata = this.buildAgentToolMetadata();
29508
29882
  const agentToolFunction = frontMcpTool(toolMetadata)(this.createAgentToolExecuteHandler());
29509
29883
  const toolRecord = normalizeTool(agentToolFunction);
29510
- this.agentToolInstance = new ToolInstance(toolRecord, this.providers, this.owner);
29884
+ this.agentToolInstance = new ToolInstance(toolRecord, this.providerRegistry, this.owner);
29511
29885
  await this.agentToolInstance.ready;
29512
29886
  this.scope.logger.debug(`Created agent tool instance: ${this.agentToolInstance.name} for agent ${this.name}`);
29513
29887
  } catch (error) {
@@ -29549,16 +29923,34 @@ var init_agent_instance = __esm({
29549
29923
  /**
29550
29924
  * Create the execute handler for the agent tool.
29551
29925
  *
29552
- * This handler is called when the tool is invoked through the standard
29553
- * tools:call-tool flow. It creates an AgentContext and runs the LLM loop.
29926
+ * The tool's `tools:call-tool` flow has already applied everything the agent declares
29927
+ * (authorities, rate limit, concurrency, timeout and plugin gates, copied onto the tool). The
29928
+ * handler then runs the agent through the `agents:call-agent` flow, so the hooks registered for
29929
+ * agent invocation (`AgentCallHook`, and the agent class's own hooks on that flow) run and the
29930
+ * agent's context gets the request's context providers. The flow's own gates are left to the
29931
+ * tool flow (`gatedBy`), so nothing is counted twice.
29554
29932
  */
29555
29933
  createAgentToolExecuteHandler() {
29556
29934
  return async (input, toolCtx) => {
29557
29935
  const authInfo = toolCtx.authInfo;
29558
- const agentCallExtra = { authInfo };
29559
- const agentContext = this.create(input, agentCallExtra);
29560
- const result = await agentContext.execute(input);
29561
- return result;
29936
+ const progressToken = toolCtx._progressTokenInternal;
29937
+ const requestId = toolCtx._jsonRpcRequestIdInternal;
29938
+ return this.scope.runFlowForOutput("agents:call-agent", {
29939
+ request: {
29940
+ method: "tools/call",
29941
+ params: {
29942
+ name: this.id,
29943
+ arguments: input,
29944
+ ...progressToken !== void 0 ? { _meta: { progressToken } } : {}
29945
+ }
29946
+ },
29947
+ ctx: {
29948
+ authInfo,
29949
+ ...requestId !== void 0 ? { requestId } : {},
29950
+ ...toolCtx.signal ? { signal: toolCtx.signal } : {}
29951
+ },
29952
+ gatedBy: "tools:call-tool"
29953
+ });
29562
29954
  };
29563
29955
  }
29564
29956
  /**
@@ -29572,6 +29964,10 @@ var init_agent_instance = __esm({
29572
29964
  getToolInstance() {
29573
29965
  return this.agentToolInstance;
29574
29966
  }
29967
+ /** The provider registry this agent was registered with (its app's). */
29968
+ get providers() {
29969
+ return this.providerRegistry;
29970
+ }
29575
29971
  /**
29576
29972
  * The tools this agent can call: those it declares and those its plugins contribute, as its private
29577
29973
  * scope holds them. Empty until the agent is initialized.
@@ -29621,7 +30017,7 @@ var init_agent_instance = __esm({
29621
30017
  case "VALUE" /* VALUE */:
29622
30018
  return this.record.useValue;
29623
30019
  case "FACTORY" /* FACTORY */:
29624
- return this.record.useFactory(this.providers);
30020
+ return this.record.useFactory(this.providerRegistry);
29625
30021
  case "ESM" /* ESM */:
29626
30022
  throw new AgentNotConfiguredError(`ESM agent "${this.name}" cannot be created via AgentInstance.create()`);
29627
30023
  case "REMOTE" /* REMOTE */:
@@ -29640,11 +30036,13 @@ var init_agent_instance = __esm({
29640
30036
  * @param llmAdapter - The LLM adapter (passed explicitly to avoid non-null assertion)
29641
30037
  */
29642
30038
  buildAgentCtorArgs(input, ctx, llmAdapter) {
29643
- const scope = this.providers.getActiveScope();
30039
+ const scope = this.providerRegistry.getActiveScope();
29644
30040
  return {
29645
30041
  metadata: this.metadata,
29646
30042
  input,
29647
- providers: this.providers,
30043
+ // The request's context-aware providers when the agents:call-agent flow built them, so
30044
+ // `this.context` and CONTEXT-scoped providers resolve inside the agent.
30045
+ providers: ctx.contextProviders ?? this.providerRegistry,
29648
30046
  logger: scope.logger,
29649
30047
  authInfo: ctx.authInfo,
29650
30048
  llmAdapter,
@@ -29655,10 +30053,17 @@ var init_agent_instance = __esm({
29655
30053
  }
29656
30054
  /**
29657
30055
  * Get tool definitions available to this agent.
29658
- * Returns only the agent's own tools (from @Agent({ tools: [...] })).
30056
+ * Returns only the agent's own tools (from @Agent({ tools: [...] })) that are offered to it.
29659
30057
  */
29660
30058
  getToolDefinitions() {
29661
- return buildAgentToolDefinitions(this.agentTools);
30059
+ return buildAgentToolDefinitions(this.getOfferedTools());
30060
+ }
30061
+ /**
30062
+ * The agent's tools its model may call: those whose `availableWhen.surface` offers them on the
30063
+ * `'agent'` surface. The others are neither shown to the model nor run for it.
30064
+ */
30065
+ getOfferedTools() {
30066
+ return this.agentTools.filter((tool) => isOfferedOnSurface(tool.metadata.availableWhen, AGENT_SURFACE));
29662
30067
  }
29663
30068
  /**
29664
30069
  * Create a tool executor for agent-scoped tools.
@@ -29681,12 +30086,13 @@ var init_agent_instance = __esm({
29681
30086
  createToolExecutor(ctx) {
29682
30087
  const useToolFlow = this.record.metadata.execution?.useToolFlow !== false;
29683
30088
  return async (toolName, args) => {
29684
- const tool = this.agentTools.find((t) => t.name === toolName || t.fullName === toolName);
30089
+ const offeredTools = this.getOfferedTools();
30090
+ const tool = offeredTools.find((t) => t.name === toolName || t.fullName === toolName);
29685
30091
  if (!tool) {
29686
30092
  throw new AgentToolNotFoundError(
29687
30093
  this.name,
29688
30094
  toolName,
29689
- this.agentTools.map((t) => t.name)
30095
+ offeredTools.map((t) => t.name)
29690
30096
  );
29691
30097
  }
29692
30098
  if (useToolFlow && this.agentScope) {
@@ -29697,16 +30103,20 @@ var init_agent_instance = __esm({
29697
30103
  },
29698
30104
  ctx: {
29699
30105
  authInfo: ctx.authInfo,
29700
- _skipUI: true
30106
+ _skipUI: true,
29701
30107
  // Skip UI rendering - agent returns structured data
30108
+ surface: AGENT_SURFACE
29702
30109
  }
29703
30110
  });
29704
30111
  return this.extractToolResult(result);
29705
30112
  }
29706
30113
  await this.assertToolAuthorized(tool, ctx.authInfo, args);
29707
30114
  const runningTool = { name: tool.name, fullName: tool.fullName };
29708
- const toolContext = runAsTool(runningTool, () => tool.create(args, ctx));
29709
- return runAsTool(runningTool, () => Promise.resolve(toolContext.execute(args)));
30115
+ return runOnSurface(AGENT_SURFACE, () => {
30116
+ const { contextProviders: _agentProviders, ...toolCtx } = ctx;
30117
+ const toolContext = runAsTool(runningTool, () => tool.create(args, toolCtx));
30118
+ return runAsTool(runningTool, () => Promise.resolve(toolContext.execute(args)));
30119
+ });
29710
30120
  };
29711
30121
  }
29712
30122
  /**
@@ -30243,6 +30653,53 @@ var init_agent_registry = __esm({
30243
30653
  }
30244
30654
  });
30245
30655
 
30656
+ // libs/sdk/src/channel/sources/completion-events.ts
30657
+ function completionEventsOf(scope) {
30658
+ let events = byScope.get(scope);
30659
+ if (!events) {
30660
+ events = new ScopeCompletionEvents();
30661
+ byScope.set(scope, events);
30662
+ }
30663
+ return events;
30664
+ }
30665
+ function completionOutputText(value) {
30666
+ if (value === void 0) return void 0;
30667
+ if (typeof value === "string") return value;
30668
+ try {
30669
+ return JSON.stringify(value);
30670
+ } catch {
30671
+ return String(value);
30672
+ }
30673
+ }
30674
+ var CompletionStream, ScopeCompletionEvents, byScope;
30675
+ var init_completion_events = __esm({
30676
+ "libs/sdk/src/channel/sources/completion-events.ts"() {
30677
+ "use strict";
30678
+ CompletionStream = class {
30679
+ listeners = /* @__PURE__ */ new Set();
30680
+ subscribe(listener) {
30681
+ this.listeners.add(listener);
30682
+ return () => {
30683
+ this.listeners.delete(listener);
30684
+ };
30685
+ }
30686
+ emit(event) {
30687
+ for (const listener of [...this.listeners]) {
30688
+ try {
30689
+ listener(event);
30690
+ } catch {
30691
+ }
30692
+ }
30693
+ }
30694
+ };
30695
+ ScopeCompletionEvents = class {
30696
+ agents = new CompletionStream();
30697
+ jobs = new CompletionStream();
30698
+ };
30699
+ byScope = /* @__PURE__ */ new WeakMap();
30700
+ }
30701
+ });
30702
+
30246
30703
  // libs/sdk/src/agent/flows/call-agent.flow.ts
30247
30704
  import { AuthorityDeniedError as AuthorityDeniedError7, resolveRequiredScopes as resolveRequiredScopes5 } from "@frontmcp/auth";
30248
30705
  import { ExecutionTimeoutError as ExecutionTimeoutError2, withTimeout as withTimeout2 } from "@frontmcp/guard";
@@ -30252,13 +30709,22 @@ var inputSchema15, outputSchema14, stateSchema14, plan14, name14, Stage14, CallA
30252
30709
  var init_call_agent_flow = __esm({
30253
30710
  "libs/sdk/src/agent/flows/call-agent.flow.ts"() {
30254
30711
  "use strict";
30712
+ init_completion_events();
30255
30713
  init_common();
30256
30714
  init_errors();
30257
30715
  init_hooks_utils();
30716
+ init_flow_context_providers();
30258
30717
  inputSchema15 = z60.object({
30259
30718
  request: CallToolRequestSchema3,
30260
30719
  // z.any() used because ctx is the MCP SDK's ToolCallExtra type which varies by SDK version
30261
- ctx: z60.any()
30720
+ ctx: z60.any(),
30721
+ /**
30722
+ * Set when the agent's `invoke_<agent>` tool runs this flow. That tool's `tools:call-tool` flow
30723
+ * has already applied the agent's authorities, rate limit, concurrency and timeout (they are
30724
+ * copied onto the tool), so this flow's stages for them leave the checks to it instead of
30725
+ * counting the call twice. Their hooks still run.
30726
+ */
30727
+ gatedBy: z60.literal("tools:call-tool").optional()
30262
30728
  });
30263
30729
  outputSchema14 = CallToolResultSchema3;
30264
30730
  stateSchema14 = z60.object({
@@ -30291,7 +30757,13 @@ var init_call_agent_flow = __esm({
30291
30757
  // Semaphore ticket for concurrency control (set by acquireSemaphore, used by releaseSemaphore)
30292
30758
  semaphoreTicket: z60.any().optional(),
30293
30759
  // A timed-out execute() that is still running; its concurrency slot is released when it settles
30294
- abandonedExecution: z60.instanceof(Promise).optional()
30760
+ abandonedExecution: z60.instanceof(Promise).optional(),
30761
+ // When execute() started, and the error it ended with (for the completion event)
30762
+ executionStartedAt: z60.number().optional(),
30763
+ executionError: z60.any().optional(),
30764
+ // The MCP result built from rawOutput (parseOutput), or why it could not be built
30765
+ parsedOutput: z60.any().optional(),
30766
+ outputError: z60.any().optional()
30295
30767
  });
30296
30768
  plan14 = {
30297
30769
  pre: [
@@ -30304,12 +30776,16 @@ var init_call_agent_flow = __esm({
30304
30776
  "acquireSemaphore"
30305
30777
  ],
30306
30778
  execute: ["validateInput", "execute", "validateOutput"],
30307
- finalize: ["releaseSemaphore", "releaseQuota", "finalize"]
30779
+ finalize: ["releaseSemaphore", "releaseQuota", "parseOutput", "emitCompletion", "finalize"]
30308
30780
  };
30309
30781
  name14 = "agents:call-agent";
30310
30782
  ({ Stage: Stage14 } = FlowHooksOf(name14));
30311
30783
  CallAgentFlow = class extends FlowBase {
30312
30784
  logger = this.scopeLogger.child("CallAgentFlow");
30785
+ /** Whether the `invoke_<agent>` tool's flow already applied the agent's gates. */
30786
+ get gatedByToolFlow() {
30787
+ return this.input.gatedBy === "tools:call-tool";
30788
+ }
30313
30789
  async parseInput() {
30314
30790
  this.logger.verbose("parseInput:start");
30315
30791
  let method;
@@ -30376,6 +30852,10 @@ var init_call_agent_flow = __esm({
30376
30852
  }
30377
30853
  async checkEntryAuthorities() {
30378
30854
  this.logger.verbose("checkEntryAuthorities:start");
30855
+ if (this.gatedByToolFlow) {
30856
+ this.logger.verbose("checkEntryAuthorities:skip (checked by the invoke tool flow)");
30857
+ return;
30858
+ }
30379
30859
  const engine = this.scope.authoritiesEngine;
30380
30860
  const ctxBuilder = this.scope.authoritiesContextBuilder;
30381
30861
  if (!engine || !ctxBuilder) {
@@ -30441,14 +30921,17 @@ var init_call_agent_flow = __esm({
30441
30921
  const progressToken = this.state.progressToken;
30442
30922
  const authInfo = this.state.authInfo;
30443
30923
  try {
30444
- const context = agent.create(input.arguments, { ...ctx, progressToken });
30924
+ const sessionKey = authInfo?.sessionId ?? "anonymous";
30925
+ const agentViews = await agent.providers.buildViews(sessionKey, new Map(this.deps), this.scope.providers);
30926
+ const contextProviders = new FlowContextProviders(agent.providers, agentViews.context);
30927
+ const context = agent.create(input.arguments, { ...ctx, progressToken, contextProviders });
30445
30928
  this.appendContextHooks(hooksBoundTo(this.scope.hooks.getClsHooks(agent.record.provide), context));
30446
30929
  context.mark("createAgentContext");
30447
30930
  const frontmcpContext = context.tryGetContext();
30448
30931
  const sdkAuthInfo = authInfo;
30449
- if (frontmcpContext && sdkAuthInfo?.transport?.sendElicitRequest) {
30932
+ const jsonRpcRequestId = this.state.jsonRpcRequestId;
30933
+ if (frontmcpContext && sdkAuthInfo?.transport?.sendElicitRequest && jsonRpcRequestId !== void 0) {
30450
30934
  const transport = sdkAuthInfo.transport;
30451
- const jsonRpcRequestId = this.state.jsonRpcRequestId;
30452
30935
  frontmcpContext.setTransport({
30453
30936
  sendElicitRequest: transport.sendElicitRequest.bind(transport),
30454
30937
  type: transport.type ?? "unknown",
@@ -30465,7 +30948,7 @@ var init_call_agent_flow = __esm({
30465
30948
  async acquireQuota() {
30466
30949
  this.logger.verbose("acquireQuota:start");
30467
30950
  const manager = this.scope.rateLimitManager;
30468
- if (!manager) {
30951
+ if (!manager || this.gatedByToolFlow) {
30469
30952
  this.state.agentContext?.mark("acquireQuota");
30470
30953
  this.logger.verbose("acquireQuota:done (no rate limit manager)");
30471
30954
  return;
@@ -30489,7 +30972,7 @@ var init_call_agent_flow = __esm({
30489
30972
  async acquireSemaphore() {
30490
30973
  this.logger.verbose("acquireSemaphore:start");
30491
30974
  const manager = this.scope.rateLimitManager;
30492
- if (!manager) {
30975
+ if (!manager || this.gatedByToolFlow) {
30493
30976
  this.state.agentContext?.mark("acquireSemaphore");
30494
30977
  this.logger.verbose("acquireSemaphore:done (no rate limit manager)");
30495
30978
  return;
@@ -30534,7 +31017,8 @@ var init_call_agent_flow = __esm({
30534
31017
  }
30535
31018
  agentContext.mark("execute");
30536
31019
  const startTime = Date.now();
30537
- const timeoutMs = agent.metadata.timeout?.executeMs ?? agent.metadata.execution?.timeout ?? this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs;
31020
+ this.state.set("executionStartedAt", startTime);
31021
+ const timeoutMs = this.gatedByToolFlow ? agent.metadata.timeout?.executeMs === void 0 ? agent.metadata.execution?.timeout : void 0 : agent.metadata.timeout?.executeMs ?? agent.metadata.execution?.timeout ?? this.scope.rateLimitManager?.config?.defaultTimeout?.executeMs;
30538
31022
  const running = (async () => {
30539
31023
  agentContext.output = await agentContext.execute(agentContext.input);
30540
31024
  })();
@@ -30545,6 +31029,7 @@ var init_call_agent_flow = __esm({
30545
31029
  });
30546
31030
  this.logger.verbose("execute:done");
30547
31031
  } catch (error) {
31032
+ this.state.set("executionError", error);
30548
31033
  if (error instanceof ExecutionTimeoutError2) {
30549
31034
  this.logger.warn("execute: agent execution timed out", {
30550
31035
  agent: agent.metadata.name,
@@ -30553,6 +31038,7 @@ var init_call_agent_flow = __esm({
30553
31038
  this.state.set("abandonedExecution", running);
30554
31039
  throw error;
30555
31040
  }
31041
+ if (this.gatedByToolFlow) throw error;
30556
31042
  throw new AgentExecutionError(agent.metadata.name, error instanceof Error ? error : void 0);
30557
31043
  }
30558
31044
  }
@@ -30590,9 +31076,34 @@ var init_call_agent_flow = __esm({
30590
31076
  this.state.agentContext?.mark("releaseQuota");
30591
31077
  this.logger.verbose("releaseQuota:done");
30592
31078
  }
31079
+ async parseOutput() {
31080
+ const { agent, rawOutput } = this.state;
31081
+ if (!agent || rawOutput === void 0) return;
31082
+ const parseResult = agent.safeParseOutput(rawOutput);
31083
+ if (parseResult.success) this.state.set("parsedOutput", parseResult.data);
31084
+ else this.state.set("outputError", parseResult.error);
31085
+ }
31086
+ async emitCompletion() {
31087
+ const { agent, agentContext, executionStartedAt, executionError, outputError, authInfo } = this.state;
31088
+ if (!agent || executionStartedAt === void 0) return;
31089
+ if (executionError instanceof ElicitationFallbackRequired || executionError instanceof FlowControl) return;
31090
+ const failure = executionError ?? (outputError !== void 0 ? new InvalidOutputError() : void 0);
31091
+ const failed = failure !== void 0;
31092
+ const requestId = this.tryGetContext()?.requestId;
31093
+ const sessionId = typeof authInfo?.sessionId === "string" && authInfo.sessionId ? authInfo.sessionId : void 0;
31094
+ completionEventsOf(this.scope).agents.emit({
31095
+ agentId: agent.id,
31096
+ agentName: agent.name,
31097
+ status: failed ? "error" : "success",
31098
+ durationMs: Date.now() - executionStartedAt,
31099
+ ...failed ? { error: failure instanceof Error ? failure.message : String(failure) } : { output: completionOutputText(agentContext?.output) },
31100
+ ...requestId ? { runId: requestId } : {},
31101
+ ...sessionId ? { sessionId } : {}
31102
+ });
31103
+ }
30593
31104
  async finalize() {
30594
31105
  this.logger.verbose("finalize:start");
30595
- const { agent, rawOutput, executionMeta } = this.state;
31106
+ const { agent, rawOutput, parsedOutput, outputError, executionMeta } = this.state;
30596
31107
  if (!agent) {
30597
31108
  this.logger.verbose("finalize: skipping (no agent in state)");
30598
31109
  return;
@@ -30601,15 +31112,14 @@ var init_call_agent_flow = __esm({
30601
31112
  this.logger.verbose("finalize: skipping (no output - execute stage likely failed)");
30602
31113
  return;
30603
31114
  }
30604
- const parseResult = agent.safeParseOutput(rawOutput);
30605
- if (!parseResult.success) {
31115
+ if (outputError !== void 0 || parsedOutput === void 0) {
30606
31116
  this.logger.error("finalize: output validation failed", {
30607
31117
  agent: agent.metadata.name,
30608
- errors: parseResult.error
31118
+ errors: outputError
30609
31119
  });
30610
31120
  throw new InvalidOutputError();
30611
31121
  }
30612
- const result = parseResult.data;
31122
+ const result = parsedOutput;
30613
31123
  if (executionMeta) {
30614
31124
  result._meta = {
30615
31125
  ...result._meta,
@@ -30675,6 +31185,12 @@ var init_call_agent_flow = __esm({
30675
31185
  __decorateClass([
30676
31186
  Stage14("releaseQuota")
30677
31187
  ], CallAgentFlow.prototype, "releaseQuota", 1);
31188
+ __decorateClass([
31189
+ Stage14("parseOutput")
31190
+ ], CallAgentFlow.prototype, "parseOutput", 1);
31191
+ __decorateClass([
31192
+ Stage14("emitCompletion")
31193
+ ], CallAgentFlow.prototype, "emitCompletion", 1);
30678
31194
  __decorateClass([
30679
31195
  Stage14("finalize")
30680
31196
  ], CallAgentFlow.prototype, "finalize", 1);
@@ -31476,10 +31992,17 @@ var init_mrtr = __esm({
31476
31992
  /** Per-kind call counters, used to derive stable keys across a replay. */
31477
31993
  counters = { elicitation: 0, sampling: 0, roots: 0 };
31478
31994
  clientCapabilities;
31995
+ /**
31996
+ * Whether the client itself declared this revision. `false` when the SERVER served an
31997
+ * unversioned call as 2026-07-28 (`transport.defaultProtocolVersion`, the Worker default): such a
31998
+ * client, typically a 2025-03-26 one, never agreed to the revision's input-request contract.
31999
+ */
32000
+ clientDeclaredRevision;
31479
32001
  binding;
31480
32002
  constructor(params) {
31481
32003
  this.responses = { ...params.carriedResponses ?? {}, ...params.inputResponses ?? {} };
31482
32004
  this.clientCapabilities = params.clientCapabilities;
32005
+ this.clientDeclaredRevision = params.clientDeclaredRevision ?? true;
31483
32006
  this.binding = params.binding;
31484
32007
  }
31485
32008
  /** True when the client declared the capability a given request kind needs. */
@@ -31832,7 +32355,7 @@ function shouldCreateTask(params) {
31832
32355
  return { kind: "create", owner: ownership.owner };
31833
32356
  }
31834
32357
  async function resumeTask(params) {
31835
- const { scope, record, authInfo, clientCapabilities, frontmcpContext } = params;
32358
+ const { scope, record, authInfo, clientCapabilities, frontmcpContext, clientDeclaredRevision } = params;
31836
32359
  const registry = scope.tasks;
31837
32360
  const runner = registry?.runner;
31838
32361
  if (!runner) {
@@ -31843,6 +32366,8 @@ async function resumeTask(params) {
31843
32366
  new MrtrExchange({
31844
32367
  carriedResponses: record.inputResponses ?? {},
31845
32368
  clientCapabilities,
32369
+ // A caller that never declared the revision is refused an elicitation as a legacy caller is.
32370
+ clientDeclaredRevision,
31846
32371
  binding: {
31847
32372
  principal: resolveTaskPrincipal(authInfo),
31848
32373
  binding: computeRequestBinding("tasks/resume", { name: record.taskId })
@@ -31889,6 +32414,7 @@ async function dispatch20260728(options) {
31889
32414
  scope,
31890
32415
  body,
31891
32416
  clientCapabilities,
32417
+ clientDeclaredRevision,
31892
32418
  frontmcpContext,
31893
32419
  authInfo,
31894
32420
  isAnonymous,
@@ -31935,7 +32461,8 @@ async function dispatch20260728(options) {
31935
32461
  record,
31936
32462
  authInfo: { ...authInfo ?? {}, sessionId: ownership.owner },
31937
32463
  clientCapabilities,
31938
- frontmcpContext
32464
+ frontmcpContext,
32465
+ clientDeclaredRevision
31939
32466
  })
31940
32467
  });
31941
32468
  return outcome.kind === "result" ? { kind: "result", result: decorate(outcome.result) } : outcome;
@@ -31965,7 +32492,8 @@ async function dispatch20260728(options) {
31965
32492
  inputResponses: params["inputResponses"],
31966
32493
  carriedResponses: carried.ok ? carried.responses : {},
31967
32494
  clientCapabilities,
31968
- binding
32495
+ binding,
32496
+ clientDeclaredRevision
31969
32497
  });
31970
32498
  frontmcpContext?.setMrtrExchange(exchange);
31971
32499
  if (notificationSink) frontmcpContext?.setRequestNotificationSink(notificationSink);
@@ -35442,13 +35970,13 @@ var init_esm_module_loader = __esm({
35442
35970
  URL.revokeObjectURL(url);
35443
35971
  }
35444
35972
  }
35445
- const { ensureDir, fileExists: fileExists9, sha256Hex: sha256Hex13, writeFile } = await import("@frontmcp/utils");
35973
+ const { ensureDir, fileExists: fileExists9, sha256Hex: sha256Hex14, writeFile } = await import("@frontmcp/utils");
35446
35974
  const nodePath = await import("node:path");
35447
35975
  const nodeOs = await import("node:os");
35448
35976
  const { pathToFileURL } = await import("node:url");
35449
35977
  const dir = nodePath.join(nodeOs.tmpdir(), "frontmcp-esm-modules");
35450
35978
  await ensureDir(dir);
35451
- const tempPath = nodePath.join(dir, `${sha256Hex13(bundleContent)}.mjs`);
35979
+ const tempPath = nodePath.join(dir, `${sha256Hex14(bundleContent)}.mjs`);
35452
35980
  if (!await fileExists9(tempPath)) {
35453
35981
  await writeFile(tempPath, bundleContent);
35454
35982
  }
@@ -37187,7 +37715,7 @@ var init_oauth_authorize_flow = __esm({
37187
37715
  });
37188
37716
  inputSchema16 = httpInputSchema;
37189
37717
  stateSchema15 = z65.object({
37190
- isDefaultAuthProvider: z65.boolean().describe("If FrontMcp initialized without auth options"),
37718
+ isDefaultAuthProvider: z65.boolean().describe("No sign-in to run: no auth options, or public mode (the same configuration, written out)"),
37191
37719
  isOrchestrated: z65.boolean().describe("If FrontMcp is orchestrated (local oauth proxy, remote oauth proxy)"),
37192
37720
  allowAnonymous: z65.boolean().describe("Allow anonymous access, force orchestrated mode"),
37193
37721
  // Validated OAuth request (after validation)
@@ -37253,9 +37781,13 @@ var init_oauth_authorize_flow = __esm({
37253
37781
  * Returns `undefined` when the configured auth instance has no issuer, so the
37254
37782
  * parameter is simply omitted rather than emitted empty — a client validating
37255
37783
  * `iss` treats absence as "not supported" and proceeds.
37784
+ *
37785
+ * With `request`, the issuer the tokens redeemed for this request name
37786
+ * (`LocalPrimaryAuth.issuerFor`); without it, the boot-time issuer.
37256
37787
  */
37257
- resolveIssuer() {
37258
- const issuer = this.scope.auth?.issuer;
37788
+ resolveIssuer(request) {
37789
+ const auth = this.scope.auth;
37790
+ const issuer = request && auth?.issuerFor ? auth.issuerFor(request) : auth?.issuer;
37259
37791
  return typeof issuer === "string" && issuer.length > 0 ? issuer : void 0;
37260
37792
  }
37261
37793
  logger = this.scope.logger.child("OauthAuthorizeFlow");
@@ -37295,7 +37827,7 @@ var init_oauth_authorize_flow = __esm({
37295
37827
  }
37296
37828
  }
37297
37829
  const existingSessionId = request.query["session_id"];
37298
- const isDefaultAuthProvider = !metadata.auth;
37830
+ const isDefaultAuthProvider = !metadata.auth || isPublicMode(metadata.auth);
37299
37831
  let requiresFederatedLogin = false;
37300
37832
  if (metadata.auth && isOrchestratedMode(metadata.auth)) {
37301
37833
  const configProviders = getConfiguredProviderIds(metadata.auth);
@@ -37368,11 +37900,26 @@ var init_oauth_authorize_flow = __esm({
37368
37900
  if (result2.data.state) {
37369
37901
  url.searchParams.set("state", result2.data.state);
37370
37902
  }
37371
- const issuer = this.resolveIssuer();
37903
+ const issuer = this.resolveIssuer(this.rawInput.request);
37372
37904
  if (issuer) url.searchParams.set("iss", issuer);
37373
37905
  this.respond(httpRespond.redirect(url.toString()));
37374
37906
  return;
37375
37907
  }
37908
+ const authConfig = this.scope.metadata.auth;
37909
+ if (!authConfig || !isOrchestratedMode(authConfig)) {
37910
+ const mode = authConfig?.mode ?? "public";
37911
+ this.logger.warn(`Authorization request refused: auth mode ${mode} does not sign users in`);
37912
+ this.respond(
37913
+ this.htmlPage(
37914
+ this.renderErrorPage(
37915
+ "unsupported_response_type",
37916
+ `This server does not sign users in (auth mode: ${mode}). Present its access token instead.`
37917
+ ),
37918
+ 400
37919
+ )
37920
+ );
37921
+ return;
37922
+ }
37376
37923
  const result = oauthAuthorizeRequestSchema.safeParse({
37377
37924
  response_type: request.query["response_type"],
37378
37925
  client_id: request.query["client_id"],
@@ -37408,10 +37955,10 @@ var init_oauth_authorize_flow = __esm({
37408
37955
  const redirectAllowlisted = !!registry?.hasRedirectAllowlist() && registry.isRedirectUriAllowed(redirect_uri);
37409
37956
  let redirectVerified = redirectAllowlisted;
37410
37957
  const isCimdClientId2 = !!cimdService?.enabled && cimdService.isCimdClientId(client_id);
37411
- const dcrError = this.checkDcrAllowlist(client_id, redirect_uri, isCimdClientId2);
37412
- if (dcrError) {
37413
- this.logger.warn(`OAuth authorize: DCR allowlist rejection \u2014 ${dcrError}`);
37414
- this.respondWithError([dcrError], redirectVerified ? redirect_uri : void 0, rawState);
37958
+ const redirectError = this.checkDcrRedirectAllowlist(redirect_uri);
37959
+ if (redirectError) {
37960
+ this.logger.warn(`OAuth authorize: DCR allowlist rejection \u2014 ${redirectError}`);
37961
+ this.respondWithError([redirectError], void 0, rawState);
37415
37962
  return;
37416
37963
  }
37417
37964
  if (isCimdClientId2) {
@@ -37435,24 +37982,34 @@ var init_oauth_authorize_flow = __esm({
37435
37982
  return;
37436
37983
  }
37437
37984
  }
37438
- if (!isCimdClientId2) {
37439
- const registered = registry?.get?.(client_id);
37440
- if (registered) {
37441
- if (!registered.redirect_uris.includes(redirect_uri)) {
37442
- this.logger.warn(`OAuth authorize: redirect_uri "${redirect_uri}" not registered for client "${client_id}"`);
37443
- this.respondWithError(["redirect_uri is not registered for this client"], void 0, rawState);
37444
- return;
37445
- }
37446
- redirectVerified = true;
37447
- } else if (authOptions?.requireRegisteredClients !== false) {
37448
- this.logger.warn(`OAuth authorize: rejecting unregistered client_id "${client_id}" (requireRegisteredClients)`);
37449
- this.respondWithError(
37450
- ["Unknown client_id: register the client (DCR / pre-registered) or use a CIMD client-id URL"],
37451
- void 0,
37452
- rawState
37453
- );
37985
+ const registered = isCimdClientId2 ? void 0 : registry?.get?.(client_id);
37986
+ if (registered) {
37987
+ if (!registered.redirect_uris.includes(redirect_uri)) {
37988
+ this.logger.warn(`OAuth authorize: redirect_uri "${redirect_uri}" not registered for client "${client_id}"`);
37989
+ this.respondWithError(["redirect_uri is not registered for this client"], void 0, rawState);
37454
37990
  return;
37455
37991
  }
37992
+ redirectVerified = true;
37993
+ }
37994
+ const clientIdError = isCimdClientId2 ? void 0 : this.checkDcrClientIdAllowlist(client_id);
37995
+ if (clientIdError) {
37996
+ this.logger.warn(`OAuth authorize: DCR allowlist rejection \u2014 ${clientIdError}`);
37997
+ this.respondWithError(
37998
+ [clientIdError],
37999
+ redirectVerified ? redirect_uri : void 0,
38000
+ rawState,
38001
+ "unauthorized_client"
38002
+ );
38003
+ return;
38004
+ }
38005
+ if (!isCimdClientId2 && !registered && authOptions?.requireRegisteredClients !== false) {
38006
+ this.logger.warn(`OAuth authorize: rejecting unregistered client_id "${client_id}" (requireRegisteredClients)`);
38007
+ this.respondWithError(
38008
+ ["Unknown client_id: register the client (DCR / pre-registered) or use a CIMD client-id URL"],
38009
+ void 0,
38010
+ rawState
38011
+ );
38012
+ return;
37456
38013
  }
37457
38014
  const canonicalResource = computeResource(request, this.scope.entryPath, this.scope.routeBase);
37458
38015
  if (!validatedRequest.resource) {
@@ -37893,26 +38450,28 @@ var init_oauth_authorize_flow = __esm({
37893
38450
  return httpRespond.html(markup, status, builtInAuthPageHeaders());
37894
38451
  }
37895
38452
  /**
37896
- * Enforce the local-AS DCR allowlists (#462) at authorize time. Returns a
37897
- * human-readable rejection reason, or `undefined` when the request is allowed
37898
- * (including when no allowlist is configured, or the auth instance does not
37899
- * expose a DCR registry — e.g. non-local modes).
37900
- *
37901
- * - `allowedRedirectUris`: applies to ALL clients (CIMD or not) as an
37902
- * open-redirect / lateral-movement guard.
37903
- * - `allowedClientIds`: applies only to non-CIMD client ids; CIMD URLs are
37904
- * validated by the CIMD layer instead.
38453
+ * Enforce the local-AS DCR `allowedRedirectUris` (#462) at authorize time,
38454
+ * for ALL clients (CIMD or not) as an open-redirect / lateral-movement guard.
38455
+ * Returns a human-readable rejection reason, or `undefined` when the
38456
+ * redirect_uri is allowed (including when no allowlist is configured, or the
38457
+ * auth instance does not expose a DCR registry — e.g. non-local modes).
37905
38458
  */
37906
- checkDcrAllowlist(clientId, redirectUri, isCimdClientId2) {
37907
- const auth = this.scope.auth;
37908
- const registry = auth?.dcrClientRegistry;
37909
- if (!registry) {
37910
- return void 0;
37911
- }
37912
- if (registry.hasRedirectAllowlist() && !registry.isRedirectUriAllowed(redirectUri)) {
38459
+ checkDcrRedirectAllowlist(redirectUri) {
38460
+ const registry = this.scope.auth?.dcrClientRegistry;
38461
+ if (registry?.hasRedirectAllowlist() && !registry.isRedirectUriAllowed(redirectUri)) {
37913
38462
  return `redirect_uri "${redirectUri}" is not in the configured allowlist`;
37914
38463
  }
37915
- if (!isCimdClientId2 && registry.hasClientIdAllowlist() && !registry.isClientIdAllowed(clientId)) {
38464
+ return void 0;
38465
+ }
38466
+ /**
38467
+ * Enforce the local-AS DCR `allowedClientIds` (#462) at authorize time, for
38468
+ * non-CIMD client ids (the caller skips CIMD URLs, which the CIMD layer
38469
+ * validates). Returns a human-readable rejection reason, or `undefined` when
38470
+ * the client is allowed or no allowlist is configured.
38471
+ */
38472
+ checkDcrClientIdAllowlist(clientId) {
38473
+ const registry = this.scope.auth?.dcrClientRegistry;
38474
+ if (registry?.hasClientIdAllowlist() && !registry.isClientIdAllowed(clientId)) {
37916
38475
  return `client_id "${clientId}" is not in the configured allowlist`;
37917
38476
  }
37918
38477
  return void 0;
@@ -37927,24 +38486,29 @@ var init_oauth_authorize_flow = __esm({
37927
38486
  });
37928
38487
  }
37929
38488
  /**
37930
- * Respond with OAuth error - redirect if possible, otherwise show error page
38489
+ * Respond with OAuth error - redirect if possible, otherwise show error page.
38490
+ *
38491
+ * @param redirectUri A redirect_uri validated for the client; omit it for an error page.
38492
+ * @param error The RFC 6749 §4.1.2.1 error code. Default `invalid_request`.
37931
38493
  */
37932
- respondWithError(errors, redirectUri, state) {
38494
+ respondWithError(errors, redirectUri, state, error = "invalid_request") {
37933
38495
  const errorDescription = errors.join("; ");
37934
38496
  if (redirectUri) {
37935
38497
  const safe = safeRedirectUriSchema.safeParse(redirectUri);
37936
38498
  if (safe.success) {
37937
38499
  const url = new URL(safe.data);
37938
- url.searchParams.set("error", "invalid_request");
38500
+ url.searchParams.set("error", error);
37939
38501
  url.searchParams.set("error_description", errorDescription);
37940
38502
  if (state) {
37941
38503
  url.searchParams.set("state", state);
37942
38504
  }
38505
+ const issuer = this.resolveIssuer(this.rawInput.request);
38506
+ if (issuer) url.searchParams.set("iss", issuer);
37943
38507
  this.respond(httpRespond.redirect(url.toString()));
37944
38508
  return;
37945
38509
  }
37946
38510
  }
37947
- this.respond(this.htmlPage(this.renderErrorPage("invalid_request", errorDescription), 400));
38511
+ this.respond(this.htmlPage(this.renderErrorPage(error, errorDescription), 400));
37948
38512
  }
37949
38513
  /**
37950
38514
  * Render the local login page.
@@ -38223,9 +38787,13 @@ var init_oauth_callback_flow = __esm({
38223
38787
  * Returns `undefined` when the configured auth instance has no issuer, so the
38224
38788
  * parameter is simply omitted rather than emitted empty — a client validating
38225
38789
  * `iss` treats absence as "not supported" and proceeds.
38790
+ *
38791
+ * With `request`, the issuer the tokens redeemed for this request name
38792
+ * (`LocalPrimaryAuth.issuerFor`); without it, the boot-time issuer.
38226
38793
  */
38227
- resolveIssuer() {
38228
- const issuer = this.scope.auth?.issuer;
38794
+ resolveIssuer(request) {
38795
+ const auth = this.scope.auth;
38796
+ const issuer = request && auth?.issuerFor ? auth.issuerFor(request) : auth?.issuer;
38229
38797
  return typeof issuer === "string" && issuer.length > 0 ? issuer : void 0;
38230
38798
  }
38231
38799
  async checkIpFilter() {
@@ -38763,7 +39331,7 @@ var init_oauth_callback_flow = __esm({
38763
39331
  if (originalState) {
38764
39332
  url.searchParams.set("state", originalState);
38765
39333
  }
38766
- const issuer = this.resolveIssuer();
39334
+ const issuer = this.resolveIssuer(this.rawInput.request);
38767
39335
  if (issuer) url.searchParams.set("iss", issuer);
38768
39336
  if (isIncremental && targetAppId) {
38769
39337
  url.searchParams.set("incremental", "true");
@@ -40094,7 +40662,7 @@ var init_oauth_provider_callback_flow = __esm({
40094
40662
  if (session.state) {
40095
40663
  url.searchParams.set("state", session.state);
40096
40664
  }
40097
- url.searchParams.set("iss", this.getLocalAuth().issuer);
40665
+ url.searchParams.set("iss", this.getLocalAuth().issuerFor(this.rawInput.request));
40098
40666
  this.logger.info(
40099
40667
  `Federated auth complete: ${selectedProviderIds.length} providers authenticated, redirecting to client`
40100
40668
  );
@@ -40665,7 +41233,7 @@ var init_oauth_token_flow = __esm({
40665
41233
  stateSchema20 = z70.object({
40666
41234
  body: tokenRequestSchema.optional(),
40667
41235
  grantType: z70.enum(["authorization_code", "refresh_token", "anonymous"]).optional(),
40668
- isDefaultAuthProvider: z70.boolean().describe("If FrontMcp initialized without auth options"),
41236
+ isDefaultAuthProvider: z70.boolean().describe("No sign-in to run: no auth options, or public mode (the same configuration, written out)"),
40669
41237
  isOrchestrated: z70.boolean().describe("If auth mode is orchestrated"),
40670
41238
  // Token response data
40671
41239
  tokenResponse: z70.object({
@@ -40695,7 +41263,7 @@ var init_oauth_token_flow = __esm({
40695
41263
  async parseInput() {
40696
41264
  const { metadata } = this.scope;
40697
41265
  const { request } = this.rawInput;
40698
- const isDefaultAuthProvider = !metadata.auth;
41266
+ const isDefaultAuthProvider = !metadata.auth || isPublicMode(metadata.auth);
40699
41267
  const isOrchestrated = !isDefaultAuthProvider;
40700
41268
  const rawBody = coerceTokenRequestBody(request);
40701
41269
  const basic = parseBasicAuthorization(request.headers?.["authorization"]);
@@ -40744,7 +41312,10 @@ var init_oauth_token_flow = __esm({
40744
41312
  const authOptions = this.scope.auth?.options;
40745
41313
  if (isDefaultAuthProvider && body.code === "anonymous" && !!authOptions && allowsPublicAccess(authOptions)) {
40746
41314
  const localAuth2 = this.scope.auth;
40747
- const accessToken = await localAuth2.signAnonymousJwt({ audience: this.canonicalResource() });
41315
+ const accessToken = await localAuth2.signAnonymousJwt({
41316
+ audience: this.canonicalResource(),
41317
+ issuer: localAuth2.issuerFor(this.rawInput.request)
41318
+ });
40748
41319
  this.state.set("tokenResponse", {
40749
41320
  access_token: accessToken,
40750
41321
  token_type: "Bearer",
@@ -40760,7 +41331,8 @@ var init_oauth_token_flow = __esm({
40760
41331
  body.redirect_uri,
40761
41332
  body.code_verifier,
40762
41333
  body.client_secret,
40763
- this.canonicalResource()
41334
+ this.canonicalResource(),
41335
+ localAuth.issuerFor(this.rawInput.request)
40764
41336
  );
40765
41337
  if ("error" in result) {
40766
41338
  this.logger.warn(`Code exchange failed: ${result.error}`);
@@ -40798,7 +41370,10 @@ var init_oauth_token_flow = __esm({
40798
41370
  return;
40799
41371
  }
40800
41372
  const localAuth2 = this.scope.auth;
40801
- const accessToken = await localAuth2.signAnonymousJwt({ audience: this.canonicalResource() });
41373
+ const accessToken = await localAuth2.signAnonymousJwt({
41374
+ audience: this.canonicalResource(),
41375
+ issuer: localAuth2.issuerFor(this.rawInput.request)
41376
+ });
40802
41377
  this.state.set("tokenResponse", {
40803
41378
  access_token: accessToken,
40804
41379
  token_type: "Bearer",
@@ -40812,7 +41387,8 @@ var init_oauth_token_flow = __esm({
40812
41387
  body.refresh_token,
40813
41388
  body.client_id,
40814
41389
  body.client_secret,
40815
- this.canonicalResource()
41390
+ this.canonicalResource(),
41391
+ localAuth.issuerFor(this.rawInput.request)
40816
41392
  );
40817
41393
  if ("error" in result) {
40818
41394
  this.logger.warn(`Refresh token failed: ${result.error}`);
@@ -40868,7 +41444,10 @@ var init_oauth_token_flow = __esm({
40868
41444
  }
40869
41445
  const localAuth = this.scope.auth;
40870
41446
  const audience = body?.grant_type === "anonymous" && body.resource ? body.resource : canonicalResource;
40871
- const accessToken = await localAuth.signAnonymousJwt({ audience });
41447
+ const accessToken = await localAuth.signAnonymousJwt({
41448
+ audience,
41449
+ issuer: localAuth.issuerFor(this.rawInput.request)
41450
+ });
40872
41451
  this.state.set("tokenResponse", {
40873
41452
  access_token: accessToken,
40874
41453
  token_type: "Bearer",
@@ -40996,7 +41575,8 @@ var init_oauth_userinfo_flow = __esm({
40996
41575
  return;
40997
41576
  }
40998
41577
  const localAuth = this.scope.auth;
40999
- const result = await localAuth.verifyGatewayToken(token, baseUrl);
41578
+ const issuer = localAuth.acceptedIssuersFor?.(this.rawInput.request);
41579
+ const result = await localAuth.verifyGatewayToken(token, baseUrl, void 0, issuer);
41000
41580
  if (!result.ok) {
41001
41581
  this.logger.warn("userinfo: token verification failed", { error: result.error });
41002
41582
  this.respond(
@@ -41054,6 +41634,7 @@ var init_oauth_userinfo_flow = __esm({
41054
41634
  // libs/sdk/src/auth/flows/session.verify.flow.ts
41055
41635
  import "reflect-metadata";
41056
41636
  import {
41637
+ anonymousCallerClaims,
41057
41638
  buildInsufficientScopeHeader,
41058
41639
  buildInvalidTokenHeader,
41059
41640
  buildUnauthorizedHeader,
@@ -41090,7 +41671,14 @@ var init_session_verify_flow = __esm({
41090
41671
  init_common();
41091
41672
  init_notification_service();
41092
41673
  init_session_id_utils();
41093
- inputSchema23 = httpRequestInputSchema;
41674
+ inputSchema23 = httpRequestInputSchema.extend({
41675
+ /**
41676
+ * The request is served without a session (MCP 2026-07-28): an anonymous or static-key caller is
41677
+ * identified for this request only, and no session is minted for it (#629). Minting one needs
41678
+ * `MCP_SESSION_SECRET`, for a session id the client would never present.
41679
+ */
41680
+ sessionless: z72.boolean().optional()
41681
+ });
41094
41682
  stateSchema22 = z72.object({
41095
41683
  baseUrl: z72.string().min(1),
41096
41684
  authorizationHeader: z72.string().optional(),
@@ -41148,20 +41736,22 @@ var init_session_verify_flow = __esm({
41148
41736
  * Encapsulates the shared logic for session creation, payload encryption, and user derivation.
41149
41737
  */
41150
41738
  createAnonymousSession(options) {
41151
- const { authMode, issuer, scopes = ["anonymous"], subject, sessionIdHeader } = options;
41739
+ const { authMode, subject, sessionIdHeader } = options;
41152
41740
  this.logger.verbose("createAnonymousSession", { authMode, hasExistingSession: !!sessionIdHeader });
41741
+ if (this.rawInput.sessionless) {
41742
+ this.respond({
41743
+ kind: "authorized",
41744
+ authorization: { token: "", user: anonymousCallerClaims(options, randomUUID20()) }
41745
+ });
41746
+ return;
41747
+ }
41153
41748
  const machineId = getMachineId2();
41154
41749
  const authSignature = subject ? `${authMode}:${subject}` : authMode;
41155
41750
  if (sessionIdHeader) {
41156
41751
  const existingPayload = decryptPublicSession(sessionIdHeader, authSignature);
41157
41752
  if (existingPayload && existingPayload.nodeId === machineId) {
41158
41753
  const anonId = existingPayload.uuid ?? `${existingPayload.iat * 1e3}`;
41159
- const user2 = {
41160
- sub: subject ?? `anon:${anonId}`,
41161
- iss: issuer,
41162
- name: subject ? "Static token" : "Anonymous",
41163
- scope: scopes.join(" ")
41164
- };
41754
+ const user2 = anonymousCallerClaims(options, anonId);
41165
41755
  this.respond({
41166
41756
  kind: "authorized",
41167
41757
  authorization: {
@@ -41175,13 +41765,8 @@ var init_session_verify_flow = __esm({
41175
41765
  this.logger.verbose("createAnonymousSession: ignoring unrecognized client session id; minting a fresh one");
41176
41766
  }
41177
41767
  const now = Date.now();
41178
- const user = {
41179
- sub: subject ?? `anon:${randomUUID20()}`,
41180
- iss: issuer,
41181
- name: subject ? "Static token" : "Anonymous",
41182
- scope: scopes.join(" ")
41183
- };
41184
41768
  const uuid = randomUUID20();
41769
+ const user = anonymousCallerClaims(options, uuid);
41185
41770
  const platformDetectionConfig = this.scope.metadata.transport?.platformDetection;
41186
41771
  const platformType = detectPlatformFromUserAgent(this.state.userAgent, platformDetectionConfig);
41187
41772
  const payload = {
@@ -41374,7 +41959,8 @@ var init_session_verify_flow = __esm({
41374
41959
  } else {
41375
41960
  const configuredAudience = isOrchestratedMode(authOptions) ? authOptions.expectedAudience : void 0;
41376
41961
  const resource = computeResource(this.rawInput.request, this.scope.entryPath, this.scope.routeBase);
41377
- verify = auth.verifyGatewayToken(token, this.state.required.baseUrl, configuredAudience ?? resource);
41962
+ const issuer = auth.acceptedIssuersFor?.(this.rawInput.request);
41963
+ verify = auth.verifyGatewayToken(token, this.state.required.baseUrl, configuredAudience ?? resource, issuer);
41378
41964
  }
41379
41965
  const result = await verify;
41380
41966
  if (result.ok) {
@@ -41621,6 +42207,7 @@ var init_well_known_jwks_flow = __esm({
41621
42207
 
41622
42208
  // libs/sdk/src/auth/flows/well-known.oauth-authorization-server.flow.ts
41623
42209
  import "reflect-metadata";
42210
+ import { advertisedScopes } from "@frontmcp/auth";
41624
42211
  import { z as z74 } from "@frontmcp/lazy-zod";
41625
42212
  import { isProduction as isProduction4 } from "@frontmcp/utils";
41626
42213
  function resolveDcrEnabled(auth) {
@@ -41665,6 +42252,9 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41665
42252
  wellKnownAsStateSchema = z74.object({
41666
42253
  baseUrl: z74.string().min(1),
41667
42254
  // baseUrl + entryPrefix (unsuffixed)
42255
+ // The issuer this server names in answer to the request (`LocalPrimaryAuth.issuerFor`, #629): the
42256
+ // one on its authorization responses and tokens. Without it, `baseUrl`.
42257
+ issuer: z74.string().min(1).optional(),
41668
42258
  // Root origin (proto://host WITHOUT the entryPath prefix). The OAuth
41669
42259
  // endpoints (/oauth/authorize, /oauth/token, /oauth/register, …) are
41670
42260
  // registered at literal root paths by their flows, NOT under entryPath, so
@@ -41696,6 +42286,9 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41696
42286
  if (!request) throw new FlowInputMissingError("request", "well-known:oauth-authorization-server");
41697
42287
  const { metadata } = this.scope;
41698
42288
  const baseUrl = getRequestBaseUrl(request, this.scope.entryPath);
42289
+ const issuer = this.scope.auth?.issuerFor?.(
42290
+ request
42291
+ );
41699
42292
  const oauthBaseUrl = getRequestBaseUrl(request);
41700
42293
  let cimdEnabled = true;
41701
42294
  if (metadata.auth && isOrchestratedMode(metadata.auth)) {
@@ -41704,8 +42297,11 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41704
42297
  this.state.set(
41705
42298
  wellKnownAsStateSchema.parse({
41706
42299
  baseUrl,
42300
+ issuer,
41707
42301
  oauthBaseUrl,
41708
- scopesSupported: [],
42302
+ // The scopes this server grants (`allowedScopes`, #262): the literal
42303
+ // entries; a `*` glob names no scope a client could ask for.
42304
+ scopesSupported: metadata.auth && isOrchestratedMode(metadata.auth) ? advertisedScopes(metadata.auth.allowedScopes) : [],
41709
42305
  tokenEndpointAuthMethods: [],
41710
42306
  // #462 — advertise registration_endpoint only when local-AS DCR is
41711
42307
  // actually active (explicit auth.dcr.enabled, else dev/prod default).
@@ -41716,6 +42312,7 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41716
42312
  );
41717
42313
  }
41718
42314
  async collectData() {
42315
+ const { issuer } = this.state;
41719
42316
  const {
41720
42317
  baseUrl,
41721
42318
  oauthBaseUrl,
@@ -41726,7 +42323,7 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41726
42323
  cimdEnabled
41727
42324
  } = this.state.required;
41728
42325
  if (isOrchestrated) {
41729
- const baseIssuer = `${baseUrl}`;
42326
+ const baseIssuer = issuer ?? baseUrl;
41730
42327
  this.respond({
41731
42328
  kind: "json",
41732
42329
  contentType: "application/json; charset=utf-8",
@@ -41753,6 +42350,8 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41753
42350
  grant_types_supported: ["authorization_code", "refresh_token"],
41754
42351
  scopes_supported: scopesSupported,
41755
42352
  code_challenge_methods_supported: ["S256"],
42353
+ // Every authorization response, error responses included, carries `iss` (RFC 9207 §2, §3).
42354
+ authorization_response_iss_parameter_supported: true,
41756
42355
  // CIMD support advertisement per draft-ietf-oauth-client-id-metadata-document-00
41757
42356
  client_id_metadata_document_supported: cimdEnabled
41758
42357
  }
@@ -41789,6 +42388,7 @@ var init_well_known_oauth_authorization_server_flow = __esm({
41789
42388
 
41790
42389
  // libs/sdk/src/auth/flows/well-known.prm.flow.ts
41791
42390
  import "reflect-metadata";
42391
+ import { resourceScopesFor } from "@frontmcp/auth";
41792
42392
  import { z as z75 } from "@frontmcp/lazy-zod";
41793
42393
  var inputSchema26, stateSchema24, outputSchema24, plan24, name25, Stage25, WellKnownPrmFlow;
41794
42394
  var init_well_known_prm_flow = __esm({
@@ -41800,14 +42400,16 @@ var init_well_known_prm_flow = __esm({
41800
42400
  stateSchema24 = z75.object({
41801
42401
  resource: z75.string().min(1),
41802
42402
  baseUrl: z75.string().min(1),
41803
- scopesSupported: z75.array(z75.string()).default(["openid", "profile", "email"]),
42403
+ // The authorization server to name: the issuer this server names for the request (#629).
42404
+ authorizationServer: z75.string().min(1).optional(),
42405
+ scopesSupported: z75.array(z75.string()),
41804
42406
  isOrchestrated: z75.boolean()
41805
42407
  });
41806
42408
  outputSchema24 = HttpJsonSchema.extend({
41807
42409
  body: z75.object({
41808
42410
  resource: z75.string().min(1),
41809
42411
  authorization_servers: z75.array(z75.string().min(1)).min(1),
41810
- scopes_supported: z75.array(z75.string()).default(["openid", "profile", "email"]),
42412
+ scopes_supported: z75.array(z75.string()).optional(),
41811
42413
  bearer_methods_supported: z75.array(z75.string()).default(["header"])
41812
42414
  }).passthrough()
41813
42415
  });
@@ -41831,11 +42433,17 @@ var init_well_known_prm_flow = __esm({
41831
42433
  if (!request) throw new FlowInputMissingError("request", "well-known:prm");
41832
42434
  const resource = computeResource(request, scope.entryPath, scope.routeBase);
41833
42435
  const baseUrl = getRequestBaseUrl(request, scope.entryPath);
42436
+ const authorizationServer = scope.auth?.issuerFor?.(request);
42437
+ const authOptions = scope.auth?.options ?? scope.metadata.auth;
42438
+ const granted = resourceScopesFor(authOptions);
42439
+ const orchestrated = authOptions?.mode === "local" || authOptions?.mode === "remote";
42440
+ const scopesSupported = orchestrated ? granted : [.../* @__PURE__ */ new Set([...granted, ...scope.getAllSupportedScopes()])];
41834
42441
  this.state.set(
41835
42442
  stateSchema24.parse({
41836
42443
  resource,
41837
42444
  baseUrl,
41838
- scopesSupported: scope.getAllSupportedScopes(),
42445
+ authorizationServer,
42446
+ scopesSupported,
41839
42447
  isOrchestrated: false
41840
42448
  //scope.orchestrated,// TODO: fix
41841
42449
  })
@@ -41843,6 +42451,8 @@ var init_well_known_prm_flow = __esm({
41843
42451
  }
41844
42452
  async collectData() {
41845
42453
  const { resource, baseUrl, scopesSupported, isOrchestrated } = this.state.required;
42454
+ const { authorizationServer } = this.state;
42455
+ const scopes = scopesSupported.length > 0 ? { scopes_supported: scopesSupported } : {};
41846
42456
  if (isOrchestrated) {
41847
42457
  this.respond({
41848
42458
  kind: "json",
@@ -41854,8 +42464,8 @@ var init_well_known_prm_flow = __esm({
41854
42464
  headers: { "cache-control": "no-store" },
41855
42465
  body: {
41856
42466
  resource,
41857
- authorization_servers: [baseUrl],
41858
- scopes_supported: scopesSupported,
42467
+ authorization_servers: [authorizationServer ?? baseUrl],
42468
+ ...scopes,
41859
42469
  bearer_methods_supported: ["header"]
41860
42470
  }
41861
42471
  });
@@ -41868,8 +42478,8 @@ var init_well_known_prm_flow = __esm({
41868
42478
  headers: { "cache-control": "no-store" },
41869
42479
  body: {
41870
42480
  resource,
41871
- authorization_servers: [baseUrl],
41872
- scopes_supported: scopesSupported,
42481
+ authorization_servers: [authorizationServer ?? baseUrl],
42482
+ ...scopes,
41873
42483
  bearer_methods_supported: ["header"]
41874
42484
  }
41875
42485
  });
@@ -41899,6 +42509,24 @@ var init_well_known_prm_flow = __esm({
41899
42509
  }
41900
42510
  });
41901
42511
 
42512
+ // libs/sdk/src/auth/session-scope-identity.ts
42513
+ import { isAnonymousSubject as isAnonymousSubject3 } from "@frontmcp/auth";
42514
+ function sessionScopeIdentity(ctx) {
42515
+ const verified = ctx.verifiedSessionId;
42516
+ if (verified) return verified;
42517
+ const authInfo = ctx.authInfo ?? {};
42518
+ const extraUser = authInfo.extra?.["user"];
42519
+ const principal = [authInfo.user?.sub, extraUser?.sub, authInfo.clientId].find(
42520
+ (candidate) => !isAnonymousSubject3(candidate)
42521
+ );
42522
+ return principal ? `principal:${principal}` : void 0;
42523
+ }
42524
+ var init_session_scope_identity = __esm({
42525
+ "libs/sdk/src/auth/session-scope-identity.ts"() {
42526
+ "use strict";
42527
+ }
42528
+ });
42529
+
41902
42530
  // libs/sdk/src/auth/secure-store/secure-store.providers.ts
41903
42531
  import {
41904
42532
  SECURE_STORE_ACCESSOR,
@@ -41906,8 +42534,9 @@ import {
41906
42534
  } from "@frontmcp/auth";
41907
42535
  import { ProviderScope as ProviderScope6 } from "@frontmcp/di";
41908
42536
  function resolveRequestSessionId(ctx) {
41909
- const sid = ctx.sessionId;
41910
- return typeof sid === "string" && sid.length > 0 ? sid : void 0;
42537
+ const identity = sessionScopeIdentity(ctx);
42538
+ if (identity) return identity;
42539
+ throw new SessionIdentityRequiredError("The secure store's session scope");
41911
42540
  }
41912
42541
  function createSecureStoreProviders(opts) {
41913
42542
  const { backend, scope, ttlMs } = opts;
@@ -41939,7 +42568,9 @@ var init_secure_store_providers = __esm({
41939
42568
  "use strict";
41940
42569
  init_common();
41941
42570
  init_frontmcp_context_provider();
42571
+ init_session_identity_required_error();
41942
42572
  init_credentials_providers();
42573
+ init_session_scope_identity();
41943
42574
  SECURE_STORE_BACKEND = /* @__PURE__ */ Symbol.for("frontmcp:SECURE_STORE_BACKEND");
41944
42575
  }
41945
42576
  });
@@ -42027,6 +42658,14 @@ function audienceMatchesResource(aud, expected) {
42027
42658
  function isUnsupportedOperation(error) {
42028
42659
  return error instanceof StorageNotSupportedError;
42029
42660
  }
42661
+ function originOf2(value) {
42662
+ try {
42663
+ const origin = new URL(value).origin;
42664
+ return origin === "null" ? void 0 : origin;
42665
+ } catch {
42666
+ return void 0;
42667
+ }
42668
+ }
42030
42669
  var MIN_HS256_SECRET_BYTES, defaultNoAuthSecret, RESERVED_JWT_CLAIMS, LocalPrimaryAuth;
42031
42670
  var init_instance_local_primary_auth = __esm({
42032
42671
  "libs/sdk/src/auth/instances/instance.local-primary-auth.ts"() {
@@ -42079,6 +42718,7 @@ var init_instance_local_primary_auth = __esm({
42079
42718
  this.port = this.providers.getActiveScope().metadata.http?.port ?? 3001;
42080
42719
  this.host = getEnv4("FRONTMCP_PUBLIC_HOST")?.trim() || "localhost";
42081
42720
  this.issuer = this.deriveIssuer(options);
42721
+ this.publicHostPinned = !!getEnv4("FRONTMCP_PUBLIC_HOST")?.trim();
42082
42722
  const jwtSecret = getEnv4("JWT_SECRET");
42083
42723
  const hasJwtSecret = !!jwtSecret?.trim();
42084
42724
  if (hasJwtSecret && jwtSecret) {
@@ -42117,7 +42757,10 @@ var init_instance_local_primary_auth = __esm({
42117
42757
  providers;
42118
42758
  host;
42119
42759
  port;
42760
+ /** The boot-time issuer: the configured one, else this server's listener address (see {@link issuerFor}). */
42120
42761
  issuer;
42762
+ /** Whether `FRONTMCP_PUBLIC_HOST` names the boot-time issuer's host, pinning it for every request. */
42763
+ publicHostPinned;
42121
42764
  keys = [];
42122
42765
  secret;
42123
42766
  logger;
@@ -42266,18 +42909,61 @@ var init_instance_local_primary_auth = __esm({
42266
42909
  * supported knob for aligning the issuer with discovery.
42267
42910
  */
42268
42911
  deriveIssuer(options) {
42269
- const basePath = `http://${this.host}:${this.port}${this.scope.fullPath}`;
42270
- if (isPublicMode(options)) {
42271
- return options.issuer ?? basePath;
42272
- }
42273
- if (isOrchestratedMode(options)) {
42274
- if (isOrchestratedLocal(options)) {
42275
- return options.local?.issuer ?? basePath;
42276
- } else {
42277
- return options.local?.issuer ?? basePath;
42278
- }
42912
+ return this.configuredIssuer(options) ?? `http://${this.host}:${this.port}${this.scope.fullPath}`;
42913
+ }
42914
+ /** The issuer the options name: `issuer` in public mode, `local.issuer` in local and remote mode. */
42915
+ configuredIssuer(options) {
42916
+ if (isPublicMode(options)) return options.issuer;
42917
+ if (isOrchestratedMode(options)) return options.local?.issuer;
42918
+ return void 0;
42919
+ }
42920
+ /**
42921
+ * The issuer this server names in answer to `request`: in its discovery
42922
+ * documents (`/.well-known/oauth-authorization-server`'s `issuer`, the
42923
+ * protected resource metadata's `authorization_servers`), on its
42924
+ * authorization responses (the RFC 9207 `iss`, errors included), and as the
42925
+ * `iss` of the tokens it issues; the tokens it accepts there must name it
42926
+ * (#269, and see {@link acceptedIssuersFor}). Every entry point asks this, so
42927
+ * none of them can disagree, on the Node server and under a Web fetch
42928
+ * handler alike (#629). In order:
42929
+ *
42930
+ * 1. a configured issuer (`issuer` / `local.issuer`);
42931
+ * 2. with `FRONTMCP_PUBLIC_URL` pinned, that URL plus this scope's path;
42932
+ * 3. with `FRONTMCP_PUBLIC_HOST` set, the boot-time issuer it names the host
42933
+ * of (`http://<host>:<http.port><path>`);
42934
+ * 4. the request's own origin plus this scope's path;
42935
+ * 5. without a request, the boot-time issuer.
42936
+ */
42937
+ issuerFor(request) {
42938
+ const configured = this.configuredIssuer(this.options);
42939
+ if (configured !== void 0) return configured;
42940
+ const pinned = getPinnedPublicUrl();
42941
+ if (pinned !== void 0) {
42942
+ return request ? computeIssuer(request, this.scope.entryPath, this.scope.routeBase) : `${pinned}${this.scope.fullPath}`;
42943
+ }
42944
+ if (this.publicHostPinned || !request) return this.issuer;
42945
+ return computeIssuer(request, this.scope.entryPath, this.scope.routeBase);
42946
+ }
42947
+ /**
42948
+ * The issuers a token presented with `request` may name: {@link issuerFor}
42949
+ * the request. When the issuer follows the request (nothing pins it) and
42950
+ * local or remote mode lists `expectedAudience`, a token for any listed
42951
+ * address was issued under that address's issuer, so each listed address's
42952
+ * issuer is accepted too, and so is the boot-time issuer that 1.8.4 named on
42953
+ * those tokens. The token's `aud` must still name a listed resource.
42954
+ */
42955
+ acceptedIssuersFor(request) {
42956
+ const issuer = this.issuerFor(request);
42957
+ const options = this.options;
42958
+ const followsRequest = !!request && this.configuredIssuer(options) === void 0 && getPinnedPublicUrl() === void 0 && !this.publicHostPinned;
42959
+ if (!followsRequest || !isOrchestratedMode(options) || options.expectedAudience === void 0) return issuer;
42960
+ const listed = Array.isArray(options.expectedAudience) ? options.expectedAudience : [options.expectedAudience];
42961
+ const accepted = /* @__PURE__ */ new Set([issuer, this.issuer]);
42962
+ for (const audience of listed) {
42963
+ const origin = originOf2(audience);
42964
+ if (origin) accepted.add(`${origin}${this.scope.fullPath}`);
42279
42965
  }
42280
- return basePath;
42966
+ return [...accepted];
42281
42967
  }
42282
42968
  /**
42283
42969
  * Read the `tokenStorage` selection off the auth options. Only
@@ -42508,9 +43194,10 @@ var init_instance_local_primary_auth = __esm({
42508
43194
  * user, and its scopes are the configured `anonymousScopes`, never a role.
42509
43195
  *
42510
43196
  * @param options.audience The protected resource the token is for (`aud`, #269).
43197
+ * @param options.issuer The issuer to name (`iss`): {@link issuerFor} the request. Default: the boot-time issuer.
42511
43198
  */
42512
43199
  async signAnonymousJwt(options = {}) {
42513
- const jwt = new SignJWT({ sub: `anon:${randomUUID21()}`, anonymous: true, scope: this.anonymousScopes().join(" ") }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer(this.issuer).setExpirationTime("1d").setJti(randomUUID21());
43200
+ const jwt = new SignJWT({ sub: `anon:${randomUUID21()}`, anonymous: true, scope: this.anonymousScopes().join(" ") }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer(options.issuer ?? this.issuer).setExpirationTime("1d").setJti(randomUUID21());
42514
43201
  if (options.audience) jwt.setAudience(options.audience);
42515
43202
  return jwt.sign(this.secret);
42516
43203
  }
@@ -42527,8 +43214,10 @@ var init_instance_local_primary_auth = __esm({
42527
43214
  *
42528
43215
  * The secret alone doesn't make a token this server's: every server started
42529
43216
  * with the same JWT_SECRET holds it. So a token must also (#269):
42530
- * - name THIS instance as its issuer (`iss`, the boot-time issuer it signs
42531
- * with, never the request-derived base URL), and
43217
+ * - name THIS instance as its issuer (`iss`): `expectedIssuer` (one issuer or
43218
+ * several), which callers take from {@link acceptedIssuersFor} the request,
43219
+ * matching what the token endpoint names when it signs ({@link issuerFor});
43220
+ * the boot-time issuer when not given. Never the token's own claim; and
42532
43221
  * - when `expectedAudience` is given, be issued for that protected resource,
42533
43222
  * or one of those resources (`aud`, compared as RFC 8707 resource URIs).
42534
43223
  * `session:verify` passes the request's resource URL, or the configured
@@ -42537,11 +43226,11 @@ var init_instance_local_primary_auth = __esm({
42537
43226
  * Lifetime: `exp` is required (#272) and checked with `nbf` by `jose`. The
42538
43227
  * algorithm is pinned to HS256 to block `alg` confusion.
42539
43228
  */
42540
- async verifyGatewayToken(token, requestBaseUrl, expectedAudience) {
43229
+ async verifyGatewayToken(token, requestBaseUrl, expectedAudience, expectedIssuer) {
42541
43230
  try {
42542
43231
  const { payload, protectedHeader } = await jwtVerify(token, this.secret, {
42543
43232
  algorithms: ["HS256"],
42544
- issuer: this.issuer,
43233
+ issuer: typeof expectedIssuer === "string" ? expectedIssuer : expectedIssuer ? [...expectedIssuer] : this.issuer,
42545
43234
  requiredClaims: ["exp"]
42546
43235
  });
42547
43236
  if (expectedAudience !== void 0 && !audienceMatchesResource(payload.aud, expectedAudience)) {
@@ -42562,7 +43251,7 @@ var init_instance_local_primary_auth = __esm({
42562
43251
  /**
42563
43252
  * Sign an access token for an authenticated user
42564
43253
  */
42565
- async signAccessToken(user, scopes, audience, consentMetadata) {
43254
+ async signAccessToken(user, scopes, audience, consentMetadata, issuer) {
42566
43255
  const claims = {
42567
43256
  sub: user.sub,
42568
43257
  scope: scopes.join(" ")
@@ -42598,7 +43287,7 @@ var init_instance_local_primary_auth = __esm({
42598
43287
  }
42599
43288
  }
42600
43289
  }
42601
- const jwt = new SignJWT(claims).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer(this.issuer).setExpirationTime(`${this.accessTokenTtlSeconds}s`).setJti(randomUUID21());
43290
+ const jwt = new SignJWT(claims).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setIssuer(issuer ?? this.issuer).setExpirationTime(`${this.accessTokenTtlSeconds}s`).setJti(randomUUID21());
42602
43291
  if (audience) {
42603
43292
  jwt.setAudience(audience);
42604
43293
  }
@@ -42607,7 +43296,7 @@ var init_instance_local_primary_auth = __esm({
42607
43296
  /**
42608
43297
  * Exchange an authorization code for tokens
42609
43298
  */
42610
- async exchangeCode(code, clientId, redirectUri, codeVerifier, clientSecret, defaultAudience) {
43299
+ async exchangeCode(code, clientId, redirectUri, codeVerifier, clientSecret, defaultAudience, issuer) {
42611
43300
  if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
42612
43301
  this.logger.warn("Client authentication failed at token endpoint (authorization_code)");
42613
43302
  return { error: "invalid_client", error_description: "Client authentication failed" };
@@ -42674,7 +43363,7 @@ var init_instance_local_primary_auth = __esm({
42674
43363
  customClaims: codeRecord.customClaims
42675
43364
  } : void 0;
42676
43365
  const resource = codeRecord.resource ?? defaultAudience;
42677
- const accessToken = await this.signAccessToken(user, codeRecord.scopes, resource, consentMetadata);
43366
+ const accessToken = await this.signAccessToken(user, codeRecord.scopes, resource, consentMetadata, issuer);
42678
43367
  if (codeRecord.pendingAuthId && codeRecord.federatedLoginUsed) {
42679
43368
  try {
42680
43369
  const pendingAuthId = `pending:${codeRecord.pendingAuthId}`;
@@ -42716,7 +43405,7 @@ var init_instance_local_primary_auth = __esm({
42716
43405
  /**
42717
43406
  * Refresh an access token using a refresh token
42718
43407
  */
42719
- async refreshAccessToken(refreshToken, clientId, clientSecret, defaultAudience) {
43408
+ async refreshAccessToken(refreshToken, clientId, clientSecret, defaultAudience, issuer) {
42720
43409
  if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
42721
43410
  this.logger.warn("Client authentication failed at token endpoint (refresh_token)");
42722
43411
  return { error: "invalid_client", error_description: "Client authentication failed" };
@@ -42753,7 +43442,7 @@ var init_instance_local_primary_auth = __esm({
42753
43442
  customClaims: tokenRecord.customClaims
42754
43443
  } : void 0;
42755
43444
  const resource = tokenRecord.resource ?? defaultAudience;
42756
- const accessToken = await this.signAccessToken(user, tokenRecord.scopes, resource, consentMetadata);
43445
+ const accessToken = await this.signAccessToken(user, tokenRecord.scopes, resource, consentMetadata, issuer);
42757
43446
  const newRefreshRecord = this.authorizationStore.createRefreshTokenRecord({
42758
43447
  clientId,
42759
43448
  userSub: tokenRecord.userSub,
@@ -43497,6 +44186,111 @@ var init_auth_registry = __esm({
43497
44186
  }
43498
44187
  });
43499
44188
 
44189
+ // libs/sdk/src/server/custom-routes.helper.ts
44190
+ function computeReservedPaths(entryPath, routeBase) {
44191
+ const prefix = normalizeEntryPrefix(entryPath);
44192
+ const base = normalizeScopeBase(routeBase);
44193
+ const mcpBase = `${prefix}${base}`;
44194
+ const exact = /* @__PURE__ */ new Set([
44195
+ mcpBase === "" ? "/" : mcpBase,
44196
+ `${mcpBase}/sse`,
44197
+ `${mcpBase}/message`,
44198
+ "/health",
44199
+ "/metrics"
44200
+ ]);
44201
+ return { exact, prefixes: ["/oauth", "/.well-known"] };
44202
+ }
44203
+ function normalizeForCompare(path3) {
44204
+ if (path3.length > 1 && path3.endsWith("/")) return path3.slice(0, -1);
44205
+ return path3;
44206
+ }
44207
+ function assertNotReserved(method, path3, reserved) {
44208
+ const normalized = normalizeForCompare(path3);
44209
+ if (reserved.exact.has(normalized)) {
44210
+ throw new ReservedRouteCollisionError(method, path3, normalized);
44211
+ }
44212
+ for (const prefix of reserved.prefixes) {
44213
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
44214
+ throw new ReservedRouteCollisionError(method, path3, prefix);
44215
+ }
44216
+ }
44217
+ }
44218
+ function wrapWithAuth(handler, verifySession, logger) {
44219
+ return async (req, res, next) => {
44220
+ let result;
44221
+ try {
44222
+ result = await verifySession(req);
44223
+ } catch (error) {
44224
+ if (error instanceof FlowControl && error.type === "respond") {
44225
+ result = error.output;
44226
+ } else {
44227
+ throw error;
44228
+ }
44229
+ }
44230
+ if (!result || result.kind === "unauthorized") {
44231
+ logger.verbose("custom-route auth: unauthorized");
44232
+ return writeHttpResponse(
44233
+ res,
44234
+ httpRespond.unauthorized({
44235
+ headers: result?.kind === "unauthorized" ? { "WWW-Authenticate": result.prmMetadataHeader } : void 0
44236
+ })
44237
+ );
44238
+ }
44239
+ if (result.kind === "forbidden") {
44240
+ logger.verbose("custom-route auth: forbidden (insufficient scope)");
44241
+ return writeHttpResponse(
44242
+ res,
44243
+ httpRespond.forbidden({
44244
+ headers: { "WWW-Authenticate": result.prmMetadataHeader }
44245
+ })
44246
+ );
44247
+ }
44248
+ req.authSession = result.authorization;
44249
+ req[ServerRequestTokens.auth] = result.authorization;
44250
+ return handler(req, res, next);
44251
+ };
44252
+ }
44253
+ function wrapWithIpFilter(handler, checkClientIp) {
44254
+ return async (req, res, next) => {
44255
+ const rejection = await checkClientIp(req, res);
44256
+ if (rejection) return writeHttpResponse(res, rejection);
44257
+ return handler(req, res, next);
44258
+ };
44259
+ }
44260
+ function registerCustomHttpRoutes(args) {
44261
+ const { routes, server, verifySession, checkClientIp, entryPath, routeBase, logger } = args;
44262
+ if (!routes || routes.length === 0) return;
44263
+ const reserved = computeReservedPaths(entryPath, routeBase);
44264
+ for (const route of routes) {
44265
+ assertNotReserved(route.method, route.path, reserved);
44266
+ const handler = route.auth === true ? wrapWithAuth(route.handler, verifySession, logger) : route.handler;
44267
+ server.registerRoute(route.method, route.path, wrapWithIpFilter(handler, checkClientIp));
44268
+ logger.info(`Registered custom http route: ${route.method} ${route.path}${route.auth ? " (auth)" : ""}`);
44269
+ }
44270
+ }
44271
+ var ReservedRouteCollisionError;
44272
+ var init_custom_routes_helper = __esm({
44273
+ "libs/sdk/src/server/custom-routes.helper.ts"() {
44274
+ "use strict";
44275
+ init_common();
44276
+ init_server_validation();
44277
+ ReservedRouteCollisionError = class extends Error {
44278
+ constructor(method, path3, reserved) {
44279
+ super(
44280
+ `Custom http.route "${method} ${path3}" collides with the reserved FrontMCP path "${reserved}". Reserved prefixes are the MCP entry path (and its /sse + /message siblings), /oauth/*, /.well-known/*, /health, and /metrics. Choose a different path.`
44281
+ );
44282
+ this.method = method;
44283
+ this.path = path3;
44284
+ this.reserved = reserved;
44285
+ this.name = "ReservedRouteCollisionError";
44286
+ }
44287
+ method;
44288
+ path;
44289
+ reserved;
44290
+ };
44291
+ }
44292
+ });
44293
+
43500
44294
  // libs/sdk/src/channel/channel-notification.service.ts
43501
44295
  var CHANNEL_NOTIFICATION_METHOD, ChannelNotificationService;
43502
44296
  var init_channel_notification_service = __esm({
@@ -44466,6 +45260,12 @@ function wireJobCompletionSource(channel2, sourceConfig, jobEmitterSubscribe, lo
44466
45260
  return;
44467
45261
  }
44468
45262
  }
45263
+ if (!event.sessionId) {
45264
+ logger.verbose(
45265
+ `Skipping job completion for channel "${channel2.name}": no sessionId (prevents cross-session leak)`
45266
+ );
45267
+ return;
45268
+ }
44469
45269
  logger.verbose(`Job completion event for channel "${channel2.name}": job=${event.jobName}, status=${event.status}`);
44470
45270
  channel2.handleEvent(event, event.sessionId).catch((err) => {
44471
45271
  logger.error(`Failed to handle job completion event in channel "${channel2.name}"`, { error: err });
@@ -44478,6 +45278,41 @@ var init_job_completion_source = __esm({
44478
45278
  }
44479
45279
  });
44480
45280
 
45281
+ // libs/sdk/src/channel/sources/webhook.source.ts
45282
+ function createWebhookMiddleware(channel2, sourceConfig, logger) {
45283
+ return async (req, res) => {
45284
+ const rawHeaders = req.headers ?? {};
45285
+ const normalizedHeaders = {};
45286
+ for (const [key, value] of Object.entries(rawHeaders)) {
45287
+ normalizedHeaders[key.toLowerCase()] = value;
45288
+ }
45289
+ const payload = {
45290
+ body: req.body,
45291
+ headers: normalizedHeaders,
45292
+ method: req.method ?? "POST",
45293
+ query: req.query
45294
+ };
45295
+ logger.verbose(`Webhook received for channel "${channel2.name}" at ${sourceConfig.path}`);
45296
+ try {
45297
+ const notification = await runOnSurface("http-trigger", () => channel2.handleEvent(payload));
45298
+ if (notification) {
45299
+ res.status(200).json({ ok: true, channel: channel2.name });
45300
+ } else {
45301
+ res.status(200).json({ ok: true, channel: channel2.name, skipped: true });
45302
+ }
45303
+ } catch (err) {
45304
+ logger.error(`Webhook handler failed for channel "${channel2.name}"`, { error: err });
45305
+ res.status(500).json({ ok: false, error: "Internal channel error" });
45306
+ }
45307
+ };
45308
+ }
45309
+ var init_webhook_source = __esm({
45310
+ "libs/sdk/src/channel/sources/webhook.source.ts"() {
45311
+ "use strict";
45312
+ init_call_surface();
45313
+ }
45314
+ });
45315
+
44481
45316
  // libs/sdk/src/channel/channel-scope.helper.ts
44482
45317
  async function registerChannelCapabilities(args) {
44483
45318
  const {
@@ -44488,11 +45323,15 @@ async function registerChannelCapabilities(args) {
44488
45323
  notificationService,
44489
45324
  flowRegistry,
44490
45325
  toolRegistry,
44491
- agentEmitterSubscribe,
44492
- jobEmitterSubscribe,
45326
+ http: http2,
44493
45327
  logger
44494
45328
  } = args;
45329
+ const completions = completionEventsOf(providers.getActiveScope());
45330
+ const agentEmitterSubscribe = args.agentEmitterSubscribe ?? ((cb) => completions.agents.subscribe(cb));
45331
+ const jobEmitterSubscribe = args.jobEmitterSubscribe ?? ((cb) => completions.jobs.subscribe(cb));
44495
45332
  const unsubscribers = [];
45333
+ const reservedPaths = http2 ? computeReservedPaths(http2.entryPath, http2.routeBase) : void 0;
45334
+ const webhookPaths = /* @__PURE__ */ new Map();
44496
45335
  const channelRegistry = new ChannelRegistry(providers, channelsList, owner);
44497
45336
  await channelRegistry.ready;
44498
45337
  const channelNotificationService = new ChannelNotificationService(
@@ -44508,31 +45347,23 @@ async function registerChannelCapabilities(args) {
44508
45347
  const sourceType = instance.source.type;
44509
45348
  switch (sourceType) {
44510
45349
  case "agent-completion": {
44511
- if (agentEmitterSubscribe) {
44512
- const unsub = wireAgentCompletionSource(
44513
- instance,
44514
- instance.metadata.source,
44515
- agentEmitterSubscribe,
44516
- logger
44517
- );
44518
- unsubscribers.push(unsub);
44519
- } else {
44520
- logger.warn(`Channel "${instance.name}" has agent-completion source but no agent emitter available`);
44521
- }
45350
+ const unsub = wireAgentCompletionSource(
45351
+ instance,
45352
+ instance.metadata.source,
45353
+ agentEmitterSubscribe,
45354
+ logger
45355
+ );
45356
+ unsubscribers.push(unsub);
44522
45357
  break;
44523
45358
  }
44524
45359
  case "job-completion": {
44525
- if (jobEmitterSubscribe) {
44526
- const unsub = wireJobCompletionSource(
44527
- instance,
44528
- instance.metadata.source,
44529
- jobEmitterSubscribe,
44530
- logger
44531
- );
44532
- unsubscribers.push(unsub);
44533
- } else {
44534
- logger.warn(`Channel "${instance.name}" has job-completion source but no job emitter available`);
44535
- }
45360
+ const unsub = wireJobCompletionSource(
45361
+ instance,
45362
+ instance.metadata.source,
45363
+ jobEmitterSubscribe,
45364
+ logger
45365
+ );
45366
+ unsubscribers.push(unsub);
44536
45367
  break;
44537
45368
  }
44538
45369
  case "app-event": {
@@ -44541,7 +45372,25 @@ async function registerChannelCapabilities(args) {
44541
45372
  unsubscribers.push(unsub);
44542
45373
  break;
44543
45374
  }
44544
- case "webhook":
45375
+ case "webhook": {
45376
+ const source = instance.metadata.source;
45377
+ if (!http2 || !reservedPaths) {
45378
+ logger.warn(`Channel "${instance.name}" has a webhook source but no HTTP server to serve ${source.path}`);
45379
+ break;
45380
+ }
45381
+ assertNotReserved("POST", source.path, reservedPaths);
45382
+ const claimedBy = webhookPaths.get(source.path);
45383
+ if (claimedBy) {
45384
+ throw new Error(
45385
+ `Channels "${claimedBy}" and "${instance.name}" both declare the webhook path ${source.path}. Give each webhook channel its own path.`
45386
+ );
45387
+ }
45388
+ webhookPaths.set(source.path, instance.name);
45389
+ const handler = createWebhookMiddleware(instance, source, logger);
45390
+ await http2.server.registerRoute("POST", source.path, wrapWithIpFilter(handler, http2.checkClientIp));
45391
+ logger.info(`Registered webhook route for channel "${instance.name}": POST ${source.path}`);
45392
+ break;
45393
+ }
44545
45394
  case "manual":
44546
45395
  break;
44547
45396
  case "service":
@@ -44624,6 +45473,7 @@ async function registerChannelCapabilities(args) {
44624
45473
  var init_channel_scope_helper = __esm({
44625
45474
  "libs/sdk/src/channel/channel-scope.helper.ts"() {
44626
45475
  "use strict";
45476
+ init_custom_routes_helper();
44627
45477
  init_tool_instance();
44628
45478
  init_tool_utils();
44629
45479
  init_channel_notification_service();
@@ -44633,7 +45483,9 @@ var init_channel_scope_helper = __esm({
44633
45483
  init_channel_reply_tool();
44634
45484
  init_agent_completion_source();
44635
45485
  init_app_event_source();
45486
+ init_completion_events();
44636
45487
  init_job_completion_source();
45488
+ init_webhook_source();
44637
45489
  }
44638
45490
  });
44639
45491
 
@@ -45876,12 +46728,12 @@ async function createElicitationStore(options = {}) {
45876
46728
  sqlite,
45877
46729
  keyPrefix = "mcp:elicit:",
45878
46730
  logger,
45879
- isEdgeRuntime: isEdgeRuntime2 = false,
46731
+ isEdgeRuntime: isEdgeRuntime3 = false,
45880
46732
  requiresPubSub = true,
45881
46733
  encryption
45882
46734
  } = options;
45883
46735
  if (sqlite) {
45884
- if (isEdgeRuntime2) {
46736
+ if (isEdgeRuntime3) {
45885
46737
  throw new ElicitationNotSupportedError(
45886
46738
  "SQLite elicitation store is not supported on Edge runtime (better-sqlite3 is a native module). Use Redis or Upstash instead."
45887
46739
  );
@@ -45955,7 +46807,7 @@ async function createElicitationStore(options = {}) {
45955
46807
  "Vercel KV is not supported for elicitation stores. Elicitation requires pub/sub for cross-node result routing, which Vercel KV does not support. Use Redis or Upstash instead."
45956
46808
  );
45957
46809
  }
45958
- if (isEdgeRuntime2 && (!finalStorageConfig || finalStorageConfig.type === "memory")) {
46810
+ if (isEdgeRuntime3 && (!finalStorageConfig || finalStorageConfig.type === "memory")) {
45959
46811
  throw new ElicitationNotSupportedError(
45960
46812
  "Elicitation requires distributed storage when running on Edge runtime. Edge functions are stateless and cannot use in-memory elicitation. Configure Redis or Upstash storage."
45961
46813
  );
@@ -45963,7 +46815,7 @@ async function createElicitationStore(options = {}) {
45963
46815
  const storage = await createStorage2({
45964
46816
  ...finalStorageConfig,
45965
46817
  prefix: keyPrefix,
45966
- fallback: isEdgeRuntime2 ? "error" : "memory"
46818
+ fallback: isEdgeRuntime3 ? "error" : "memory"
45967
46819
  });
45968
46820
  if (requiresPubSub && !storage.supportsPubSub()) {
45969
46821
  throw new ElicitationNotSupportedError(
@@ -47629,6 +48481,7 @@ var WorkflowStepExecutor;
47629
48481
  var init_workflow_step_executor = __esm({
47630
48482
  "libs/sdk/src/workflow/engine/workflow-step.executor.ts"() {
47631
48483
  "use strict";
48484
+ init_call_surface();
47632
48485
  init_errors();
47633
48486
  init_job_errors();
47634
48487
  init_workflow_errors();
@@ -47691,7 +48544,7 @@ var init_workflow_step_executor = __esm({
47691
48544
  const timer = setTimeout(() => {
47692
48545
  reject(new WorkflowJobTimeoutError(job.name, timeout));
47693
48546
  }, timeout);
47694
- Promise.resolve(ctx.execute(parsedInput)).then((result) => {
48547
+ runOnSurface("job", async () => ctx.execute(parsedInput)).then((result) => {
47695
48548
  clearTimeout(timer);
47696
48549
  resolve(result);
47697
48550
  }).catch((err) => {
@@ -47705,6 +48558,7 @@ var init_workflow_step_executor = __esm({
47705
48558
  });
47706
48559
 
47707
48560
  // libs/sdk/src/workflow/engine/workflow.engine.ts
48561
+ import { getAsyncContextMode } from "@frontmcp/utils";
47708
48562
  var WorkflowEngine;
47709
48563
  var init_workflow_engine = __esm({
47710
48564
  "libs/sdk/src/workflow/engine/workflow.engine.ts"() {
@@ -47774,7 +48628,7 @@ var init_workflow_engine = __esm({
47774
48628
  }
47775
48629
  continue;
47776
48630
  }
47777
- const batch = ready.slice(0, this.maxConcurrency);
48631
+ const batch = ready.slice(0, getAsyncContextMode() === "serialized" ? 1 : this.maxConcurrency);
47778
48632
  await Promise.allSettled(
47779
48633
  batch.map(async (step) => {
47780
48634
  if (step.condition) {
@@ -47881,12 +48735,13 @@ var init_workflow_engine = __esm({
47881
48735
  });
47882
48736
 
47883
48737
  // libs/sdk/src/job/execution/job-execution.manager.ts
47884
- import { randomUUID as randomUUID23 } from "@frontmcp/utils";
48738
+ import { randomUUID as randomUUID23, runRequestExclusive as runRequestExclusive4 } from "@frontmcp/utils";
47885
48739
  var JobExecutionManager;
47886
48740
  var init_job_execution_manager = __esm({
47887
48741
  "libs/sdk/src/job/execution/job-execution.manager.ts"() {
47888
48742
  "use strict";
47889
48743
  init_principal_utils();
48744
+ init_call_surface();
47890
48745
  init_errors();
47891
48746
  init_workflow_engine();
47892
48747
  init_job_permission_guard();
@@ -47923,7 +48778,7 @@ var init_job_execution_manager = __esm({
47923
48778
  };
47924
48779
  await this.stateStore.createRun(runRecord);
47925
48780
  if (opts.background) {
47926
- this.executeJobBackground(job, input, runId, opts).catch(async (err) => {
48781
+ runRequestExclusive4(() => this.executeJobBackground(job, input, runId, opts)).catch(async (err) => {
47927
48782
  this.logger.error(`Background job execution failed: ${err}`);
47928
48783
  try {
47929
48784
  await this.updateState(runId, {
@@ -47963,18 +48818,20 @@ var init_job_execution_manager = __esm({
47963
48818
  };
47964
48819
  await this.stateStore.createRun(runRecord);
47965
48820
  if (opts.background) {
47966
- this.executeWorkflowBackground(workflow, jobRegistry, runId, opts).catch(async (err) => {
47967
- this.logger.error(`Background workflow execution failed: ${err}`);
47968
- try {
47969
- await this.updateState(runId, {
47970
- state: "failed",
47971
- error: { message: err?.message ?? String(err), name: err?.name ?? "Error" },
47972
- completedAt: Date.now()
47973
- });
47974
- } catch (updateErr) {
47975
- this.logger.error(`Failed to update run state after error: ${updateErr}`);
48821
+ runRequestExclusive4(() => this.executeWorkflowBackground(workflow, jobRegistry, runId, opts)).catch(
48822
+ async (err) => {
48823
+ this.logger.error(`Background workflow execution failed: ${err}`);
48824
+ try {
48825
+ await this.updateState(runId, {
48826
+ state: "failed",
48827
+ error: { message: err?.message ?? String(err), name: err?.name ?? "Error" },
48828
+ completedAt: Date.now()
48829
+ });
48830
+ } catch (updateErr) {
48831
+ this.logger.error(`Failed to update run state after error: ${updateErr}`);
48832
+ }
47976
48833
  }
47977
- });
48834
+ );
47978
48835
  return { runId, state: "pending" };
47979
48836
  }
47980
48837
  return this.executeWorkflowInline(workflow, jobRegistry, runId, opts);
@@ -48027,7 +48884,7 @@ var init_job_execution_manager = __esm({
48027
48884
  authInfo: opts.authInfo ?? {},
48028
48885
  contextProviders: opts.contextProviders
48029
48886
  });
48030
- const result = await ctx.execute(parsedInput);
48887
+ const result = await runOnSurface("job", async () => ctx.execute(parsedInput));
48031
48888
  const logs = ctx.getLogs();
48032
48889
  await this.updateState(runId, {
48033
48890
  state: "completed",
@@ -48262,6 +49119,10 @@ var init_job_interface = __esm({
48262
49119
  getLogs() {
48263
49120
  return this._logs;
48264
49121
  }
49122
+ /** A job (or a workflow step running it) calls tools on the `'job'` surface. */
49123
+ callToolSurface() {
49124
+ return "job";
49125
+ }
48265
49126
  };
48266
49127
  }
48267
49128
  });
@@ -49364,7 +50225,11 @@ async function registerJobCapabilities(args) {
49364
50225
  for (const dynWf of dynamicWorkflows) {
49365
50226
  workflowRegistry.registerDynamic(dynWf);
49366
50227
  }
49367
- const executionManager = new JobExecutionManager(stateStore, logger, notifyFn);
50228
+ const completions = completionEventsOf(providers.getActiveScope());
50229
+ const executionManager = new JobExecutionManager(stateStore, logger, async (data) => {
50230
+ await notifyFn?.(data);
50231
+ await publishJobCompletion(data, stateStore, completions);
50232
+ });
49368
50233
  const managementTools = [
49369
50234
  ListJobsTool,
49370
50235
  ExecuteJobTool,
@@ -49393,9 +50258,27 @@ async function registerJobCapabilities(args) {
49393
50258
  managementTools
49394
50259
  };
49395
50260
  }
50261
+ async function publishJobCompletion(data, stateStore, completions) {
50262
+ const { type, state, runId } = data;
50263
+ if (type !== "job:status" && type !== "workflow:status") return;
50264
+ if (state !== "completed" && state !== "failed" || typeof runId !== "string") return;
50265
+ const run = await stateStore.getRun(runId);
50266
+ if (!run) return;
50267
+ const workflowName = type === "workflow:status" ? run.workflowName : void 0;
50268
+ completions.jobs.emit({
50269
+ jobName: workflowName ?? run.jobName,
50270
+ jobId: run.jobId,
50271
+ status: state === "completed" ? "success" : "error",
50272
+ ...run.completedAt !== void 0 ? { durationMs: run.completedAt - run.startedAt } : {},
50273
+ ...state === "completed" ? { output: completionOutputText(run.result) } : { error: run.error?.message },
50274
+ attempt: run.attempt,
50275
+ ...run.sessionId ? { sessionId: run.sessionId } : {}
50276
+ });
50277
+ }
49396
50278
  var init_job_scope_helper = __esm({
49397
50279
  "libs/sdk/src/job/job-scope.helper.ts"() {
49398
50280
  "use strict";
50281
+ init_completion_events();
49399
50282
  init_execute_workflow_tool();
49400
50283
  init_get_workflow_status_tool();
49401
50284
  init_list_workflows_tool();
@@ -49620,111 +50503,6 @@ var init_enforced_metadata_check = __esm({
49620
50503
  }
49621
50504
  });
49622
50505
 
49623
- // libs/sdk/src/server/custom-routes.helper.ts
49624
- function computeReservedPaths(entryPath, routeBase) {
49625
- const prefix = normalizeEntryPrefix(entryPath);
49626
- const base = normalizeScopeBase(routeBase);
49627
- const mcpBase = `${prefix}${base}`;
49628
- const exact = /* @__PURE__ */ new Set([
49629
- mcpBase === "" ? "/" : mcpBase,
49630
- `${mcpBase}/sse`,
49631
- `${mcpBase}/message`,
49632
- "/health",
49633
- "/metrics"
49634
- ]);
49635
- return { exact, prefixes: ["/oauth", "/.well-known"] };
49636
- }
49637
- function normalizeForCompare(path3) {
49638
- if (path3.length > 1 && path3.endsWith("/")) return path3.slice(0, -1);
49639
- return path3;
49640
- }
49641
- function assertNotReserved(method, path3, reserved) {
49642
- const normalized = normalizeForCompare(path3);
49643
- if (reserved.exact.has(normalized)) {
49644
- throw new ReservedRouteCollisionError(method, path3, normalized);
49645
- }
49646
- for (const prefix of reserved.prefixes) {
49647
- if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
49648
- throw new ReservedRouteCollisionError(method, path3, prefix);
49649
- }
49650
- }
49651
- }
49652
- function wrapWithAuth(handler, verifySession, logger) {
49653
- return async (req, res, next) => {
49654
- let result;
49655
- try {
49656
- result = await verifySession(req);
49657
- } catch (error) {
49658
- if (error instanceof FlowControl && error.type === "respond") {
49659
- result = error.output;
49660
- } else {
49661
- throw error;
49662
- }
49663
- }
49664
- if (!result || result.kind === "unauthorized") {
49665
- logger.verbose("custom-route auth: unauthorized");
49666
- return writeHttpResponse(
49667
- res,
49668
- httpRespond.unauthorized({
49669
- headers: result?.kind === "unauthorized" ? { "WWW-Authenticate": result.prmMetadataHeader } : void 0
49670
- })
49671
- );
49672
- }
49673
- if (result.kind === "forbidden") {
49674
- logger.verbose("custom-route auth: forbidden (insufficient scope)");
49675
- return writeHttpResponse(
49676
- res,
49677
- httpRespond.forbidden({
49678
- headers: { "WWW-Authenticate": result.prmMetadataHeader }
49679
- })
49680
- );
49681
- }
49682
- req.authSession = result.authorization;
49683
- req[ServerRequestTokens.auth] = result.authorization;
49684
- return handler(req, res, next);
49685
- };
49686
- }
49687
- function wrapWithIpFilter(handler, checkClientIp) {
49688
- return async (req, res, next) => {
49689
- const rejection = await checkClientIp(req, res);
49690
- if (rejection) return writeHttpResponse(res, rejection);
49691
- return handler(req, res, next);
49692
- };
49693
- }
49694
- function registerCustomHttpRoutes(args) {
49695
- const { routes, server, verifySession, checkClientIp, entryPath, routeBase, logger } = args;
49696
- if (!routes || routes.length === 0) return;
49697
- const reserved = computeReservedPaths(entryPath, routeBase);
49698
- for (const route of routes) {
49699
- assertNotReserved(route.method, route.path, reserved);
49700
- const handler = route.auth === true ? wrapWithAuth(route.handler, verifySession, logger) : route.handler;
49701
- server.registerRoute(route.method, route.path, wrapWithIpFilter(handler, checkClientIp));
49702
- logger.info(`Registered custom http route: ${route.method} ${route.path}${route.auth ? " (auth)" : ""}`);
49703
- }
49704
- }
49705
- var ReservedRouteCollisionError;
49706
- var init_custom_routes_helper = __esm({
49707
- "libs/sdk/src/server/custom-routes.helper.ts"() {
49708
- "use strict";
49709
- init_common();
49710
- init_server_validation();
49711
- ReservedRouteCollisionError = class extends Error {
49712
- constructor(method, path3, reserved) {
49713
- super(
49714
- `Custom http.route "${method} ${path3}" collides with the reserved FrontMCP path "${reserved}". Reserved prefixes are the MCP entry path (and its /sse + /message siblings), /oauth/*, /.well-known/*, /health, and /metrics. Choose a different path.`
49715
- );
49716
- this.method = method;
49717
- this.path = path3;
49718
- this.reserved = reserved;
49719
- this.name = "ReservedRouteCollisionError";
49720
- }
49721
- method;
49722
- path;
49723
- reserved;
49724
- };
49725
- }
49726
- });
49727
-
49728
50506
  // libs/sdk/src/skill/errors/tool-not-allowed.error.ts
49729
50507
  var ToolNotAllowedError, ToolApprovalRequiredError;
49730
50508
  var init_tool_not_allowed_error = __esm({
@@ -50899,7 +51677,7 @@ var init_load_skill_flow = __esm({
50899
51677
  }
50900
51678
  await assertSkillAuthorized(this.scope, entry, authInfo);
50901
51679
  }
50902
- loadResults.push({ loadResult: result });
51680
+ loadResults.push({ loadResult: skillToolsForCaller(result, this.scope.tools, ctx) });
50903
51681
  }
50904
51682
  this.state.set({ loadResults, warnings });
50905
51683
  this.logger.verbose("loadSkills:done", { loaded: loadResults.length, notFound: warnings.length });
@@ -51145,7 +51923,10 @@ async function authorizeSkillHttpRequest(scope, skillsConfig, request, logger) {
51145
51923
  if (mode === "inherit") {
51146
51924
  const authOptions = scope.auth?.options;
51147
51925
  if (!authOptions || isPublicMode(authOptions)) return { allowed: true, authInfo: {} };
51148
- const verified = await scope.runFlow("session:verify", { request });
51926
+ const verified = await scope.runFlow("session:verify", {
51927
+ request,
51928
+ sessionless: true
51929
+ });
51149
51930
  if (verified?.kind === "authorized") {
51150
51931
  return { allowed: true, authInfo: { ...authInfoFromAuthorization(verified.authorization) } };
51151
51932
  }
@@ -52114,7 +52895,12 @@ var init_skills_api_flow = __esm({
52114
52895
  respondNotFound();
52115
52896
  return;
52116
52897
  }
52117
- const { skill, availableTools, missingTools, isComplete, warning } = loadResult;
52898
+ const { skill, availableTools, missingTools, isComplete, warning } = skillToolsForCaller(
52899
+ loadResult,
52900
+ toolRegistry,
52901
+ void 0,
52902
+ SKILLS_HTTP_SURFACE
52903
+ );
52118
52904
  const skillEntry = createSkillEntryResolver(skillRegistry)(skillId, skill.id);
52119
52905
  if (skillEntry) {
52120
52906
  if (!await isSkillServable(this.scope, skillEntry, void 0, SKILLS_HTTP_SURFACE)) {
@@ -52401,6 +53187,8 @@ var init_server_interface = __esm({
52401
53187
  ServerRequest = class extends IncomingMessage {
52402
53188
  path;
52403
53189
  query;
53190
+ /** Path parameters of the matched route (`:providerId` in `/oauth/provider/:providerId/callback`). */
53191
+ params;
52404
53192
  body;
52405
53193
  authSession;
52406
53194
  };
@@ -52473,6 +53261,8 @@ var init_tool_interface = __esm({
52473
53261
  _toolNameInternal;
52474
53262
  /** @internal Tool input for fallback elicitation - set by CallToolFlow */
52475
53263
  _toolInputInternal;
53264
+ /** @internal JSON-RPC id of the tools/call request - set by CallToolFlow, for flows the call is handed on to */
53265
+ _jsonRpcRequestIdInternal;
52476
53266
  // ---- INPUT storages (backing fields)
52477
53267
  _rawInput;
52478
53268
  _input;
@@ -52505,6 +53295,10 @@ var init_tool_interface = __esm({
52505
53295
  this._progressToken = progressToken;
52506
53296
  this.signal = signal;
52507
53297
  }
53298
+ /** @internal The request's progress token, for flows the call is handed on to (the agent tool). */
53299
+ get _progressTokenInternal() {
53300
+ return this._progressToken;
53301
+ }
52508
53302
  /**
52509
53303
  * Like {@link ExecutionContextBase.fetch}, and also aborted with {@link signal}, so a
52510
53304
  * cancelled or timed-out call stops its outbound requests.
@@ -52923,9 +53717,10 @@ var init_agent_execution_loop = __esm({
52923
53717
  let iterations = 0;
52924
53718
  let totalPromptTokens = 0;
52925
53719
  let totalCompletionTokens = 0;
53720
+ let timeout;
52926
53721
  try {
52927
53722
  const timeoutPromise = new Promise((_, reject) => {
52928
- setTimeout(
53723
+ timeout = setTimeout(
52929
53724
  () => reject(new Error(`Agent execution timed out after ${this.config.timeout}ms`)),
52930
53725
  this.config.timeout
52931
53726
  );
@@ -52969,6 +53764,8 @@ var init_agent_execution_loop = __esm({
52969
53764
  error,
52970
53765
  durationMs: Date.now() - startTime
52971
53766
  };
53767
+ } finally {
53768
+ clearTimeout(timeout);
52972
53769
  }
52973
53770
  }
52974
53771
  /**
@@ -53288,6 +54085,7 @@ var init_agent_interface = __esm({
53288
54085
  "libs/sdk/src/common/interfaces/agent.interface.ts"() {
53289
54086
  "use strict";
53290
54087
  init_agent_execution_loop();
54088
+ init_frontmcp_context_storage();
53291
54089
  init_elicitation2();
53292
54090
  init_errors();
53293
54091
  init_execution_context_interface();
@@ -53555,6 +54353,10 @@ var init_agent_interface = __esm({
53555
54353
  async invokeAgent(agentId, input) {
53556
54354
  throw new AgentMethodNotAvailableError("invokeAgent", agentId);
53557
54355
  }
54356
+ /** An agent calls tools on the `'agent'` surface, as its model's tool calls do. */
54357
+ callToolSurface() {
54358
+ return "agent";
54359
+ }
53558
54360
  // ============================================================================
53559
54361
  // Notification Methods
53560
54362
  // ============================================================================
@@ -53671,7 +54473,9 @@ var init_agent_interface = __esm({
53671
54473
  {
53672
54474
  sessionId: this.authInfo.sessionId,
53673
54475
  getClientCapabilities: (sid) => this.scope.notifications.getClientCapabilities(sid),
53674
- tryGetContext: () => this.tryGetContext(),
54476
+ // The request holds what answers this question: a fallback answer the owner already sent
54477
+ // (the re-run of its call), or the 2026-07-28 exchange.
54478
+ tryGetContext: () => this.tryGetContext() ?? this.runningRequestContext(),
53675
54479
  entryName: this._agentNameInternal ?? this.agentName,
53676
54480
  entryInput: this._agentInputInternal ?? this.input,
53677
54481
  elicitationEnabled: this.scope.metadata.elicitation?.enabled === true
@@ -53681,6 +54485,17 @@ var init_agent_interface = __esm({
53681
54485
  options
53682
54486
  );
53683
54487
  }
54488
+ /**
54489
+ * The context of the request this agent runs in. An agent is built with its own provider registry,
54490
+ * which holds no request-scoped instances, so the context is read from the running request.
54491
+ */
54492
+ runningRequestContext() {
54493
+ try {
54494
+ return this.scope.providers.get(FrontMcpContextStorage).getStore();
54495
+ } catch {
54496
+ return void 0;
54497
+ }
54498
+ }
53684
54499
  /**
53685
54500
  * Respond with the final output and end execution.
53686
54501
  *
@@ -53836,6 +54651,7 @@ var ChannelContext;
53836
54651
  var init_channel_interface = __esm({
53837
54652
  "libs/sdk/src/common/interfaces/channel.interface.ts"() {
53838
54653
  "use strict";
54654
+ init_call_surface();
53839
54655
  init_mcp_error();
53840
54656
  init_execution_context_interface();
53841
54657
  ChannelContext = class extends ExecutionContextBase {
@@ -53913,13 +54729,27 @@ var init_channel_interface = __esm({
53913
54729
  this.logger.warn(`Channel "${this.channelName}": pushIncoming called but no handler is wired`);
53914
54730
  }
53915
54731
  }
54732
+ /**
54733
+ * While it handles an HTTP trigger (a webhook source's request), a channel calls tools on the
54734
+ * `'http-trigger'` surface. Handling any other event, it dispatches in-process.
54735
+ */
54736
+ callToolSurface() {
54737
+ return getCallSurface() === "http-trigger" ? "http-trigger" : void 0;
54738
+ }
53916
54739
  };
53917
54740
  }
53918
54741
  });
53919
54742
 
53920
54743
  // libs/sdk/src/common/utils/auth-info.utils.ts
53921
- function authInfoFromAuthorization(authorization) {
54744
+ function sessionIdPresentedBy(request) {
54745
+ const header = request.headers?.["mcp-session-id"];
54746
+ if (typeof header === "string" && header.length > 0) return header;
54747
+ const query = request.query?.["sessionId"];
54748
+ return typeof query === "string" && query.length > 0 ? query : void 0;
54749
+ }
54750
+ function authInfoFromAuthorization(authorization, presentedSessionId) {
53922
54751
  const { token, user, session } = authorization;
54752
+ const verifiedSession = session !== void 0 && presentedSessionId !== void 0 && session.id === presentedSessionId ? session : void 0;
53923
54753
  return {
53924
54754
  token,
53925
54755
  clientId: user?.sub,
@@ -53929,8 +54759,8 @@ function authInfoFromAuthorization(authorization) {
53929
54759
  user,
53930
54760
  extra: {
53931
54761
  user,
53932
- sessionId: session?.id,
53933
- sessionPayload: session?.payload
54762
+ sessionId: verifiedSession?.id,
54763
+ sessionPayload: verifiedSession?.payload
53934
54764
  }
53935
54765
  };
53936
54766
  }
@@ -54308,7 +55138,7 @@ var init_decide_request_intent_utils = __esm({
54308
55138
  });
54309
55139
 
54310
55140
  // libs/sdk/src/common/utils/guard.utils.ts
54311
- import { isAnonymousSubject as isAnonymousSubject3 } from "@frontmcp/auth";
55141
+ import { isAnonymousSubject as isAnonymousSubject4 } from "@frontmcp/auth";
54312
55142
  import {
54313
55143
  ConcurrencyLimitError
54314
55144
  } from "@frontmcp/guard";
@@ -54321,15 +55151,19 @@ function enforceIpFilter(scope, clientIp, buildRejection = () => httpRespond.for
54321
55151
  function buildPartitionContext(source) {
54322
55152
  if (!source) return void 0;
54323
55153
  const clientId = source.authInfo?.clientId;
54324
- const userId = isAnonymousSubject3(clientId) ? void 0 : String(clientId);
54325
- const verifiedSessionId = source.authInfo?.sessionId ?? source.authInfo?.extra?.sessionId;
55154
+ const userId = isAnonymousSubject4(clientId) ? void 0 : String(clientId);
54326
55155
  const callerKey = userId ? `user:${userId}` : "anonymous";
54327
55156
  return {
54328
- sessionId: verifiedSessionId === source.sessionId ? source.sessionId : callerKey,
55157
+ sessionId: verifiedSessionOf(source) ?? callerKey,
54329
55158
  clientIp: source.metadata?.clientIp,
54330
55159
  userId
54331
55160
  };
54332
55161
  }
55162
+ function verifiedSessionOf(source) {
55163
+ if ("verifiedSessionId" in source) return source.verifiedSessionId;
55164
+ const confirmed = source.authInfo?.sessionId ?? source.authInfo?.extra?.sessionId;
55165
+ return confirmed === source.sessionId ? source.sessionId : void 0;
55166
+ }
54333
55167
  function partitionsByIdentity(partitionBy) {
54334
55168
  return partitionBy === "session" || partitionBy === "userId" || typeof partitionBy === "function";
54335
55169
  }
@@ -54512,8 +55346,12 @@ var init_primary_auth_provider_interface = __esm({
54512
55346
  * @param _expectedAudience The protected resource(s) the token must be for
54513
55347
  * (`aud` names at least one), when the caller is a
54514
55348
  * resource endpoint.
55349
+ * @param _expectedIssuer The issuer the token must name (or one of them):
55350
+ * the ones this instance accepts for the request
55351
+ * (`LocalPrimaryAuth.acceptedIssuersFor`). Default:
55352
+ * its boot-time issuer.
54515
55353
  */
54516
- verifyGatewayToken(_token, _requestBaseUrl, _expectedAudience) {
55354
+ verifyGatewayToken(_token, _requestBaseUrl, _expectedAudience, _expectedIssuer) {
54517
55355
  return Promise.resolve({
54518
55356
  ok: false,
54519
55357
  error: "gateway token verification is not supported by this auth mode"
@@ -54586,7 +55424,10 @@ function findSkillByPath(scope, skillPath) {
54586
55424
  const segments = skillPath.split("/").filter((s) => s.length > 0);
54587
55425
  if (segments.length === 0) return void 0;
54588
55426
  const lastSegment = segments[segments.length - 1];
54589
- return visible.find((s) => s.name === lastSegment && s.getSkillPath() === lastSegment);
55427
+ const byName = visible.find((s) => s.name === lastSegment && s.getSkillPath() === lastSegment);
55428
+ if (byName) return byName;
55429
+ if (segments.length !== 1) return void 0;
55430
+ return visible.find((s) => s.metadata.id === lastSegment && s.getSkillPath() === s.metadata.name);
54590
55431
  }
54591
55432
  async function findAndLoadSkillByPath(scope, skillPath) {
54592
55433
  const registry = scope.skills;
@@ -55045,7 +55886,10 @@ function buildPerSkillResourceRecord(scope, skill, opts = {}) {
55045
55886
  description: skill.metadata.description,
55046
55887
  mimeType: SKILL_MD_MIME_TYPE,
55047
55888
  annotations,
55048
- _meta: meta
55889
+ _meta: meta,
55890
+ // The skill's own `availableWhen`, so `resources/list` and `resources/read` offer its SKILL.md
55891
+ // on the surfaces that offer the skill, and answer others as for an unknown resource.
55892
+ ...skill.metadata.availableWhen !== void 0 && { availableWhen: skill.metadata.availableWhen }
55049
55893
  };
55050
55894
  const handler = async () => {
55051
55895
  const { loadResult } = await findAndLoadSkillByPath(scope, skillPath);
@@ -55119,6 +55963,8 @@ function refreshFromSkill(resource, skill) {
55119
55963
  }
55120
55964
  Object.assign(resource.metadata, extensions);
55121
55965
  resource.metadata["description"] = skill.metadata.description;
55966
+ if (skill.metadata.availableWhen === void 0) Reflect.deleteProperty(resource.metadata, "availableWhen");
55967
+ else resource.metadata["availableWhen"] = skill.metadata.availableWhen;
55122
55968
  resource.extensionKeys = Object.keys(extensions);
55123
55969
  }
55124
55970
  var init_sep_2640_per_skill = __esm({
@@ -55218,11 +56064,23 @@ var init_skill_audit_helper = __esm({
55218
56064
  });
55219
56065
 
55220
56066
  // libs/sdk/src/skill/skill-scope.helper.ts
56067
+ function declaresDynamicSkills(metadata) {
56068
+ return metadata.dynamicSkills === true || (metadata.plugins ?? []).some((plugin) => declaresDynamicSkills(normalizePlugin(plugin).metadata));
56069
+ }
56070
+ function pluginsRegisterDynamicSkills(plugins) {
56071
+ for (const plugin of plugins) {
56072
+ if (declaresDynamicSkills(normalizePlugin(plugin).metadata)) return true;
56073
+ }
56074
+ return false;
56075
+ }
55221
56076
  async function registerSkillCapabilities(options) {
55222
- const { skillRegistry, flowRegistry, resourceRegistry, providers, skillsConfig, logger } = options;
56077
+ const { skillRegistry, flowRegistry, resourceRegistry, providers, skillsConfig, logger, dynamicSkills } = options;
55223
56078
  registerSkillAuditWriter({ providers, audit: skillsConfig?.audit, logger });
55224
56079
  await flowRegistry.registryFlows([FilterSkillsFlow]);
55225
- if (!skillRegistry.hasAny()) {
56080
+ if (dynamicSkills) {
56081
+ skillRegistry.expectDynamicSkills();
56082
+ }
56083
+ if (!skillRegistry.servesSkills()) {
55226
56084
  return;
55227
56085
  }
55228
56086
  await flowRegistry.registryFlows([SearchSkillsFlow, LoadSkillFlow]);
@@ -55264,6 +56122,7 @@ async function registerSep2640Resources(options) {
55264
56122
  var init_skill_scope_helper = __esm({
55265
56123
  "libs/sdk/src/skill/skill-scope.helper.ts"() {
55266
56124
  "use strict";
56125
+ init_plugin_utils();
55267
56126
  init_flows2();
55268
56127
  init_resources();
55269
56128
  init_sep_2640_last_modified();
@@ -55849,8 +56708,10 @@ var init_session_store_factory = __esm({
55849
56708
  });
55850
56709
 
55851
56710
  // libs/sdk/src/transport/flows/handle.mcp-20260728.flow.ts
56711
+ import { isAnonymousSubject as isAnonymousSubject5 } from "@frontmcp/auth";
55852
56712
  import { z as z102 } from "@frontmcp/lazy-zod";
55853
56713
  import { MCP_20260728_META as MCP_20260728_META5 } from "@frontmcp/protocol";
56714
+ import { runRequestExclusive as runRequestExclusive5 } from "@frontmcp/utils";
55854
56715
  function frame2(message) {
55855
56716
  return encoder3.encode(`event: message
55856
56717
  data: ${JSON.stringify(message)}
@@ -55900,6 +56761,11 @@ async function* streamMessageResponse(options, sink, requestId, runInContext) {
55900
56761
  }
55901
56762
  yield frame2({ jsonrpc: "2.0", id, result: outcome?.result });
55902
56763
  }
56764
+ function isAnonymousCaller(auth) {
56765
+ if (!auth) return true;
56766
+ if (!isAnonymousSubject5(auth.user?.sub)) return false;
56767
+ return !auth.token || auth.session?.payload?.isPublic === true;
56768
+ }
55903
56769
  function errorResponse(status, error, id) {
55904
56770
  return httpRespond.json(
55905
56771
  {
@@ -55942,7 +56808,9 @@ var init_handle_mcp_20260728_flow = __esm({
55942
56808
  /** Negotiated protocol version for this request. */
55943
56809
  version: z102.string().optional(),
55944
56810
  requestType: z102.enum(["notification", "subscriptions", "message"]).optional(),
55945
- isAnonymous: z102.boolean().default(true)
56811
+ isAnonymous: z102.boolean().default(true),
56812
+ /** Whether the client declared this revision itself (not served under it by default). */
56813
+ clientDeclaredRevision: z102.boolean().optional()
55946
56814
  });
55947
56815
  name38 = "handle:mcp-202607280728";
55948
56816
  ({ Stage: Stage38 } = FlowHooksOf(name38));
@@ -55956,11 +56824,7 @@ var init_handle_mcp_20260728_flow = __esm({
55956
56824
  async parseInput() {
55957
56825
  const { request } = this.rawInput;
55958
56826
  const auth = request[ServerRequestTokens.auth];
55959
- this.state.set(
55960
- stateSchema37.parse({
55961
- isAnonymous: !auth?.token || auth.token.length === 0 || auth.session?.payload?.isPublic === true
55962
- })
55963
- );
56827
+ this.state.set(stateSchema37.parse({ isAnonymous: isAnonymousCaller(auth) }));
55964
56828
  }
55965
56829
  async validate() {
55966
56830
  const { request } = this.rawInput;
@@ -55993,6 +56857,7 @@ var init_handle_mcp_20260728_flow = __esm({
55993
56857
  return;
55994
56858
  }
55995
56859
  this.state.set("version", result.version);
56860
+ this.state.set("clientDeclaredRevision", strictHeaders);
55996
56861
  }
55997
56862
  /**
55998
56863
  * Resolve a tool's input JSON Schema so `x-mcp-header` annotations can be
@@ -56017,7 +56882,7 @@ var init_handle_mcp_20260728_flow = __esm({
56017
56882
  if (!context) return (fn) => fn();
56018
56883
  const storage = this.scope.providers.get(FrontMcpContextStorage);
56019
56884
  return async (fn) => {
56020
- await storage.runWithContext(context, fn);
56885
+ await runRequestExclusive5(() => storage.runWithContext(context, fn));
56021
56886
  };
56022
56887
  }
56023
56888
  findToolSchema(scope, toolName) {
@@ -56103,13 +56968,17 @@ var init_handle_mcp_20260728_flow = __esm({
56103
56968
  clientCapabilities,
56104
56969
  frontmcpContext: this.tryGetContext(),
56105
56970
  authInfo: auth ? {
56971
+ // No session is presented: this revision has none, so the request has no verified
56972
+ // session (`extra.sessionId`), and anything kept across requests falls back to the
56973
+ // authenticated caller.
56106
56974
  ...authInfoFromAuthorization(auth),
56107
- // Sessions no longer exist at the protocol level, but the shared
56108
- // handlers key per-request state (memory, credentials) off an id.
56109
- // Derive a request-scoped one so nothing leaks between calls.
56110
- sessionId: auth.session?.id
56975
+ // The shared handlers still key per-request state (provider views, notifications)
56976
+ // off an id. It names the request only, the same per-request id as the request
56977
+ // context's `sessionId`; the request context does not vouch for it (#629).
56978
+ sessionId: auth.session?.id ?? this.tryGetContext()?.sessionId
56111
56979
  } : void 0,
56112
56980
  isAnonymous: this.state.required.isAnonymous,
56981
+ clientDeclaredRevision: this.state.clientDeclaredRevision !== false,
56113
56982
  composeInstructions: () => this.scope.metadata.instructions,
56114
56983
  notificationSink: sink,
56115
56984
  traceContext: extractTraceContext(meta)
@@ -56991,6 +57860,7 @@ var init_handle_streamable_http_flow = __esm({
56991
57860
  init_ext_apps2();
56992
57861
  init_ha_constants();
56993
57862
  init_skill_mode_utils();
57863
+ init_mcp_surface();
56994
57864
  plan40 = {
56995
57865
  pre: ["parseInput", "router"],
56996
57866
  execute: ["onInitialize", "onMessage", "onElicitResult", "onSseListener", "onExtApps"],
@@ -57373,7 +58243,8 @@ var init_handle_streamable_http_flow = __esm({
57373
58243
  sessionId: session.id,
57374
58244
  sessionIdPayload: session.payload,
57375
58245
  token
57376
- }
58246
+ },
58247
+ surface: mcpRequestSurface(this.scope)
57377
58248
  }
57378
58249
  });
57379
58250
  const parsed = CallToolResultSchema4.safeParse(result);
@@ -59695,6 +60566,41 @@ var init_request_log_redaction = __esm({
59695
60566
  }
59696
60567
  });
59697
60568
 
60569
+ // libs/sdk/src/transport/persistent-session-owner.ts
60570
+ var persistent_session_owner_exports = {};
60571
+ __export(persistent_session_owner_exports, {
60572
+ claimPersistentSession: () => claimPersistentSession,
60573
+ persistentSessionCallerKey: () => persistentSessionCallerKey
60574
+ });
60575
+ import { isAnonymousSubject as isAnonymousSubject6 } from "@frontmcp/auth";
60576
+ import { sha256Hex as sha256Hex13 } from "@frontmcp/utils";
60577
+ function persistentSessionCallerKey(authorization) {
60578
+ const sub = authorization?.user?.sub;
60579
+ if (!isAnonymousSubject6(sub)) {
60580
+ const iss = authorization?.user?.iss;
60581
+ return `sub:${sha256Hex13(JSON.stringify([typeof iss === "string" ? iss : "", sub]))}`;
60582
+ }
60583
+ if (authorization?.token) return `token:${sha256Hex13(authorization.token)}`;
60584
+ return null;
60585
+ }
60586
+ function claimPersistentSession(session, callerKey, store) {
60587
+ if (!owners.has(session) && store && store.initial !== void 0) {
60588
+ owners.set(session, store.initial);
60589
+ }
60590
+ if (!owners.has(session)) {
60591
+ owners.set(session, callerKey);
60592
+ return "claimed";
60593
+ }
60594
+ return owners.get(session) === callerKey ? "owner" : "refused";
60595
+ }
60596
+ var owners;
60597
+ var init_persistent_session_owner = __esm({
60598
+ "libs/sdk/src/transport/persistent-session-owner.ts"() {
60599
+ "use strict";
60600
+ owners = /* @__PURE__ */ new WeakMap();
60601
+ }
60602
+ });
60603
+
59698
60604
  // libs/sdk/src/transport/web-standard-mcp.ts
59699
60605
  var web_standard_mcp_exports = {};
59700
60606
  __export(web_standard_mcp_exports, {
@@ -59721,7 +60627,8 @@ async function wireServer(scope, serverOptions, sessionId, sessionIdGenerator, e
59721
60627
  }
59722
60628
  async function buildPersistentWebStandardMcp(scope, options) {
59723
60629
  const serverOptions = buildScopedServerOptions(scope);
59724
- return wireServer(scope, serverOptions, options.sessionId, () => options.sessionId, false);
60630
+ const pair = await wireServer(scope, serverOptions, options.sessionId, () => options.sessionId, false);
60631
+ return options.owner ? { ...pair, owner: options.owner } : pair;
59725
60632
  }
59726
60633
  async function runWebStandardMcp(scope, request, options) {
59727
60634
  if (options.persistent) {
@@ -59730,13 +60637,7 @@ async function runWebStandardMcp(scope, request, options) {
59730
60637
  const sessionId = `web:${randomUUID26()}`;
59731
60638
  const accept = request.headers.get("accept") ?? "";
59732
60639
  const wantsStream = request.method === "GET" || options.sse && accept.includes("text/event-stream");
59733
- const { mcpServer, transport } = await wireServer(
59734
- scope,
59735
- options.serverOptions,
59736
- sessionId,
59737
- void 0,
59738
- !wantsStream
59739
- );
60640
+ const { mcpServer, transport } = await wireServer(scope, options.serverOptions, sessionId, void 0, !wantsStream);
59740
60641
  const response = await transport.handleRequest(request, {
59741
60642
  authInfo: options.authInfo
59742
60643
  });
@@ -59798,6 +60699,10 @@ var init_http_request_flow = __esm({
59798
60699
  "router"
59799
60700
  ],
59800
60701
  execute: [
60702
+ // A Durable Object's persistent session serves only the caller that opened
60703
+ // it: anyone else presenting its id is answered as an unknown session. First,
60704
+ // before every protocol handler (2026-07-28 included), so none can serve it.
60705
+ "checkPersistentSessionOwner",
59801
60706
  // Protocol 2026-07-28. Runs before EVERY session-era handler — including
59802
60707
  // `handleWebFetch`, which answers any request carrying a Web `Request`
59803
60708
  // regardless of intent and would otherwise swallow 2026 traffic on a
@@ -59922,7 +60827,10 @@ var init_http_request_flow = __esm({
59922
60827
  const { request } = this.rawInput;
59923
60828
  this.logger.verbose(`[${this.requestId}] checkAuthorization: verifying session`);
59924
60829
  try {
59925
- const result = await this.scope.runFlow("session:verify", { request });
60830
+ const result = await this.scope.runFlow("session:verify", {
60831
+ request,
60832
+ sessionless: this.servesProtocol20260728()
60833
+ });
59926
60834
  if (!result) {
59927
60835
  this.logger.error(`[${this.requestId}] failed to verify session`);
59928
60836
  throw new SessionVerificationFailedError();
@@ -59934,7 +60842,8 @@ var init_http_request_flow = __esm({
59934
60842
  const ctx = this.tryGetContext();
59935
60843
  if (ctx) {
59936
60844
  const { token, user } = result.authorization;
59937
- ctx.updateAuthInfo(authInfoFromAuthorization(result.authorization));
60845
+ const presentedSessionId = this.servesProtocol20260728() ? void 0 : sessionIdPresentedBy(request);
60846
+ ctx.updateAuthInfo(authInfoFromAuthorization(result.authorization, presentedSessionId));
59938
60847
  await this.bindOrchestrationAccessor(ctx, token, user);
59939
60848
  }
59940
60849
  }
@@ -59986,6 +60895,24 @@ var init_http_request_flow = __esm({
59986
60895
  });
59987
60896
  ctx.setContextToken(ORCHESTRATED_AUTH_ACCESSOR2, new OrchestratedAuthAccessorAdapter(authorization));
59988
60897
  }
60898
+ /**
60899
+ * Whether this request is served under MCP protocol 2026-07-28.
60900
+ *
60901
+ * A V8-isolate deployment (Cloudflare Worker) defaults to the stateless revision: it needs no
60902
+ * session storage, so an unversioned call is served directly instead of minting a session in a
60903
+ * Durable Object. Node keeps defaulting to the session pipeline. Either way an explicit declaration
60904
+ * from the client always wins.
60905
+ */
60906
+ servesProtocol20260728() {
60907
+ const { request } = this.rawInput;
60908
+ const isWebMode = !!request[ServerRequestTokens.webRequest];
60909
+ const defaultProtocolVersion = this.scope.metadata.transport?.defaultProtocolVersion ?? (isWebMode ? "2026-07-28" : "legacy");
60910
+ return isProtocol20260728Request({
60911
+ headers: request.headers,
60912
+ body: request.body,
60913
+ defaultVersion: defaultProtocolVersion
60914
+ });
60915
+ }
59989
60916
  async router() {
59990
60917
  try {
59991
60918
  const { request } = this.rawInput;
@@ -60019,12 +60946,7 @@ var init_http_request_flow = __esm({
60019
60946
  reasons: decision.reasons,
60020
60947
  debug: decision.debug
60021
60948
  });
60022
- const defaultProtocolVersion = transportConfig.defaultProtocolVersion ?? (isWebMode ? "2026-07-28" : "legacy");
60023
- if (isProtocol20260728Request({
60024
- headers: request.headers,
60025
- body: request.body,
60026
- defaultVersion: defaultProtocolVersion
60027
- })) {
60949
+ if (this.servesProtocol20260728()) {
60028
60950
  const verify = this.state.required.verifyResult;
60029
60951
  if (verify.kind === "authorized") {
60030
60952
  request[ServerRequestTokens.auth] = verify.authorization;
@@ -60070,6 +60992,11 @@ var init_http_request_flow = __esm({
60070
60992
  const deleteVerify = this.state.required.verifyResult;
60071
60993
  const requestedSessionId = request[ServerRequestTokens.sessionId] ?? request.headers["mcp-session-id"] ?? request.query?.["sessionId"];
60072
60994
  const deleteAuthz = authorizeSessionTermination(deleteVerify, requestedSessionId);
60995
+ if (request[ServerRequestTokens.webTransport] && deleteAuthz.kind !== "unauthorized") {
60996
+ request[ServerRequestTokens.auth] = deleteVerify.authorization;
60997
+ this.state.set("intent", decision.intent);
60998
+ return;
60999
+ }
60073
61000
  if (deleteAuthz.kind === "unauthorized") {
60074
61001
  this.logger.warn(`[${this.requestId}] DELETE session denied: request is not authenticated`);
60075
61002
  this.respond(
@@ -60183,6 +61110,28 @@ var init_http_request_flow = __esm({
60183
61110
  throw error;
60184
61111
  }
60185
61112
  }
61113
+ async checkPersistentSessionOwner() {
61114
+ const req = this.rawInput.request;
61115
+ const persistent = req[ServerRequestTokens.webTransport];
61116
+ if (!req[ServerRequestTokens.webRequest] || !persistent || typeof persistent !== "object") return;
61117
+ const { claimPersistentSession: claimPersistentSession2, persistentSessionCallerKey: persistentSessionCallerKey2 } = await Promise.resolve().then(() => (init_persistent_session_owner(), persistent_session_owner_exports));
61118
+ const authorization = req[ServerRequestTokens.auth];
61119
+ const callerKey = persistentSessionCallerKey2(authorization);
61120
+ const store = persistent.owner;
61121
+ const claim = claimPersistentSession2(persistent, callerKey, store);
61122
+ if (claim === "claimed") {
61123
+ await store?.save(callerKey);
61124
+ return;
61125
+ }
61126
+ if (claim === "owner") return;
61127
+ this.logger.warn(`[${this.requestId}] Persistent session refused: it was opened by another caller`);
61128
+ this.respond(
61129
+ httpRespond.json(
61130
+ { jsonrpc: "2.0", error: { code: -32001, message: "Session not found" }, id: null },
61131
+ { status: 404 }
61132
+ )
61133
+ );
61134
+ }
60186
61135
  async handleWebFetch() {
60187
61136
  const { request } = this.rawInput;
60188
61137
  const req = request;
@@ -60197,7 +61146,7 @@ var init_http_request_flow = __esm({
60197
61146
  const proto = expandProtocolConfig2(scope.metadata.transport?.protocol);
60198
61147
  const sse = proto.streamable && !proto.json;
60199
61148
  const authorization = req[ServerRequestTokens.auth];
60200
- const authInfo = authorization ? authInfoFromAuthorization(authorization) : void 0;
61149
+ const authInfo = authorization ? authInfoFromAuthorization(authorization, sessionIdPresentedBy(request)) : void 0;
60201
61150
  const persistent = req[ServerRequestTokens.webTransport];
60202
61151
  const response = await runWebStandardMcp2(scope, webRequest, {
60203
61152
  serverOptions: buildScopedServerOptions2(scope),
@@ -60403,6 +61352,9 @@ var init_http_request_flow = __esm({
60403
61352
  __decorateClass([
60404
61353
  Stage43("router")
60405
61354
  ], HttpRequestFlow.prototype, "router", 1);
61355
+ __decorateClass([
61356
+ Stage43("checkPersistentSessionOwner")
61357
+ ], HttpRequestFlow.prototype, "checkPersistentSessionOwner", 1);
60406
61358
  __decorateClass([
60407
61359
  Stage43("handleWebFetch")
60408
61360
  ], HttpRequestFlow.prototype, "handleWebFetch", 1);
@@ -60990,7 +61942,11 @@ var init_scope_instance = __esm({
60990
61942
  if (!this.cliMode) {
60991
61943
  await this.buildAuthUiRegistry();
60992
61944
  }
60993
- if (this.scopeSkills.hasAny()) {
61945
+ const dynamicSkills = pluginsRegisterDynamicSkills([
61946
+ ...serverPlugins,
61947
+ ...this.scopeApps.getApps().flatMap((app) => "plugins" in app.metadata ? app.metadata.plugins ?? [] : [])
61948
+ ]);
61949
+ if (this.scopeSkills.hasAny() || dynamicSkills) {
60994
61950
  const store = createSkillSessionStore({ type: "memory" });
60995
61951
  this._skillSession = new SkillSessionManager(
60996
61952
  {
@@ -61041,7 +61997,8 @@ var init_scope_instance = __esm({
61041
61997
  resourceRegistry: this.scopeResources,
61042
61998
  providers: this.scopeProviders,
61043
61999
  skillsConfig: this.metadata.skillsConfig,
61044
- logger: this.logger
62000
+ logger: this.logger,
62001
+ dynamicSkills
61045
62002
  });
61046
62003
  const jobsConfig = this.metadata.jobs;
61047
62004
  const metaRecord = this.metadata;
@@ -61157,6 +62114,15 @@ var init_scope_instance = __esm({
61157
62114
  notificationService: this.notificationService,
61158
62115
  flowRegistry: this.scopeFlows,
61159
62116
  toolRegistry: this.scopeTools,
62117
+ http: {
62118
+ server: this.server,
62119
+ checkClientIp: (request, response) => this.runFlow("http:ip-filter", {
62120
+ request,
62121
+ response
62122
+ }),
62123
+ entryPath: this.entryPath,
62124
+ routeBase: this.routeBase
62125
+ },
61160
62126
  logger: this.logger
61161
62127
  });
61162
62128
  this._scopeChannels = channelResult.channelRegistry;
@@ -61561,12 +62527,12 @@ var init_scope_instance = __esm({
61561
62527
  return this._authoritiesScopeMapping;
61562
62528
  }
61563
62529
  /**
61564
- * Collect all supported OAuth scopes from base OIDC scopes and
61565
- * tool-level authProvider scope declarations.
61566
- * Used by PRM endpoint to populate `scopes_supported` (RFC 9728).
62530
+ * Collect the OAuth scopes entries declare on their `authProviders`. The PRM
62531
+ * endpoint advertises them in `scopes_supported` (RFC 9728) next to the scopes
62532
+ * the auth mode grants (`resourceScopesFor`), not a fixed OIDC list (#629).
61567
62533
  */
61568
62534
  getAllSupportedScopes() {
61569
- const scopes = /* @__PURE__ */ new Set(["openid", "profile", "email"]);
62535
+ const scopes = /* @__PURE__ */ new Set();
61570
62536
  const collectScopes = (metadata) => {
61571
62537
  const authProviders = metadata?.["authProviders"];
61572
62538
  if (!Array.isArray(authProviders)) return;
@@ -61844,7 +62810,7 @@ import {
61844
62810
  ProviderScope as ProviderScope8,
61845
62811
  tokenName as tokenName18
61846
62812
  } from "@frontmcp/di";
61847
- function verifiedSessionOf(requestContext) {
62813
+ function verifiedSessionOf2(requestContext) {
61848
62814
  const { authInfo } = requestContext;
61849
62815
  const verified = authInfo.sessionId ?? authInfo.extra?.["sessionId"];
61850
62816
  return typeof verified === "string" && verified.length > 0 && verified !== STATELESS_SESSION_ID ? verified : void 0;
@@ -62605,7 +63571,7 @@ var init_provider_registry = __esm({
62605
63571
  if (!(requestContext instanceof FrontMcpContext)) {
62606
63572
  return newProviderStore();
62607
63573
  }
62608
- if (!this.sessionCacheEnabled || verifiedSessionOf(requestContext) !== sessionKey) {
63574
+ if (!this.sessionCacheEnabled || verifiedSessionOf2(requestContext) !== sessionKey) {
62609
63575
  let stores = requestContext.get(REQUEST_PROVIDER_STORES);
62610
63576
  if (!stores) {
62611
63577
  stores = /* @__PURE__ */ new WeakMap();
@@ -63027,6 +63993,7 @@ var init_web_response_renderer = __esm({
63027
63993
  });
63028
63994
 
63029
63995
  // libs/sdk/src/transport/web-fetch-handler.ts
63996
+ import { runRequestExclusive as runRequestExclusive6 } from "@frontmcp/utils";
63030
63997
  function isCloudflareWorkersRuntime(request) {
63031
63998
  if (typeof navigator !== "undefined" && navigator.userAgent === CLOUDFLARE_WORKERS_USER_AGENT) return true;
63032
63999
  const cloudflareProperties = request.cf;
@@ -63140,10 +64107,12 @@ async function runHttpRequestFlowWeb(scope, request, opts = {}) {
63140
64107
  const serverRequest = await toServerRequest(request, url, opts.ctx, opts.persistent, opts.env);
63141
64108
  let output;
63142
64109
  try {
63143
- output = await scope.runFlow("http:request", {
63144
- request: serverRequest,
63145
- response: {}
63146
- });
64110
+ output = await runRequestExclusive6(
64111
+ () => scope.runFlow("http:request", {
64112
+ request: serverRequest,
64113
+ response: {}
64114
+ })
64115
+ );
63147
64116
  } catch (error) {
63148
64117
  output = flowErrorToHttpOutput(error);
63149
64118
  }
@@ -63156,10 +64125,12 @@ async function runMatchingHttpFlowWeb(scope, request, opts = {}) {
63156
64125
  if (!flowName) return void 0;
63157
64126
  let output;
63158
64127
  try {
63159
- output = await scope.runFlow(flowName, {
63160
- request: serverRequest,
63161
- response: {}
63162
- });
64128
+ output = await runRequestExclusive6(
64129
+ () => scope.runFlow(flowName, {
64130
+ request: serverRequest,
64131
+ response: {}
64132
+ })
64133
+ );
63163
64134
  } catch (error) {
63164
64135
  output = flowErrorToHttpOutput(error);
63165
64136
  }
@@ -63170,6 +64141,7 @@ async function toServerRequest(request, url, ctx, persistent, env) {
63170
64141
  request.headers.forEach((v, k) => {
63171
64142
  headers[k] = v;
63172
64143
  });
64144
+ headers["host"] = url.host;
63173
64145
  const query = {};
63174
64146
  url.searchParams.forEach((v, k) => {
63175
64147
  const existing = query[k];
@@ -63200,6 +64172,8 @@ async function toServerRequest(request, url, ctx, persistent, env) {
63200
64172
  const webRequest = new Request(url.toString(), init);
63201
64173
  const serverRequest = {
63202
64174
  method,
64175
+ // The scheme the client used, as Express reports it (`req.protocol`).
64176
+ protocol: url.protocol.slice(0, -1),
63203
64177
  path: url.pathname,
63204
64178
  url: url.pathname + url.search,
63205
64179
  headers,
@@ -63229,10 +64203,57 @@ function findMisconfiguration(error) {
63229
64203
  if (typeof code === "string" && MISCONFIGURATION_REMEDIES[code]) {
63230
64204
  return { code, remedy: MISCONFIGURATION_REMEDIES[code] };
63231
64205
  }
64206
+ if (current.name === "ZodError") return { code: "CONFIG_INVALID", remedy: CONFIG_INVALID_REMEDY };
63232
64207
  current = current.cause;
63233
64208
  }
63234
64209
  return void 0;
63235
64210
  }
64211
+ function createDeferredServerBuild(build, options = {}) {
64212
+ const now = options.now ?? Date.now;
64213
+ let built;
64214
+ let inFlight;
64215
+ let failure;
64216
+ return {
64217
+ get(arg) {
64218
+ if (built) return Promise.resolve(built.value);
64219
+ if (inFlight) return inFlight;
64220
+ if (failure && now() < failure.retryAt) return Promise.reject(failure.error);
64221
+ const failures = failure?.count ?? 0;
64222
+ inFlight = build(arg).then(
64223
+ (value) => {
64224
+ built = { value };
64225
+ failure = void 0;
64226
+ inFlight = void 0;
64227
+ return value;
64228
+ },
64229
+ (error) => {
64230
+ const delay = Math.min(STARTUP_RETRY_MAX_MS, STARTUP_RETRY_MIN_MS * 2 ** failures);
64231
+ failure = { error, retryAt: now() + delay, count: failures + 1 };
64232
+ inFlight = void 0;
64233
+ options.onFailure?.(error);
64234
+ throw error;
64235
+ }
64236
+ );
64237
+ return inFlight;
64238
+ },
64239
+ retryAfterSeconds() {
64240
+ if (!failure) return 0;
64241
+ return Math.max(1, Math.ceil((failure.retryAt - now()) / 1e3));
64242
+ }
64243
+ };
64244
+ }
64245
+ function startupFailureResponse(error, retryAfterSeconds = 1) {
64246
+ const misconfigured = misconfigurationResponse(error);
64247
+ if (misconfigured) return misconfigured;
64248
+ return Response.json(
64249
+ {
64250
+ error: "server_unavailable",
64251
+ code: "SERVER_START_FAILED",
64252
+ message: "The server failed to start, so it refuses requests. Its log has the cause; a request after the Retry-After delay tries to start it again."
64253
+ },
64254
+ { status: 503, headers: { "Retry-After": String(Math.max(1, retryAfterSeconds)) } }
64255
+ );
64256
+ }
63236
64257
  function flowErrorToHttpOutput(error) {
63237
64258
  const misconfiguration = findMisconfiguration(error);
63238
64259
  if (misconfiguration) {
@@ -63266,7 +64287,7 @@ function flowErrorToHttpOutput(error) {
63266
64287
  }
63267
64288
  return { kind: "text", status: 500, body: "Internal Server Error", contentType: "text/plain; charset=utf-8" };
63268
64289
  }
63269
- var CLOUDFLARE_WORKERS_USER_AGENT, MISCONFIGURATION_REMEDIES;
64290
+ var CLOUDFLARE_WORKERS_USER_AGENT, MISCONFIGURATION_REMEDIES, CONFIG_INVALID_REMEDY, STARTUP_RETRY_MIN_MS, STARTUP_RETRY_MAX_MS;
63270
64291
  var init_web_fetch_handler = __esm({
63271
64292
  "libs/sdk/src/transport/web-fetch-handler.ts"() {
63272
64293
  "use strict";
@@ -63280,8 +64301,14 @@ var init_web_fetch_handler = __esm({
63280
64301
  MISCONFIGURATION_REMEDIES = {
63281
64302
  SESSION_SECRET_REQUIRED: "Set MCP_SESSION_SECRET in the deployment environment (e.g. `wrangler secret put MCP_SESSION_SECRET`). Session IDs are encrypted with it, and production refuses the development machine-id fallback.",
63282
64303
  JWT_SECRET_INVALID: "JWT_SECRET is present but too weak: HS256 requires at least 32 bytes (RFC 7518). Replace it with `openssl rand -hex 32`.",
63283
- JWT_SECRET_REQUIRED: "Set JWT_SECRET in the deployment environment (e.g. `wrangler secret put JWT_SECRET`). Tokens are signed with it; production refuses the random per-process fallback because tokens would not survive a restart or verify across instances."
64304
+ JWT_SECRET_REQUIRED: "Set JWT_SECRET in the deployment environment (e.g. `wrangler secret put JWT_SECRET`). Tokens are signed with it; production refuses the random per-process fallback because tokens would not survive a restart or verify across instances.",
64305
+ // The startup checks: a server whose entries ask for protection nothing gives them does not start.
64306
+ UNENFORCED_METADATA: "An entry declares a field only a plugin enforces (approval, featureFlag, ...) and no installed plugin that enforces it reaches the entry, so the server refuses to start. Install the plugin or remove the field; the server log names the entries.",
64307
+ AUTH_CONFIGURATION_ERROR: "The auth or authorities configuration is invalid (for example, entries declare authorities and the server has no authorities option, or a rule checks nothing), so the server refuses to start. The server log names the entries."
63284
64308
  };
64309
+ CONFIG_INVALID_REMEDY = "The FrontMCP configuration failed validation, so the server refuses to start. The server log names the invalid fields.";
64310
+ STARTUP_RETRY_MIN_MS = 1e3;
64311
+ STARTUP_RETRY_MAX_MS = 6e4;
63285
64312
  }
63286
64313
  });
63287
64314
 
@@ -63930,12 +64957,172 @@ var init_front_mcp_providers = __esm({
63930
64957
  }
63931
64958
  });
63932
64959
 
64960
+ // libs/sdk/src/front-mcp/static-startup.check.ts
64961
+ function tryNormalize(normalize) {
64962
+ try {
64963
+ return normalize();
64964
+ } catch {
64965
+ return void 0;
64966
+ }
64967
+ }
64968
+ function asMetadata(value) {
64969
+ return value !== null && typeof value === "object" ? value : void 0;
64970
+ }
64971
+ function isServedEverywhere(metadata) {
64972
+ const availability = asMetadata(metadata["availableWhen"]);
64973
+ if (!availability) return true;
64974
+ return Object.entries(availability).every(
64975
+ ([axis, value]) => value === void 0 || axis === "surface" && Array.isArray(value) && value.length > 0
64976
+ );
64977
+ }
64978
+ function pluginClassOf(record) {
64979
+ switch (record.kind) {
64980
+ case "CLASS_TOKEN" /* CLASS_TOKEN */:
64981
+ return record.provide;
64982
+ case "CLASS" /* CLASS */:
64983
+ return record.useClass;
64984
+ case "VALUE" /* VALUE */:
64985
+ return record.useValue?.constructor;
64986
+ default:
64987
+ return void 0;
64988
+ }
64989
+ }
64990
+ function pluginHooksOf(record) {
64991
+ const pluginClass = pluginClassOf(record);
64992
+ if (!pluginClass) return void 0;
64993
+ return [...collectHook(pluginClass), ...peekPendingTC39HooksForClass(pluginClass)];
64994
+ }
64995
+ function keysEnforcedByPlugins2(plugins, into = /* @__PURE__ */ new Set()) {
64996
+ for (const plugin of plugins ?? []) {
64997
+ const record = tryNormalize(() => normalizePlugin(plugin));
64998
+ if (!record) continue;
64999
+ for (const key of record.metadata.enforcesMetadata ?? []) into.add(key);
65000
+ keysEnforcedByPlugins2(record.metadata.plugins, into);
65001
+ }
65002
+ return into;
65003
+ }
65004
+ function collectStaticEntries(config) {
65005
+ const found = [];
65006
+ const everyAppKeys = /* @__PURE__ */ new Set();
65007
+ const ownAppKeys = /* @__PURE__ */ new Map();
65008
+ const add = (label, metadata, reach) => {
65009
+ const fields = asMetadata(metadata);
65010
+ if (!fields || !isServedEverywhere(fields)) return false;
65011
+ found.push({ label, metadata: fields, reach });
65012
+ return true;
65013
+ };
65014
+ const keysOfApp = (app) => {
65015
+ const keys = ownAppKeys.get(app) ?? /* @__PURE__ */ new Set();
65016
+ ownAppKeys.set(app, keys);
65017
+ return keys;
65018
+ };
65019
+ const visitPlugins = (plugins, app) => {
65020
+ for (const plugin of plugins ?? []) {
65021
+ const record = tryNormalize(() => normalizePlugin(plugin));
65022
+ if (!record) continue;
65023
+ const hooks = pluginHooksOf(record);
65024
+ if (hooks === void 0 || hooks.length > 0) {
65025
+ const gatesOnlyItsApp = hooks !== void 0 && !hooks.some((hook) => hook.appliesTo === "uncovered-apps");
65026
+ const keys = app && gatesOnlyItsApp ? keysOfApp(app) : everyAppKeys;
65027
+ for (const key of record.metadata.enforcesMetadata ?? []) keys.add(key);
65028
+ }
65029
+ visitEntries(record.metadata, app);
65030
+ }
65031
+ };
65032
+ const visitEntries = (owner, app) => {
65033
+ const reach = app ? { app } : "every-plugin";
65034
+ for (const item of owner.tools ?? []) {
65035
+ const record = tryNormalize(() => normalizeTool(item));
65036
+ if (record) add(`Tool "${record.metadata.id || record.metadata.name}"`, record.metadata, reach);
65037
+ }
65038
+ for (const item of owner.resources ?? []) {
65039
+ if (tryNormalize(() => isResourceTemplate(item))) {
65040
+ const record = tryNormalize(() => normalizeResourceTemplate(item));
65041
+ if (record) add(`Resource template "${record.metadata.name}"`, record.metadata, reach);
65042
+ } else {
65043
+ const record = tryNormalize(() => normalizeResource(item));
65044
+ if (record) add(`Resource "${record.metadata.name}"`, record.metadata, reach);
65045
+ }
65046
+ }
65047
+ for (const item of owner.prompts ?? []) {
65048
+ const record = tryNormalize(() => normalizePrompt(item));
65049
+ if (record) add(`Prompt "${record.metadata.name}"`, record.metadata, reach);
65050
+ }
65051
+ for (const item of owner.agents ?? []) {
65052
+ const record = tryNormalize(() => normalizeAgent(item));
65053
+ if (!record) continue;
65054
+ const agentName = record.metadata.id ?? record.metadata.name;
65055
+ if (!add(`Agent "${agentName}"`, record.metadata, reach)) continue;
65056
+ const agentKeys = record.metadata.execution?.useToolFlow === false ? /* @__PURE__ */ new Set() : keysEnforcedByPlugins2(record.metadata.plugins);
65057
+ for (const toolItem of record.metadata.tools ?? []) {
65058
+ const toolRecord = tryNormalize(() => normalizeTool(toolItem));
65059
+ if (toolRecord) add(`Tool "${agentName}:${toolRecord.metadata.name}"`, toolRecord.metadata, { agentKeys });
65060
+ }
65061
+ }
65062
+ for (const item of owner.skills ?? []) {
65063
+ const record = tryNormalize(() => normalizeSkill(item));
65064
+ if (record) add(`Skill "${record.metadata.id ?? record.metadata.name}"`, record.metadata, "every-plugin");
65065
+ }
65066
+ visitPlugins(owner.plugins, app);
65067
+ };
65068
+ const server = config;
65069
+ visitEntries({ skills: server.skills, plugins: server.plugins }, void 0);
65070
+ for (const app of server.apps ?? []) {
65071
+ const record = tryNormalize(() => normalizeApp(app));
65072
+ if (record?.kind === "LOCAL_CLASS" /* LOCAL_CLASS */) visitEntries(record.metadata, record);
65073
+ }
65074
+ const everyPluginKeys = /* @__PURE__ */ new Set([...everyAppKeys, ...[...ownAppKeys.values()].flatMap((keys) => [...keys])]);
65075
+ const keysReaching = (reach) => {
65076
+ if (reach === "every-plugin") return everyPluginKeys;
65077
+ if ("agentKeys" in reach) return reach.agentKeys;
65078
+ return /* @__PURE__ */ new Set([...everyAppKeys, ...ownAppKeys.get(reach.app) ?? []]);
65079
+ };
65080
+ return found.map(({ label, metadata, reach }) => ({ label, metadata, enforcedBy: keysReaching(reach) }));
65081
+ }
65082
+ function assertStaticStartupConfig(config) {
65083
+ const entries = collectStaticEntries(config);
65084
+ if (!config.authorities) {
65085
+ const labels = entries.filter(({ metadata }) => isEnforcementRequested(metadata["authorities"])).map(({ label }) => label);
65086
+ if (labels.length > 0) {
65087
+ const names = labels.slice(0, 5).join(", ");
65088
+ const suffix = labels.length > 5 ? ` and ${labels.length - 5} more` : "";
65089
+ throw new AuthConfigurationError(
65090
+ `Authorities configuration required: ${names}${suffix} declare 'authorities' metadata but authorities enforcement is not fully configured (engine/context builder missing). Add 'authorities: { claimsMapping: {...}, profiles: {...} }' to your @FrontMcp() decorator to enable enforcement, or remove 'authorities' from entry metadata.`,
65091
+ { suggestion: "Add authorities config to @FrontMcp() or remove authorities from entry metadata" }
65092
+ );
65093
+ }
65094
+ }
65095
+ const problems = entries.flatMap(
65096
+ ({ label, metadata, enforcedBy }) => getEnforcedMetadataKeys().filter((key) => isEnforcementRequested(metadata[key]) && !enforcedBy.has(key)).map((key) => {
65097
+ const enforcer = describeMetadataEnforcer(key);
65098
+ return `${label} declares '${key}'${enforcer ? ` (enforced by ${enforcer})` : ""}`;
65099
+ })
65100
+ );
65101
+ if (problems.length > 0) throw new UnenforcedMetadataError(problems);
65102
+ }
65103
+ var init_static_startup_check = __esm({
65104
+ "libs/sdk/src/front-mcp/static-startup.check.ts"() {
65105
+ "use strict";
65106
+ init_agent_utils();
65107
+ init_app_utils();
65108
+ init_hook_decorator();
65109
+ init_enforced_metadata_utils();
65110
+ init_errors();
65111
+ init_hooks_utils();
65112
+ init_plugin_utils();
65113
+ init_prompt_utils();
65114
+ init_resource_utils();
65115
+ init_skill_utils();
65116
+ init_tool_utils();
65117
+ }
65118
+ });
65119
+
63933
65120
  // libs/sdk/src/front-mcp/front-mcp.ts
63934
65121
  var front_mcp_exports = {};
63935
65122
  __export(front_mcp_exports, {
63936
65123
  FrontMcpInstance: () => FrontMcpInstance
63937
65124
  });
63938
- import { fileExists as fileExists7, randomUUID as randomUUID28, unlink as unlink2 } from "@frontmcp/utils";
65125
+ import { fileExists as fileExists7, isEdgeRuntime as isEdgeRuntime2, randomUUID as randomUUID28, unlink as unlink2 } from "@frontmcp/utils";
63939
65126
  function resolveConfigInput(optionsOrClass) {
63940
65127
  if (typeof optionsOrClass === "function") {
63941
65128
  const stored = getDecoratorConfig(optionsOrClass);
@@ -63963,6 +65150,7 @@ var init_front_mcp = __esm({
63963
65150
  init_task();
63964
65151
  init_web_fetch_handler();
63965
65152
  init_front_mcp_providers();
65153
+ init_static_startup_check();
63966
65154
  stdioServing = false;
63967
65155
  FrontMcpInstance = class _FrontMcpInstance2 {
63968
65156
  config;
@@ -64121,17 +65309,30 @@ var init_front_mcp = __esm({
64121
65309
  * Express / Node `req`/`res` and runs on V8-isolate targets (Cloudflare
64122
65310
  * Workers, Deno Deploy, Bun).
64123
65311
  *
65312
+ * A server the startup checks refuse (an entry whose `approval`, `featureFlag`
65313
+ * or `authorities` nothing enforces, a missing secret, ...) rejects here, as
65314
+ * with {@link createDirect}. On an edge isolate the server is built on its
65315
+ * first request instead (see below).
65316
+ *
64124
65317
  * @example
64125
65318
  * // worker entry
64126
65319
  * const handler = await FrontMcpInstance.createFetchHandler(config);
64127
65320
  * export default { fetch: (request) => handler(request) };
64128
65321
  */
64129
65322
  static async createFetchHandler(options) {
64130
- let inner;
64131
- let building;
64132
- const build = async () => {
64133
- const frontMcp = new _FrontMcpInstance2(frontMcpMetadataSchema.parse(options));
64134
- await frontMcp.ready;
65323
+ const build = async (deferred) => {
65324
+ let frontMcp;
65325
+ try {
65326
+ frontMcp = new _FrontMcpInstance2(frontMcpMetadataSchema.parse(options));
65327
+ await frontMcp.ready;
65328
+ } catch (err) {
65329
+ if (deferred) {
65330
+ const message = "FrontMCP failed to start; requests are refused until a retry succeeds";
65331
+ if (frontMcp?.log) frontMcp.log.error(message, err);
65332
+ else console.error(`[frontmcp] ${message}`, err);
65333
+ }
65334
+ throw err;
65335
+ }
64135
65336
  const scope = frontMcp.getPrimaryScope();
64136
65337
  if (!scope) {
64137
65338
  throw new ServerNotFoundError();
@@ -64139,21 +65340,15 @@ var init_front_mcp = __esm({
64139
65340
  frontMcp.log?.info("FrontMCP fetch handler created (web-standard transport)");
64140
65341
  return createWebFetchHandler(scope);
64141
65342
  };
65343
+ if (!isEdgeRuntime2()) return build(false);
65344
+ assertStaticStartupConfig(options);
65345
+ const server = createDeferredServerBuild(() => build(true));
64142
65346
  return async (request, ctx, env) => {
64143
- if (!inner) {
64144
- if (!building) {
64145
- building = build().catch((err) => {
64146
- building = void 0;
64147
- throw err;
64148
- });
64149
- }
64150
- try {
64151
- inner = await building;
64152
- } catch (err) {
64153
- const misconfigured = misconfigurationResponse(err);
64154
- if (misconfigured) return misconfigured;
64155
- throw err;
64156
- }
65347
+ let inner;
65348
+ try {
65349
+ inner = await server.get();
65350
+ } catch (err) {
65351
+ return startupFailureResponse(err, server.retryAfterSeconds());
64157
65352
  }
64158
65353
  return inner(request, ctx, env);
64159
65354
  };
@@ -64649,7 +65844,8 @@ async function executeTaskWorker(options, taskId) {
64649
65844
  await runTaskInBackground({
64650
65845
  record,
64651
65846
  cleanedRequestParams: record.request.params,
64652
- ctx: { authInfo: { sessionId } },
65847
+ // On the surface of the call that created the task, as the in-process runner runs it.
65848
+ ctx: { authInfo: { sessionId }, ...record.surface !== void 0 && { surface: record.surface } },
64653
65849
  scope,
64654
65850
  store: taskStore,
64655
65851
  registry,
@@ -66519,6 +67715,7 @@ var init_common = __esm({
66519
67715
  // libs/sdk/src/index.ts
66520
67716
  init_common();
66521
67717
  init_front_mcp2();
67718
+ init_static_startup_check();
66522
67719
  init_serverless_handler();
66523
67720
  init_common();
66524
67721
  init_errors();
@@ -66856,6 +68053,39 @@ init_agent_decorator();
66856
68053
 
66857
68054
  // libs/sdk/src/index.ts
66858
68055
  init_adapters();
68056
+
68057
+ // libs/sdk/src/hooks/hook-coverage.ts
68058
+ init_lineage_utils();
68059
+ function gateOf(scope, entry) {
68060
+ if ("tool" in entry) {
68061
+ return {
68062
+ flow: "tools:call-tool",
68063
+ ownerId: hookOwnerIdOf(scope.tools.lineageOf(entry.tool) ?? [], entry.tool.owner)
68064
+ };
68065
+ }
68066
+ if ("resource" in entry) {
68067
+ return {
68068
+ flow: "resources:read-resource",
68069
+ ownerId: appOwnerIdOf(scope.resources.lineageOf(entry.resource) ?? [], entry.resource.owner)
68070
+ };
68071
+ }
68072
+ if ("prompt" in entry) {
68073
+ return {
68074
+ flow: "prompts:get-prompt",
68075
+ ownerId: appOwnerIdOf(scope.prompts.lineageOf(entry.prompt) ?? [], entry.prompt.owner)
68076
+ };
68077
+ }
68078
+ return {
68079
+ flow: "skills:filter",
68080
+ ownerId: appOwnerIdOf(scope.skills.lineageOf?.(entry.skill) ?? [], entry.skill.owner)
68081
+ };
68082
+ }
68083
+ function isEntryGatedBy(scope, entry, target) {
68084
+ const { flow, ownerId } = gateOf(scope, entry);
68085
+ return scope.hooks.getFlowHooksForOwner(flow, ownerId).some((hook) => hook.metadata.target === target);
68086
+ }
68087
+
68088
+ // libs/sdk/src/index.ts
66859
68089
  init_config();
66860
68090
  init_config();
66861
68091
  init_direct();
@@ -66876,6 +68106,7 @@ init_mcp_stateless_client_adapter();
66876
68106
  init_ext_apps2();
66877
68107
  import { PROTOCOL_2026_07_28 as PROTOCOL_2026_07_284 } from "@frontmcp/protocol";
66878
68108
  import { InMemoryOrchestratedTokenStore as InMemoryOrchestratedTokenStore2 } from "@frontmcp/auth";
68109
+ import { isAnonymousSubject as isAnonymousSubject7 } from "@frontmcp/auth";
66879
68110
  (function suppressZodExpressWarning() {
66880
68111
  if (typeof process === "undefined" || typeof process.emitWarning !== "function") return;
66881
68112
  const originalEmitWarning = process.emitWarning.bind(process);
@@ -67270,6 +68501,7 @@ export {
67270
68501
  SessionClaimConflictError,
67271
68502
  SessionHookStage,
67272
68503
  SessionIdEmptyError,
68504
+ SessionIdentityRequiredError,
67273
68505
  SessionMissingError,
67274
68506
  SessionSecretRequiredError,
67275
68507
  SessionVerificationFailedError,
@@ -67397,6 +68629,7 @@ export {
67397
68629
  appAuthOptionsSchema,
67398
68630
  appFilterConfigSchema,
67399
68631
  applyPrimitiveFilter,
68632
+ assertStaticStartupConfig,
67400
68633
  authInfoFromAuthorization,
67401
68634
  authOptionsSchema,
67402
68635
  authenticateFnSchema,
@@ -67442,6 +68675,7 @@ export {
67442
68675
  create,
67443
68676
  createChildSpanContext,
67444
68677
  createContextResolver,
68678
+ createDeferredServerBuild,
67445
68679
  createDirectResolver,
67446
68680
  createElicitationStore,
67447
68681
  createElicitationStoreFromStorage,
@@ -67549,6 +68783,7 @@ export {
67549
68783
  getElicitationSecret,
67550
68784
  getGlobalStoreConfig,
67551
68785
  getNestedValue,
68786
+ getPinnedPublicUrl,
67552
68787
  getRegistryUrl,
67553
68788
  getRequestBaseUrl,
67554
68789
  getRunningTool,
@@ -67568,6 +68803,7 @@ export {
67568
68803
  incrementalAuthConfigSchema,
67569
68804
  intentSchema,
67570
68805
  isAlive,
68806
+ isAnonymousSubject7 as isAnonymousSubject,
67571
68807
  isAuthError,
67572
68808
  isChannelClass,
67573
68809
  isChannelFunction,
@@ -67577,6 +68813,7 @@ export {
67577
68813
  isDynamicPluginClass,
67578
68814
  isElicitationEncryptionAvailable,
67579
68815
  isEncryptedBlob,
68816
+ isEntryGatedBy,
67580
68817
  isFileInstructions,
67581
68818
  isInlineInstructions,
67582
68819
  isLazy,
@@ -67657,6 +68894,7 @@ export {
67657
68894
  parseTraceContext,
67658
68895
  partitionsByIdentity,
67659
68896
  pathToEnvKey,
68897
+ peekPendingTC39HooksForClass,
67660
68898
  performElicit,
67661
68899
  performListRoots,
67662
68900
  performSample,
@@ -67702,6 +68940,7 @@ export {
67702
68940
  serializeHttpCookie,
67703
68941
  serverInfoOptionsSchema,
67704
68942
  sessionIdPayloadSchema2 as sessionIdPayloadSchema,
68943
+ sessionIdPresentedBy,
67705
68944
  sessionIdSchema,
67706
68945
  sessionOptionsSchema,
67707
68946
  setNestedValue,
@@ -67720,6 +68959,7 @@ export {
67720
68959
  skillsConfigOptionsSchema,
67721
68960
  skippedAppBehaviorSchema,
67722
68961
  sqliteOptionsSchema,
68962
+ startupFailureResponse,
67723
68963
  staticAuthOptionsSchema,
67724
68964
  storageProviderSchema,
67725
68965
  supportsStreaming,