@kya-os/mcp-i-cloudflare 1.5.8-canary.4 → 1.5.8-canary.40
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.
- package/README.md +130 -0
- package/dist/__tests__/e2e/test-config.d.ts +37 -0
- package/dist/__tests__/e2e/test-config.d.ts.map +1 -0
- package/dist/__tests__/e2e/test-config.js +62 -0
- package/dist/__tests__/e2e/test-config.js.map +1 -0
- package/dist/adapter.d.ts +44 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +655 -87
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts +8 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +114 -5
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +19 -3
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -4
- package/dist/config.js.map +1 -1
- package/dist/helpers/env-mapper.d.ts +60 -1
- package/dist/helpers/env-mapper.d.ts.map +1 -1
- package/dist/helpers/env-mapper.js +136 -6
- package/dist/helpers/env-mapper.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +96 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +276 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/oauth-handler.d.ts +5 -0
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +152 -35
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/runtime.d.ts +12 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +34 -4
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -1
- package/dist/server.js.map +1 -1
- package/dist/services/admin.service.d.ts.map +1 -1
- package/dist/services/admin.service.js +15 -1
- package/dist/services/admin.service.js.map +1 -1
- package/dist/services/consent-audit.service.d.ts +91 -0
- package/dist/services/consent-audit.service.d.ts.map +1 -0
- package/dist/services/consent-audit.service.js +243 -0
- package/dist/services/consent-audit.service.js.map +1 -0
- package/dist/services/consent-config.service.d.ts +2 -2
- package/dist/services/consent-config.service.d.ts.map +1 -1
- package/dist/services/consent-config.service.js +55 -24
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent.service.d.ts +49 -1
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +1491 -28
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +67 -29
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/services/proof.service.d.ts +5 -3
- package/dist/services/proof.service.d.ts.map +1 -1
- package/dist/services/proof.service.js +35 -8
- package/dist/services/proof.service.js.map +1 -1
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -9
package/dist/adapter.js
CHANGED
|
@@ -13,6 +13,7 @@ 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";
|
|
16
17
|
const INITIALIZE_CONTEXT_TTL_MS = 60_000;
|
|
17
18
|
/**
|
|
18
19
|
* Lightweight MCP protocol implementation for Cloudflare Workers
|
|
@@ -77,60 +78,145 @@ class CloudflareMCPServer {
|
|
|
77
78
|
}
|
|
78
79
|
// Call tool with identity/proof wrapping
|
|
79
80
|
if (method === "tools/call") {
|
|
81
|
+
// Entry point logging for debugging
|
|
82
|
+
console.log("🔵 [CloudflareMCPServer] handleRequest: tools/call", {
|
|
83
|
+
toolName: params.name,
|
|
84
|
+
hasParams: !!params,
|
|
85
|
+
paramsKeys: params ? Object.keys(params) : [],
|
|
86
|
+
});
|
|
80
87
|
const toolName = params.name;
|
|
81
88
|
const tool = this.tools.get(toolName);
|
|
82
89
|
if (!tool) {
|
|
83
90
|
throw new Error(`Tool not found: ${toolName}`);
|
|
84
91
|
}
|
|
85
92
|
// Get current session if available (stateful environments like Node.js)
|
|
86
|
-
|
|
93
|
+
// Wrap in try-catch to catch any exceptions
|
|
94
|
+
let session = null;
|
|
95
|
+
try {
|
|
96
|
+
session = await this.runtime.getCurrentSession();
|
|
97
|
+
console.log("[CloudflareMCPServer] Session check:", {
|
|
98
|
+
hasSession: !!session,
|
|
99
|
+
sessionId: session?.id?.slice(0, 20) + "..." || "none",
|
|
100
|
+
hasDelegationToken: !!session?.delegationToken,
|
|
101
|
+
hasDelegationStorage: !!this.delegationStorage,
|
|
102
|
+
method: "tools/call",
|
|
103
|
+
toolName,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error("[CloudflareMCPServer] Error in getCurrentSession:", error);
|
|
108
|
+
// Continue with null session (will create ephemeral)
|
|
109
|
+
}
|
|
87
110
|
// For stateless environments (Cloudflare Workers), create ephemeral session
|
|
88
111
|
if (!session) {
|
|
112
|
+
// Extract session_id from params if available (from consent flow)
|
|
113
|
+
// This allows us to retrieve delegation tokens stored with the original session_id
|
|
114
|
+
const providedSessionId = params.session_id || params.sessionId;
|
|
89
115
|
const timestamp = Date.now();
|
|
90
116
|
const randomSuffix = Math.random().toString(36).substring(2, 10);
|
|
91
117
|
const agentDid = (await this.runtime.getIdentity()).did;
|
|
92
|
-
|
|
118
|
+
// Use provided session_id if available, otherwise create ephemeral
|
|
119
|
+
const sessionId = providedSessionId || `ephemeral-${timestamp}-${randomSuffix}`;
|
|
120
|
+
console.log("[CloudflareMCPServer] Creating session:", {
|
|
121
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
122
|
+
isProvided: !!providedSessionId,
|
|
123
|
+
isEphemeral: !providedSessionId,
|
|
124
|
+
});
|
|
125
|
+
// Extract OAuth identity from request or session cache for persistent userDid lookup
|
|
126
|
+
let oauthIdentity = undefined;
|
|
127
|
+
if (meta?.request) {
|
|
128
|
+
oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
|
|
129
|
+
if (oauthIdentity) {
|
|
130
|
+
console.log("[CloudflareMCPServer] Extracted OAuth identity from request:", {
|
|
131
|
+
provider: oauthIdentity.provider,
|
|
132
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// If OAuth identity not in request, try to get from session cache
|
|
137
|
+
if (!oauthIdentity && sessionId && this.delegationStorage) {
|
|
138
|
+
try {
|
|
139
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
140
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
141
|
+
if (sessionData?.oauthIdentity?.provider) {
|
|
142
|
+
// Note: We only have subjectHash in session cache (PII protection)
|
|
143
|
+
// We can't fully reconstruct OAuth identity, but we can use it to lookup userDid
|
|
144
|
+
console.log("[CloudflareMCPServer] Found OAuth identity in session cache:", {
|
|
145
|
+
provider: sessionData.oauthIdentity.provider,
|
|
146
|
+
hasSubjectHash: !!sessionData.oauthIdentity.subjectHash,
|
|
147
|
+
});
|
|
148
|
+
// OAuth identity from session cache is incomplete (subjectHash only), so we'll use it for userDid lookup via storage
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.warn("[CloudflareMCPServer] Failed to get OAuth identity from session:", error);
|
|
153
|
+
// Non-fatal - continue without OAuth identity
|
|
154
|
+
}
|
|
155
|
+
}
|
|
93
156
|
// Check KV storage for stored delegation token if not provided in params
|
|
94
157
|
let delegationToken = params.delegationToken;
|
|
95
158
|
if (!delegationToken && this.delegationStorage) {
|
|
96
159
|
try {
|
|
97
|
-
// Get userDID from params
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (agentToken) {
|
|
116
|
-
delegationToken = agentToken;
|
|
117
|
-
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key:", {
|
|
118
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
119
|
-
key: agentKey,
|
|
120
|
-
tokenLength: agentToken.length,
|
|
121
|
-
});
|
|
122
|
-
// Cache it for this session for faster future lookups
|
|
123
|
-
if (sessionId) {
|
|
124
|
-
await this.delegationStorage.put(`session:${sessionId}`, agentToken, {
|
|
125
|
-
expirationTtl: 1800, // 30 minutes
|
|
126
|
-
});
|
|
160
|
+
// Get userDID from params first
|
|
161
|
+
let userDid = params.clientDid || params.userDid;
|
|
162
|
+
// ✅ PRIORITY 1: If OAuth identity available, use it to lookup persistent userDid
|
|
163
|
+
if (!userDid && oauthIdentity && this.delegationStorage) {
|
|
164
|
+
try {
|
|
165
|
+
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
166
|
+
const mappedUserDid = await this.delegationStorage.get(oauthKey, "text");
|
|
167
|
+
if (mappedUserDid) {
|
|
168
|
+
userDid = mappedUserDid;
|
|
169
|
+
console.log("[CloudflareMCPServer] ✅ Retrieved persistent userDid from OAuth mapping:", {
|
|
170
|
+
provider: oauthIdentity.provider,
|
|
171
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.warn("[CloudflareMCPServer] Failed to lookup userDid from OAuth mapping:", error);
|
|
177
|
+
// Non-fatal - continue with session cache lookup
|
|
127
178
|
}
|
|
128
179
|
}
|
|
129
|
-
// PRIORITY 2:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
// ✅ PRIORITY 2: If userDid not in params or OAuth mapping, try to retrieve from session cache
|
|
181
|
+
if (!userDid && sessionId) {
|
|
182
|
+
try {
|
|
183
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
184
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
185
|
+
if (sessionData?.userDid) {
|
|
186
|
+
userDid = sessionData.userDid;
|
|
187
|
+
console.log("[CloudflareMCPServer] ✅ Retrieved userDid from session cache:", {
|
|
188
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
189
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.warn("[CloudflareMCPServer] Failed to get userDid from session:", error);
|
|
195
|
+
// Non-fatal - continue without userDid
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
console.log("[CloudflareMCPServer] 🔍 Starting delegation token lookup from KV:", {
|
|
199
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
200
|
+
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
201
|
+
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
202
|
+
hasDelegationStorage: !!this.delegationStorage,
|
|
203
|
+
});
|
|
204
|
+
// PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
|
|
205
|
+
// This is the preferred approach for multi-user scenarios with proper user isolation
|
|
206
|
+
if (userDid) {
|
|
207
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
208
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key:", {
|
|
209
|
+
key: userAgentKey,
|
|
210
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
211
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
212
|
+
});
|
|
213
|
+
const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
|
|
214
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
|
|
215
|
+
key: userAgentKey,
|
|
216
|
+
found: !!userAgentToken,
|
|
217
|
+
tokenLength: userAgentToken?.length || 0,
|
|
218
|
+
tokenPreview: userAgentToken ? userAgentToken.substring(0, 20) + "..." : null,
|
|
219
|
+
});
|
|
134
220
|
if (userAgentToken) {
|
|
135
221
|
delegationToken = userAgentToken;
|
|
136
222
|
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from user+agent scoped key:", {
|
|
@@ -140,19 +226,40 @@ class CloudflareMCPServer {
|
|
|
140
226
|
tokenLength: userAgentToken.length,
|
|
141
227
|
});
|
|
142
228
|
// Cache it for this session for faster future lookups
|
|
229
|
+
// Store full session data object to match consent service format
|
|
143
230
|
if (sessionId) {
|
|
144
|
-
|
|
231
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
232
|
+
// Read existing session data to preserve userDid and other fields
|
|
233
|
+
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
234
|
+
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
235
|
+
...existingSession,
|
|
236
|
+
userDid,
|
|
237
|
+
agentDid,
|
|
238
|
+
delegationToken: userAgentToken,
|
|
239
|
+
cachedAt: Date.now(),
|
|
240
|
+
}), {
|
|
145
241
|
expirationTtl: 1800, // 30 minutes
|
|
146
242
|
});
|
|
147
243
|
}
|
|
148
244
|
}
|
|
149
245
|
}
|
|
150
|
-
// PRIORITY
|
|
246
|
+
// PRIORITY 2: Try session-scoped token (if session ID was persisted)
|
|
151
247
|
// Note: Sessions are ephemeral in Cloudflare Workers, so this is mainly
|
|
152
248
|
// useful for caching tokens within a single request chain
|
|
153
249
|
if (!delegationToken && sessionId) {
|
|
154
250
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
155
|
-
|
|
251
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key:", {
|
|
252
|
+
key: sessionKey,
|
|
253
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
254
|
+
});
|
|
255
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
256
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
|
|
257
|
+
key: sessionKey,
|
|
258
|
+
found: !!sessionData,
|
|
259
|
+
hasDelegationToken: !!sessionData?.delegationToken,
|
|
260
|
+
tokenLength: sessionData?.delegationToken?.length || 0,
|
|
261
|
+
tokenPreview: sessionData?.delegationToken ? sessionData.delegationToken.substring(0, 20) + "..." : null,
|
|
262
|
+
});
|
|
156
263
|
if (sessionData?.delegationToken) {
|
|
157
264
|
delegationToken = sessionData.delegationToken;
|
|
158
265
|
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from session cache:", {
|
|
@@ -161,16 +268,71 @@ class CloudflareMCPServer {
|
|
|
161
268
|
});
|
|
162
269
|
}
|
|
163
270
|
}
|
|
271
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
272
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
273
|
+
// WARNING: This allows cross-user delegation sharing - migrate to user+agent scoped tokens
|
|
274
|
+
if (!delegationToken && !userDid) {
|
|
275
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
276
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
277
|
+
key: agentKey,
|
|
278
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
279
|
+
reason: "userDid unavailable",
|
|
280
|
+
});
|
|
281
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy):", {
|
|
282
|
+
key: agentKey,
|
|
283
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
284
|
+
});
|
|
285
|
+
const agentToken = await this.delegationStorage.get(agentKey, "text");
|
|
286
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
|
|
287
|
+
key: agentKey,
|
|
288
|
+
found: !!agentToken,
|
|
289
|
+
tokenLength: agentToken?.length || 0,
|
|
290
|
+
tokenPreview: agentToken ? agentToken.substring(0, 20) + "..." : null,
|
|
291
|
+
});
|
|
292
|
+
if (agentToken) {
|
|
293
|
+
delegationToken = agentToken;
|
|
294
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy):", {
|
|
295
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
296
|
+
key: agentKey,
|
|
297
|
+
tokenLength: agentToken.length,
|
|
298
|
+
});
|
|
299
|
+
// Cache it for this session for faster future lookups
|
|
300
|
+
// Store full session data object to match consent service format
|
|
301
|
+
if (sessionId) {
|
|
302
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
303
|
+
// Read existing session data to preserve userDid and other fields
|
|
304
|
+
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
305
|
+
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
306
|
+
...existingSession,
|
|
307
|
+
userDid,
|
|
308
|
+
agentDid,
|
|
309
|
+
delegationToken: agentToken,
|
|
310
|
+
cachedAt: Date.now(),
|
|
311
|
+
}), {
|
|
312
|
+
expirationTtl: 1800, // 30 minutes
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
164
317
|
if (!delegationToken) {
|
|
318
|
+
const checkedKeys = [];
|
|
319
|
+
// Only include agent-scoped key if it was checked (when userDid unavailable)
|
|
320
|
+
if (!userDid) {
|
|
321
|
+
checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
|
|
322
|
+
}
|
|
323
|
+
// Always include user+agent scoped key if userDid is available
|
|
324
|
+
if (userDid) {
|
|
325
|
+
checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
|
|
326
|
+
}
|
|
327
|
+
// Always include session-scoped key if sessionId is available
|
|
328
|
+
if (sessionId) {
|
|
329
|
+
checkedKeys.push(STORAGE_KEYS.session(sessionId));
|
|
330
|
+
}
|
|
165
331
|
console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage:", {
|
|
166
332
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
167
333
|
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
168
334
|
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
169
|
-
checkedKeys:
|
|
170
|
-
agentKey,
|
|
171
|
-
userDid ? `delegation:user:${userDid}:agent:${agentDid}` : null,
|
|
172
|
-
sessionId ? STORAGE_KEYS.session(sessionId) : null,
|
|
173
|
-
].filter(Boolean),
|
|
335
|
+
checkedKeys: checkedKeys.filter(Boolean),
|
|
174
336
|
});
|
|
175
337
|
}
|
|
176
338
|
}
|
|
@@ -193,9 +355,261 @@ class CloudflareMCPServer {
|
|
|
193
355
|
serverOrigin: this.requestOrigin,
|
|
194
356
|
// These fields are for ephemeral sessions - won't persist
|
|
195
357
|
};
|
|
358
|
+
// ✅ CRITICAL: Log delegation token status before processToolCall
|
|
359
|
+
console.log("[CloudflareMCPServer] ✅ Session created with delegation token status:", {
|
|
360
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
361
|
+
hasDelegationToken: !!delegationToken,
|
|
362
|
+
delegationTokenLength: delegationToken?.length || 0,
|
|
363
|
+
delegationTokenSource: params.delegationToken ? "params" : (delegationToken ? "kv-storage" : "none"),
|
|
364
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
365
|
+
});
|
|
196
366
|
}
|
|
197
367
|
else {
|
|
198
|
-
// For existing sessions,
|
|
368
|
+
// For existing sessions, check KV storage for delegation token if not already set
|
|
369
|
+
// This ensures newly created delegations are found even for existing sessions
|
|
370
|
+
console.log("[CloudflareMCPServer] Existing session detected, checking KV storage:", {
|
|
371
|
+
hasSession: !!session,
|
|
372
|
+
hasDelegationToken: !!session?.delegationToken,
|
|
373
|
+
hasDelegationStorage: !!this.delegationStorage,
|
|
374
|
+
sessionId: session?.id?.slice(0, 20) + "...",
|
|
375
|
+
sessionKeys: Object.keys(session || {}),
|
|
376
|
+
});
|
|
377
|
+
if (!session.delegationToken && this.delegationStorage) {
|
|
378
|
+
console.log("[CloudflareMCPServer] ⚠️ Session has no delegationToken, attempting KV lookup...");
|
|
379
|
+
try {
|
|
380
|
+
const agentDid = (await this.runtime.getIdentity()).did;
|
|
381
|
+
// ✅ CRITICAL: Retrieve userDid from session cache if not in session object
|
|
382
|
+
// Check session cache first, then params, then session object
|
|
383
|
+
let userDid = session.userDid || params.clientDid || params.userDid || session.clientDid;
|
|
384
|
+
const sessionId = session.id;
|
|
385
|
+
// If userDid still not found, try retrieving from session cache
|
|
386
|
+
if (!userDid && sessionId && this.delegationStorage) {
|
|
387
|
+
try {
|
|
388
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
389
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
390
|
+
if (sessionData?.userDid) {
|
|
391
|
+
userDid = sessionData.userDid;
|
|
392
|
+
// Update session object with retrieved userDid
|
|
393
|
+
session.userDid = userDid;
|
|
394
|
+
console.log("[CloudflareMCPServer] ✅ Retrieved userDid from session cache for delegation lookup:", {
|
|
395
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
396
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.warn("[CloudflareMCPServer] Failed to get userDid from session cache:", error);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
console.log("[CloudflareMCPServer] 🔍 Looking up delegation token for existing session:", {
|
|
405
|
+
sessionId: sessionId?.slice(0, 20) + "...",
|
|
406
|
+
userDid: userDid?.slice(0, 20) + "...",
|
|
407
|
+
agentDid: agentDid?.slice(0, 20) + "...",
|
|
408
|
+
});
|
|
409
|
+
// PRIORITY 1: Try user+agent scoped token (user-specific, most secure)
|
|
410
|
+
// This is the preferred approach for multi-user scenarios with proper user isolation
|
|
411
|
+
let delegationToken = undefined;
|
|
412
|
+
if (userDid) {
|
|
413
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
414
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: Checking user+agent scoped key (existing session):", {
|
|
415
|
+
key: userAgentKey,
|
|
416
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
417
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
418
|
+
});
|
|
419
|
+
const userAgentToken = await this.delegationStorage.get(userAgentKey, "text");
|
|
420
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
|
|
421
|
+
key: userAgentKey,
|
|
422
|
+
found: !!userAgentToken,
|
|
423
|
+
tokenLength: userAgentToken?.length || 0,
|
|
424
|
+
});
|
|
425
|
+
if (userAgentToken) {
|
|
426
|
+
delegationToken = userAgentToken;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// PRIORITY 2: Try session-scoped token (if session ID was persisted)
|
|
430
|
+
if (!delegationToken && sessionId) {
|
|
431
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
432
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Checking session-scoped key (existing session):", {
|
|
433
|
+
key: sessionKey,
|
|
434
|
+
sessionId: sessionId.slice(0, 20) + "...",
|
|
435
|
+
});
|
|
436
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
437
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 2: Session-scoped lookup result:", {
|
|
438
|
+
key: sessionKey,
|
|
439
|
+
found: !!sessionData,
|
|
440
|
+
hasDelegationToken: !!sessionData?.delegationToken,
|
|
441
|
+
tokenLength: sessionData?.delegationToken?.length || 0,
|
|
442
|
+
});
|
|
443
|
+
if (sessionData?.delegationToken) {
|
|
444
|
+
delegationToken = sessionData.delegationToken;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
448
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
449
|
+
if (!delegationToken && !userDid) {
|
|
450
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
451
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
452
|
+
key: agentKey,
|
|
453
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
454
|
+
reason: "userDid unavailable",
|
|
455
|
+
});
|
|
456
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Checking agent-scoped key (legacy, existing session):", {
|
|
457
|
+
key: agentKey,
|
|
458
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
const agentToken = await this.delegationStorage.get(agentKey, "text");
|
|
462
|
+
console.log("[CloudflareMCPServer] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
|
|
463
|
+
key: agentKey,
|
|
464
|
+
found: !!agentToken,
|
|
465
|
+
tokenLength: agentToken?.length || 0,
|
|
466
|
+
});
|
|
467
|
+
if (agentToken) {
|
|
468
|
+
delegationToken = agentToken;
|
|
469
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
|
|
470
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
471
|
+
key: agentKey,
|
|
472
|
+
tokenLength: agentToken.length,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
console.error("[CloudflareMCPServer] Error retrieving agent token:", error);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ✅ CRITICAL: Set delegation token on session if found
|
|
481
|
+
if (delegationToken) {
|
|
482
|
+
session = {
|
|
483
|
+
...session,
|
|
484
|
+
delegationToken,
|
|
485
|
+
};
|
|
486
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token set on existing session:", {
|
|
487
|
+
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
488
|
+
tokenLength: delegationToken.length,
|
|
489
|
+
sessionHasToken: !!session.delegationToken,
|
|
490
|
+
source: "kv-storage",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const checkedKeys = [];
|
|
495
|
+
// Only include agent-scoped key if it was checked (when userDid unavailable)
|
|
496
|
+
if (!userDid) {
|
|
497
|
+
checkedKeys.push(STORAGE_KEYS.legacyDelegation(agentDid));
|
|
498
|
+
}
|
|
499
|
+
// Always include user+agent scoped key if userDid is available
|
|
500
|
+
if (userDid) {
|
|
501
|
+
checkedKeys.push(STORAGE_KEYS.delegation(userDid, agentDid));
|
|
502
|
+
}
|
|
503
|
+
// Always include session-scoped key if sessionId is available
|
|
504
|
+
if (sessionId) {
|
|
505
|
+
checkedKeys.push(STORAGE_KEYS.session(sessionId));
|
|
506
|
+
}
|
|
507
|
+
console.log("[CloudflareMCPServer] ⚠️ No delegation token found in KV storage for existing session:", {
|
|
508
|
+
sessionId: sessionId?.slice(0, 20) + "..." || "none",
|
|
509
|
+
checkedKeys: checkedKeys.filter(Boolean),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// PRIORITY 2: Try user+agent scoped token (if userDID is available)
|
|
513
|
+
if (!session.delegationToken && userDid) {
|
|
514
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
515
|
+
console.log("[CloudflareMCPServer] PRIORITY 2: Looking up user+agent key:", {
|
|
516
|
+
userAgentKey,
|
|
517
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
518
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
519
|
+
});
|
|
520
|
+
try {
|
|
521
|
+
const userAgentToken = await this.delegationStorage.get(userAgentKey);
|
|
522
|
+
if (userAgentToken) {
|
|
523
|
+
session = {
|
|
524
|
+
...session,
|
|
525
|
+
delegationToken: userAgentToken,
|
|
526
|
+
};
|
|
527
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from user+agent scoped key (existing session):", {
|
|
528
|
+
userDid: userDid.slice(0, 20) + "...",
|
|
529
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
530
|
+
key: userAgentKey,
|
|
531
|
+
tokenLength: userAgentToken.length,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
console.log("[CloudflareMCPServer] ⚠️ No token found at user+agent key");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
console.error("[CloudflareMCPServer] Error retrieving user+agent token:", error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
console.log("[CloudflareMCPServer] ⚠️ Skipping PRIORITY 2:", {
|
|
544
|
+
hasToken: !!session.delegationToken,
|
|
545
|
+
hasUserDid: !!userDid,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
// PRIORITY 3: Try agent-scoped token (legacy fallback - DEPRECATED)
|
|
549
|
+
// Only use when userDid is unavailable (backward compatibility)
|
|
550
|
+
if (!session.delegationToken && !userDid) {
|
|
551
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
552
|
+
console.warn("[CloudflareMCPServer] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
|
|
553
|
+
key: agentKey,
|
|
554
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
555
|
+
reason: "userDid unavailable",
|
|
556
|
+
});
|
|
557
|
+
console.log("[CloudflareMCPServer] PRIORITY 3: Looking up agent key (legacy):", {
|
|
558
|
+
agentKey,
|
|
559
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
560
|
+
});
|
|
561
|
+
try {
|
|
562
|
+
const agentToken = await this.delegationStorage.get(agentKey);
|
|
563
|
+
if (agentToken) {
|
|
564
|
+
session = {
|
|
565
|
+
...session,
|
|
566
|
+
delegationToken: agentToken,
|
|
567
|
+
};
|
|
568
|
+
console.log("[CloudflareMCPServer] ✅ Delegation token retrieved from agent-scoped key (legacy, existing session):", {
|
|
569
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
570
|
+
key: agentKey,
|
|
571
|
+
tokenLength: agentToken.length,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
console.log("[CloudflareMCPServer] ⚠️ No token found at agent key");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
console.error("[CloudflareMCPServer] Error retrieving agent token:", error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
console.log("[CloudflareMCPServer] ✅ Already have token, skipping PRIORITY 3");
|
|
584
|
+
}
|
|
585
|
+
// Final check - log if we still don't have a token
|
|
586
|
+
if (!session.delegationToken) {
|
|
587
|
+
console.log("[CloudflareMCPServer] ❌ FINAL CHECK: Still no delegation token after all KV lookups:", {
|
|
588
|
+
sessionId: session?.id?.slice(0, 20) + "..." || "none",
|
|
589
|
+
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
590
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
591
|
+
checkedKeys: [
|
|
592
|
+
sessionId ? STORAGE_KEYS.session(sessionId) : null,
|
|
593
|
+
userDid
|
|
594
|
+
? STORAGE_KEYS.delegation(userDid, agentDid)
|
|
595
|
+
: null,
|
|
596
|
+
STORAGE_KEYS.legacyDelegation(agentDid),
|
|
597
|
+
].filter(Boolean),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
console.log("[CloudflareMCPServer] ✅ FINAL CHECK: Delegation token found:", {
|
|
602
|
+
tokenLength: session.delegationToken.length,
|
|
603
|
+
sessionHasToken: !!session.delegationToken,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
// Log but don't fail - delegation lookup is best-effort
|
|
609
|
+
console.error("[CloudflareMCPServer] Failed to retrieve delegation token from KV (existing session):", error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Update delegation fields if provided in params (takes precedence)
|
|
199
613
|
if (params.delegationToken) {
|
|
200
614
|
session.delegationToken = params.delegationToken;
|
|
201
615
|
}
|
|
@@ -227,6 +641,55 @@ class CloudflareMCPServer {
|
|
|
227
641
|
session.toolName = toolName;
|
|
228
642
|
session.toolParams = params.arguments || {};
|
|
229
643
|
session.scopeId = scopeId; // ✅ ADDED: Pass scopeId for tool auto-discovery
|
|
644
|
+
// ✅ CRITICAL: Verify delegation token is set on session before processToolCall
|
|
645
|
+
// Also do a final KV lookup if token is still missing (defensive check)
|
|
646
|
+
// Use same 3-priority lookup: user+agent scoped, then session-scoped, then agent-scoped (last resort)
|
|
647
|
+
if (!session?.delegationToken && this.delegationStorage) {
|
|
648
|
+
try {
|
|
649
|
+
const agentDid = (await this.runtime.getIdentity()).did;
|
|
650
|
+
const userDid = session.userDid || params.clientDid || params.userDid;
|
|
651
|
+
let finalToken = undefined;
|
|
652
|
+
// PRIORITY 1: User+agent scoped token
|
|
653
|
+
if (userDid) {
|
|
654
|
+
const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
|
|
655
|
+
const token = await this.delegationStorage.get(userAgentKey, "text");
|
|
656
|
+
finalToken = token || undefined; // Convert null to undefined
|
|
657
|
+
}
|
|
658
|
+
// PRIORITY 2: Session-scoped token
|
|
659
|
+
if (!finalToken && session.id) {
|
|
660
|
+
const sessionKey = STORAGE_KEYS.session(session.id);
|
|
661
|
+
const sessionData = (await this.delegationStorage.get(sessionKey, "json"));
|
|
662
|
+
finalToken = sessionData?.delegationToken;
|
|
663
|
+
}
|
|
664
|
+
// PRIORITY 3: Agent-scoped token (legacy fallback - only if userDid unavailable)
|
|
665
|
+
if (!finalToken && !userDid) {
|
|
666
|
+
const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
|
|
667
|
+
const token = await this.delegationStorage.get(agentKey, "text");
|
|
668
|
+
finalToken = token || undefined; // Convert null to undefined
|
|
669
|
+
}
|
|
670
|
+
if (finalToken) {
|
|
671
|
+
session.delegationToken = finalToken;
|
|
672
|
+
console.log("[CloudflareMCPServer] ✅ Final KV lookup succeeded, token set on session:", {
|
|
673
|
+
toolName,
|
|
674
|
+
agentDid: agentDid.slice(0, 20) + "...",
|
|
675
|
+
userDid: userDid?.slice(0, 20) + "..." || "none",
|
|
676
|
+
tokenLength: finalToken.length,
|
|
677
|
+
source: userDid ? (session.id ? "session-scoped" : "user+agent-scoped") : "agent-scoped (legacy)",
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
console.error("[CloudflareMCPServer] Final KV lookup failed:", error);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
console.log("[CloudflareMCPServer] ✅ About to call processToolCall with session:", {
|
|
686
|
+
toolName,
|
|
687
|
+
sessionId: session?.id?.slice(0, 20) + "..." || "none",
|
|
688
|
+
hasDelegationToken: !!session?.delegationToken,
|
|
689
|
+
delegationTokenLength: session?.delegationToken?.length || 0,
|
|
690
|
+
hasConsentProof: !!session?.consentProof,
|
|
691
|
+
agentDid: session?.agentDid?.slice(0, 20) + "..." || "none",
|
|
692
|
+
});
|
|
230
693
|
// ✅ Use processToolCall which handles delegation checks AND proof generation
|
|
231
694
|
// This ensures delegation is checked BEFORE tool execution
|
|
232
695
|
// If delegation is required but not provided, this will throw DelegationRequiredError
|
|
@@ -277,31 +740,27 @@ class CloudflareMCPServer {
|
|
|
277
740
|
normalizedClientInfo.capabilities;
|
|
278
741
|
}
|
|
279
742
|
}
|
|
280
|
-
// Phase 4 PR #5: Extract OAuth identity
|
|
281
|
-
let
|
|
282
|
-
if (
|
|
283
|
-
|
|
743
|
+
// Phase 4 PR #5: Extract OAuth identity BEFORE handshake and pass to runtime
|
|
744
|
+
let oauthIdentity = undefined;
|
|
745
|
+
if (meta?.request) {
|
|
746
|
+
oauthIdentity = this.extractOAuthIdentityFromRequest(meta.request);
|
|
284
747
|
if (oauthIdentity) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
console.warn("[Adapter] Failed to lookup persistent User DID:", error);
|
|
298
|
-
// Non-fatal - continue with handshake
|
|
299
|
-
}
|
|
748
|
+
console.log("[Adapter] Extracted OAuth identity for handshake:", {
|
|
749
|
+
provider: oauthIdentity.provider,
|
|
750
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
751
|
+
});
|
|
752
|
+
// Add OAuth identity to handshake payload for persistent user DID lookup
|
|
753
|
+
// Type assertion needed because HandshakeRequest doesn't include oauthIdentity,
|
|
754
|
+
// but handleHandshake accepts it via intersection type
|
|
755
|
+
handshakePayload.oauthIdentity = oauthIdentity;
|
|
300
756
|
}
|
|
301
757
|
}
|
|
302
758
|
const handshakeResult = await this.runtime.handleHandshake(handshakePayload);
|
|
303
|
-
//
|
|
304
|
-
|
|
759
|
+
// Get userDid from handshake result (may have been retrieved via OAuth mapping)
|
|
760
|
+
const userDid = handshakeResult.userDid;
|
|
761
|
+
// Phase 4 PR #5: Store User DID, OAuth identity (with redacted subject), and clientId in session AFTER handshake
|
|
762
|
+
// Only store if userDid is available (from OAuth mapping or handshake result)
|
|
763
|
+
if (this.delegationStorage && handshakeResult.sessionId && userDid) {
|
|
305
764
|
try {
|
|
306
765
|
const sessionKey = STORAGE_KEYS.session(handshakeResult.sessionId);
|
|
307
766
|
const existingSession = (await this.delegationStorage.get(sessionKey, "json"));
|
|
@@ -309,16 +768,29 @@ class CloudflareMCPServer {
|
|
|
309
768
|
const clientId = handshakePayload.clientInfo?.clientId;
|
|
310
769
|
// Get agentDid from runtime identity
|
|
311
770
|
const agentDid = (await this.runtime.getIdentity()).did;
|
|
771
|
+
// Store OAuth identity with redacted subject for PII protection
|
|
772
|
+
const oauthIdentityForStorage = oauthIdentity
|
|
773
|
+
? {
|
|
774
|
+
provider: oauthIdentity.provider,
|
|
775
|
+
subjectHash: oauthIdentity.subject.substring(0, 8), // Redact full subject
|
|
776
|
+
// Don't store email, name, or full subject for PII protection
|
|
777
|
+
}
|
|
778
|
+
: undefined;
|
|
312
779
|
await this.delegationStorage.put(sessionKey, JSON.stringify({
|
|
313
780
|
...(existingSession || {}),
|
|
314
781
|
userDid,
|
|
315
782
|
agentDid,
|
|
316
783
|
...(clientId && { clientId }),
|
|
784
|
+
...(oauthIdentityForStorage && { oauthIdentity: oauthIdentityForStorage }),
|
|
317
785
|
}), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
318
|
-
console.log("[Adapter] Stored User DID, agentDid, and
|
|
786
|
+
console.log("[Adapter] Stored User DID, agentDid, clientId, and OAuth identity (redacted) in session", {
|
|
787
|
+
hasUserDid: !!userDid,
|
|
788
|
+
hasOAuth: !!oauthIdentity,
|
|
789
|
+
provider: oauthIdentity?.provider,
|
|
790
|
+
});
|
|
319
791
|
}
|
|
320
792
|
catch (error) {
|
|
321
|
-
console.warn("[Adapter] Failed to store
|
|
793
|
+
console.warn("[Adapter] Failed to store session data:", error);
|
|
322
794
|
// Non-fatal - continue
|
|
323
795
|
}
|
|
324
796
|
}
|
|
@@ -466,8 +938,7 @@ class CloudflareMCPServer {
|
|
|
466
938
|
const name = typeof nameValue === "string" && nameValue.trim().length > 0
|
|
467
939
|
? nameValue.trim()
|
|
468
940
|
: "unknown";
|
|
469
|
-
const clientId = handshakeClientId ||
|
|
470
|
-
crypto.randomUUID();
|
|
941
|
+
const clientId = handshakeClientId || crypto.randomUUID();
|
|
471
942
|
const capabilities = this.isRecord(request.clientCapabilities)
|
|
472
943
|
? request.clientCapabilities
|
|
473
944
|
: initializeContext?.capabilities;
|
|
@@ -548,16 +1019,104 @@ class CloudflareMCPServer {
|
|
|
548
1019
|
return null;
|
|
549
1020
|
const cookieValue = oauthCookie.substring(equalsIndex + 1);
|
|
550
1021
|
const parsed = JSON.parse(decodeURIComponent(cookieValue));
|
|
551
|
-
// Validate
|
|
552
|
-
|
|
553
|
-
|
|
1022
|
+
// ✅ SECURITY: Validate OAuth identity format and content
|
|
1023
|
+
const validationResult = this.validateOAuthIdentity(parsed);
|
|
1024
|
+
if (!validationResult.valid) {
|
|
1025
|
+
console.warn("[Adapter] ⚠️ OAuth identity validation failed:", validationResult.reason, { parsed });
|
|
1026
|
+
return null;
|
|
554
1027
|
}
|
|
1028
|
+
return parsed;
|
|
555
1029
|
}
|
|
556
1030
|
catch (error) {
|
|
557
1031
|
console.warn("[Adapter] Failed to extract OAuth identity from cookies:", error);
|
|
558
1032
|
}
|
|
559
1033
|
return null;
|
|
560
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Validate OAuth identity format and content
|
|
1037
|
+
*
|
|
1038
|
+
* Ensures:
|
|
1039
|
+
* - Provider is non-empty string (1-50 chars)
|
|
1040
|
+
* - Subject is non-empty string (1-255 chars)
|
|
1041
|
+
* - Provider matches expected format (alphanumeric, hyphens, underscores)
|
|
1042
|
+
* - Subject matches expected format (non-empty, reasonable length)
|
|
1043
|
+
*
|
|
1044
|
+
* @param identity - Parsed OAuth identity object
|
|
1045
|
+
* @returns Validation result
|
|
1046
|
+
*/
|
|
1047
|
+
validateOAuthIdentity(identity) {
|
|
1048
|
+
// Check if identity is an object
|
|
1049
|
+
if (!identity || typeof identity !== "object") {
|
|
1050
|
+
return { valid: false, reason: "OAuth identity must be an object" };
|
|
1051
|
+
}
|
|
1052
|
+
const oauth = identity;
|
|
1053
|
+
// Validate provider
|
|
1054
|
+
if (!oauth.provider || typeof oauth.provider !== "string") {
|
|
1055
|
+
return { valid: false, reason: "OAuth provider is required and must be a string" };
|
|
1056
|
+
}
|
|
1057
|
+
const provider = oauth.provider.trim();
|
|
1058
|
+
if (provider.length === 0) {
|
|
1059
|
+
return { valid: false, reason: "OAuth provider cannot be empty" };
|
|
1060
|
+
}
|
|
1061
|
+
if (provider.length > 50) {
|
|
1062
|
+
return { valid: false, reason: "OAuth provider must be 50 characters or less" };
|
|
1063
|
+
}
|
|
1064
|
+
// Provider format: alphanumeric, hyphens, underscores, dots (e.g., "google", "microsoft", "github", "custom-provider")
|
|
1065
|
+
const providerPattern = /^[a-zA-Z0-9._-]+$/;
|
|
1066
|
+
if (!providerPattern.test(provider)) {
|
|
1067
|
+
return {
|
|
1068
|
+
valid: false,
|
|
1069
|
+
reason: `OAuth provider must match pattern [a-zA-Z0-9._-]: "${provider}"`,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
// Validate subject
|
|
1073
|
+
if (!oauth.subject || typeof oauth.subject !== "string") {
|
|
1074
|
+
return { valid: false, reason: "OAuth subject is required and must be a string" };
|
|
1075
|
+
}
|
|
1076
|
+
const subject = oauth.subject.trim();
|
|
1077
|
+
if (subject.length === 0) {
|
|
1078
|
+
return { valid: false, reason: "OAuth subject cannot be empty" };
|
|
1079
|
+
}
|
|
1080
|
+
if (subject.length > 255) {
|
|
1081
|
+
return { valid: false, reason: "OAuth subject must be 255 characters or less" };
|
|
1082
|
+
}
|
|
1083
|
+
// Subject format: non-empty, reasonable characters (allows most Unicode, but prevents control chars)
|
|
1084
|
+
// OAuth subjects can be numeric IDs, email-like strings, or other identifiers
|
|
1085
|
+
const subjectPattern = /^[\S]+$/; // At least one non-whitespace character
|
|
1086
|
+
if (!subjectPattern.test(subject)) {
|
|
1087
|
+
return {
|
|
1088
|
+
valid: false,
|
|
1089
|
+
reason: `OAuth subject contains invalid characters: "${subject.substring(0, 20)}..."`,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
// Validate optional email if present
|
|
1093
|
+
if (oauth.email !== undefined) {
|
|
1094
|
+
if (typeof oauth.email !== "string") {
|
|
1095
|
+
return { valid: false, reason: "OAuth email must be a string if provided" };
|
|
1096
|
+
}
|
|
1097
|
+
const email = oauth.email.trim();
|
|
1098
|
+
if (email.length > 0) {
|
|
1099
|
+
// Basic email format validation
|
|
1100
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1101
|
+
if (!emailPattern.test(email)) {
|
|
1102
|
+
return { valid: false, reason: `OAuth email format invalid: "${email}"` };
|
|
1103
|
+
}
|
|
1104
|
+
if (email.length > 255) {
|
|
1105
|
+
return { valid: false, reason: "OAuth email must be 255 characters or less" };
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Validate optional name if present
|
|
1110
|
+
if (oauth.name !== undefined) {
|
|
1111
|
+
if (typeof oauth.name !== "string") {
|
|
1112
|
+
return { valid: false, reason: "OAuth name must be a string if provided" };
|
|
1113
|
+
}
|
|
1114
|
+
if (oauth.name.length > 255) {
|
|
1115
|
+
return { valid: false, reason: "OAuth name must be 255 characters or less" };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return { valid: true };
|
|
1119
|
+
}
|
|
561
1120
|
}
|
|
562
1121
|
function buildRequestMeta(request) {
|
|
563
1122
|
const ip = request.headers.get("cf-connecting-ip") ??
|
|
@@ -582,25 +1141,33 @@ function buildRequestMeta(request) {
|
|
|
582
1141
|
*
|
|
583
1142
|
* Supports SSE (Server-Sent Events) and HTTP JSON-RPC transports for compatibility
|
|
584
1143
|
* with Claude Desktop, Cursor, MCP Inspector, and other MCP clients.
|
|
1144
|
+
*
|
|
1145
|
+
* Automatically handles prefixed KV bindings via `envPrefix` parameter or auto-detection.
|
|
585
1146
|
*/
|
|
586
1147
|
export function createMCPICloudflareAdapter(config) {
|
|
587
|
-
//
|
|
588
|
-
|
|
1148
|
+
// Normalize environment to handle prefixed KV bindings
|
|
1149
|
+
// This ensures consistent KV access regardless of prefix usage
|
|
1150
|
+
const mappedEnv = normalizeCloudflareEnv(config.env, config.envPrefix);
|
|
1151
|
+
// Create the runtime with normalized environment
|
|
1152
|
+
const runtime = createCloudflareRuntime({
|
|
1153
|
+
...config,
|
|
1154
|
+
env: mappedEnv,
|
|
1155
|
+
});
|
|
589
1156
|
// Server info
|
|
590
1157
|
const serverInfo = config.serverInfo || {
|
|
591
1158
|
name: "MCP-I Cloudflare Server",
|
|
592
1159
|
version: "1.0.0",
|
|
593
1160
|
};
|
|
594
1161
|
// Initialize proof archive if PROOF_ARCHIVE KV is available
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
? new KVProofArchive(env.PROOF_ARCHIVE)
|
|
1162
|
+
const proofArchive = mappedEnv.PROOF_ARCHIVE
|
|
1163
|
+
? new KVProofArchive(mappedEnv.PROOF_ARCHIVE)
|
|
598
1164
|
: undefined;
|
|
599
|
-
// Get delegation storage from env if available
|
|
600
|
-
const delegationStorage =
|
|
1165
|
+
// Get delegation storage from normalized env if available
|
|
1166
|
+
const delegationStorage = mappedEnv.DELEGATION_STORAGE;
|
|
601
1167
|
// Create lightweight MCP server
|
|
602
1168
|
const server = new CloudflareMCPServer(runtime, serverInfo, config.tools || [], proofArchive, delegationStorage);
|
|
603
1169
|
// Return fetch handler
|
|
1170
|
+
// Note: mappedEnv is captured in closure for admin endpoints
|
|
604
1171
|
return {
|
|
605
1172
|
server,
|
|
606
1173
|
runtime,
|
|
@@ -791,15 +1358,16 @@ export function createMCPICloudflareAdapter(config) {
|
|
|
791
1358
|
headers: { "Content-Type": "application/json" },
|
|
792
1359
|
});
|
|
793
1360
|
}
|
|
794
|
-
|
|
1361
|
+
// Use normalized environment (handles prefixed KV bindings)
|
|
1362
|
+
// mappedEnv is already normalized in createMCPICloudflareAdapter
|
|
795
1363
|
// GET /admin/nonces - List active nonces
|
|
796
1364
|
if (url.pathname === "/admin/nonces") {
|
|
797
1365
|
try {
|
|
798
1366
|
// Use KV list to get nonce keys
|
|
799
|
-
const noncesList = await
|
|
1367
|
+
const noncesList = await mappedEnv.NONCE_CACHE.list({ prefix: "nonce:" });
|
|
800
1368
|
const nonces = [];
|
|
801
1369
|
for (const key of noncesList.keys) {
|
|
802
|
-
const value = await
|
|
1370
|
+
const value = await mappedEnv.NONCE_CACHE.get(key.name);
|
|
803
1371
|
if (value) {
|
|
804
1372
|
nonces.push({
|
|
805
1373
|
nonce: key.name.replace("nonce:", ""),
|
|
@@ -825,8 +1393,8 @@ export function createMCPICloudflareAdapter(config) {
|
|
|
825
1393
|
}
|
|
826
1394
|
}
|
|
827
1395
|
// Initialize proof archive if available
|
|
828
|
-
const proofArchive =
|
|
829
|
-
? new KVProofArchive(
|
|
1396
|
+
const proofArchive = mappedEnv.PROOF_ARCHIVE
|
|
1397
|
+
? new KVProofArchive(mappedEnv.PROOF_ARCHIVE)
|
|
830
1398
|
: null;
|
|
831
1399
|
if (!proofArchive) {
|
|
832
1400
|
return new Response(JSON.stringify({
|
|
@@ -899,7 +1467,7 @@ export function createMCPICloudflareAdapter(config) {
|
|
|
899
1467
|
if (url.pathname === "/admin/stats") {
|
|
900
1468
|
try {
|
|
901
1469
|
const [noncesList, proofStats] = await Promise.all([
|
|
902
|
-
|
|
1470
|
+
mappedEnv.NONCE_CACHE.list({ prefix: "nonce:" }),
|
|
903
1471
|
proofArchive.getStats(),
|
|
904
1472
|
]);
|
|
905
1473
|
return new Response(JSON.stringify({
|