@kya-os/mcp-i-cloudflare 1.5.10-canary.9 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +130 -0
  2. package/dist/__tests__/e2e/test-config.d.ts +37 -0
  3. package/dist/__tests__/e2e/test-config.d.ts.map +1 -0
  4. package/dist/__tests__/e2e/test-config.js +62 -0
  5. package/dist/__tests__/e2e/test-config.js.map +1 -0
  6. package/dist/adapter.d.ts +44 -1
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +712 -112
  9. package/dist/adapter.js.map +1 -1
  10. package/dist/agent.d.ts +117 -25
  11. package/dist/agent.d.ts.map +1 -1
  12. package/dist/agent.js +664 -40
  13. package/dist/agent.js.map +1 -1
  14. package/dist/app.d.ts +0 -8
  15. package/dist/app.d.ts.map +1 -1
  16. package/dist/app.js +348 -119
  17. package/dist/app.js.map +1 -1
  18. package/dist/cache/kv-oauth-config-cache.d.ts +47 -0
  19. package/dist/cache/kv-oauth-config-cache.d.ts.map +1 -0
  20. package/dist/cache/kv-oauth-config-cache.js +82 -0
  21. package/dist/cache/kv-oauth-config-cache.js.map +1 -0
  22. package/dist/cache/kv-tool-protection-cache.d.ts +26 -1
  23. package/dist/cache/kv-tool-protection-cache.d.ts.map +1 -1
  24. package/dist/cache/kv-tool-protection-cache.js +19 -11
  25. package/dist/cache/kv-tool-protection-cache.js.map +1 -1
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +39 -14
  28. package/dist/config.js.map +1 -1
  29. package/dist/helpers/env-mapper.d.ts +60 -1
  30. package/dist/helpers/env-mapper.d.ts.map +1 -1
  31. package/dist/helpers/env-mapper.js +136 -6
  32. package/dist/helpers/env-mapper.js.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +16 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/runtime/audit-logger.d.ts +96 -0
  38. package/dist/runtime/audit-logger.d.ts.map +1 -0
  39. package/dist/runtime/audit-logger.js +276 -0
  40. package/dist/runtime/audit-logger.js.map +1 -0
  41. package/dist/runtime/oauth-handler.d.ts +5 -0
  42. package/dist/runtime/oauth-handler.d.ts.map +1 -1
  43. package/dist/runtime/oauth-handler.js +287 -35
  44. package/dist/runtime/oauth-handler.js.map +1 -1
  45. package/dist/runtime.d.ts +12 -1
  46. package/dist/runtime.d.ts.map +1 -1
  47. package/dist/runtime.js +34 -4
  48. package/dist/runtime.js.map +1 -1
  49. package/dist/server.d.ts +7 -0
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +120 -29
  52. package/dist/server.js.map +1 -1
  53. package/dist/services/admin.service.d.ts +1 -3
  54. package/dist/services/admin.service.d.ts.map +1 -1
  55. package/dist/services/admin.service.js +175 -146
  56. package/dist/services/admin.service.js.map +1 -1
  57. package/dist/services/consent-audit.service.d.ts +91 -0
  58. package/dist/services/consent-audit.service.d.ts.map +1 -0
  59. package/dist/services/consent-audit.service.js +243 -0
  60. package/dist/services/consent-audit.service.js.map +1 -0
  61. package/dist/services/consent-config.service.d.ts +2 -2
  62. package/dist/services/consent-config.service.d.ts.map +1 -1
  63. package/dist/services/consent-config.service.js +55 -28
  64. package/dist/services/consent-config.service.js.map +1 -1
  65. package/dist/services/consent-page-renderer.d.ts +14 -0
  66. package/dist/services/consent-page-renderer.d.ts.map +1 -1
  67. package/dist/services/consent-page-renderer.js +54 -27
  68. package/dist/services/consent-page-renderer.js.map +1 -1
  69. package/dist/services/consent.service.d.ts +93 -8
  70. package/dist/services/consent.service.d.ts.map +1 -1
  71. package/dist/services/consent.service.js +1817 -553
  72. package/dist/services/consent.service.js.map +1 -1
  73. package/dist/services/delegation.service.d.ts.map +1 -1
  74. package/dist/services/delegation.service.js +67 -29
  75. package/dist/services/delegation.service.js.map +1 -1
  76. package/dist/services/idp-token-storage.d.ts +68 -0
  77. package/dist/services/idp-token-storage.d.ts.map +1 -0
  78. package/dist/services/idp-token-storage.js +157 -0
  79. package/dist/services/idp-token-storage.js.map +1 -0
  80. package/dist/services/oauth-service.d.ts +66 -0
  81. package/dist/services/oauth-service.d.ts.map +1 -0
  82. package/dist/services/oauth-service.js +223 -0
  83. package/dist/services/oauth-service.js.map +1 -0
  84. package/dist/services/proof.service.d.ts +8 -6
  85. package/dist/services/proof.service.d.ts.map +1 -1
  86. package/dist/services/proof.service.js +131 -75
  87. package/dist/services/proof.service.js.map +1 -1
  88. package/dist/services/tool-context-builder.d.ts +55 -0
  89. package/dist/services/tool-context-builder.d.ts.map +1 -0
  90. package/dist/services/tool-context-builder.js +124 -0
  91. package/dist/services/tool-context-builder.js.map +1 -0
  92. package/dist/types/tool-context.d.ts +35 -0
  93. package/dist/types/tool-context.d.ts.map +1 -0
  94. package/dist/types/tool-context.js +13 -0
  95. package/dist/types/tool-context.js.map +1 -0
  96. package/dist/types.d.ts +31 -2
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/utils/oauth-service-registry.d.ts +65 -0
  99. package/dist/utils/oauth-service-registry.d.ts.map +1 -0
  100. package/dist/utils/oauth-service-registry.js +125 -0
  101. package/dist/utils/oauth-service-registry.js.map +1 -0
  102. package/package.json +27 -60
