@kya-os/mcp-i-cloudflare 1.9.6 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/adapter.d.ts.map +1 -1
  2. package/dist/adapter.js +47 -16
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/agent.d.ts +9 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +203 -53
  7. package/dist/agent.js.map +1 -1
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +86 -3
  10. package/dist/app.js.map +1 -1
  11. package/dist/config.d.ts +54 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +58 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/constants/storage-keys.d.ts +6 -0
  16. package/dist/constants/storage-keys.d.ts.map +1 -1
  17. package/dist/constants/storage-keys.js +6 -0
  18. package/dist/constants/storage-keys.js.map +1 -1
  19. package/dist/delegation-http/anchor-do.d.ts +84 -0
  20. package/dist/delegation-http/anchor-do.d.ts.map +1 -0
  21. package/dist/delegation-http/anchor-do.js +253 -0
  22. package/dist/delegation-http/anchor-do.js.map +1 -0
  23. package/dist/delegation-http/anchor-routing.d.ts +20 -0
  24. package/dist/delegation-http/anchor-routing.d.ts.map +1 -0
  25. package/dist/delegation-http/anchor-routing.js +27 -0
  26. package/dist/delegation-http/anchor-routing.js.map +1 -0
  27. package/dist/delegation-http/bearer-retry.d.ts +54 -0
  28. package/dist/delegation-http/bearer-retry.d.ts.map +1 -0
  29. package/dist/delegation-http/bearer-retry.js +195 -0
  30. package/dist/delegation-http/bearer-retry.js.map +1 -0
  31. package/dist/delegation-http/challenge-middleware.d.ts +28 -0
  32. package/dist/delegation-http/challenge-middleware.d.ts.map +1 -0
  33. package/dist/delegation-http/challenge-middleware.js +190 -0
  34. package/dist/delegation-http/challenge-middleware.js.map +1 -0
  35. package/dist/delegation-http/challenge.d.ts +86 -0
  36. package/dist/delegation-http/challenge.d.ts.map +1 -0
  37. package/dist/delegation-http/challenge.js +173 -0
  38. package/dist/delegation-http/challenge.js.map +1 -0
  39. package/dist/delegation-http/consent-anchor-bridge.d.ts +35 -0
  40. package/dist/delegation-http/consent-anchor-bridge.d.ts.map +1 -0
  41. package/dist/delegation-http/consent-anchor-bridge.js +355 -0
  42. package/dist/delegation-http/consent-anchor-bridge.js.map +1 -0
  43. package/dist/delegation-http/constants.d.ts +51 -0
  44. package/dist/delegation-http/constants.d.ts.map +1 -0
  45. package/dist/delegation-http/constants.js +60 -0
  46. package/dist/delegation-http/constants.js.map +1 -0
  47. package/dist/delegation-http/hash.d.ts +21 -0
  48. package/dist/delegation-http/hash.d.ts.map +1 -0
  49. package/dist/delegation-http/hash.js +92 -0
  50. package/dist/delegation-http/hash.js.map +1 -0
  51. package/dist/delegation-http/pickup-routes.d.ts +42 -0
  52. package/dist/delegation-http/pickup-routes.d.ts.map +1 -0
  53. package/dist/delegation-http/pickup-routes.js +79 -0
  54. package/dist/delegation-http/pickup-routes.js.map +1 -0
  55. package/dist/delegation-http/register.d.ts +19 -0
  56. package/dist/delegation-http/register.d.ts.map +1 -0
  57. package/dist/delegation-http/register.js +72 -0
  58. package/dist/delegation-http/register.js.map +1 -0
  59. package/dist/delegation-http/sid.d.ts +20 -0
  60. package/dist/delegation-http/sid.d.ts.map +1 -0
  61. package/dist/delegation-http/sid.js +32 -0
  62. package/dist/delegation-http/sid.js.map +1 -0
  63. package/dist/delegation-http/types.d.ts +64 -0
  64. package/dist/delegation-http/types.d.ts.map +1 -0
  65. package/dist/delegation-http/types.js +11 -0
  66. package/dist/delegation-http/types.js.map +1 -0
  67. package/dist/helpers/env-mapper.d.ts.map +1 -1
  68. package/dist/helpers/env-mapper.js +24 -0
  69. package/dist/helpers/env-mapper.js.map +1 -1
  70. package/dist/index.d.ts +5 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +10 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/providers/crypto.d.ts +1 -1
  75. package/dist/providers/crypto.d.ts.map +1 -1
  76. package/dist/providers/crypto.js +1 -1
  77. package/dist/providers/crypto.js.map +1 -1
  78. package/dist/providers/kv-identity.d.ts +2 -2
  79. package/dist/providers/kv-identity.d.ts.map +1 -1
  80. package/dist/providers/kv-identity.js +1 -1
  81. package/dist/providers/kv-identity.js.map +1 -1
  82. package/dist/providers/storage.d.ts +1 -1
  83. package/dist/providers/storage.d.ts.map +1 -1
  84. package/dist/providers/storage.js +1 -1
  85. package/dist/providers/storage.js.map +1 -1
  86. package/dist/runtime/audit-logger.d.ts +1 -1
  87. package/dist/runtime/audit-logger.d.ts.map +1 -1
  88. package/dist/runtime/oauth-handler.d.ts.map +1 -1
  89. package/dist/runtime/oauth-handler.js +31 -3
  90. package/dist/runtime/oauth-handler.js.map +1 -1
  91. package/dist/runtime/oidc/authorize-url.d.ts +19 -0
  92. package/dist/runtime/oidc/authorize-url.d.ts.map +1 -0
  93. package/dist/runtime/oidc/authorize-url.js +94 -0
  94. package/dist/runtime/oidc/authorize-url.js.map +1 -0
  95. package/dist/runtime/oidc/token-exchange.d.ts +62 -0
  96. package/dist/runtime/oidc/token-exchange.d.ts.map +1 -0
  97. package/dist/runtime/oidc/token-exchange.js +75 -0
  98. package/dist/runtime/oidc/token-exchange.js.map +1 -0
  99. package/dist/runtime.d.ts +1 -1
  100. package/dist/runtime.d.ts.map +1 -1
  101. package/dist/services/consent-audit.service.d.ts +1 -1
  102. package/dist/services/consent-audit.service.d.ts.map +1 -1
  103. package/dist/services/consent-audit.service.js.map +1 -1
  104. package/dist/services/consent.service.d.ts +2 -2
  105. package/dist/services/consent.service.d.ts.map +1 -1
  106. package/dist/services/consent.service.js +48 -59
  107. package/dist/services/consent.service.js.map +1 -1
  108. package/dist/services/delegation.service.d.ts +9 -0
  109. package/dist/services/delegation.service.d.ts.map +1 -1
  110. package/dist/services/delegation.service.js +21 -7
  111. package/dist/services/delegation.service.js.map +1 -1
  112. package/dist/services/oauth-security.service.d.ts.map +1 -1
  113. package/dist/services/oauth-security.service.js +6 -10
  114. package/dist/services/oauth-security.service.js.map +1 -1
  115. package/dist/services/provider-loader.service.js +1 -1
  116. package/dist/services/provider-loader.service.js.map +1 -1
  117. package/dist/types.d.ts +5 -1
  118. package/dist/types.d.ts.map +1 -1
  119. package/dist/utils/known-clients.d.ts.map +1 -1
  120. package/dist/utils/known-clients.js +19 -10
  121. package/dist/utils/known-clients.js.map +1 -1
  122. package/package.json +3 -1
