@kya-os/mcp-i-cloudflare 1.5.10-canary.9 → 1.6.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 (102) hide show
  1. package/README.md +130 -0
  2. package/dist/__tests__/e2e/test-config.d.ts +37 -0
  3. package/dist/__tests__/e2e/test-config.d.ts.map +1 -0
  4. package/dist/__tests__/e2e/test-config.js +62 -0
  5. package/dist/__tests__/e2e/test-config.js.map +1 -0
  6. package/dist/adapter.d.ts +44 -1
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +712 -112
  9. package/dist/adapter.js.map +1 -1
  10. package/dist/agent.d.ts +103 -25
  11. package/dist/agent.d.ts.map +1 -1
  12. package/dist/agent.js +617 -40
  13. package/dist/agent.js.map +1 -1
  14. package/dist/app.d.ts +0 -8
  15. package/dist/app.d.ts.map +1 -1
  16. package/dist/app.js +277 -119
  17. package/dist/app.js.map +1 -1
  18. package/dist/cache/kv-oauth-config-cache.d.ts +47 -0
  19. package/dist/cache/kv-oauth-config-cache.d.ts.map +1 -0
  20. package/dist/cache/kv-oauth-config-cache.js +82 -0
  21. package/dist/cache/kv-oauth-config-cache.js.map +1 -0
  22. package/dist/cache/kv-tool-protection-cache.d.ts +26 -1
  23. package/dist/cache/kv-tool-protection-cache.d.ts.map +1 -1
  24. package/dist/cache/kv-tool-protection-cache.js +19 -11
  25. package/dist/cache/kv-tool-protection-cache.js.map +1 -1
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +39 -14
  28. package/dist/config.js.map +1 -1
  29. package/dist/helpers/env-mapper.d.ts +60 -1
  30. package/dist/helpers/env-mapper.d.ts.map +1 -1
  31. package/dist/helpers/env-mapper.js +136 -6
  32. package/dist/helpers/env-mapper.js.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +16 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/runtime/audit-logger.d.ts +96 -0
  38. package/dist/runtime/audit-logger.d.ts.map +1 -0
  39. package/dist/runtime/audit-logger.js +276 -0
  40. package/dist/runtime/audit-logger.js.map +1 -0
  41. package/dist/runtime/oauth-handler.d.ts +5 -0
  42. package/dist/runtime/oauth-handler.d.ts.map +1 -1
  43. package/dist/runtime/oauth-handler.js +287 -35
  44. package/dist/runtime/oauth-handler.js.map +1 -1
  45. package/dist/runtime.d.ts +12 -1
  46. package/dist/runtime.d.ts.map +1 -1
  47. package/dist/runtime.js +34 -4
  48. package/dist/runtime.js.map +1 -1
  49. package/dist/server.d.ts +7 -0
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +120 -29
  52. package/dist/server.js.map +1 -1
  53. package/dist/services/admin.service.d.ts +1 -3
  54. package/dist/services/admin.service.d.ts.map +1 -1
  55. package/dist/services/admin.service.js +175 -146
  56. package/dist/services/admin.service.js.map +1 -1
  57. package/dist/services/consent-audit.service.d.ts +91 -0
  58. package/dist/services/consent-audit.service.d.ts.map +1 -0
  59. package/dist/services/consent-audit.service.js +243 -0
  60. package/dist/services/consent-audit.service.js.map +1 -0
  61. package/dist/services/consent-config.service.d.ts +2 -2
  62. package/dist/services/consent-config.service.d.ts.map +1 -1
  63. package/dist/services/consent-config.service.js +55 -28
  64. package/dist/services/consent-config.service.js.map +1 -1
  65. package/dist/services/consent-page-renderer.d.ts +14 -0
  66. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  67. package/dist/services/consent-page-renderer.js +54 -27
  68. package/dist/services/consent-page-renderer.js.map +1 -1
  69. package/dist/services/consent.service.d.ts +93 -8
  70. package/dist/services/consent.service.d.ts.map +1 -1
  71. package/dist/services/consent.service.js +1817 -553
  72. package/dist/services/consent.service.js.map +1 -1
  73. package/dist/services/delegation.service.d.ts.map +1 -1
  74. package/dist/services/delegation.service.js +67 -29
  75. package/dist/services/delegation.service.js.map +1 -1
  76. package/dist/services/idp-token-storage.d.ts +68 -0
  77. package/dist/services/idp-token-storage.d.ts.map +1 -0
  78. package/dist/services/idp-token-storage.js +157 -0
  79. package/dist/services/idp-token-storage.js.map +1 -0
  80. package/dist/services/oauth-service.d.ts +66 -0
  81. package/dist/services/oauth-service.d.ts.map +1 -0
  82. package/dist/services/oauth-service.js +223 -0
  83. package/dist/services/oauth-service.js.map +1 -0
  84. package/dist/services/proof.service.d.ts +8 -6
  85. package/dist/services/proof.service.d.ts.map +1 -1
  86. package/dist/services/proof.service.js +131 -75
  87. package/dist/services/proof.service.js.map +1 -1
  88. package/dist/services/tool-context-builder.d.ts +55 -0
  89. package/dist/services/tool-context-builder.d.ts.map +1 -0
  90. package/dist/services/tool-context-builder.js +124 -0
  91. package/dist/services/tool-context-builder.js.map +1 -0
  92. package/dist/types/tool-context.d.ts +35 -0
  93. package/dist/types/tool-context.d.ts.map +1 -0
  94. package/dist/types/tool-context.js +13 -0
  95. package/dist/types/tool-context.js.map +1 -0
  96. package/dist/types.d.ts +31 -2
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/utils/oauth-service-registry.d.ts +65 -0
  99. package/dist/utils/oauth-service-registry.d.ts.map +1 -0
  100. package/dist/utils/oauth-service-registry.js +125 -0
  101. package/dist/utils/oauth-service-registry.js.map +1 -0
  102. package/package.json +27 -60
package/dist/adapter.js CHANGED
@@ -13,6 +13,8 @@ import { WELL_KNOWN_CORS_HEADERS, MCP_CORS_HEADERS, PREFLIGHT_CORS_HEADERS, } fr
13
13
  import { KVProofArchive } from "./storage/kv-proof-archive";
14
14
  import { STORAGE_KEYS } from "./constants/storage-keys";
15
15
  import { DEFAULT_SESSION_CACHE_TTL } from "./constants";
16
+ import { normalizeCloudflareEnv } from "./helpers/env-mapper";
17
+ import { AdminService } from "./services/admin.service";
16
18
  const INITIALIZE_CONTEXT_TTL_MS = 60_000;
