@kya-os/mcp-i-cloudflare 1.5.10-canary.8 → 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.
- 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 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +712 -112
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts +103 -25
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +617 -40
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts +0 -8
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +277 -119
- package/dist/app.js.map +1 -1
- package/dist/cache/kv-oauth-config-cache.d.ts +47 -0
- package/dist/cache/kv-oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/kv-oauth-config-cache.js +82 -0
- package/dist/cache/kv-oauth-config-cache.js.map +1 -0
- package/dist/cache/kv-tool-protection-cache.d.ts +26 -1
- package/dist/cache/kv-tool-protection-cache.d.ts.map +1 -1
- package/dist/cache/kv-tool-protection-cache.js +19 -11
- package/dist/cache/kv-tool-protection-cache.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -14
- 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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- 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 +287 -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 +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +133 -18
- package/dist/server.js.map +1 -1
- package/dist/services/admin.service.d.ts +1 -3
- package/dist/services/admin.service.d.ts.map +1 -1
- package/dist/services/admin.service.js +175 -146
- 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 -28
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent-page-renderer.d.ts +14 -0
- package/dist/services/consent-page-renderer.d.ts.map +1 -1
- package/dist/services/consent-page-renderer.js +54 -27
- package/dist/services/consent-page-renderer.js.map +1 -1
- package/dist/services/consent.service.d.ts +93 -8
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +1817 -553
- 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/idp-token-storage.d.ts +68 -0
- package/dist/services/idp-token-storage.d.ts.map +1 -0
- package/dist/services/idp-token-storage.js +157 -0
- package/dist/services/idp-token-storage.js.map +1 -0
- package/dist/services/oauth-service.d.ts +66 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +223 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/proof.service.d.ts +8 -6
- package/dist/services/proof.service.d.ts.map +1 -1
- package/dist/services/proof.service.js +131 -75
- package/dist/services/proof.service.js.map +1 -1
- package/dist/services/tool-context-builder.d.ts +55 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +124 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/types/tool-context.d.ts +35 -0
- package/dist/types/tool-context.d.ts.map +1 -0
- package/dist/types/tool-context.js +13 -0
- package/dist/types/tool-context.js.map +1 -0
- package/dist/types.d.ts +31 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/oauth-service-registry.d.ts +65 -0
- package/dist/utils/oauth-service-registry.d.ts.map +1 -0
- package/dist/utils/oauth-service-registry.js +125 -0
- package/dist/utils/oauth-service-registry.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
261
|
-
let
|
|
262
|
-
if (
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
//
|
|
284
|
-
|
|
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
|
|
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
|
|
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"}"`}.
|
|
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,
|
|
332
|
-
authorizationUrl: error.consentUrl, // Compatibility alias
|
|
770
|
+
consentUrl: error.consentUrl,
|
|
333
771
|
resumeToken: error.resumeToken, // Include resume token for resumption
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
//
|
|
576
|
-
|
|
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
|
|
584
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
818
|
-
? new KVProofArchive(
|
|
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
|
-
|
|
1491
|
+
mappedEnv.NONCE_CACHE.list({ prefix: "nonce:" }),
|
|
892
1492
|
proofArchive.getStats(),
|
|
893
1493
|
]);
|
|
894
1494
|
return new Response(JSON.stringify({
|