package/dist/agent.js CHANGED
@@ -10,8 +10,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { CloudflareRuntime } from "./runtime";
11
11
  import { createCloudflareRuntime } from "./index";
12
12
  import { mapPrefixedEnv } from "./helpers/env-mapper";
13
- // Global registry for agent options (set by createMCPIApp)
14
- const agentOptionsRegistry = new Map();
13
+ import { STORAGE_KEYS } from "./constants/storage-keys";
14
+ import { z } from "zod";
15
+ import { OAuthService, IdpTokenResolver, ToolContextBuilder, DelegationRequiredError, } from "@kya-os/mcp-i-core";
16
+ // CRITICAL: Import the OAuth service registry - this triggers globalThis registration
17
+ // at module load time, preventing esbuild from tree-shaking the OAuth services
18
+ import { getOAuthServices, isOAuthServicesAvailable, } from "./utils/oauth-service-registry";
19
+ import { KVOAuthConfigCache } from "./cache/kv-oauth-config-cache";
20
+ import { IdpTokenStorage } from "./services/idp-token-storage";
21
+ import { OAuthSecurityService } from "./services/oauth-security.service";
22
+ // CRITICAL: Force OAuth registry evaluation at module load time
23
+ // This creates an observable side effect that esbuild cannot tree-shake
24
+ // Without this, esbuild may eliminate oauth-service-registry.ts entirely
25
+ // because the exports are only used inside conditional async code
26
+ const _oauthRegistryLoaded = isOAuthServicesAvailable();
27
+ if (!_oauthRegistryLoaded) {
28
+ console.warn("[MCPICloudflareAgent] OAuth service registry not loaded at module init - services may be unavailable");
29
+ }
15
30
  /**
16
31
  * Base class for MCP agents with MCP-I features
17
32
  *
@@ -21,45 +36,45 @@ const agentOptionsRegistry = new Map();
21
36
  * - Session management
22
37
  * - Durable Object routing
23
38
  *
24
- * Users extend this class and implement `registerTools()` to add their custom tools.
39
+ * Users extend this class and implement:
40
+ * - `getAgentName()`: Return the agent name
41
+ * - `getAgentVersion()`: Return the agent version (optional)
42
+ * - `getEnvPrefix()`: Return the KV environment prefix (optional)
43
+ * - `getRuntimeConfig()`: Return the runtime configuration
44
+ * - `registerTools()`: Register tools with the MCP server
25
45
  */
26
46
  export class MCPICloudflareAgent extends McpAgent {
27
- // @ts-ignore - Type incompatibility between different versions of @modelcontextprotocol/sdk
28
- // Runtime behavior is correct, this is a type-only issue
29
47
  // Initialize server as class field (before constructor) so parent class can access it
48
+ // Note: SDK 1.x uses single parameter constructor (server info only)
30
49
  server = new McpServer({
31
50
  name: "MCP-I Agent",
32
51
  version: "1.0.0",
33
52
  });
34
53
  mcpiRuntime;
35
54
  env;
36
- options;
37
55
  constructor(state, env) {
38
56
  // Call super() with just state and env (agents@0.2.21+ only accepts 2 parameters)
39
57
  // The config is no longer passed to the constructor - it's set via the server property
40
58
  super(state, env);
41
- // Get options from registry AFTER calling super() (can now access 'this')
42
- const AgentClass = new.target;
43
- const options = agentOptionsRegistry.get(AgentClass);
44
- // Now we can safely access 'this' and set properties
45
- if (!options) {
46
- throw new Error(`Agent options not found. Make sure to call createMCPIApp() before instantiating ${AgentClass.name}.`);
47
- }
48
59
  this.env = env;
49
- this.options = options;
50
- // Update server with correct config from options (override the default)
60
+ // Get agent configuration from subclass methods
61
+ const agentName = this.getAgentName();
62
+ const agentVersion = this.getAgentVersion();
63
+ const envPrefix = this.getEnvPrefix();
64
+ // Update server with correct config
65
+ // Note: SDK 1.x uses single parameter constructor (server info only)
51
66
  this.server = new McpServer({
52
- name: this.options.name,
53
- version: this.options.version || "1.0.0",
67
+ name: agentName,
68
+ version: agentVersion || "1.0.0",
54
69
  });
55
70
  // Map prefixed environment to standard CloudflareEnv
56
- const mappedEnv = this.options.envPrefix
57
- ? mapPrefixedEnv(env, this.options.envPrefix)
71
+ const mappedEnv = envPrefix
72
+ ? mapPrefixedEnv(env, envPrefix)
58
73
  : env;
59
- // Add DurableObjectState to mapped env
74
+ // Add DurableObjectState to mapped env for identity persistence
60
75
  mappedEnv._durableObjectState = state;
61
- // Load runtime configuration
62
- const runtimeConfig = this.options.getRuntimeConfig(mappedEnv);
76
+ // Load runtime configuration from subclass
77
+ const runtimeConfig = this.getRuntimeConfigInternal(mappedEnv);
63
78
  // Create tool protection service if configured
64
79
  const toolProtectionService = runtimeConfig.toolProtection &&
65
80
  mappedEnv.TOOL_PROTECTION_KV &&
@@ -73,10 +88,13 @@ export class MCPICloudflareAgent extends McpAgent {
73
88
  mappedEnv.AGENTSHIELD_PROJECT_ID,
74
89
  cacheTtl: runtimeConfig.toolProtection.agentShield?.cacheTtl || 300000,
75
90
  debug: runtimeConfig.environment === "development",
91
+ // Wrap fallback ToolProtectionMap into ToolProtectionConfig format
92
+ // Handle both wrapped and unwrapped formats for backward compatibility
76
93
  fallbackConfig: runtimeConfig.toolProtection.fallback
77
- ? {
78
- toolProtections: runtimeConfig.toolProtection.fallback,
79
- }
94
+ ? runtimeConfig.toolProtection.fallback.toolProtections !==
95
+ undefined
96
+ ? runtimeConfig.toolProtection.fallback // Already wrapped
97
+ : { toolProtections: runtimeConfig.toolProtection.fallback } // Wrap it
80
98
  : undefined,
81
99
  })
82
100
  : undefined;
@@ -94,40 +112,185 @@ export class MCPICloudflareAgent extends McpAgent {
94
112
  });