17
19
  /**
18
20
  * Lightweight MCP protocol implementation for Cloudflare Workers
@@ -77,81 +79,273 @@ class CloudflareMCPServer {
77
79
  }
78
80
  // Call tool with identity/proof wrapping
79
81
  if (method === "tools/call") {
82
+ // Entry point logging for debugging
83
+ console.log("🔵 [CloudflareMCPServer] handleRequest: tools/call", {
84
+ toolName: params.name,
85
+ hasParams: !!params,
86
+ paramsKeys: params ? Object.keys(params) : [],
87
+ });
80
88
  const toolName = params.name;
81
89
  const tool = this.tools.get(toolName);
82
90
  if (!tool) {
83
91
  throw new Error(`Tool not found: ${toolName}`);
84
92
  }
85
93
  // Get current session if available (stateful environments like Node.js)
86
- let session = await this.runtime.getCurrentSession();
94
+ // Wrap in try-catch to catch any exceptions
95
+ let session = null;
96
+ try {
97
+ session = await this.runtime.getCurrentSession();
98
+ console.log("[CloudflareMCPServer] Session check:", {
99
+ hasSession: !!session,
100
+ sessionId: session?.id?.slice(0, 20) + "..." || "none",
101
+ hasDelegationToken: !!session?.delegationToken,
102
+ hasDelegationStorage: !!this.delegationStorage,
103
+ method: "tools/call",
104
+ toolName,
105
+ });
106
+ }
107
+ catch (error) {
108
+ console.error("[CloudflareMCPServer] Error in getCurrentSession:", error);
109
+ // Continue with null session (will create ephemeral)
110
+ }
87
111
  // For stateless environments (Cloudflare Workers), create ephemeral session
88
112
  if (!session) {
113
+ // Extract session_id from params if available (from consent flow)
114
+ // This allows us to retrieve delegation tokens stored with the original session_id
115
+ const providedSessionId = params.session_id || params.sessionId;
89
116
  const timestamp = Date.now();
90
117
  const randomSuffix = Math.random().toString(36).substring(2, 10);
91
118
  const agentDid = (await this.runtime.getIdentity()).did;
92
- const sessionId = `ephemeral-${timestamp}-${randomSuffix}`;
119
+ // Use provided session_id if available, otherwise create ephemeral
120
+ const sessionId = providedSessionId || `ephemeral-${timestamp}-${randomSuffix}`;
121
+ console.log("[CloudflareMCPServer] Creating session:", {
122
+ sessionId: sessionId.slice(0, 20) + "...",
123
+ isProvided: !!providedSessionId,
124
+ isEphemeral: !providedSessionId,
125
+ });
126
+ // Extract OAuth identity from request or session cache for persistent userDid lookup
127
+ let oauthIdentity = undefined;
128
+ if (meta?.request) {
129
+ oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
130
+ if (oauthIdentity) {
131
+ console.log("[CloudflareMCPServer] Extracted OAuth identity from request:", {
132
+ provider: oauthIdentity.provider,
133
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
134
+ });
135
+ }
136
+ }
137
+ // If OAuth identity not in request, try to get from session cache
138
+ if (!oauthIdentity && sessionId && this.delegationStorage) {
139
+ try {
140
+ const sessionKey = STORAGE_KEYS.session(sessionId);
141
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
142
+ if (sessionData?.oauthIdentity?.provider) {
143
+ // Note: We only have subjectHash in session cache (PII protection)
144
+ // We can't fully reconstruct OAuth identity, but we can use it to lookup userDid
145
+ console.log("[CloudflareMCPServer] Found OAuth identity in session cache:", {
146
+ provider: sessionData.oauthIdentity.provider,
147
+ hasSubjectHash: !!sessionData.oauthIdentity.subjectHash,
148
+ });
149
+ // OAuth identity from session cache is incomplete (subjectHash only), so we'll use it for userDid lookup via storage
150
+ }
151
+ }
152
+ catch (error) {
153
+ console.warn("[CloudflareMCPServer] Failed to get OAuth identity from session:", error);
154
+ // Non-fatal - continue without OAuth identity
155
+ }
156
+ }
93
157
  // Check KV storage for stored delegation token if not provided in params
94
158
  let delegationToken = params.delegationToken;
95
159
  if (!delegationToken && this.delegationStorage) {
96
160
  try {
97
- // Get userDID from params or session if available
98
- // TODO: Properly map credentials to userDID for multi-user scenarios
99
- // Currently, we use agent-scoped keys which can cause conflicts when multiple
100
- // users delegate to the same agent. The ideal key structure should be:
101
- // - `user:${userDid}:agent:${agentDid}:delegation` (user+agent scoped - most specific)
102
- // - `agent:${agentDid}:delegation` (agent-scoped fallback for single-user agents)
103
- //
104
- // To implement this properly:
105
- // 1. Capture userDID in consent approval flow (from OAuth or session)
106
- // 2. Store tokens with userDID in the key: `user:${userDid}:agent:${agentDid}:delegation`
107
- // 3. Retrieve tokens by userDID + agentDID combination
108
- // 4. Fall back to agent-scoped key for backward compatibility
109
- const userDid = params.clientDid || params.userDid; // Try to get userDID from request
110
- // Try session-scoped token first (if session ID was persisted)
111
- // Note: Sessions are ephemeral in Cloudflare Workers, so this is mainly
112
- // useful for caching tokens within a single request chain
113
- if (sessionId) {
114
- const sessionToken = await this.delegationStorage.get(`session:${sessionId}`);
115
- if (sessionToken) {
116
- delegationToken = sessionToken;
161
+ // Get userDID from params first
162
+ let userDid = params.clientDid || params.userDid;
163
+ // PRIORITY 1: If OAuth identity available, use it to lookup persistent userDid
164
+ if (!userDid && oauthIdentity && this.delegationStorage) {
165
+ try {
166
+ const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
167
+ const mappedUserDid = await this.delegationStorage.get(oauthKey, "text");
168
+ if (mappedUserDid) {
169
+ userDid = mappedUserDid;
170
+ console.log("[CloudflareMCPServer] Retrieved persistent userDid from OAuth mapping:", {
171
+ provider: oauthIdentity.provider,
172
+ userDid: userDid.slice(0, 20) + "...",
173
+ });
174
+ }
175
+ }
176
+ catch (error) {
177
+ console.warn("[CloudflareMCPServer] Failed to lookup userDid from OAuth mapping:", error);
178
+ // Non-fatal - continue with session cache lookup
117
179
  }
118
180
  }
119
- // Try user+agent scoped token (if userDID is available)
120
- // This is the preferred approach for multi-user scenarios
121
- if (!delegationToken && userDid) {
122
- const userAgentKey = `user:${userDid}:agent:${agentDid}:delegation`;
123
- const userAgentToken = await this.delegationStorage.get(userAgentKey);
181
+ // PRIORITY 2: If userDid not in params or OAuth mapping, try to retrieve from session cache
182
+ if (!userDid && sessionId) {
183
+ try {
184
+ const sessionKey = STORAGE_KEYS.session(sessionId);
185
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
186
+ if (sessionData?.userDid) {
187
+ userDid = sessionData.userDid;
188
+ console.log("[CloudflareMCPServer] ✅ Retrieved userDid from session cache:", {
189
+ userDid: userDid.slice(0, 20) + "...",
190
+ sessionId: sessionId.slice(0, 20) + "...",
191
+ });
192
+ }
193
+ }
194
+ catch (error) {
195
+ console.warn("[CloudflareMCPServer] Failed to get userDid from session:", error);
196
+ // Non-fatal - continue without userDid
197
+ }
198
+ }
199
+ console.log("[CloudflareMCPServer] 🔍 Starting delegation token lookup from KV:", {
200
+ agentDid: agentDid.slice(0, 20) + "...",
201
+ userDid: userDid?.slice(0, 20) + "..." || "none",
202
+ sessionId: sessionId?.slice(0, 20) + "..." || "none",
203
+ hasDelegationStorage: !!this.delegationStorage,
204
+ });
205
+ // PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
206
+ // This is the preferred approach for multi-user scenarios with proper user isolation
207
+ if (userDid) {
208
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
209
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key:", {
210
+ key: userAgentKey,
211
+ userDid: userDid.slice(0, 20) + "...",
212
+ agentDid: agentDid.slice(0, 20) + "...",
213
+ });
214
+ const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
215
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
216
+ key: userAgentKey,
217
+ found: !!userAgentToken,
218
+ tokenLength: userAgentToken?.length || 0,
219
+ tokenPreview: userAgentToken
220
+ ? userAgentToken.substring(0, 20) + "..."
221
+ : null,
222
+ });
124
223
  if (userAgentToken) {
125
224
  delegationToken = userAgentToken;
225
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from user+agent scoped key:", {
226
+ userDid: userDid.slice(0, 20) + "...",
227
+ agentDid: agentDid.slice(0, 20) + "...",
228
+ key: userAgentKey,
229
+ tokenLength: userAgentToken.length,
230
+ });
126
231
  // Cache it for this session for faster future lookups
127
- if (sessionId) {
128
- await this.delegationStorage.put(`session:${sessionId}`, userAgentToken, {
232
+ // Store full session data object to match consent service format
233
+ if (this.delegationStorage && sessionId) {
234
+ const sessionKey = STORAGE_KEYS.session(sessionId);
235
+ // Read existing session data to preserve userDid and other fields
236
+ const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
237
+ await this.delegationStorage.put(sessionKey, JSON.stringify({
238
+ ...existingSession,
239
+ userDid,
240
+ agentDid,
241
+ delegationToken: userAgentToken,
242
+ cachedAt: Date.now(),
243
+ }), {
129
244
  expirationTtl: 1800, // 30 minutes
130
245
  });
131
246
  }
132
247
  }
133
248
  }
134
- // Fallback to agent-scoped token (stable across sessions, but shared across users)
135
- // WARNING: This can cause conflicts when multiple users delegate to the same agent.
136
- // Each user's delegation will overwrite the previous one. This is acceptable for
137
- // single-user agents, but multi-user agents should use user+agent scoped keys above.
138
- if (!delegationToken) {
139
- const agentKey = `agent:${agentDid}:delegation`;
140
- const agentToken = await this.delegationStorage.get(agentKey);
249
+ // PRIORITY 2: Try session-scoped token (if session ID was persisted)
250
+ // Note: Sessions are ephemeral in Cloudflare Workers, so this is mainly
251
+ // useful for caching tokens within a single request chain
252
+ if (!delegationToken && sessionId) {
253
+ const sessionKey = STORAGE_KEYS.session(sessionId);
254
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key:", {
255
+ key: sessionKey,
256
+ sessionId: sessionId.slice(0, 20) + "...",
257
+ });
258
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
259
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
260
+ key: sessionKey,
261
+ found: !!sessionData,
262
+ hasDelegationToken: !!sessionData?.delegationToken,
263
+ tokenLength: sessionData?.delegationToken?.length || 0,
264
+ tokenPreview: sessionData?.delegationToken
265
+ ? sessionData.delegationToken.substring(0, 20) + "..."
266
+ : null,
267
+ });
268
+ if (sessionData?.delegationToken) {
269
+ delegationToken = sessionData.delegationToken;
270
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from session cache:", {
271
+ sessionId: sessionId.slice(0, 20) + "...",
272
+ tokenLength: delegationToken.length,
273
+ });
274
+ }
275
+ }
276
+ // PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
277
+ // Only use when userDid is unavailable (backward compatibility)
278
+ // WARNING: This allows cross-user delegation sharing - migrate to user+agent scoped tokens
279
+ if (!delegationToken && !userDid) {
280
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
281
+ console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
282
+ key: agentKey,
283
+ agentDid: agentDid.slice(0, 20) + "...",
284
+ reason: "userDid unavailable",
285
+ });
286
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy):", {
287
+ key: agentKey,
288
+ agentDid: agentDid.slice(0, 20) + "...",
289
+ });
290
+ const agentToken = await this.delegationStorage.get(agentKey, "text");
291
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
292
+ key: agentKey,
293
+ found: !!agentToken,
294
+ tokenLength: agentToken?.length || 0,
295
+ tokenPreview: agentToken
296
+ ? agentToken.substring(0, 20) + "..."
297
+ : null,
298
+ });
141
299
  if (agentToken) {
142
300
  delegationToken = agentToken;
301
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy):", {
302
+ agentDid: agentDid.slice(0, 20) + "...",
303
+ key: agentKey,
304
+ tokenLength: agentToken.length,
305
+ });
143
306
  // Cache it for this session for faster future lookups
144
- if (sessionId) {
145
- await this.delegationStorage.put(`session:${sessionId}`, agentToken, {
307
+ // Store full session data object to match consent service format
308
+ if (this.delegationStorage && sessionId) {
309
+ const sessionKey = STORAGE_KEYS.session(sessionId);
310
+ // Read existing session data to preserve userDid and other fields
311
+ const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
312
+ await this.delegationStorage.put(sessionKey, JSON.stringify({
313
+ ...existingSession,
314
+ userDid,
315
+ agentDid,
316
+ delegationToken: agentToken,
317
+ cachedAt: Date.now(),
318
+ }), {
146
319
  expirationTtl: 1800, // 30 minutes
147
320
  });
148
321
  }
149
322
  }
150
323
  }
324
+ if (!delegationToken) {
325
+ const checkedKeys = [];
326
+ // Only include agent-scoped key if it was checked (when userDid unavailable)
327
+ if (!userDid) {
328
+ checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
329
+ }
330
+ // Always include user+agent scoped key if userDid is available
331
+ if (userDid) {
332
+ checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
333
+ }
334
+ // Always include session-scoped key if sessionId is available
335
+ if (sessionId) {
336
+ checkedKeys.push(STORAGE_KEYS.session(sessionId));
337
+ }
338
+ console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage:", {
339
+ agentDid: agentDid.slice(0, 20) + "...",
340
+ userDid: userDid?.slice(0, 20) + "..." || "none",
341
+ sessionId: sessionId?.slice(0, 20) + "..." || "none",
342
+ checkedKeys: checkedKeys.filter(Boolean),
343
+ });
344
+ }
151
345
  }
152
346
  catch (error) {
153
347
  // Log but don't fail - delegation lookup is best-effort
154
- console.warn("[CloudflareMCPServer] Failed to retrieve delegation token from KV:", error);
348
+ console.error("[CloudflareMCPServer] Failed to retrieve delegation token from KV:", error);
155
349
  }
156
350
  }
157
351
  session = {
@@ -165,13 +359,198 @@ class CloudflareMCPServer {
165
359
  delegationToken,
166
360
  consentProof: params.consentProof,
167
361
  // Auto-detect server origin for consent URL building
168
- // Priority: 1) requestOrigin (set via setRequestOrigin), 2) meta.serverOrigin (from header), 3) undefined
169
- serverOrigin: this.requestOrigin || meta?.serverOrigin,
362
+ serverOrigin: this.requestOrigin,
170
363
  // These fields are for ephemeral sessions - won't persist
171
364
  };
365
+ // ✅ CRITICAL: Log delegation token status before processToolCall
366
+ console.log("[CloudflareMCPServer] ✅ Session created with delegation token status:", {
367
+ sessionId: sessionId.slice(0, 20) + "...",
368
+ hasDelegationToken: !!delegationToken,
369
+ delegationTokenLength: delegationToken?.length || 0,
370
+ delegationTokenSource: params.delegationToken
371
+ ? "params"
372
+ : delegationToken
373
+ ? "kv-storage"
374
+ : "none",
375
+ agentDid: agentDid.slice(0, 20) + "...",
376
+ });
172
377
  }
173
378
  else {
174
- // For existing sessions, update delegation fields if provided
379
+ // For existing sessions, check KV storage for delegation token if not already set
380
+ // This ensures newly created delegations are found even for existing sessions
381
+ console.log("[CloudflareMCPServer] Existing session detected, checking KV storage:", {
382
+ hasSession: !!session,
383
+ hasDelegationToken: !!session?.delegationToken,
384
+ hasDelegationStorage: !!this.delegationStorage,
385
+ sessionId: session?.id?.slice(0, 20) + "...",
386
+ sessionKeys: Object.keys(session || {}),
387
+ });
388
+ if (!session.delegationToken && this.delegationStorage) {
389
+ console.log("[CloudflareMCPServer] ⚠️ Session has no delegationToken, attempting KV lookup...");
390
+ try {
391
+ const agentDid = (await this.runtime.getIdentity()).did;
392
+ // ✅ CRITICAL: Retrieve userDid from session cache if not in session object
393
+ // Check session cache first, then params, then session object
394
+ let userDid = session.userDid ||
395
+ params.clientDid ||
396
+ params.userDid ||
397
+ session.clientDid;
398
+ const sessionId = session.id;
399
+ // If userDid still not found, try retrieving from session cache
400
+ if (!userDid && sessionId && this.delegationStorage) {
401
+ try {
402
+ const sessionKey = STORAGE_KEYS.session(sessionId);
403
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
404
+ if (sessionData?.userDid) {
405
+ userDid = sessionData.userDid;
406
+ // Update session object with retrieved userDid
407
+ session.userDid = userDid;
408
+ console.log("[CloudflareMCPServer] ✅ Retrieved userDid from session cache for delegation lookup:", {
409
+ userDid: userDid.slice(0, 20) + "...",
410
+ sessionId: sessionId.slice(0, 20) + "...",
411
+ });
412
+ }
413
+ }
414
+ catch (error) {
415
+ console.warn("[CloudflareMCPServer] Failed to get userDid from session cache:", error);
416
+ }
417
+ }
418
+ console.log("[CloudflareMCPServer] 🔍 Looking up delegation token for existing session:", {
419
+ sessionId: sessionId?.slice(0, 20) + "...",
420
+ userDid: userDid?.slice(0, 20) + "...",
421
+ agentDid: agentDid?.slice(0, 20) + "...",
422
+ });
423
+ // PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
424
+ // This is the preferred approach for multi-user scenarios with proper user isolation
425
+ let delegationToken = undefined;
426
+ if (userDid) {
427
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
428
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key (existing session):", {
429
+ key: userAgentKey,
430
+ userDid: userDid.slice(0, 20) + "...",
431
+ agentDid: agentDid.slice(0, 20) + "...",
432
+ });
433
+ const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
434
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
435
+ key: userAgentKey,
436
+ found: !!userAgentToken,
437
+ tokenLength: userAgentToken?.length || 0,
438
+ });
439
+ if (userAgentToken) {
440
+ delegationToken = userAgentToken;
441
+ }
442
+ }
443
+ // PRIORITY 2: Try session-scoped token (if session ID was persisted)
444
+ if (!delegationToken && sessionId) {
445
+ const sessionKey = STORAGE_KEYS.session(sessionId);
446
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key (existing session):", {
447
+ key: sessionKey,
448
+ sessionId: sessionId.slice(0, 20) + "...",
449
+ });
450
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
451
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
452
+ key: sessionKey,
453
+ found: !!sessionData,
454
+ hasDelegationToken: !!sessionData?.delegationToken,
455
+ tokenLength: sessionData?.delegationToken?.length || 0,
456
+ });
457
+ if (sessionData?.delegationToken) {
458
+ delegationToken = sessionData.delegationToken;
459
+ }
460
+ }
461
+ // PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
462
+ // Only use when userDid is unavailable (backward compatibility)
463
+ if (!delegationToken && !userDid) {
464
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
465
+ console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
466
+ key: agentKey,
467
+ agentDid: agentDid.slice(0, 20) + "...",
468
+ reason: "userDid unavailable",
469
+ });
470
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy, existing session):", {
471
+ key: agentKey,
472
+ agentDid: agentDid.slice(0, 20) + "...",
473
+ });
474
+ try {
475
+ const agentToken = await this.delegationStorage.get(agentKey, "text");
476
+ console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
477
+ key: agentKey,
478
+ found: !!agentToken,
479
+ tokenLength: agentToken?.length || 0,
480
+ });
481
+ if (agentToken) {
482
+ delegationToken = agentToken;
483
+ console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
484
+ agentDid: agentDid.slice(0, 20) + "...",
485
+ key: agentKey,
486
+ tokenLength: agentToken.length,
487
+ });
488
+ }
489
+ }
490
+ catch (error) {
491
+ console.error("[CloudflareMCPServer] Error retrieving agent token:", error);
492
+ }
493
+ }
494
+ // ✅ CRITICAL: Set delegation token on session if found
495
+ if (delegationToken) {
496
+ session = {
497
+ ...session,
498
+ delegationToken,
499
+ };
500
+ console.log("[CloudflareMCPServer] ✅ Delegation token set on existing session:", {
501
+ sessionId: sessionId?.slice(0, 20) + "..." || "none",
502
+ tokenLength: delegationToken.length,
503
+ sessionHasToken: !!session.delegationToken,
504
+ source: "kv-storage",
505
+ });
506
+ }
507
+ else {
508
+ const checkedKeys = [];
509
+ // Only include agent-scoped key if it was checked (when userDid unavailable)
510
+ if (!userDid) {
511
+ checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
512
+ }
513
+ // Always include user+agent scoped key if userDid is available
514
+ if (userDid) {
515
+ checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
516
+ }
517
+ // Always include session-scoped key if sessionId is available
518
+ if (sessionId) {
519
+ checkedKeys.push(STORAGE_KEYS.session(sessionId));
520
+ }
521
+ console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage for existing session:", {
522
+ sessionId: sessionId?.slice(0, 20) + "..." || "none",
523
+ checkedKeys: checkedKeys.filter(Boolean),
524
+ });
525
+ }
526
+ // Final check - log if we still don't have a token
527
+ if (!session.delegationToken) {
528
+ console.log("[CloudflareMCPServer] ❌ FINAL CHECK: Still no delegation token after all KV lookups:", {
529
+ sessionId: session?.id?.slice(0, 20) + "..." || "none",
530
+ userDid: userDid?.slice(0, 20) + "..." || "none",
531
+ agentDid: agentDid.slice(0, 20) + "...",
532
+ checkedKeys: [
533
+ sessionId ? STORAGE_KEYS.session(sessionId) : null,
534
+ userDid
535
+ ? STORAGE_KEYS.delegation(userDid, agentDid)
536
+ : null,
537
+ STORAGE_KEYS.legacyDelegation(agentDid),
538
+ ].filter(Boolean),
539
+ });
540
+ }
541
+ else {
542
+ console.log("[CloudflareMCPServer] ✅ FINAL CHECK: Delegation token found:", {
543
+ tokenLength: session.delegationToken.length,
544
+ sessionHasToken: !!session.delegationToken,
545
+ });
546
+ }
547
+ }
548
+ catch (error) {
549
+ // Log but don't fail - delegation lookup is best-effort
550
+ console.error("[CloudflareMCPServer] Failed to retrieve delegation token from KV (existing session):", error);
551
+ }
552
+ }
553
+ // Update delegation fields if provided in params (takes precedence)
175
554
  if (params.delegationToken) {
176
555
  session.delegationToken = params.delegationToken;
177
556
  }
@@ -179,13 +558,9 @@ class CloudflareMCPServer {
179
558
  session.consentProof = params.consentProof;
180
559
  }
181
560
  // Update server origin if available (for consent URL building)
182
- // Priority: 1) requestOrigin (set via setRequestOrigin), 2) meta.serverOrigin (from header)
183
561
  if (this.requestOrigin) {
184
562
  session.serverOrigin = this.requestOrigin;
185
563
  }
186
- else if (meta?.serverOrigin) {
187
- session.serverOrigin = meta.serverOrigin;
188
- }
189
564
  }
190
565
  // ✅ NEW: Determine scopeId from runtime config (for tool auto-discovery in AgentShield)
191
566
  let scopeId;
@@ -207,6 +582,59 @@ class CloudflareMCPServer {
207
582
  session.toolName = toolName;
208
583
  session.toolParams = params.arguments || {};
209
584
  session.scopeId = scopeId; // ✅ ADDED: Pass scopeId for tool auto-discovery
585
+ // ✅ CRITICAL: Verify delegation token is set on session before processToolCall
586
+ // Also do a final KV lookup if token is still missing (defensive check)
587
+ // Use same 3-priority lookup: user+agent scoped, then session-scoped, then agent-scoped (last resort)
588
+ if (!session?.delegationToken && this.delegationStorage) {
589
+ try {
590
+ const agentDid = (await this.runtime.getIdentity()).did;
591
+ const userDid = session.userDid || params.clientDid || params.userDid;
592
+ let finalToken = undefined;
593
+ // PRIORITY 1: User+agent scoped token
594
+ if (userDid) {
595
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
596
+ const token = await this.delegationStorage.get(userAgentKey, "text");
597
+ finalToken = token || undefined; // Convert null to undefined
598
+ }
599
+ // PRIORITY 2: Session-scoped token
600
+ if (!finalToken && session.id) {
601
+ const sessionKey = STORAGE_KEYS.session(session.id);
602
+ const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
603
+ finalToken = sessionData?.delegationToken;
604
+ }
605
+ // PRIORITY 3: Agent-scoped token (legacy fallback - only if userDid unavailable)
606
+ if (!finalToken && !userDid) {
607
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
608
+ const token = await this.delegationStorage.get(agentKey, "text");
609
+ finalToken = token || undefined; // Convert null to undefined
610
+ }
611
+ if (finalToken) {
612
+ session.delegationToken = finalToken;
613
+ console.log("[CloudflareMCPServer] ✅ Final KV lookup succeeded, token set on session:", {
614
+ toolName,
615
+ agentDid: agentDid.slice(0, 20) + "...",
616
+ userDid: userDid?.slice(0, 20) + "..." || "none",
617
+ tokenLength: finalToken.length,
618
+ source: userDid
619
+ ? session.id
620
+ ? "session-scoped"
621
+ : "user+agent-scoped"
622
+ : "agent-scoped (legacy)",
623
+ });
624
+ }
625
+ }
626
+ catch (error) {
627
+ console.error("[CloudflareMCPServer] Final KV lookup failed:", error);
628
+ }
629
+ }
630
+ console.log("[CloudflareMCPServer] ✅ About to call processToolCall with session:", {
631
+ toolName,
632
+ sessionId: session?.id?.slice(0, 20) + "..." || "none",
633
+ hasDelegationToken: !!session?.delegationToken,
634
+ delegationTokenLength: session?.delegationToken?.length || 0,
635
+ hasConsentProof: !!session?.consentProof,
636
+ agentDid: session?.agentDid?.slice(0, 20) + "..." || "none",
637
+ });
210
638
  // ✅ Use processToolCall which handles delegation checks AND proof generation
211
639
  // This ensures delegation is checked BEFORE tool execution
212
640
  // If delegation is required but not provided, this will throw DelegationRequiredError
@@ -257,31 +685,27 @@ class CloudflareMCPServer {
257
685
  normalizedClientInfo.capabilities;
258
686
  }
259
687
  }
260
- // Phase 4 PR #5: Extract OAuth identity and lookup persistent User DID BEFORE handshake
261
- let userDid = undefined;
262
- if (this.delegationStorage && meta?.request) {
263
- const oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
688
+ // Phase 4 PR #5: Extract OAuth identity BEFORE handshake and pass to runtime
689
+ let oauthIdentity = undefined;
690
+ if (meta?.request) {
691
+ oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
264
692
  if (oauthIdentity) {
265
- try {
266
- const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
267
- const mappedUserDid = await this.delegationStorage.get(oauthKey, "text");
268
- if (mappedUserDid) {
269
- userDid = mappedUserDid;
270
- console.log("[Adapter] Found persistent User DID for return user:", {
271
- provider: oauthIdentity.provider,
272
- userDid: userDid.substring(0, 20) + "...",
273
- });
274
- }
275
- }
276
- catch (error) {
277
- console.warn("[Adapter] Failed to lookup persistent User DID:", error);
278
- // Non-fatal - continue with handshake
279
- }
693
+ console.log("[Adapter] Extracted OAuth identity for handshake:", {
694
+ provider: oauthIdentity.provider,
695
+ subject: oauthIdentity.subject.substring(0, 20) + "...",
696
+ });
697
+ // Add OAuth identity to handshake payload for persistent user DID lookup
698
+ // Type assertion needed because HandshakeRequest doesn't include oauthIdentity,
699
+ // but handleHandshake accepts it via intersection type
700
+ handshakePayload.oauthIdentity = oauthIdentity;
280
701
  }
281
702
  }
282
703
  const handshakeResult = await this.runtime.handleHandshake(handshakePayload);
283
- // Phase 4 PR #5: Store User DID and clientId in session AFTER handshake
284
- if (userDid && this.delegationStorage && handshakeResult.sessionId) {
704
+ // Get userDid from handshake result (may have been retrieved via OAuth mapping)
705
+ const userDid = handshakeResult.userDid;
706
+ // Phase 4 PR #5: Store User DID, OAuth identity (with redacted subject), and clientId in session AFTER handshake
707
+ // Only store if userDid is available (from OAuth mapping or handshake result)
708
+ if (this.delegationStorage && handshakeResult.sessionId && userDid) {
285
709
  try {
286
710
  const sessionKey = STORAGE_KEYS.session(handshakeResult.sessionId);
287
711
  const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
@@ -289,16 +713,31 @@ class CloudflareMCPServer {
289
713
  const clientId = handshakePayload.clientInfo?.clientId;
290
714
  // Get agentDid from runtime identity
291
715
  const agentDid = (await this.runtime.getIdentity()).did;
716
+ // Store OAuth identity with redacted subject for PII protection
717
+ const oauthIdentityForStorage = oauthIdentity
718
+ ? {
719
+ provider: oauthIdentity.provider,
720
+ subjectHash: oauthIdentity.subject.substring(0, 8), // Redact full subject
721
+ // Don't store email, name, or full subject for PII protection
722
+ }
723
+ : undefined;
292
724
  await this.delegationStorage.put(sessionKey, JSON.stringify({
293
725
  ...(existingSession || {}),
294
726
  userDid,
295
727
  agentDid,
296
728
  ...(clientId && { clientId }),
729
+ ...(oauthIdentityForStorage && {
730
+ oauthIdentity: oauthIdentityForStorage,
731
+ }),
297
732
  }), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
298
- console.log("[Adapter] Stored User DID, agentDid, and clientId in session for return user");
733
+ console.log("[Adapter] Stored User DID, agentDid, clientId, and OAuth identity (redacted) in session", {
734
+ hasUserDid: !!userDid,
735
+ hasOAuth: !!oauthIdentity,
736
+ provider: oauthIdentity?.provider,
737
+ });
299
738
  }
300
739
  catch (error) {
301
- console.warn("[Adapter] Failed to store User DID in session:", error);
740
+ console.warn("[Adapter] Failed to store session data:", error);
302
741
  // Non-fatal - continue
303
742
  }
304
743
  }
@@ -322,19 +761,41 @@ class CloudflareMCPServer {
322
761
  error: {
323
762
  code: -32001, // Custom error code for authorization required
324
763
  message: error.consentUrl
325
- ? `${error.message || `Delegation required for tool "${error.toolName || "unknown"}"`}. Authorization URL: ${error.consentUrl} <authorization_url>${error.consentUrl}</authorization_url>`
764
+ ? `${error.message || `Delegation required for tool "${error.toolName || "unknown"}"`}. <authorization_url>${error.consentUrl}</authorization_url>`
326
765
  : error.message ||
327
766
  `Delegation required for tool "${error.toolName || "unknown"}"`,
328
767
  data: {
329
768
  toolName: error.toolName,
330
769
  requiredScopes: error.requiredScopes || [],
331
- consentUrl: error.consentUrl, // Primary location - clients should check here first
332
- authorizationUrl: error.consentUrl, // Compatibility alias
770
+ consentUrl: error.consentUrl,
333
771
  resumeToken: error.resumeToken, // Include resume token for resumption
334
- // Helpful message for clients to display to users
335
- message: error.consentUrl
336
- ? `Please visit ${error.consentUrl} to authorize this tool`
337
- : undefined,
772
+ // MCP-I specific: provide authorization URL for client to display
773
+ authorizationUrl: error.consentUrl,
774
+ },
775
+ },
776
+ };
777
+ }
778
+ // Check if this is an OAuthRequiredError - include OAuth URL in response
779
+ if (error &&
780
+ (error.name === "OAuthRequiredError" ||
781
+ error.constructor?.name === "OAuthRequiredError")) {
782
+ return {
783
+ jsonrpc: "2.0",
784
+ id,
785
+ error: {
786
+ code: -32002, // Custom error code for OAuth required
787
+ message: error.oauthUrl
788
+ ? `${error.message || `OAuth required for tool "${error.toolName || "unknown"}"`}. <authorization_url>${error.oauthUrl}</authorization_url>`
789
+ : error.message ||
790
+ `OAuth required for tool "${error.toolName || "unknown"}"`,
791
+ data: {
792
+ toolName: error.toolName,
793
+ requiredScopes: error.requiredScopes || [],
794
+ provider: error.provider,
795
+ oauthUrl: error.oauthUrl,
796
+ resumeToken: error.resumeToken,
797
+ // MCP-I specific: provide authorization URL for client to display
798
+ authorizationUrl: error.oauthUrl,
338
799
  },
339
800
  },
340
801
  };
@@ -530,16 +991,131 @@ class CloudflareMCPServer {
530
991
  return null;
531
992
  const cookieValue = oauthCookie.substring(equalsIndex + 1);
532
993
  const parsed = JSON.parse(decodeURIComponent(cookieValue));
533
- // Validate required fields
534
- if (parsed && parsed.provider && parsed.subject) {
535
- return parsed;
994
+ // ✅ SECURITY: Validate OAuth identity format and content
995
+ const validationResult = this.validateOAuthIdentity(parsed);
996
+ if (!validationResult.valid) {
997
+ console.warn("[Adapter] ⚠️ OAuth identity validation failed:", validationResult.reason, { parsed });
998
+ return null;
536
999
  }
1000
+ return parsed;
537
1001
  }
538
1002
  catch (error) {
539
1003
  console.warn("[Adapter] Failed to extract OAuth identity from cookies:", error);
540
1004
  }
541
1005
  return null;
542
1006
  }
1007
+ /**
1008
+ * Validate OAuth identity format and content
1009
+ *
1010
+ * Ensures:
1011
+ * - Provider is non-empty string (1-50 chars)
1012
+ * - Subject is non-empty string (1-255 chars)
1013
+ * - Provider matches expected format (alphanumeric, hyphens, underscores)
1014
+ * - Subject matches expected format (non-empty, reasonable length)
1015
+ *
1016
+ * @param identity - Parsed OAuth identity object
1017
+ * @returns Validation result
1018
+ */
1019
+ validateOAuthIdentity(identity) {
1020
+ // Check if identity is an object
1021
+ if (!identity || typeof identity !== "object") {
1022
+ return { valid: false, reason: "OAuth identity must be an object" };
1023
+ }
1024
+ const oauth = identity;
1025
+ // Validate provider
1026
+ if (!oauth.provider || typeof oauth.provider !== "string") {
1027
+ return {
1028
+ valid: false,
1029
+ reason: "OAuth provider is required and must be a string",
1030
+ };
1031
+ }
1032
+ const provider = oauth.provider.trim();
1033
+ if (provider.length === 0) {
1034
+ return { valid: false, reason: "OAuth provider cannot be empty" };
1035
+ }
1036
+ if (provider.length > 50) {
1037
+ return {
1038
+ valid: false,
1039
+ reason: "OAuth provider must be 50 characters or less",
1040
+ };
1041
+ }
1042
+ // Provider format: alphanumeric, hyphens, underscores, dots (e.g., "google", "microsoft", "github", "custom-provider")
1043
+ const providerPattern = /^[a-zA-Z0-9._-]+$/;
1044
+ if (!providerPattern.test(provider)) {
1045
+ return {
1046
+ valid: false,
1047
+ reason: `OAuth provider must match pattern [a-zA-Z0-9._-]: "${provider}"`,
1048
+ };
1049
+ }
1050
+ // Validate subject
1051
+ if (!oauth.subject || typeof oauth.subject !== "string") {
1052
+ return {
1053
+ valid: false,
1054
+ reason: "OAuth subject is required and must be a string",
1055
+ };
1056
+ }
1057
+ const subject = oauth.subject.trim();
1058
+ if (subject.length === 0) {
1059
+ return { valid: false, reason: "OAuth subject cannot be empty" };
1060
+ }
1061
+ if (subject.length > 255) {
1062
+ return {
1063
+ valid: false,
1064
+ reason: "OAuth subject must be 255 characters or less",
1065
+ };
1066
+ }
1067
+ // Subject format: non-empty, reasonable characters (allows most Unicode, but prevents control chars)
1068
+ // OAuth subjects can be numeric IDs, email-like strings, or other identifiers
1069
+ const subjectPattern = /^[\S]+$/; // At least one non-whitespace character
1070
+ if (!subjectPattern.test(subject)) {
1071
+ return {
1072
+ valid: false,
1073
+ reason: `OAuth subject contains invalid characters: "${subject.substring(0, 20)}..."`,
1074
+ };
1075
+ }
1076
+ // Validate optional email if present
1077
+ if (oauth.email !== undefined) {
1078
+ if (typeof oauth.email !== "string") {
1079
+ return {
1080
+ valid: false,
1081
+ reason: "OAuth email must be a string if provided",
1082
+ };
1083
+ }
1084
+ const email = oauth.email.trim();
1085
+ if (email.length > 0) {
1086
+ // Basic email format validation
1087
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1088
+ if (!emailPattern.test(email)) {
1089
+ return {
1090
+ valid: false,
1091
+ reason: `OAuth email format invalid: "${email}"`,
1092
+ };
1093
+ }
1094
+ if (email.length > 255) {
1095
+ return {
1096
+ valid: false,
1097
+ reason: "OAuth email must be 255 characters or less",
1098
+ };
1099
+ }
1100
+ }
1101
+ }
1102
+ // Validate optional name if present
1103
+ if (oauth.name !== undefined) {
1104
+ if (typeof oauth.name !== "string") {
1105
+ return {
1106
+ valid: false,
1107
+ reason: "OAuth name must be a string if provided",
1108
+ };
1109
+ }
1110
+ if (oauth.name.length > 255) {
1111
+ return {
1112
+ valid: false,
1113
+ reason: "OAuth name must be 255 characters or less",
1114
+ };
1115
+ }
1116
+ }
1117
+ return { valid: true };
1118
+ }
543
1119
  }