package/dist/agent.js CHANGED
@@ -10,10 +10,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { CloudflareRuntime } from "./runtime";
11
11
  import { createCloudflareRuntime } from "./index";
12
12
  import { mapPrefixedEnv } from "./helpers/env-mapper";
13
+ import { handlePickupInternal } from "./delegation-http/anchor-do";
13
14
  import { STORAGE_KEYS } from "./constants/storage-keys";
14
15
  import { DEFAULT_SESSION_TTL } from "./constants";
15
16
  import { z } from "zod";
16
- import { OAuthService, IdpTokenResolver, ToolContextBuilder, OAuthRequiredError, DelegationRequiredError, DelegationErrorFormatter, SessionRegistrationService, generateDidKeyFromBase64, logger, } from "@kya-os/mcp-i-core";
17
+ import { generateDidKeyFromBase64, logger } from "@kya-os/mcp";
18
+ import { OAuthService, IdpTokenResolver, ToolContextBuilder, OAuthRequiredError, DelegationRequiredError, DelegationErrorFormatter, SessionRegistrationService, SESSION_ID_PREFIX, recordWireForm, generateResumeToken, } from "@kya-os/mcp-i-core"; // product layer — stays (C2/C3)
17
19
  import { WebCryptoProvider } from "./providers/crypto";
18
20
  // CRITICAL: Import the OAuth service registry - this triggers globalThis registration
19
21
  // at module load time, preventing esbuild from tree-shaking the OAuth services
@@ -24,10 +26,11 @@ import { OAuthSecurityService } from "./services/oauth-security.service";
24
26
  import { WorkersFetchProvider } from "./providers/storage";
25
27
  import { resolveClientIdentity, } from "./services/kta-client-lookup";
26
28
  import { findKnownClient } from "./utils/known-clients";
27
- import { didKeyFragment } from "@kya-os/mcp-i-core/utils/did-helpers";
29
+ import { shouldUsePopup, buildPopupResource } from "@kya-os/consent";
30
+ import { didKeyFragment } from "@kya-os/mcp";
28
31
  import { calculateDOInstanceId, parseDORoutingStrategy, parseDOShardCount, } from "./utils/do-routing";
29
32
  import { defaultProviderRegistry, } from "@kya-os/provider-registry";
30
- import { hasApiKeyAuthorization, AuthRequiredError } from "@kya-os/contracts/tool-protection";
33
+ import { hasApiKeyAuthorization, AuthRequiredError, } from "@kya-os/contracts/tool-protection";
31
34
  import { VaultResolver } from "./services/vault-resolver";
32
35
  // CRITICAL: Force OAuth registry evaluation at module load time