95
113
  }
96
114
  /**
97
- * Register options for an agent class
98
- * Called internally by createMCPIApp
115
+ * Get agent version
116
+ * Subclasses can override this to provide a custom version
117
+ */
118
+ getAgentVersion() {
119
+ return "1.0.0";
120
+ }
121
+ /**
122
+ * Get environment prefix for KV bindings
123
+ * Subclasses can override this for multi-agent deployments
99
124
  */
100
- static registerOptions(AgentClass, options) {
101
- agentOptionsRegistry.set(AgentClass, options);
125
+ getEnvPrefix() {
126
+ return undefined;
102
127
  }
103
128
  /**
104
129
  * Initialize the agent
105
130
  * Call this after construction to set up the runtime and register tools
106
131
  */
107
132
  async init() {
108
- await this.mcpiRuntime?.initialize();
109
- const identity = await this.mcpiRuntime?.getIdentity();
110
- console.log("[MCP-I] Initialized with DID:", identity?.did);
111
- // Register tools (implemented by subclasses)
112
- await this.registerTools();
133
+ try {
134
+ await this.mcpiRuntime?.initialize();
135
+ const identity = await this.mcpiRuntime?.getIdentity();
136
+ console.log("[MCP-I] Initialized with DID:", identity?.did);
137
+ // Ensure server is initialized before registering tools
138
+ if (!this.server) {
139
+ throw new Error("Server not initialized. This should not happen - server is initialized in constructor.");
140
+ }
141
+ // Register tools (implemented by subclasses)
142
+ await this.registerTools();
143
+ }
144
+ catch (error) {
145
+ console.error("[MCPICloudflareAgent] Initialization failed:", error);
146
+ throw error;
147
+ }
148
+ }
149
+ /**
150
+ * Register a tool with automatic session ID extraction and proof generation
151
+ *
152
+ * This helper method automatically extracts `sessionId` from `ToolExtraArguments`
153
+ * and passes it to `executeToolWithProof`, eliminating the need for manual extraction
154
+ * in every tool handler.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * // For JSON Schema format (from template):
159
+ * this.registerToolWithProof(
160
+ * greetTool.name,
161
+ * greetTool.description,
162
+ * greetTool.inputSchema, // Full JSON Schema object
163
+ * greetTool.handler
164
+ * );
165
+ *
166
+ * // For Zod schema format (from examples):
167
+ * this.registerToolWithProof(
168
+ * greetTool.name,
169
+ * greetTool.description,
170
+ * greetTool.inputSchema.shape, // Zod schema shape
171
+ * greetTool.handler
172
+ * );
173
+ * ```
174
+ *
175
+ * @param name - Tool name
176
+ * @param description - Tool description
177
+ * @param schema - Tool input schema (JSON Schema object OR Zod schema shape)
178
+ * @param handler - Tool handler function that receives args and returns result
179
+ */
180
+ registerToolWithProof(name, description, schema, // Accept JSON Schema object or Zod schema shape
181
+ handler) {
182
+ // Safety check: ensure server is initialized
183
+ if (!this.server) {
184
+ throw new Error(`Cannot register tool "${name}": server not initialized. Make sure to call init() before registering tools.`);
185
+ }
186
+ try {
187
+ // Normalize schema format for MCP SDK
188
+ // MCP SDK expects Zod schema shape (Record<string, ZodType>), not JSON Schema
189
+ // Convert JSON Schema to Zod schema shape if needed
190
+ let zodSchemaShape = {};
191
+ if (schema && typeof schema === "object") {
192
+ // Check if it's already a Zod schema shape (has ZodType instances)
193
+ if (schema.type === "object" && schema.properties) {
194
+ // It's a JSON Schema object - convert to Zod schema shape
195
+ const jsonSchema = schema;
196
+ const requiredFields = new Set(jsonSchema.required || []);
197
+ zodSchemaShape = Object.fromEntries(Object.entries(jsonSchema.properties).map(([key, prop]) => {
198
+ // Convert JSON Schema property to Zod type
199
+ let zodType;
200
+ if (prop.type === "string") {
201
+ zodType = z.string();
202
+ if (prop.description) {
203
+ zodType = zodType.describe(prop.description);
204
+ }
205
+ }
206
+ else if (prop.type === "number") {
207
+ zodType = z.number();
208
+ if (prop.description) {
209
+ zodType = zodType.describe(prop.description);
210
+ }
211
+ }
212
+ else if (prop.type === "boolean") {
213
+ zodType = z.boolean();
214
+ if (prop.description) {
215
+ zodType = zodType.describe(prop.description);
216
+ }
217
+ }
218
+ else if (prop.type === "array") {
219
+ zodType = z.array(z.any());
220
+ if (prop.description) {
221
+ zodType = zodType.describe(prop.description);
222
+ }
223
+ }
224
+ else {
225
+ // Fallback to any for unknown types
226
+ zodType = z.any();
227
+ if (prop.description) {
228
+ zodType = zodType.describe(prop.description);
229
+ }
230
+ }
231
+ // Make optional if not in required array
232
+ if (!requiredFields.has(key)) {
233
+ zodType = zodType.optional();
234
+ }
235
+ return [key, zodType];
236
+ }));
237
+ }
238
+ else {
239
+ // Assume it's already a Zod schema shape or compatible object
240
+ zodSchemaShape = schema;
241
+ }
242
+ }
243
+ this.server.tool(name, description, zodSchemaShape, async (args, extra) => {
244
+ // ✅ Automatically extract sessionId from ToolExtraArguments
245
+ // This ensures delegation tokens stored with the original session ID
246
+ // can be retrieved on subsequent tool calls
247
+ // Note: extra is typed as 'any' to match MCP SDK's ToolExtraArguments type
248
+ const sessionId = extra?.sessionId;
249
+ return this.executeToolWithProof(name, args, handler, sessionId);
250
+ });
251
+ }
252
+ catch (error) {
253
+ console.error(`[MCPICloudflareAgent] Failed to register tool "${name}":`, error);
254
+ throw error;
255
+ }
113
256
  }
114
257
  /**
115
258
  * Execute a tool with automatic proof generation
116
259
  * Use this helper method when registering tools to ensure proofs are generated
260
+ *
261
+ * @param toolName - Name of the tool being executed
262
+ * @param args - Tool arguments
263
+ * @param handler - Tool handler function
264
+ * @param providedSessionId - Optional session ID from MCP request (extracted from ToolExtraArguments.sessionId)
265
+ * If provided, uses this session ID for delegation token lookup.
266
+ * If not provided, falls back to getSessionId() or generates ephemeral ID.
117
267
  */
118
- async executeToolWithProof(toolName, args, handler) {
268
+ async executeToolWithProof(toolName, args, handler, providedSessionId) {
119
269
  if (!this.mcpiRuntime) {
120
270
  throw new Error("MCP-I runtime not initialized. Call init() first.");
121
271
  }
122
272
  // Create session
273
+ // ✅ CRITICAL: Use provided sessionId if available (from MCP request params)
274
+ // This ensures delegation tokens stored with the original session ID can be retrieved
123
275
  const timestamp = Date.now();
124
- const sessionId = this.getSessionId() ||
276
+ const sessionId = providedSessionId ||
277
+ this.getSessionId() ||
125
278
  `ephemeral-${timestamp}-${Math.random().toString(36).substring(2, 10)}`;
279
+ // Log session ID source for debugging
280
+ const envPrefix = this.getEnvPrefix();
281
+ const mappedEnv = envPrefix
282
+ ? mapPrefixedEnv(this.env, envPrefix)
283
+ : this.env;
284
+ if (this.getRuntimeConfigInternal(mappedEnv).environment === "development") {
285
+ console.log("[MCPICloudflareAgent] Session ID source:", {
286
+ provided: !!providedSessionId,
287
+ fromGetSessionId: !!this.getSessionId(),
288
+ isEphemeral: !providedSessionId && !this.getSessionId(),
289
+ sessionId: sessionId.slice(0, 20) + "...",
290
+ });
291
+ }
126
292
  // Get server URL from env var (MCP_SERVER_URL) for consent URL building
127
293
  // In production, this should be set to the deployed worker URL
128
- const mappedEnv = this.options?.envPrefix
129
- ? mapPrefixedEnv(this.env, this.options.envPrefix)
130
- : this.env;
131
294
  let serverUrl = mappedEnv.MCP_SERVER_URL;
132
295
  // Ensure URL has protocol (https://) if provided
133
296
  if (serverUrl &&
@@ -136,6 +299,92 @@ export class MCPICloudflareAgent extends McpAgent {
136
299
  // Auto-add https:// if protocol missing
137
300
  serverUrl = `https://${serverUrl}`;
138
301
  }
302
+ // ✅ Look up delegation token from KV storage (3-priority system)
303
+ let delegationToken;
304
+ let userDid; // Extract userDid for IDP token resolution
305
+ const delegationStorage = mappedEnv.DELEGATION_STORAGE;
306
+ if (delegationStorage) {
307
+ try {
308
+ const agentDid = (await this.mcpiRuntime.getIdentity()).did;
309
+ console.log("[MCPICloudflareAgent] 🔍 Starting delegation token lookup:", {
310
+ sessionId: sessionId.slice(0, 20) + "...",
311
+ agentDid: agentDid.slice(0, 20) + "...",
312
+ hasDelegationStorage: !!delegationStorage,
313
+ });
314
+ // PRIORITY 1: User+Agent scoped token (user-specific, most secure)
315
+ // Try to get userDid from session cache
316
+ const sessionKey = STORAGE_KEYS.session(sessionId);
317
+ const sessionData = (await delegationStorage.get(sessionKey, "json"));
318
+ userDid = sessionData?.userDid;
319
+ if (userDid) {
320
+ const userAgentKey = STORAGE_KEYS.delegation(userDid, agentDid);
321
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 1: Checking user+agent scoped key:", {
322
+ key: userAgentKey,
323
+ userDid: userDid.slice(0, 20) + "...",
324
+ agentDid: agentDid.slice(0, 20) + "...",
325
+ });
326
+ const userAgentToken = await delegationStorage.get(userAgentKey, "text");
327
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 1: User+agent scoped lookup result:", {
328
+ found: !!userAgentToken,
329
+ tokenLength: userAgentToken?.length || 0,
330
+ });
331
+ if (userAgentToken) {
332
+ delegationToken = userAgentToken;
333
+ }
334
+ }
335
+ // PRIORITY 2: Session-scoped token (if session ID persists)
336
+ if (!delegationToken) {
337
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 2: Checking session-scoped key:", {
338
+ key: sessionKey,
339
+ sessionId: sessionId.slice(0, 20) + "...",
340
+ });
341
+ const sessionTokenData = (await delegationStorage.get(sessionKey, "json"));
342
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 2: Session-scoped lookup result:", {
343
+ found: !!sessionTokenData,
344
+ hasDelegationToken: !!sessionTokenData?.delegationToken,
345
+ tokenLength: sessionTokenData?.delegationToken?.length || 0,
346
+ });
347
+ if (sessionTokenData?.delegationToken) {
348
+ delegationToken = sessionTokenData.delegationToken;
349
+ }
350
+ }
351
+ // PRIORITY 3: Agent-scoped token (legacy fallback - DEPRECATED)
352
+ // Only use when userDid is unavailable (backward compatibility)
353
+ if (!delegationToken && !userDid) {
354
+ const agentKey = STORAGE_KEYS.legacyDelegation(agentDid);
355
+ console.warn("[MCPICloudflareAgent] ⚠️ DEPRECATION: Using agent-scoped token (legacy format). Migrate to user+agent scoped tokens for proper user isolation.", {
356
+ key: agentKey,
357
+ agentDid: agentDid.slice(0, 20) + "...",
358
+ reason: "userDid unavailable",
359
+ });
360
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 3: Checking agent-scoped key (legacy):", {
361
+ key: agentKey,
362
+ agentDid: agentDid.slice(0, 20) + "...",
363
+ });
364
+ delegationToken = await delegationStorage.get(agentKey, "text");
365
+ console.log("[MCPICloudflareAgent] 🔍 PRIORITY 3: Agent-scoped lookup result:", {
366
+ found: !!delegationToken,
367
+ tokenLength: delegationToken?.length || 0,
368
+ });
369
+ }
370
+ if (delegationToken) {
371
+ console.log("[MCPICloudflareAgent] ✅ Delegation token retrieved:", {
372
+ sessionId: sessionId.slice(0, 20) + "...",
373
+ tokenLength: delegationToken.length,
374
+ source: "kv-storage",
375
+ });
376
+ }
377
+ else {
378
+ console.log("[MCPICloudflareAgent] ⚠️ No delegation token found in KV storage:", {
379
+ sessionId: sessionId.slice(0, 20) + "...",
380
+ agentDid: agentDid.slice(0, 20) + "...",
381
+ });
382
+ }
383
+ }
384
+ catch (error) {
385
+ console.warn("[MCPICloudflareAgent] Failed to retrieve delegation token:", error);
386
+ }
387
+ }
139
388
  const session = {
140
389
  id: sessionId,
141
390
  audience: "https://kya.vouched.id",
@@ -143,11 +392,259 @@ export class MCPICloudflareAgent extends McpAgent {
143
392
  createdAt: timestamp,
144
393
  expiresAt: timestamp + 30 * 60 * 1000, // 30 minutes
145
394
  serverOrigin: serverUrl, // Use MCP_SERVER_URL for consent URL building
395
+ delegationToken, // ✅ Include token in session if found
396
+ };
397
+ // ✅ NEW: Build tool execution context with IDP token (Phase 1 - MH-7)
398
+ let toolContext;
399
+ try {
400
+ toolContext = await this.buildToolContext(toolName, userDid, sessionId, delegationToken, mappedEnv);
401
+ }
402
+ catch (error) {
403
+ // Handle OAuthRequiredError - build OAuth URL and throw DelegationRequiredError
404
+ // Use property checks instead of instanceof for Cloudflare Workers compatibility
405
+ if (error instanceof Error &&
406
+ (error.name === "OAuthRequiredError" ||
407
+ error.constructor?.name === "OAuthRequiredError" ||
408
+ ("oauthUrl" in error &&
409
+ "provider" in error &&
410
+ "requiredScopes" in error))) {
411
+ try {
412
+ // Type assertion for OAuthRequiredError properties
413
+ const oauthError = error;
414
+ const oauthUrl = await this.buildOAuthUrlForError(oauthError, sessionId, mappedEnv);
415
+ // Generate resume token using runtime's internal method
416
+ // The runtime will generate it when DelegationRequiredError is thrown
417
+ const resumeToken = `resume_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
418
+ // Throw DelegationRequiredError with OAuth URL
419
+ throw new DelegationRequiredError(oauthError.toolName, oauthError.requiredScopes, oauthUrl, // Use OAuth URL as consent URL
420
+ undefined, // interceptedCall
421
+ resumeToken);
422
+ }
423
+ catch (oauthError) {
424
+ // If OAuth URL building fails, re-throw the original error
425
+ throw oauthError instanceof DelegationRequiredError
426
+ ? oauthError
427
+ : error;
428
+ }
429
+ }
430
+ // Log other errors but don't fail tool execution (backward compatibility)
431
+ console.warn("[MCPICloudflareAgent] Failed to build tool context:", error);
432
+ }
433
+ // Wrap handler to pass context as optional second parameter
434
+ // For backward compatibility, handlers can accept (args) or (args, context)
435
+ // If handler doesn't accept context, it will be ignored
436
+ const handlerWithContext = async (handlerArgs) => {
437
+ // Try to call handler with context if available
438
+ // TypeScript function types don't preserve length, so we try both signatures
439
+ try {
440
+ // Try calling with context (for handlers that accept it)
441
+ if (toolContext) {
442
+ return handler(handlerArgs, toolContext);
443
+ }
444
+ else {
445
+ return handler(handlerArgs);
446
+ }
447
+ }
448
+ catch (error) {
449
+ // If handler doesn't accept context parameter, fall back to args-only call
450
+ // This maintains backward compatibility
451
+ return handler(handlerArgs);
452
+ }
146
453
  };
147
454
  // Execute tool with automatic proof generation
148
- const result = await this.mcpiRuntime.processToolCall(toolName, args, handler, session);
455
+ const result = await this.mcpiRuntime.processToolCall(toolName, args, handlerWithContext, session);
149
456
  return result;
150
457
  }
458
+ /**
459
+ * Build tool execution context with IDP token (Phase 1 - MH-7)
460
+ *
461
+ * @private
462
+ */
463
+ async buildToolContext(toolName, userDid, sessionId, delegationToken, env) {
464
+ // Only build context if userDid is available and services are configured
465
+ if (!userDid || !env.AGENTSHIELD_API_KEY || !env.DELEGATION_STORAGE) {
466
+ return undefined;
467
+ }
468
+ // Check if tool protection service is available
469
+ const toolProtectionService = this.mcpiRuntime.config
470
+ ?.toolProtectionService;
471
+ if (!toolProtectionService) {
472
+ return undefined;
473
+ }
474
+ try {
475
+ // Get tool protection configuration
476
+ const agentDid = (await this.mcpiRuntime.getIdentity()).did;
477
+ const protection = await toolProtectionService.checkToolProtection(toolName, agentDid);
478
+ // Only build context if tool requires OAuth
479
+ if (!protection?.requiredScopes?.length) {
480
+ return undefined;
481
+ }
482
+ // Get project ID from tool protection service
483
+ const projectId = toolProtectionService.getProjectId();
484
+ if (!projectId) {
485
+ return undefined;
486
+ }
487
+ // Initialize OAuth services lazily
488
+ const fetchProvider = this.mcpiRuntime.fetch;
489
+ if (!fetchProvider) {
490
+ return undefined;
491
+ }
492
+ // Check if OAuth services are available (registered via oauth-service-registry)
493
+ if (!isOAuthServicesAvailable()) {
494
+ console.warn("[MCPICloudflareAgent] OAuth services not available - skipping tool context build");
495
+ return undefined;
496
+ }
497
+ // Get OAuth services from centralized registry (DRY - single source of truth)
498
+ const { OAuthConfigService: ConfigService, OAuthProviderRegistry: ProviderRegistry, ProviderResolver: Resolver, } = getOAuthServices();
499
+ // Use KV cache for OAuth config if available, otherwise in-memory
500
+ const oauthConfigCache = env.TOOL_PROTECTION_KV
501
+ ? new KVOAuthConfigCache({ kv: env.TOOL_PROTECTION_KV })
502
+ : undefined;
503
+ const oauthConfigService = new ConfigService({
504
+ baseUrl: env.AGENTSHIELD_API_URL || "https://kya.vouched.id",
505
+ apiKey: env.AGENTSHIELD_API_KEY,
506
+ fetchProvider,
507
+ cache: oauthConfigCache,
508
+ });
509
+ const providerRegistry = new ProviderRegistry(oauthConfigService);
510
+ const providerResolver = new Resolver(providerRegistry, oauthConfigService);
511
+ const oauthService = new OAuthService({
512
+ configService: oauthConfigService,
513
+ fetchProvider,
514
+ agentShieldApiUrl: env.AGENTSHIELD_API_URL || "https://kya.vouched.id",
515
+ agentShieldApiKey: env.AGENTSHIELD_API_KEY,
516
+ projectId,
517
+ });
518
+ const oauthSecurityService = new OAuthSecurityService(env.DELEGATION_STORAGE, env.OAUTH_ENCRYPTION_SECRET);
519
+ // Only create IdpTokenStorage if DELEGATION_STORAGE is available
520
+ if (!env.DELEGATION_STORAGE) {
521
+ console.warn("[MCPICloudflareAgent] DELEGATION_STORAGE not configured, skipping IDP token storage");
522
+ return undefined;
523
+ }
524
+ const idpTokenStorage = new IdpTokenStorage({
525
+ storage: env.DELEGATION_STORAGE,
526
+ oauthSecurityService,
527
+ });
528
+ const tokenResolver = new IdpTokenResolver({
529
+ tokenStorage: idpTokenStorage,
530
+ oauthService,
531
+ });
532
+ const contextBuilder = new ToolContextBuilder({
533
+ tokenResolver,
534
+ configService: oauthConfigService,
535
+ providerResolver: providerResolver, // Phase 2: Pass ProviderResolver (type assertion for dist/src compatibility)
536
+ projectId,
537
+ });
538
+ // Build context
539
+ return await contextBuilder.buildContext(toolName, userDid, sessionId, delegationToken, protection);
540
+ }
541
+ catch (error) {
542
+ // Re-throw OAuthRequiredError so it can be handled by executeToolWithProof
543
+ // Use property checks instead of instanceof for Cloudflare Workers compatibility
544
+ if (error instanceof Error &&
545
+ (error.name === "OAuthRequiredError" ||
546
+ error.constructor?.name === "OAuthRequiredError" ||
547
+ ("oauthUrl" in error &&
548
+ "provider" in error &&
549
+ "requiredScopes" in error))) {
550
+ throw error;
551
+ }
552
+ console.warn("[MCPICloudflareAgent] Failed to build tool context:", error);
553
+ return undefined;
554
+ }
555
+ }
556
+ /**
557
+ * Build OAuth URL for OAuthRequiredError
558
+ *
559
+ * Creates OAuth authorization URL with PKCE challenge for secure token exchange.
560
+ *
561
+ * @private
562
+ */
563
+ async buildOAuthUrlForError(error, sessionId, env) {
564
+ const agentShieldUrl = env.AGENTSHIELD_API_URL || "https://kya.vouched.id";
565
+ const toolProtectionService = this.mcpiRuntime.config
566
+ ?.toolProtectionService;
567
+ const projectId = toolProtectionService?.getProjectId();
568
+ if (!projectId) {
569
+ throw new Error("Project ID not available for OAuth URL building");
570
+ }
571
+ // Get server URL
572
+ let serverUrl = env.MCP_SERVER_URL;
573
+ if (serverUrl &&
574
+ !serverUrl.startsWith("http://") &&
575
+ !serverUrl.startsWith("https://")) {
576
+ serverUrl = `https://${serverUrl}`;
577
+ }
578
+ if (!serverUrl) {
579
+ throw new Error("MCP_SERVER_URL not configured for OAuth callback");
580
+ }
581
+ // TypeScript: projectId is guaranteed to be defined after the check above
582
+ // Use non-null assertion since we've already validated it
583
+ const safeProjectId = projectId;
584
+ const agentDid = (await this.mcpiRuntime.getIdentity()).did;
585
+ // Generate PKCE challenge if OAuthSecurityService is available
586
+ let codeChallenge;
587
+ let stateParam;
588
+ if (env.DELEGATION_STORAGE && env.OAUTH_ENCRYPTION_SECRET) {
589
+ try {
590
+ const oauthSecurityService = new OAuthSecurityService(env.DELEGATION_STORAGE, env.OAUTH_ENCRYPTION_SECRET);
591
+ const pkceChallenge = await oauthSecurityService.generatePKCEChallenge();
592
+ codeChallenge = pkceChallenge.challenge;
593
+ // Build state data
594
+ const stateData = {
595
+ project_id: safeProjectId,
596
+ agent_did: agentDid,
597
+ session_id: sessionId || "",
598
+ scopes: error.requiredScopes,
599
+ storedAt: Date.now(),
600
+ code_verifier: pkceChallenge.verifier,
601
+ code_challenge: pkceChallenge.challenge,
602
+ redirect_uri: `${serverUrl}/oauth/callback`,
603
+ };
604
+ // Store state securely
605
+ const randomBytes = crypto.getRandomValues(new Uint8Array(32));
606
+ const stateValue = btoa(String.fromCharCode(...randomBytes))
607
+ .replace(/\+/g, "-")
608
+ .replace(/\//g, "_")
609
+ .replace(/=/g, "");
610
+ await oauthSecurityService.storeOAuthState(stateValue, stateData, 600);
611
+ stateParam = stateValue;
612
+ }
613
+ catch (err) {
614
+ console.warn("[MCPICloudflareAgent] Failed to generate PKCE challenge, using insecure state:", err);
615
+ // Fallback to insecure state encoding
616
+ const stateData = {
617
+ project_id: safeProjectId,
618
+ agent_did: agentDid,
619
+ session_id: sessionId || "",
620
+ scopes: error.requiredScopes,
621
+ };
622
+ stateParam = btoa(JSON.stringify(stateData));
623
+ }
624
+ }
625
+ else {
626
+ // Fallback to insecure state encoding
627
+ const stateData = {
628
+ project_id: safeProjectId,
629
+ agent_did: agentDid,
630
+ session_id: sessionId || "",
631
+ scopes: error.requiredScopes,
632
+ };
633
+ stateParam = btoa(JSON.stringify(stateData));
634
+ }
635
+ // Build OAuth authorization URL
636
+ const oauthUrl = new URL(`${agentShieldUrl}/bouncer/oauth/authorize`);
637
+ oauthUrl.searchParams.set("response_type", "code");
638
+ oauthUrl.searchParams.set("client_id", safeProjectId);
639
+ oauthUrl.searchParams.set("redirect_uri", `${serverUrl}/oauth/callback`);
640
+ oauthUrl.searchParams.set("scope", error.requiredScopes.join(" "));
641
+ oauthUrl.searchParams.set("state", stateParam);
642
+ if (codeChallenge) {
643
+ oauthUrl.searchParams.set("code_challenge", codeChallenge);
644
+ oauthUrl.searchParams.set("code_challenge_method", "S256");
645
+ }
646
+ return oauthUrl.toString();
647
+ }
151
648
  /**
152
649
  * Get Durable Object instance ID based on routing strategy
153
650
  */
@@ -176,5 +673,132 @@ export class MCPICloudflareAgent extends McpAgent {
176
673
  return "default";
177
674
  }
178
675
  }
676
+ /**
677
+ * Handle incoming fetch requests to the Durable Object
678
+ *
679
+ * Intercepts internal cache-clear requests and routes them to clearToolProtectionCache().
680
+ * All other requests are passed to the parent McpAgent class.
681
+ *
682
+ * This is CRITICAL for fixing the KV edge caching issue:
683
+ * - The admin endpoint (HTTP handler) runs at one edge location
684
+ * - The DO (where tool calls run) is pinned to a different edge location
685
+ * - KV.delete() only clears the global store, not edge caches
686
+ * - By routing cache-clear through the DO, we ensure the cache is cleared
687
+ * and warmed at the same edge location where tool calls execute
688
+ */
689
+ async fetch(request) {
690
+ const url = new URL(request.url);
691
+ // Handle internal cache-clear request
692
+ if (url.pathname === "/_do/cache-clear" && request.method === "POST") {
693
+ console.log("[MCPICloudflareAgent] Handling internal cache-clear request");
694
+ try {
695
+ const result = await this.clearToolProtectionCache();
696
+ return new Response(JSON.stringify({
697
+ success: result.success,
698
+ data: {
699
+ message: result.success
700
+ ? `Cache cleared and refreshed. ${result.toolCount} tool(s), ${result.protectedTools.length} protected: ${result.protectedTools.join(", ") || "none"}`
701
+ : result.error || "Failed to clear cache",
702
+ cache_cleared: result.cacheCleared,
703
+ config_refreshed: result.configRefreshed,
704
+ tool_count: result.toolCount,
705
+ protected_tools: result.protectedTools,
706
+ },
707
+ }), {
708
+ status: result.success ? 200 : 500,
709
+ headers: { "Content-Type": "application/json" },
710
+ });
711
+ }
712
+ catch (error) {
713
+ console.error("[MCPICloudflareAgent] Cache-clear request failed:", error);
714
+ return new Response(JSON.stringify({
715
+ success: false,
716
+ error: error instanceof Error ? error.message : String(error),
717
+ }), { status: 500, headers: { "Content-Type": "application/json" } });
718
+ }
719
+ }
720
+ // Pass all other requests to parent class (McpAgent handles SSE/WebSocket)
721
+ return super.fetch(request);
722
+ }
723
+ /**
724
+ * Clear tool protection cache and refresh from API
725
+ *
726
+ * This method is designed to be called from within the Durable Object itself,
727
+ * ensuring that the cache clear operation happens at the same edge location
728
+ * where the DO is pinned. This bypasses KV's edge caching issues.
729
+ *
730
+ * Flow:
731
+ * 1. Admin endpoint routes cache-clear request to this DO
732
+ * 2. DO calls clearToolProtectionCache()
733
+ * 3. ToolProtectionService clears KV cache and fetches fresh config
734
+ * 4. Fresh config is written to KV (warming the edge cache for this location)
735
+ * 5. Subsequent tool calls from this DO get the fresh config
736
+ *
737
+ * @returns Result of cache clear operation
738
+ */
739
+ async clearToolProtectionCache() {
740
+ console.log("[MCPICloudflareAgent] clearToolProtectionCache called");
741
+ try {
742
+ // Get the ToolProtectionService from the runtime
743
+ const toolProtectionService = this.mcpiRuntime?.config
744
+ ?.toolProtectionService;
745
+ if (!toolProtectionService) {
746
+ console.warn("[MCPICloudflareAgent] No tool protection service available");
747
+ return {
748
+ success: false,
749
+ cacheCleared: false,
750
+ configRefreshed: false,
751
+ toolCount: 0,
752
+ protectedTools: [],
753
+ error: "Tool protection service not available",
754
+ };
755
+ }
756
+ // Get agent DID for cache key
757
+ const agentDid = (await this.mcpiRuntime.getIdentity()).did;
758
+ // Use clearAndRefresh if available (mcp-i-core >= 1.2.3)
759
+ if (typeof toolProtectionService.clearAndRefresh === "function") {
760
+ const result = await toolProtectionService.clearAndRefresh(agentDid);
761
+ const protectedTools = Object.entries(result.config.toolProtections || {})
762
+ .filter(([_, cfg]) => cfg.requiresDelegation)
763
+ .map(([name]) => name);
764
+ console.log("[MCPICloudflareAgent] Cache cleared and refreshed", {
765
+ cacheKey: result.cacheKey,
766
+ source: result.source,
767
+ toolCount: Object.keys(result.config.toolProtections || {}).length,
768
+ protectedTools,
769
+ });
770
+ return {
771
+ success: true,
772
+ cacheCleared: true,
773
+ configRefreshed: true,
774
+ toolCount: Object.keys(result.config.toolProtections || {}).length,
775
+ protectedTools,
776
+ };
777
+ }
778
+ else {
779
+ // Fallback for older mcp-i-core versions: just clear cache
780
+ await toolProtectionService.clearCache(agentDid);
781
+ console.log("[MCPICloudflareAgent] Cache cleared (legacy mode - no refresh)");
782
+ return {
783
+ success: true,
784
+ cacheCleared: true,
785
+ configRefreshed: false,
786
+ toolCount: 0,
787
+ protectedTools: [],
788
+ };
789
+ }
790
+ }
791
+ catch (error) {
792
+ console.error("[MCPICloudflareAgent] clearToolProtectionCache failed:", error);
793
+ return {
794
+ success: false,
795
+ cacheCleared: false,
796
+ configRefreshed: false,
797
+ toolCount: 0,
798
+ protectedTools: [],
799
+ error: error instanceof Error ? error.message : String(error),
800
+ };
801
+ }
802
+ }
179
803
  }
180
804
  //# sourceMappingURL=agent.js.map