544
1120
  function buildRequestMeta(request) {
545
1121
  const ip = request.headers.get("cf-connecting-ip") ??
@@ -549,10 +1125,6 @@ function buildRequestMeta(request) {
549
1125
  const cfRay = request.headers.get("cf-ray") ?? undefined;
550
1126
  const origin = request.headers.get("origin") ?? undefined;
551
1127
  const secChUa = request.headers.get("sec-ch-ua") ?? undefined;
552
- // Extract server origin from custom header (set by createMCPIApp wrapper)
553
- // Falls back to request URL origin if header not present
554
- const serverOrigin = request.headers.get("X-MCP-Server-Origin") ??
555
- (request.url ? new URL(request.url).origin : undefined);
556
1128
  const fingerprintParts = [ip, userAgent, cfRay, origin, secChUa].filter((value) => typeof value === "string" && value.length > 0);
557
1129
  return {
558
1130
  fingerprint: fingerprintParts.length > 0 ? fingerprintParts.join("|") : undefined,
@@ -560,8 +1132,6 @@ function buildRequestMeta(request) {
560
1132
  userAgent,
561
1133
  cfRay,
562
1134
  request, // Include request for cookie access
563
- // Store server origin in meta for session creation
564
- serverOrigin: serverOrigin, // Add to RequestMeta interface
565
1135
  };
566
1136
  }
567
1137
  /**
@@ -570,25 +1140,39 @@ function buildRequestMeta(request) {
570
1140
  *
571
1141
  * Supports SSE (Server-Sent Events) and HTTP JSON-RPC transports for compatibility
572
1142
  * with Claude Desktop, Cursor, MCP Inspector, and other MCP clients.
1143
+ *
1144
+ * Automatically handles prefixed KV bindings via `envPrefix` parameter or auto-detection.
573
1145
  */
574
1146
  export function createMCPICloudflareAdapter(config) {
575
- // Create the runtime with Cloudflare providers
576
- const runtime = createCloudflareRuntime(config);
1147
+ // Normalize environment to handle prefixed KV bindings
1148
+ // This ensures consistent KV access regardless of prefix usage
1149
+ const mappedEnv = normalizeCloudflareEnv(config.env, config.envPrefix);
1150
+ // Create the runtime with normalized environment
1151
+ const runtime = createCloudflareRuntime({
1152
+ ...config,
1153
+ env: mappedEnv,
1154
+ });
577
1155
  // Server info
578
1156
  const serverInfo = config.serverInfo || {
579
1157
  name: "MCP-I Cloudflare Server",
580
1158
  version: "1.0.0",
581
1159
  };
582
1160
  // Initialize proof archive if PROOF_ARCHIVE KV is available
583
- const env = config.env;
584
- const proofArchive = env.PROOF_ARCHIVE
585
- ? new KVProofArchive(env.PROOF_ARCHIVE)
1161
+ const proofArchive = mappedEnv.PROOF_ARCHIVE
1162
+ ? new KVProofArchive(mappedEnv.PROOF_ARCHIVE)
586
1163
  : undefined;
587
- // Get delegation storage from env if available
588
- const delegationStorage = env.DELEGATION_STORAGE;
1164
+ // Get delegation storage from normalized env if available
1165
+ const delegationStorage = mappedEnv.DELEGATION_STORAGE;
1166
+ // Initialize AdminService (if API key is present)
1167
+ // This enables cache clearing and other admin operations
1168
+ let adminService;
1169
+ if (mappedEnv.AGENTSHIELD_API_KEY) {
1170
+ adminService = new AdminService(mappedEnv);
1171
+ }
589
1172
  // Create lightweight MCP server
590
1173
  const server = new CloudflareMCPServer(runtime, serverInfo, config.tools || [], proofArchive, delegationStorage);
591
1174
  // Return fetch handler
1175
+ // Note: mappedEnv is captured in closure for admin endpoints
592
1176
  return {
593
1177
  server,
594
1178
  runtime,
@@ -654,19 +1238,6 @@ export function createMCPICloudflareAdapter(config) {
654
1238
  }
655
1239
  }
656
1240
  // Main MCP protocol endpoint - supports both POST and SSE
657
- // Handle DELETE requests first (session termination) - can be /mcp or /mcp/{sessionId}
658
- if (request.method === "DELETE" && url.pathname.startsWith("/mcp")) {
659
- // MCP Inspector and Streamable HTTP clients send DELETE to terminate sessions
660
- // Path might be /mcp or /mcp/{sessionId} - handle both
661
- // Return 200 OK with empty body (some environments don't support 204 well)
662
- return new Response("", {
663
- status: 200,
664
- headers: {
665
- "Content-Length": "0",
666
- ...MCP_CORS_HEADERS,
667
- },
668
- });
669
- }
670
1241
  if (url.pathname === "/mcp") {
671
1242
  // Handle SSE streaming (for Claude Desktop via mcp-remote)
672
1243
  if (request.method === "GET") {
@@ -683,7 +1254,19 @@ export function createMCPICloudflareAdapter(config) {
683
1254
  status: 405,
684
1255
  headers: {
685
1256
  "Content-Type": "text/plain",
686
- Allow: "POST, GET, DELETE",
1257
+ Allow: "POST, GET",
1258
+ ...MCP_CORS_HEADERS,
1259
+ },
1260
+ });
1261
+ }
1262
+ // Handle DELETE requests (session termination)
1263
+ if (request.method === "DELETE") {
1264
+ // MCP Inspector and clients may send DELETE to terminate sessions
1265
+ // Return 200 OK with empty body (some environments don't support 204 well)
1266
+ return new Response("", {
1267
+ status: 200,
1268
+ headers: {
1269
+ "Content-Length": "0",
687
1270
  ...MCP_CORS_HEADERS,
688
1271
  },
689
1272
  });
@@ -771,6 +1354,20 @@ export function createMCPICloudflareAdapter(config) {
771
1354
  }
772
1355
  // Admin endpoints for viewing nonces and proofs
773
1356
  if (url.pathname.startsWith("/admin/")) {
1357
+ // Handle clear-cache (POST) - Delegate to AdminService
1358
+ if (url.pathname === "/admin/clear-cache" &&
1359
+ request.method === "POST") {
1360
+ if (adminService) {
1361
+ return adminService.handle(request);
1362
+ }
1363
+ return new Response(JSON.stringify({
1364
+ success: false,
1365
+ error: "Admin service not initialized (missing AGENTSHIELD_API_KEY)",
1366
+ }), {
1367
+ status: 403,
1368
+ headers: { "Content-Type": "application/json" },
1369
+ });
1370
+ }
774
1371
  // Only allow in development or if explicitly configured
775
1372
  if (config.environment === "production") {
776
1373
  return new Response(JSON.stringify({
@@ -780,15 +1377,18 @@ export function createMCPICloudflareAdapter(config) {
780
1377
  headers: { "Content-Type": "application/json" },
781
1378
  });
782
1379
  }
783
- const env = config.env;
1380
+ // Use normalized environment (handles prefixed KV bindings)
1381
+ // mappedEnv is already normalized in createMCPICloudflareAdapter
784
1382
  // GET /admin/nonces - List active nonces
785
1383
  if (url.pathname === "/admin/nonces") {
786
1384
  try {
787
1385
  // Use KV list to get nonce keys
788
- const noncesList = await env.NONCE_CACHE.list({ prefix: "nonce:" });
1386
+ const noncesList = await mappedEnv.NONCE_CACHE.list({
1387
+ prefix: "nonce:",
1388
+ });
789
1389
  const nonces = [];
790
1390
  for (const key of noncesList.keys) {
791
- const value = await env.NONCE_CACHE.get(key.name);
1391
+ const value = await mappedEnv.NONCE_CACHE.get(key.name);
792
1392
  if (value) {
793
1393
  nonces.push({
794
1394
  nonce: key.name.replace("nonce:", ""),
@@ -814,8 +1414,8 @@ export function createMCPICloudflareAdapter(config) {
814
1414
  }
815
1415
  }
816
1416
  // Initialize proof archive if available
817
- const proofArchive = env.PROOF_ARCHIVE
818
- ? new KVProofArchive(env.PROOF_ARCHIVE)
1417
+ const proofArchive = mappedEnv.PROOF_ARCHIVE
1418
+ ? new KVProofArchive(mappedEnv.PROOF_ARCHIVE)
819
1419
  : null;
820
1420
  if (!proofArchive) {
821
1421
  return new Response(JSON.stringify({
@@ -888,7 +1488,7 @@ export function createMCPICloudflareAdapter(config) {
888
1488
  if (url.pathname === "/admin/stats") {
889
1489
  try {
890
1490
  const [noncesList, proofStats] = await Promise.all([
891
- env.NONCE_CACHE.list({ prefix: "nonce:" }),
1491
+ mappedEnv.NONCE_CACHE.list({ prefix: "nonce:" }),
892
1492
  proofArchive.getStats(),
893
1493
  ]);
894
1494
  return new Response(JSON.stringify({