33
36
  // This creates an observable side effect that esbuild cannot tree-shake
@@ -183,7 +186,9 @@ export class MCPICloudflareAgent extends McpAgent {
183
186
  hasCapabilities: !!capabilities,
184
187
  capabilityKeys: capabilities ? Object.keys(capabilities) : [],
185
188
  hasExtensions: !!capabilities?.extensions,
186
- extensionKeys: capabilities?.extensions ? Object.keys(capabilities.extensions) : [],
189
+ extensionKeys: capabilities?.extensions
190
+ ? Object.keys(capabilities.extensions)
191
+ : [],
187
192
  rawCapabilities: JSON.stringify(capabilities)?.substring(0, 500),
188
193
  });
189
194
  // Store client info for later retrieval (always, not just in dev)
@@ -243,7 +248,10 @@ export class MCPICloudflareAgent extends McpAgent {
243
248
  logger.info(`║ Client DID: ${clientIdentity.did.slice(0, 40).padEnd(42)}║`);
244
249
  logger.info(`║ DID Source: ${clientIdentity.source.padEnd(42)}║`);
245
250
  logger.info("╠════════════════════════════════════════════════════════╣");
246
- logger.info(`║ Your Agent: ${isAgentRegistered ? "✓ REGISTERED" : "○ LOCAL"} ${agentDid.slice(0, 33).padEnd(36)}║`);
251
+ const agentStatus = isAgentRegistered ? "✓ REGISTERED" : "○ LOCAL";
252
+ const agentPrefix = ` Your Agent: ${agentStatus} `;
253
+ const agentPadWidth = 56 - agentPrefix.length;
254
+ logger.info(`║${agentPrefix}${agentDid.slice(0, agentPadWidth).padEnd(agentPadWidth)}║`);
247
255
  logger.info("╚════════════════════════════════════════════════════════╝");
248
256
  logger.info("");
249
257
  }
@@ -381,10 +389,10 @@ export class MCPICloudflareAgent extends McpAgent {
381
389
  : this.env;
382
390
  // ✅ CRITICAL: Use same session ID resolution logic as executeToolWithProof
383
391
  // This ensures session registration uses the same ID that proofs will use
384
- // Priority: getSessionId() (from agents SDK) > DO ID fallback > mcpi_{uuid}
392
+ // Priority: getSessionId() (from agents SDK) > DO ID fallback > kyaos_{uuid}
385
393
  const mcpSessionId = this.getSessionId();
386
394
  const doId = this.ctx.id.toString();
387
- const sessionId = mcpSessionId || doId || `mcpi_${crypto.randomUUID()}`;
395
+ const sessionId = mcpSessionId || doId || `${SESSION_ID_PREFIX}${crypto.randomUUID()}`;
388
396
  // Log session ID source for debugging (matches executeToolWithProof pattern)
389
397
  if (this._environment === "development") {
390
398
  logger.debug("[SessionRegistration] Session ID resolved:", {
@@ -507,6 +515,17 @@ export class MCPICloudflareAgent extends McpAgent {
507
515
  setClientMessagesConfig(config) {
508
516
  this._clientMessagesConfig = config;
509
517
  }
518
+ /**
519
+ * Whether to attach a popup-launch hint when this client supports it.
520
+ * Subclasses may override; defaults to `true` so popup-aware clients
521
+ * (Claude Desktop with `.mcpb`) get the stripped-chrome popup
522
+ * automatically. Operators can disable per-deployment by overriding
523
+ * this method or the `popupEnabled` field in the resolved consent
524
+ * config.
525
+ */
526
+ get popupEnabledForClient() {
527
+ return true;
528
+ }
510
529
  /**
511
530
  * Initialize the agent
512
531
  * Call this after construction to set up the runtime and register tools
@@ -548,7 +567,7 @@ export class MCPICloudflareAgent extends McpAgent {
548
567
  const [extApps, consentMcpApp, extAppsConstants] = await Promise.all([
549
568
  import("@modelcontextprotocol/ext-apps/server").catch(() => null),
550
569
  import("@kya-os/consent/mcp-app").catch(() => null),
551
- import("@kya-os/mcp-i-core/runtime/ext-apps-constants").catch(() => null),
570
+ import("@kya-os/mcp-i-runtime/runtime/ext-apps-constants").catch(() => null),
552
571
  ]);
553
572
  if (!extApps || !consentMcpApp || !extAppsConstants) {
554
573
  logger.info("[MCPICloudflareAgent] MCP Apps consent UI not available (missing optional dependencies)");
@@ -797,7 +816,7 @@ export class MCPICloudflareAgent extends McpAgent {
797
816
  send({
798
817
  jsonrpc: "2.0", id: reqId, method: "tools/call",
799
818
  params: {
800
- name: "_mcpi_credential_auth",
819
+ name: "_kyaos_credential_auth",
801
820
  arguments: {
802
821
  tool: consentData.tool,
803
822
  scopes: consentData.scopes,
@@ -815,7 +834,7 @@ export class MCPICloudflareAgent extends McpAgent {
815
834
  send({
816
835
  jsonrpc: "2.0", id: reqId, method: "tools/call",
817
836
  params: {
818
- name: "_mcpi_approve_consent",
837
+ name: "_kyaos_approve_consent",
819
838
  arguments: {
820
839
  tool: consentData.tool,
821
840
  scopes: consentData.scopes,
@@ -903,14 +922,21 @@ export class MCPICloudflareAgent extends McpAgent {
903
922
  // The sandboxed MCP Apps iframe cannot call fetch() directly, so it
904
923
  // uses tools/call (proxied via postMessage through the host) to invoke
905
924
  // this tool which handles the /consent/approve logic server-side.
906
- this.server.tool("_mcpi_approve_consent", "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process approval actions. Only the embedded consent form should invoke this.", {
925
+ // C4 (#2918) dual-accept: register the consent-approval tool under the
926
+ // canonical `_kyaos_` name and keep the legacy `_mcpi_` name as a thin
927
+ // alias during the window. The server-rendered iframe already calls the
928
+ // new name; the alias is defensive for an in-flight pre-deploy iframe.
929
+ // Same handler, two names — DIF aliases `_kyaos_handshake` the same way.
930
+ const approveConsentDescription = "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process approval actions. Only the embedded consent form should invoke this.";
931
+ const approveConsentSchema = {
907
932
  tool: z.string(),
908
933
  scopes: z.array(z.string()),
909
934
  session_id: z.string(),
910
935
  agent_did: z.string(),
911
936
  project_id: z.string(),
912
937
  resume_token: z.string(),
913
- }, async (args) => {
938
+ };
939
+ const approveConsentHandler = async (args) => {
914
940
  const envPrefix = this.getEnvPrefix();
915
941
  const mappedEnv = envPrefix
916
942
  ? mapPrefixedEnv(this.env, envPrefix)
@@ -918,7 +944,17 @@ export class MCPICloudflareAgent extends McpAgent {
918
944
  const serverUrl = mappedEnv.MCP_SERVER_URL ??
919
945
  this._autoDetectedOrigin;
920
946
  if (!serverUrl) {
921
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "No server URL configured" }) }] };
947
+ return {
948
+ content: [
949
+ {
950
+ type: "text",
951
+ text: JSON.stringify({
952
+ success: false,
953
+ error: "No server URL configured",
954
+ }),
955
+ },
956
+ ],
957
+ };
922
958
  }
923
959
  try {
924
960
  const resp = await fetch(`${serverUrl}/consent/approve`, {
@@ -936,21 +972,48 @@ export class MCPICloudflareAgent extends McpAgent {
936
972
  }),
937
973
  });
938
974
  if (!resp.ok) {
939
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Server error: ${resp.status}` }) }] };
975
+ return {
976
+ content: [
977
+ {
978
+ type: "text",
979
+ text: JSON.stringify({
980
+ success: false,
981
+ error: `Server error: ${resp.status}`,
982
+ }),
983
+ },
984
+ ],
985
+ };
940
986
  }
941
987
  const data = await resp.json();
942
988
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
943
989
  }
944
990
  catch (err) {
945
991
  const msg = err instanceof Error ? err.message : "Unknown error";
946
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }] };
992
+ return {
993
+ content: [
994
+ {
995
+ type: "text",
996
+ text: JSON.stringify({ success: false, error: msg }),
997
+ },
998
+ ],
999
+ };
947
1000
  }
1001
+ };
1002
+ this.server.tool("_kyaos_approve_consent", approveConsentDescription, approveConsentSchema, approveConsentHandler);
1003
+ this.server.tool("_mcpi_approve_consent", approveConsentDescription, approveConsentSchema, async (args) => {
1004
+ recordWireForm("tool_name", "old", {
1005
+ tool: "_kyaos_approve_consent",
1006
+ });
1007
+ return approveConsentHandler(args);
948
1008
  });
949
1009
  // Register internal tool for iframe-based credential authentication.
950
1010
  // Same sandbox workaround as _mcpi_approve_consent: the iframe calls
951
1011
  // tools/call via postMessage, which the host proxies to this tool.
952
1012
  // This tool does credential auth + delegation in a single step.
953
- this.server.tool("_mcpi_credential_auth", "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process credential authentication. Only the embedded consent form should invoke this.", {
1013
+ // C4 (#2918) dual-accept: same accept-both treatment as approve_consent
1014
+ // canonical `_kyaos_credential_auth` plus the legacy `_mcpi_` alias.
1015
+ const credentialAuthDescription = "DO NOT call this tool directly. Internal system tool used by the consent UI iframe to process credential authentication. Only the embedded consent form should invoke this.";
1016
+ const credentialAuthSchema = {
954
1017
  tool: z.string(),
955
1018
  scopes: z.array(z.string()),
956
1019
  session_id: z.string(),
@@ -960,7 +1023,8 @@ export class MCPICloudflareAgent extends McpAgent {
960
1023
  username: z.string(),
961
1024
  password: z.string(),
962
1025
  provider: z.string(),
963
- }, async (args) => {
1026
+ };
1027
+ const credentialAuthHandler = async (args) => {
964
1028
  const envPrefix = this.getEnvPrefix();
965
1029
  const mappedEnv = envPrefix
966
1030
  ? mapPrefixedEnv(this.env, envPrefix)
@@ -1000,15 +1064,39 @@ export class MCPICloudflareAgent extends McpAgent {
1000
1064
  }),
1001
1065
  });
1002
1066
  if (!resp.ok) {
1003
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: `Server error: ${resp.status}` }) }] };
1067
+ return {
1068
+ content: [
1069
+ {
1070
+ type: "text",
1071
+ text: JSON.stringify({
1072
+ success: false,
1073
+ error: `Server error: ${resp.status}`,
1074
+ }),
1075
+ },
1076
+ ],
1077
+ };
1004
1078
  }
1005
1079
  const data = await resp.json();
1006
1080
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
1007
1081
  }
1008
1082
  catch (err) {
1009
1083
  const msg = err instanceof Error ? err.message : "Unknown error";
1010
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }] };
1084
+ return {
1085
+ content: [
1086
+ {
1087
+ type: "text",
1088
+ text: JSON.stringify({ success: false, error: msg }),
1089
+ },
1090
+ ],
1091
+ };
1011
1092
  }
1093
+ };
1094
+ this.server.tool("_kyaos_credential_auth", credentialAuthDescription, credentialAuthSchema, credentialAuthHandler);
1095
+ this.server.tool("_mcpi_credential_auth", credentialAuthDescription, credentialAuthSchema, async (args) => {
1096
+ recordWireForm("tool_name", "old", {
1097
+ tool: "_kyaos_credential_auth",
1098
+ });
1099
+ return credentialAuthHandler(args);
1012
1100
  });
1013
1101
  logger.info("[MCPICloudflareAgent] Registered MCP Apps consent UI resource");
1014
1102
  }
@@ -1116,7 +1204,8 @@ export class MCPICloudflareAgent extends McpAgent {
1116
1204
  // ✅ Automatically extract sessionId from ToolExtraArguments
1117
1205
  // This ensures delegation tokens stored with the original session ID
1118
1206
  // can be retrieved on subsequent tool calls
1119
- const sessionId = extra?.sessionId;
1207
+ const sessionId = extra
1208
+ ?.sessionId;
1120
1209
  return this.executeToolWithProof(name, args, handler, sessionId);
1121
1210
  };
1122
1211
  // Build tool config with optional MCP Apps UI metadata
@@ -1193,7 +1282,7 @@ export class MCPICloudflareAgent extends McpAgent {
1193
1282
  body: JSON.stringify({ mcpSessionId: mcpSdkSessionId }),
1194
1283
  });
1195
1284
  if (response.ok) {
1196
- const result = await response.json();
1285
+ const result = (await response.json());
1197
1286
  if (result.success && result.handshakeSessionId) {
1198
1287
  handshakeSessionId = result.handshakeSessionId;
1199
1288
  if (this._environment === "development") {
@@ -1238,15 +1327,26 @@ export class MCPICloudflareAgent extends McpAgent {
1238
1327
  sessionId: handshakeSessionId.slice(0, 20) + "...",
1239
1328
  reason: validationResult.reason,
1240
1329
  });
1241
- // Fall back to other session sources (don't trust the client's session ID)
1330
+ // Fall back to other session sources (don't trust the client's session ID).
1331
+ // Prefer the stable Durable Object id over an ephemeral mcpi_{uuid} so the
1332
+ // consent→reuse storage key matches across calls to this DO (mirrors the
1333
+ // session-registration path); the random uuid is a last resort only.
1242
1334
  sessionId =
1243
- providedSessionId || mcpSdkSessionId || `mcpi_${crypto.randomUUID()}`;
1335
+ providedSessionId ||
1336
+ mcpSdkSessionId ||
1337
+ this.ctx.id.toString() ||
1338
+ `${SESSION_ID_PREFIX}${crypto.randomUUID()}`;
1244
1339
  }
1245
1340
  }
1246
1341
  else {
1247
- // No handshake session provided - use fallback
1342
+ // No handshake session provided use fallback. Prefer the stable Durable
1343
+ // Object id over an ephemeral mcpi_{uuid} so the consent→reuse storage key
1344
+ // stays stable across calls to this DO; the random uuid is a last resort.
1248
1345
  sessionId =
1249
- providedSessionId || mcpSdkSessionId || `mcpi_${crypto.randomUUID()}`;
1346
+ providedSessionId ||
1347
+ mcpSdkSessionId ||
1348
+ this.ctx.id.toString() ||
1349
+ `${SESSION_ID_PREFIX}${crypto.randomUUID()}`;
1250
1350
  }
1251
1351
  // Log session ID source for debugging
1252
1352
  if (this._environment === "development") {
@@ -1302,8 +1402,8 @@ export class MCPICloudflareAgent extends McpAgent {
1302
1402
  if (delegationStorage) {
1303
1403
  const identityKey = STORAGE_KEYS.sessionIdentity(sessionId);
1304
1404
  const sessionKey = STORAGE_KEYS.session(sessionId);
1305
- const identityData = await delegationStorage.get(identityKey, "json");
1306
- const sessionData = await delegationStorage.get(sessionKey, "json");
1405
+ const identityData = (await delegationStorage.get(identityKey, "json"));
1406
+ const sessionData = (await delegationStorage.get(sessionKey, "json"));
1307
1407
  userDid = identityData?.userDid || sessionData?.userDid;
1308
1408
  clientDid = sessionData?.clientDid;
1309
1409
  }
@@ -1433,7 +1533,10 @@ export class MCPICloudflareAgent extends McpAgent {
1433
1533
  const scopes = credError.toolProtection?.requiredScopes || [
1434
1534
  `${toolName}:execute`,
1435
1535
  ];
1436
- const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1536
+ // CSPRNG resume token (capability-bearing) — replaces the prior
1537
+ // Date.now()+Math.random() form which leaked creation time and was
1538
+ // weakly unguessable. See generateResumeToken in @kya-os/mcp-i-core.
1539
+ const resumeToken = generateResumeToken("resume");
1437
1540
  // Try MCP Apps inline credential UI first (preferred path)
1438
1541
  // Note: clientCapabilities not available in this context — pass undefined.
1439
1542
  // buildConsentUIResult gates on isExtAppsAvailable(), not client caps.
@@ -1528,7 +1631,10 @@ export class MCPICloudflareAgent extends McpAgent {
1528
1631
  isCredentialProvider,
1529
1632
  });
1530
1633
  let authUrl;
1531
- const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1634
+ // CSPRNG resume token (capability-bearing) — replaces the prior
1635
+ // Date.now()+Math.random() form which leaked creation time and was
1636
+ // weakly unguessable. See generateResumeToken in @kya-os/mcp-i-core.
1637
+ const resumeToken = generateResumeToken("resume");
1532
1638
  if (isCredentialProvider) {
1533
1639
  // Try MCP Apps inline credential UI first
1534
1640
  const inlineResult = this.mcpiRuntime?.buildConsentUIResult(oauthError.toolName, oauthError.requiredScopes, session, resumeToken, session.projectId, session.serverOrigin, undefined, "credentials", oauthError.provider || "credentials");
@@ -1581,7 +1687,9 @@ export class MCPICloudflareAgent extends McpAgent {
1581
1687
  // we MUST re-throw the original error to prevent unauthorized tool execution.
1582
1688
  // Only log and continue for non-auth errors (backward compatibility).
1583
1689
  // Note: pendingInlineResult means auth is handled via inline UI — skip re-throw.
1584
- if (!pendingAuthError && !pendingInlineResult && !pendingAuthRequiredError) {
1690
+ if (!pendingAuthError &&
1691
+ !pendingInlineResult &&
1692
+ !pendingAuthRequiredError) {
1585
1693
  if (originalAuthError) {
1586
1694
  // Auth was required but URL building failed - MUST block execution
1587
1695
  logger.error("[MCPICloudflareAgent] SECURITY: Auth required but URL building failed, blocking tool execution:", { originalError: originalAuthError.message, buildError: error });
@@ -1667,12 +1775,18 @@ export class MCPICloudflareAgent extends McpAgent {
1667
1775
  scopes,
1668
1776
  clientId,
1669
1777
  });
1778
+ // Popup launch — only attached as a hint in `_meta` so popup-aware
1779
+ // clients (today: `.mcpb`-installed Claude Desktop) can open the
1780
+ // consent URL in a stripped-chrome window. Standard clients ignore
1781
+ // the hint and surface the markdown link as before. The popup hint
1782
+ // is gated on `popupEnabled` from the resolved consent config so
1783
+ // operators can opt out per-deployment.
1784
+ const popupEligible = consentUrl && shouldUsePopup(clientId, this.popupEnabledForClient);
1785
+ const popupResource = popupEligible
1786
+ ? buildPopupResource(consentUrl)
1787
+ : undefined;
1670
1788
  // Return tool result with isError: true so MCP clients (Claude Desktop, Inspector)
1671
1789
  // surface the consent URL to users and trigger browser popup for authorization.
1672
- // TODO(ext-apps): When MCP-UI / ext-apps support lands, make this dynamic:
1673
- // - isError: false when the client supports inline consent UI (MCP Apps extension)
1674
- // - isError: true as fallback for standard MCP clients (Claude Desktop, etc.)
1675
- // See PR #274 for the inline consent UI implementation.
1676
1790
  return {
1677
1791
  content: [
1678
1792
  {
@@ -1688,6 +1802,7 @@ export class MCPICloudflareAgent extends McpAgent {
1688
1802
  consentUrl,
1689
1803
  authorizationUrl: consentUrl,
1690
1804
  resumeToken,
1805
+ ...(popupResource ? { popupResource } : {}),
1691
1806
  },
1692
1807
  };
1693
1808
  }
@@ -1943,7 +2058,9 @@ export class MCPICloudflareAgent extends McpAgent {
1943
2058
  if (!this._vaultResolver) {
1944
2059
  const identity = await this.mcpiRuntime.getIdentity();
1945
2060
  // Decode base64/base64url private key to bytes (self-contained, no private method access)
1946
- const b64 = identity.privateKey.replace(/-/g, "+").replace(/_/g, "/");
2061
+ const b64 = identity.privateKey
2062
+ .replace(/-/g, "+")
2063
+ .replace(/_/g, "/");
1947
2064
  const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
1948
2065
  const binaryStr = atob(padded);
1949
2066
  const keyBytes = new Uint8Array(binaryStr.length);
@@ -1960,8 +2077,8 @@ export class MCPICloudflareAgent extends McpAgent {
1960
2077
  : keyBytes;
1961
2078
  // Wrap as PKCS#8 for crypto.subtle.importKey
1962
2079
  const pkcs8Header = new Uint8Array([
1963
- 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05,
1964
- 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
2080
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
2081
+ 0x70, 0x04, 0x22, 0x04, 0x20,
1965
2082
  ]);
1966
2083
  const pkcs8 = new Uint8Array(pkcs8Header.length + rawKey.length);
1967
2084
  pkcs8.set(pkcs8Header);
@@ -2299,6 +2416,16 @@ export class MCPICloudflareAgent extends McpAgent {
2299
2416
  */
2300
2417
  async fetch(request) {
2301
2418
  const url = new URL(request.url);
2419
+ // draft-kya-http-02 §13 pickup-anchor dispatch.
2420
+ // These requests target dedicated `pickup:<sid>` durable-object instances and
2421
+ // must run BEFORE any origin detection, handshake-session side effects, or
2422
+ // super/PartyServer routing — a pickup-anchor instance must never trip agent
2423
+ // init. Precedent: the `/_internal/delegation/*` routes handled further below.
2424
+ if (url.pathname.startsWith("/_internal/pickup/")) {
2425
+ // `this.ctx` is the McpAgent base's DurableObjectState, typed against the
2426
+ // SDK's bundled @cloudflare/workers-types; bridge it to this package's copy.
2427
+ return handlePickupInternal(this.ctx, request);
2428
+ }
2302
2429
  // Auto-detect and store server origin for consent URL building.
2303
2430
  // CRITICAL: When requests are routed through Cloudflare DO routing, the URL is
2304
2431
  // transformed to an internal URL (e.g., http://dummy-example.cloudflare.com).
@@ -2533,12 +2660,19 @@ export class MCPICloudflareAgent extends McpAgent {
2533
2660
  // This is called by consent.service.ts to store delegation tokens directly in DO storage.
2534
2661
  // Using DO storage instead of KV eliminates eventual consistency issues since
2535
2662
  // both storage and retrieval happen on the same DO instance (strongly consistent).
2536
- if (url.pathname === "/_internal/delegation/store" && request.method === "POST") {
2663
+ if (url.pathname === "/_internal/delegation/store" &&
2664
+ request.method === "POST") {
2537
2665
  logger.info("[MCPICloudflareAgent] Handling internal delegation store request");
2538
2666
  try {
2539
- const body = await request.json();
2540
- if (!body.sessionId || !body.delegationToken || !body.delegationId || !body.agentDid) {
2541
- return new Response(JSON.stringify({ success: false, error: "Missing required fields" }), { status: 400, headers: { "Content-Type": "application/json" } });
2667
+ const body = (await request.json());
2668
+ if (!body.sessionId ||
2669
+ !body.delegationToken ||
2670
+ !body.delegationId ||
2671
+ !body.agentDid) {
2672
+ return new Response(JSON.stringify({
2673
+ success: false,
2674
+ error: "Missing required fields",
2675
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
2542
2676
  }
2543
2677
  await this.storeDelegationToStorage(body);
2544
2678
  logger.info("[MCPICloudflareAgent] ✅ Delegation stored to DO storage:", {
@@ -2546,7 +2680,10 @@ export class MCPICloudflareAgent extends McpAgent {
2546
2680
  delegationId: body.delegationId,
2547
2681
  hasUserDid: !!body.userDid,
2548
2682
  });
2549
- return new Response(JSON.stringify({ success: true }), { status: 200, headers: { "Content-Type": "application/json" } });
2683
+ return new Response(JSON.stringify({ success: true }), {
2684
+ status: 200,
2685
+ headers: { "Content-Type": "application/json" },
2686
+ });
2550
2687
  }
2551
2688
  catch (error) {
2552
2689
  logger.error("[MCPICloudflareAgent] Delegation store request failed:", error);
@@ -2560,10 +2697,11 @@ export class MCPICloudflareAgent extends McpAgent {
2560
2697
  // This is called by getDelegationFromStorage() to read from singleton DO
2561
2698
  // when using singleton routing strategy. This ensures the agent reads from
2562
2699
  // the same DO that consent.service.ts wrote to.
2563
- if (url.pathname === "/_internal/delegation/get" && request.method === "POST") {
2700
+ if (url.pathname === "/_internal/delegation/get" &&
2701
+ request.method === "POST") {
2564
2702
  logger.debug("[MCPICloudflareAgent] Handling internal delegation get request");
2565
2703
  try {
2566
- const body = await request.json();
2704
+ const body = (await request.json());
2567
2705
  if (!body.sessionId) {
2568
2706
  return new Response(JSON.stringify({ success: false, error: "Missing sessionId" }), { status: 400, headers: { "Content-Type": "application/json" } });
2569
2707
  }
@@ -2594,7 +2732,10 @@ export class MCPICloudflareAgent extends McpAgent {
2594
2732
  hasUserDid: !!body.userDid,
2595
2733
  hasAgentDid: !!body.agentDid,
2596
2734
  });
2597
- return new Response(JSON.stringify({ success: true, data: null }), { status: 200, headers: { "Content-Type": "application/json" } });
2735
+ return new Response(JSON.stringify({ success: true, data: null }), {
2736
+ status: 200,
2737
+ headers: { "Content-Type": "application/json" },
2738
+ });
2598
2739
  }
2599
2740
  catch (error) {
2600
2741
  logger.error("[MCPICloudflareAgent] Delegation get request failed:", error);
@@ -2607,12 +2748,16 @@ export class MCPICloudflareAgent extends McpAgent {
2607
2748
  // Handle internal session mapping storage request
2608
2749
  // This is called to store MCP session ID → handshake session ID mapping in DO storage
2609
2750
  // for strong consistency (avoiding KV eventual consistency issues)
2610
- if (url.pathname === "/_internal/session-mapping/store" && request.method === "POST") {
2751
+ if (url.pathname === "/_internal/session-mapping/store" &&
2752
+ request.method === "POST") {
2611
2753
  logger.debug("[MCPICloudflareAgent] Handling internal session mapping store request");
2612
2754
  try {
2613
- const body = await request.json();
2755
+ const body = (await request.json());
2614
2756
  if (!body.mcpSessionId || !body.handshakeSessionId) {
2615
- return new Response(JSON.stringify({ success: false, error: "Missing required fields" }), { status: 400, headers: { "Content-Type": "application/json" } });
2757
+ return new Response(JSON.stringify({
2758
+ success: false,
2759
+ error: "Missing required fields",
2760
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
2616
2761
  }
2617
2762
  // Store in DO storage
2618
2763
  // Note: DO storage doesn't support TTL like KV, but the mapping is cleaned up
@@ -2623,7 +2768,10 @@ export class MCPICloudflareAgent extends McpAgent {
2623
2768
  mcpSessionId: body.mcpSessionId.slice(0, 20) + "...",
2624
2769
  handshakeSessionId: body.handshakeSessionId.slice(0, 20) + "...",
2625
2770
  });
2626
- return new Response(JSON.stringify({ success: true }), { status: 200, headers: { "Content-Type": "application/json" } });
2771
+ return new Response(JSON.stringify({ success: true }), {
2772
+ status: 200,
2773
+ headers: { "Content-Type": "application/json" },
2774
+ });
2627
2775
  }
2628
2776
  catch (error) {
2629
2777
  logger.error("[MCPICloudflareAgent] Session mapping store request failed:", error);
@@ -2635,10 +2783,11 @@ export class MCPICloudflareAgent extends McpAgent {
2635
2783
  }
2636
2784
  // Handle internal session mapping retrieval request
2637
2785
  // This is called to retrieve MCP session ID → handshake session ID mapping from DO storage
2638
- if (url.pathname === "/_internal/session-mapping/get" && request.method === "POST") {
2786
+ if (url.pathname === "/_internal/session-mapping/get" &&
2787
+ request.method === "POST") {
2639
2788
  logger.debug("[MCPICloudflareAgent] Handling internal session mapping get request");
2640
2789
  try {
2641
- const body = await request.json();
2790
+ const body = (await request.json());
2642
2791
  if (!body.mcpSessionId) {
2643
2792
  return new Response(JSON.stringify({ success: false, error: "Missing mcpSessionId" }), { status: 400, headers: { "Content-Type": "application/json" } });
2644
2793
  }
@@ -2667,7 +2816,8 @@ export class MCPICloudflareAgent extends McpAgent {
2667
2816
  }
2668
2817
  // Handle internal client capabilities request
2669
2818
  // Returns stored MCP client capabilities from the initialize message
2670
- if (url.pathname === "/_internal/client-capabilities" && request.method === "GET") {
2819
+ if (url.pathname === "/_internal/client-capabilities" &&
2820
+ request.method === "GET") {
2671
2821
  try {
2672
2822
  const mcpClientInfo = await this.getMcpClientInfo();
2673
2823
  return new Response(JSON.stringify({
@@ -2903,7 +3053,7 @@ export class MCPICloudflareAgent extends McpAgent {
2903
3053
  body: JSON.stringify({ sessionId, userDid, agentDid }),
2904
3054
  });
2905
3055
  if (response.ok) {
2906
- const result = await response.json();
3056
+ const result = (await response.json());
2907
3057
  if (result.success && result.data) {
2908
3058
  logger.debug("[MCPICloudflareAgent] ✅ Delegation found via DO:", {
2909
3059
  sessionId: sessionId.slice(0, 20) + "...",