@kya-os/mcp-i-core 1.4.18 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -0
  3. package/dist/auth/handshake.d.ts +119 -0
  4. package/dist/auth/handshake.js +250 -0
  5. package/dist/auth/index.d.ts +6 -0
  6. package/dist/auth/index.js +11 -0
  7. package/dist/auth/types.d.ts +46 -0
  8. package/dist/auth/types.js +10 -0
  9. package/dist/delegation/index.d.ts +1 -0
  10. package/dist/delegation/index.js +1 -0
  11. package/dist/delegation/outbound-proof.d.ts +70 -0
  12. package/dist/delegation/outbound-proof.js +67 -0
  13. package/dist/identity/user-did-manager.js +5 -3
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +25 -2
  16. package/dist/proof/generator.d.ts +109 -0
  17. package/dist/proof/generator.js +236 -0
  18. package/dist/proof/index.d.ts +5 -0
  19. package/dist/proof/index.js +11 -0
  20. package/dist/providers/base.d.ts +5 -1
  21. package/dist/runtime/base.d.ts +127 -13
  22. package/dist/runtime/base.js +199 -51
  23. package/dist/runtime/ext-apps-constants.d.ts +14 -0
  24. package/dist/runtime/ext-apps-constants.js +17 -0
  25. package/dist/services/batch-delegation.service.d.ts +1 -1
  26. package/dist/services/batch-delegation.service.js +4 -4
  27. package/dist/services/proof-verifier.js +1 -1
  28. package/dist/session/index.d.ts +5 -0
  29. package/dist/session/index.js +11 -0
  30. package/dist/session/manager.d.ts +113 -0
  31. package/dist/session/manager.js +273 -0
  32. package/docs/API_REFERENCE.md +76 -0
  33. package/docs/COMPLIANCE_MATRIX.md +691 -0
  34. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  35. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  36. package/package.json +22 -6
  37. package/vitest.config.mts +8 -7
@@ -8,9 +8,13 @@
8
8
  import { CryptoProvider, ClockProvider, FetchProvider, StorageProvider, NonceCacheProvider, IdentityProvider, AgentIdentity } from "../providers/base";
9
9
  import { type Ed25519JWK } from "../services/crypto.service.js";
10
10
  import { ProofVerifier } from "../services/proof-verifier.js";
11
+ import type { DetachedProof } from "@kya-os/contracts/proof";
12
+ import type { HandshakeRequest } from "@kya-os/contracts/handshake";
11
13
  import type { MCPIdentity, WellKnownConfig, WellKnownResponse } from "@kya-os/contracts/well-known";
12
14
  import type { AccessControlApiService } from "../services/access-control.service.js";
13
15
  import type { ProviderRuntimeConfig } from "../config";
16
+ import { type OAuthIdentity } from "../identity/user-did-manager";
17
+ import type { IAuditLogger } from "./audit-logger.js";
14
18
  /**
15
19
  * Interface for runtime instances that have AccessControlApiService available
16
20
  * This allows type-safe access to the access control service without using `as any`
@@ -20,6 +24,65 @@ import type { ProviderRuntimeConfig } from "../config";
20
24
  export interface RuntimeWithAccessControl {
21
25
  accessControlService?: AccessControlApiService;
22
26
  }
27
+ type RuntimeRecord = Record<string, unknown>;
28
+ type RuntimeHandshakeRequest = Partial<HandshakeRequest> & RuntimeRecord & {
29
+ audience?: string;
30
+ agentDid?: string;
31
+ clientInfo?: RuntimeRecord;
32
+ clientProtocolVersion?: unknown;
33
+ clientCapabilities?: unknown;
34
+ clientDid?: string;
35
+ oauthIdentity?: OAuthIdentity | null;
36
+ };
37
+ interface RuntimeHandshakeResponse {
38
+ sessionId: string;
39
+ agentDid: string;
40
+ timestamp: number;
41
+ capabilities: string[];
42
+ userDid?: string;
43
+ signature: string;
44
+ }
45
+ interface RuntimeSessionContext {
46
+ id?: string;
47
+ audience?: string;
48
+ createdAt?: number;
49
+ expiresAt?: number;
50
+ clientDid?: string;
51
+ userDid?: string;
52
+ agentDid?: string;
53
+ serverDid?: string;
54
+ nonce?: string;
55
+ delegationToken?: string;
56
+ consentProof?: string;
57
+ projectId?: string;
58
+ serverOrigin?: string;
59
+ userAgent?: string;
60
+ clientId?: string;
61
+ agentName?: string;
62
+ clientInfo?: Record<string, unknown>;
63
+ identityState?: "anonymous" | "authenticated";
64
+ oauthIdentity?: OAuthIdentity;
65
+ canonicalPayload?: string;
66
+ [key: string]: unknown;
67
+ }
68
+ interface StoredRuntimeSession extends RuntimeSessionContext {
69
+ id: string;
70
+ createdAt: number;
71
+ expiresAt: number;
72
+ }
73
+ interface LegacyProof {
74
+ timestamp: number;
75
+ nonce: string;
76
+ did: string;
77
+ signature: string;
78
+ algorithm: "Ed25519";
79
+ sessionId?: string;
80
+ audience?: string;
81
+ }
82
+ type RuntimeProof = LegacyProof | DetachedProof;
83
+ type RuntimeAuditLogger = IAuditLogger | {
84
+ log: (event: string, data: unknown) => void;
85
+ };
23
86
  export declare class MCPIRuntimeBase {
24
87
  protected crypto: CryptoProvider;
25
88
  protected clock: ClockProvider;
@@ -36,6 +99,12 @@ export declare class MCPIRuntimeBase {
36
99
  private cryptoService?;
37
100
  protected proofVerifier?: ProofVerifier;
38
101
  protected accessControlService?: AccessControlApiService;
102
+ /**
103
+ * Cached flag indicating whether @modelcontextprotocol/ext-apps is available.
104
+ * Set by subclasses after a successful dynamic import (avoids require() vs ESM issues).
105
+ * When undefined, falls back to synchronous require() probe.
106
+ */
107
+ private _extAppsAvailable?;
39
108
  constructor(config: ProviderRuntimeConfig);
40
109
  /**
41
110
  * Initialize the runtime
@@ -56,9 +125,7 @@ export declare class MCPIRuntimeBase {
56
125
  * @param request - Handshake request object (may include oauthIdentity for persistent user DID lookup)
57
126
  * @returns Handshake response with session ID and agent DID
58
127
  */
59
- handleHandshake(request: any & {
60
- oauthIdentity?: import("../identity/user-did-manager").OAuthIdentity | null;
61
- }): Promise<any>;
128
+ handleHandshake(request: RuntimeHandshakeRequest): Promise<RuntimeHandshakeResponse>;
62
129
  /**
63
130
  * Update session identity after OAuth resolution (Phase 5)
64
131
  *
@@ -78,7 +145,7 @@ export declare class MCPIRuntimeBase {
78
145
  /**
79
146
  * Get session by ID
80
147
  */
81
- getSession(sessionId: string): any | undefined;
148
+ getSession(sessionId: string): StoredRuntimeSession | undefined;
82
149
  /**
83
150
  * Extract the correct provider for consent URL from tool protection config
84
151
  *
@@ -101,7 +168,7 @@ export declare class MCPIRuntimeBase {
101
168
  * @param handler - Tool execution handler
102
169
  * @param session - Session context (expected fields: id, audience, nonce?, delegationToken?, consentProof?)
103
170
  */
104
- processToolCall(toolName: string, args: any, handler: (args: any) => Promise<any>, session?: any): Promise<any>;
171
+ processToolCall<TArgs = unknown, TResult = unknown>(toolName: string, args: TArgs, handler: (args: TArgs) => Promise<TResult>, session?: RuntimeSessionContext): Promise<TResult | RuntimeRecord>;
105
172
  /**
106
173
  * Resume a tool call after authorization
107
174
  *
@@ -110,9 +177,12 @@ export declare class MCPIRuntimeBase {
110
177
  * @param delegationToken - Delegation token from authorization
111
178
  * @returns Tool execution result
112
179
  */
113
- resumeToolCall(resumeToken: string, handler: (args: any) => Promise<any>, delegationToken?: string): Promise<any>;
180
+ resumeToolCall(resumeToken: string, handler: (args: unknown) => Promise<unknown>, delegationToken?: string): Promise<unknown>;
114
181
  /**
115
182
  * Generate a resume token for intercepted tool call
183
+ *
184
+ * Uses cryptographically secure random bytes for token generation.
185
+ * The token is used as an opaque lookup key in the interceptedCalls Map.
116
186
  */
117
187
  private generateResumeToken;
118
188
  /**
@@ -139,7 +209,50 @@ export declare class MCPIRuntimeBase {
139
209
  * @param provider - Provider name (e.g., "github", "credentials") to select specific auth method
140
210
  * @returns Full consent URL with snake_case parameters
141
211
  */
142
- protected buildConsentUrl(toolName: string, scopes: string[], session?: any, resumeToken?: string, projectId?: string, provider?: string): string;
212
+ /**
213
+ * MCP Apps UI consent resource URI.
214
+ * Single shared resource for all consent flows - tool-specific data
215
+ * is passed via structuredContent in the tool result.
216
+ */
217
+ protected static readonly CONSENT_UI_RESOURCE_URI = "ui://mcpi-consent/authorize";
218
+ /**
219
+ * Check if the connected MCP client supports MCP Apps UI rendering.
220
+ * Returns false if @modelcontextprotocol/ext-apps is not installed
221
+ * or the client didn't advertise the io.modelcontextprotocol/ui capability.
222
+ *
223
+ * Override in subclasses to provide client capabilities from the MCP session.
224
+ */
225
+ protected hasUICapability(clientCapabilities?: Record<string, unknown>): boolean;
226
+ /**
227
+ * Mark the ext-apps SDK as available.
228
+ * Called by subclasses after a successful dynamic import() of the SDK,
229
+ * which avoids the CJS require() vs ESM incompatibility that causes
230
+ * the synchronous probe to fail in Cloudflare Workers.
231
+ */
232
+ setExtAppsAvailable(available: boolean): void;
233
+ /**
234
+ * Check if the MCP Apps inline consent UI is enabled.
235
+ * Requires BOTH conditions:
236
+ * 1. The SDK is available (set via setExtAppsAvailable(true) after dynamic import)
237
+ * 2. The config flag is enabled (config.inlineConsent === true OR MCPI_INLINE_CONSENT env var)
238
+ * This allows merging the feature while keeping it off by default.
239
+ */
240
+ protected isExtAppsAvailable(): boolean;
241
+ /**
242
+ * Build an MCP Apps-compatible tool result that renders an inline consent UI.
243
+ * Returns null when the host doesn't support MCP Apps or the SDK is unavailable,
244
+ * allowing callers to fall back to DelegationRequiredError.
245
+ *
246
+ * @param toolName - Tool requiring delegation
247
+ * @param scopes - Required scopes
248
+ * @param session - Current session context
249
+ * @param resumeToken - Token to resume after delegation
250
+ * @param projectId - AgentShield project ID
251
+ * @param serverUrl - Server URL for consent approval POST
252
+ * @param clientCapabilities - MCP client capabilities from the session
253
+ */
254
+ buildConsentUIResult(toolName: string, scopes: string[], session?: Record<string, unknown>, resumeToken?: string, projectId?: string, serverUrl?: string, clientCapabilities?: Record<string, unknown>, authMode?: "consent-only" | "credentials", provider?: string): Record<string, unknown> | null;
255
+ protected buildConsentUrl(toolName: string, scopes: string[], session?: RuntimeSessionContext, resumeToken?: string, projectId?: string, provider?: string): string;
143
256
  /**
144
257
  * Issue a new nonce and register it in the cache
145
258
  * Use this to get a nonce for the session context before calling processToolCall
@@ -148,7 +261,7 @@ export declare class MCPIRuntimeBase {
148
261
  /**
149
262
  * Create cryptographic proof for data
150
263
  */
151
- createProof(data: any, session?: any): Promise<any>;
264
+ createProof(data: unknown, session?: RuntimeSessionContext): Promise<RuntimeProof>;
152
265
  /**
153
266
  * Verify a proof
154
267
  *
@@ -159,7 +272,7 @@ export declare class MCPIRuntimeBase {
159
272
  * @param proofOrSession - Either proof object (old format) or session context (new format)
160
273
  * @returns true if proof is valid, false otherwise
161
274
  */
162
- verifyProof(dataOrProof: any, proofOrSession?: any): Promise<boolean>;
275
+ verifyProof(dataOrProof: unknown, proofOrSession?: unknown): Promise<boolean>;
163
276
  /**
164
277
  * Legacy proof verification (backward compatibility)
165
278
  * @internal
@@ -180,11 +293,11 @@ export declare class MCPIRuntimeBase {
180
293
  /**
181
294
  * Get current session
182
295
  */
183
- getCurrentSession(): Promise<any>;
296
+ getCurrentSession(): Promise<StoredRuntimeSession | null>;
184
297
  /**
185
298
  * Get the last generated proof for out-of-band transport
186
299
  */
187
- getLastProof(): any;
300
+ getLastProof(): RuntimeProof | undefined;
188
301
  /**
189
302
  * Create well-known handler for identity verification
190
303
  */
@@ -192,11 +305,11 @@ export declare class MCPIRuntimeBase {
192
305
  /**
193
306
  * Create debug endpoint (development only)
194
307
  */
195
- createDebugEndpoint(): any;
308
+ createDebugEndpoint(): (() => Promise<RuntimeRecord>) | null;
196
309
  /**
197
310
  * Get audit logger
198
311
  */
199
- getAuditLogger(): any;
312
+ getAuditLogger(): RuntimeAuditLogger | undefined;
200
313
  /**
201
314
  * Rotate keys
202
315
  */
@@ -258,4 +371,5 @@ export declare class MCPIRuntimeBase {
258
371
  private base64ToBytes;
259
372
  private bytesToHex;
260
373
  }
374
+ export {};
261
375
  //# sourceMappingURL=base.d.ts.map
@@ -13,6 +13,7 @@ const crypto_service_js_1 = require("../services/crypto.service.js");
13
13
  const access_control_service_js_1 = require("../services/access-control.service.js");
14
14
  const agentshield_api_1 = require("@kya-os/contracts/agentshield-api");
15
15
  const user_did_manager_1 = require("../identity/user-did-manager");
16
+ const isRecord = (value) => typeof value === "object" && value !== null;
16
17
  class MCPIRuntimeBase {
17
18
  crypto;
18
19
  clock;
@@ -29,6 +30,12 @@ class MCPIRuntimeBase {
29
30
  cryptoService;
30
31
  proofVerifier; // Optional ProofVerifier (injected by subclasses)
31
32
  accessControlService; // Optional AccessControlApiService (injected by subclasses)
33
+ /**
34
+ * Cached flag indicating whether @modelcontextprotocol/ext-apps is available.
35
+ * Set by subclasses after a successful dynamic import (avoids require() vs ESM issues).
36
+ * When undefined, falls back to synchronous require() probe.
37
+ */
38
+ _extAppsAvailable;
32
39
  constructor(config) {
33
40
  this.config = config;
34
41
  this.crypto = config.cryptoProvider;
@@ -63,9 +70,9 @@ class MCPIRuntimeBase {
63
70
  });
64
71
  }
65
72
  // Initialize nonce cache if it has an initialize method
66
- if ("initialize" in this.nonceCache &&
67
- typeof this.nonceCache.initialize === "function") {
68
- await this.nonceCache.initialize();
73
+ const nonceCacheWithInitialize = this.nonceCache;
74
+ if (typeof nonceCacheWithInitialize.initialize === "function") {
75
+ await nonceCacheWithInitialize.initialize();
69
76
  }
70
77
  // Log initialization if audit is enabled
71
78
  if (this.config.audit?.enabled) {
@@ -254,7 +261,10 @@ class MCPIRuntimeBase {
254
261
  auth.type === "oauth2" ||
255
262
  auth.type === "password" ||
256
263
  auth.type === "idv") {
257
- return auth.provider;
264
+ // Return auth.provider if set, otherwise fall through to legacy oauthProvider
265
+ if (auth.provider) {
266
+ return auth.provider;
267
+ }
258
268
  }
259
269
  // Other types don't have provider (none, mdl, verifiable_credential, webauthn, siwe)
260
270
  // Fall through to legacy oauthProvider
@@ -298,7 +308,7 @@ class MCPIRuntimeBase {
298
308
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
299
309
  };
300
310
  // Generate resume token
301
- const resumeToken = this.generateResumeToken(interceptedCall);
311
+ const resumeToken = await this.generateResumeToken(interceptedCall);
302
312
  // Build consent URL with resume token
303
313
  // Note: projectId is not available in base class - subclasses should override buildConsentUrl
304
314
  // Pass oauthProvider to ensure correct auth method is selected (e.g., "credentials" vs "github")
@@ -320,6 +330,16 @@ class MCPIRuntimeBase {
320
330
  consentUrl,
321
331
  });
322
332
  }
333
+ // Try MCP Apps inline consent UI before falling back to error
334
+ // Client capabilities are stored under session.clientInfo.capabilities (set during handleHandshake)
335
+ const clientInfo = session?.clientInfo;
336
+ const clientCaps = isRecord(clientInfo?.capabilities)
337
+ ? clientInfo.capabilities
338
+ : undefined;
339
+ const uiResult = this.buildConsentUIResult(toolName, protection.requiredScopes, session, resumeToken, session?.projectId || undefined, session?.serverOrigin || undefined, clientCaps);
340
+ if (uiResult) {
341
+ return uiResult;
342
+ }
323
343
  throw error;
324
344
  }
325
345
  // Delegation provided - verify it with AccessControlApiService
@@ -411,7 +431,7 @@ class MCPIRuntimeBase {
411
431
  timestamp: this.clock.now(),
412
432
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
413
433
  };
414
- const resumeToken = this.generateResumeToken(interceptedCall);
434
+ const resumeToken = await this.generateResumeToken(interceptedCall);
415
435
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
416
436
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
417
437
  );
@@ -461,7 +481,7 @@ class MCPIRuntimeBase {
461
481
  timestamp: this.clock.now(),
462
482
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
463
483
  };
464
- const resumeToken = this.generateResumeToken(interceptedCall);
484
+ const resumeToken = await this.generateResumeToken(interceptedCall);
465
485
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
466
486
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
467
487
  );
@@ -525,7 +545,7 @@ class MCPIRuntimeBase {
525
545
  timestamp: this.clock.now(),
526
546
  expiresAt: this.clock.calculateExpiry(1800),
527
547
  };
528
- const resumeToken = this.generateResumeToken(interceptedCall);
548
+ const resumeToken = await this.generateResumeToken(interceptedCall);
529
549
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
530
550
  this.interceptedCalls.set(resumeToken, interceptedCall);
531
551
  this.cleanupExpiredInterceptedCalls();
@@ -533,7 +553,14 @@ class MCPIRuntimeBase {
533
553
  }
534
554
  // Both tool and delegation have authorization - compare them
535
555
  if (!(0, access_control_service_js_1.authorizationMatches)(delegationAuth, toolAuth)) {
536
- const authMismatchReason = `Authorization method mismatch: delegation has ${delegationAuth.type}${delegationAuth.provider ? `:${delegationAuth.provider}` : ""}${delegationAuth.credentialType ? `:${delegationAuth.credentialType}` : ""} but tool requires ${toolAuth.type}${toolAuth.provider ? `:${toolAuth.provider}` : ""}${toolAuth.credentialType ? `:${toolAuth.credentialType}` : ""}`;
556
+ const toolAuthRecord = toolAuth;
557
+ const toolAuthProvider = typeof toolAuthRecord.provider === "string"
558
+ ? `:${toolAuthRecord.provider}`
559
+ : "";
560
+ const toolAuthCredentialType = typeof toolAuthRecord.credentialType === "string"
561
+ ? `:${toolAuthRecord.credentialType}`
562
+ : "";
563
+ const authMismatchReason = `Authorization method mismatch: delegation has ${delegationAuth.type}${delegationAuth.provider ? `:${delegationAuth.provider}` : ""}${delegationAuth.credentialType ? `:${delegationAuth.credentialType}` : ""} but tool requires ${toolAuth.type}${toolAuthProvider}${toolAuthCredentialType}`;
537
564
  if (this.config.audit?.enabled) {
538
565
  console.error("[MCP-I] ❌ Authorization method validation FAILED", {
539
566
  tool: toolName,
@@ -551,7 +578,7 @@ class MCPIRuntimeBase {
551
578
  timestamp: this.clock.now(),
552
579
  expiresAt: this.clock.calculateExpiry(1800),
553
580
  };
554
- const resumeToken = this.generateResumeToken(interceptedCall);
581
+ const resumeToken = await this.generateResumeToken(interceptedCall);
555
582
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
556
583
  this.interceptedCalls.set(resumeToken, interceptedCall);
557
584
  this.cleanupExpiredInterceptedCalls();
@@ -605,7 +632,7 @@ class MCPIRuntimeBase {
605
632
  timestamp: this.clock.now(),
606
633
  expiresAt: this.clock.calculateExpiry(1800),
607
634
  };
608
- const resumeToken = this.generateResumeToken(interceptedCall);
635
+ const resumeToken = await this.generateResumeToken(interceptedCall);
609
636
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
610
637
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
611
638
  );
@@ -618,8 +645,8 @@ class MCPIRuntimeBase {
618
645
  console.error("[MCP-I] ❌ Unexpected error during delegation verification", {
619
646
  tool: toolName,
620
647
  agentDid: identity.did.slice(0, 20) + "...",
621
- error: error.message || String(error),
622
- errorStack: error.stack,
648
+ error: error instanceof Error ? error.message : String(error),
649
+ errorStack: error instanceof Error ? error.stack : undefined,
623
650
  });
624
651
  }
625
652
  // Fail securely - require delegation on unexpected errors
@@ -630,7 +657,7 @@ class MCPIRuntimeBase {
630
657
  timestamp: this.clock.now(),
631
658
  expiresAt: this.clock.calculateExpiry(1800),
632
659
  };
633
- const resumeToken = this.generateResumeToken(interceptedCall);
660
+ const resumeToken = await this.generateResumeToken(interceptedCall);
634
661
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
635
662
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
636
663
  );
@@ -704,23 +731,13 @@ class MCPIRuntimeBase {
704
731
  }
705
732
  /**
706
733
  * Generate a resume token for intercepted tool call
734
+ *
735
+ * Uses cryptographically secure random bytes for token generation.
736
+ * The token is used as an opaque lookup key in the interceptedCalls Map.
707
737
  */
708
- generateResumeToken(call) {
709
- // Create a deterministic token from the call context
710
- const tokenData = JSON.stringify({
711
- tool: call.toolName,
712
- args: call.args,
713
- sessionId: call.sessionId,
714
- timestamp: call.timestamp,
715
- });
716
- // Simple hash-based token (in production, use proper crypto)
717
- let hash = 0;
718
- for (let i = 0; i < tokenData.length; i++) {
719
- const char = tokenData.charCodeAt(i);
720
- hash = (hash << 5) - hash + char;
721
- hash = hash & hash; // Convert to 32bit integer
722
- }
723
- return `resume_${Math.abs(hash).toString(36)}_${Date.now().toString(36)}`;
738
+ async generateResumeToken(_call) {
739
+ const bytes = await this.crypto.randomBytes(16);
740
+ return `resume_${this.bytesToHex(bytes)}`;
724
741
  }
725
742
  /**
726
743
  * Clean up expired intercepted calls
@@ -753,6 +770,114 @@ class MCPIRuntimeBase {
753
770
  * @param provider - Provider name (e.g., "github", "credentials") to select specific auth method
754
771
  * @returns Full consent URL with snake_case parameters
755
772
  */
773
+ /**
774
+ * MCP Apps UI consent resource URI.
775
+ * Single shared resource for all consent flows - tool-specific data
776
+ * is passed via structuredContent in the tool result.
777
+ */
778
+ static CONSENT_UI_RESOURCE_URI = "ui://mcpi-consent/authorize";
779
+ /**
780
+ * Check if the connected MCP client supports MCP Apps UI rendering.
781
+ * Returns false if @modelcontextprotocol/ext-apps is not installed
782
+ * or the client didn't advertise the io.modelcontextprotocol/ui capability.
783
+ *
784
+ * Override in subclasses to provide client capabilities from the MCP session.
785
+ */
786
+ hasUICapability(clientCapabilities) {
787
+ if (!clientCapabilities) {
788
+ console.error("[MCP-I] hasUICapability: no clientCapabilities provided");
789
+ return false;
790
+ }
791
+ try {
792
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
793
+ const { getUiCapability, RESOURCE_MIME_TYPE } = require("@modelcontextprotocol/ext-apps/server");
794
+ const uiCap = getUiCapability(clientCapabilities);
795
+ const hasUI = uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE) ?? false;
796
+ console.error("[MCP-I] hasUICapability check:", {
797
+ hasUI,
798
+ uiCap: uiCap ? JSON.stringify(uiCap) : "null",
799
+ capabilityKeys: Object.keys(clientCapabilities),
800
+ });
801
+ return hasUI;
802
+ }
803
+ catch {
804
+ // ext-apps not installed - graceful degradation
805
+ console.error("[MCP-I] hasUICapability: ext-apps not available");
806
+ return false;
807
+ }
808
+ }
809
+ /**
810
+ * Mark the ext-apps SDK as available.
811
+ * Called by subclasses after a successful dynamic import() of the SDK,
812
+ * which avoids the CJS require() vs ESM incompatibility that causes
813
+ * the synchronous probe to fail in Cloudflare Workers.
814
+ */
815
+ setExtAppsAvailable(available) {
816
+ this._extAppsAvailable = available;
817
+ }
818
+ /**
819
+ * Check if the MCP Apps inline consent UI is enabled.
820
+ * Requires BOTH conditions:
821
+ * 1. The SDK is available (set via setExtAppsAvailable(true) after dynamic import)
822
+ * 2. The config flag is enabled (config.inlineConsent === true OR MCPI_INLINE_CONSENT env var)
823
+ * This allows merging the feature while keeping it off by default.
824
+ */
825
+ isExtAppsAvailable() {
826
+ return this._extAppsAvailable === true && this.config.inlineConsent === true;
827
+ }
828
+ /**
829
+ * Build an MCP Apps-compatible tool result that renders an inline consent UI.
830
+ * Returns null when the host doesn't support MCP Apps or the SDK is unavailable,
831
+ * allowing callers to fall back to DelegationRequiredError.
832
+ *
833
+ * @param toolName - Tool requiring delegation
834
+ * @param scopes - Required scopes
835
+ * @param session - Current session context
836
+ * @param resumeToken - Token to resume after delegation
837
+ * @param projectId - AgentShield project ID
838
+ * @param serverUrl - Server URL for consent approval POST
839
+ * @param clientCapabilities - MCP client capabilities from the session
840
+ */
841
+ buildConsentUIResult(toolName, scopes, session, resumeToken, projectId, serverUrl, clientCapabilities, authMode = "consent-only", provider) {
842
+ // Per the MCP Apps spec, always return UI metadata when the SDK is available.
843
+ // Clients that support MCP Apps will render the inline iframe;
844
+ // clients that don't will use the text content as fallback.
845
+ // The capability check is logged but not gating.
846
+ this.hasUICapability(clientCapabilities);
847
+ if (!this.isExtAppsAvailable())
848
+ return null;
849
+ // Build fallback URL for graceful degradation within the UI itself
850
+ const consentUrl = this.buildConsentUrl(toolName, scopes, session, resumeToken, projectId);
851
+ return {
852
+ content: [
853
+ {
854
+ type: "text",
855
+ text: `Authorization required for tool "${toolName}". Please approve in the consent form below, or open this link to authorize in your browser: ${consentUrl}`,
856
+ },
857
+ ],
858
+ structuredContent: {
859
+ type: "consent_required",
860
+ authMode,
861
+ tool: toolName,
862
+ scopes,
863
+ agentDid: session?.agentDid || "",
864
+ agentName: session?.agentName || "",
865
+ sessionId: session?.id || "",
866
+ projectId: projectId || "",
867
+ resumeToken: resumeToken || "",
868
+ serverUrl: serverUrl || "",
869
+ consentUrl,
870
+ ...(provider ? { provider } : {}),
871
+ },
872
+ _meta: {
873
+ ui: {
874
+ resourceUri: MCPIRuntimeBase.CONSENT_UI_RESOURCE_URI,
875
+ },
876
+ // Legacy flat format for backward compatibility with older clients
877
+ "ui/resourceUri": MCPIRuntimeBase.CONSENT_UI_RESOURCE_URI,
878
+ },
879
+ };
880
+ }
756
881
  buildConsentUrl(toolName, scopes, session, resumeToken, projectId, provider) {
757
882
  // Default implementation - override in subclasses
758
883
  // This URL should point to AgentShield's consent page
@@ -855,7 +980,9 @@ class MCPIRuntimeBase {
855
980
  "meta" in dataOrProof) {
856
981
  // New DetachedProof format
857
982
  const detachedProof = dataOrProof;
858
- const session = proofOrSession;
983
+ const session = isRecord(proofOrSession)
984
+ ? proofOrSession
985
+ : undefined;
859
986
  // Use ProofVerifier if available
860
987
  if (this.proofVerifier) {
861
988
  try {
@@ -883,12 +1010,12 @@ class MCPIRuntimeBase {
883
1010
  else {
884
1011
  // Fallback to old verification if ProofVerifier not available
885
1012
  console.warn("[MCPIRuntimeBase] ProofVerifier not available, using fallback verification");
886
- return this.verifyProofLegacy(dataOrProof, proofOrSession);
1013
+ return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
887
1014
  }
888
1015
  }
889
1016
  else {
890
1017
  // Old format (data, proof)
891
- return this.verifyProofLegacy(dataOrProof, proofOrSession);
1018
+ return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
892
1019
  }
893
1020
  }
894
1021
  /**
@@ -897,33 +1024,41 @@ class MCPIRuntimeBase {
897
1024
  */
898
1025
  async verifyProofLegacy(data, proof) {
899
1026
  try {
1027
+ const nonce = typeof proof.nonce === "string" ? proof.nonce : undefined;
1028
+ const did = typeof proof.did === "string" ? proof.did : undefined;
1029
+ const timestamp = typeof proof.timestamp === "number" ? proof.timestamp : undefined;
1030
+ const signature = typeof proof.signature === "string" ? proof.signature : undefined;
1031
+ const sessionId = typeof proof.sessionId === "string" ? proof.sessionId : undefined;
1032
+ if (!nonce || !did || !timestamp || !signature) {
1033
+ return false;
1034
+ }
900
1035
  // Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
901
- if (await this.nonceCache.has(proof.nonce, proof.did)) {
1036
+ if (await this.nonceCache.has(nonce, did)) {
902
1037
  return false;
903
1038
  }
904
1039
  // Check timestamp is within skew
905
- if (!this.clock.isWithinSkew(proof.timestamp, this.config.session?.timestampSkewSeconds || 120)) {
1040
+ if (!this.clock.isWithinSkew(timestamp, this.config.session?.timestampSkewSeconds || 300)) {
906
1041
  return false;
907
1042
  }
908
1043
  // Resolve DID to get public key
909
- const didDoc = await this.fetch.resolveDID(proof.did);
1044
+ const didDoc = await this.fetch.resolveDID(did);
910
1045
  const publicKey = this.extractPublicKey(didDoc);
911
1046
  // Verify signature
912
1047
  const proofData = {
913
1048
  data,
914
- timestamp: proof.timestamp,
915
- nonce: proof.nonce,
916
- did: proof.did,
917
- sessionId: proof.sessionId,
1049
+ timestamp,
1050
+ nonce,
1051
+ did,
1052
+ sessionId,
918
1053
  };
919
1054
  const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
920
- const signatureBytes = this.base64ToBytes(proof.signature);
1055
+ const signatureBytes = this.base64ToBytes(signature);
921
1056
  const isValid = await this.crypto.verify(dataBytes, signatureBytes, publicKey);
922
1057
  // If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
923
1058
  if (isValid) {
924
1059
  // Pass TTL in seconds, not absolute timestamp
925
1060
  const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
926
- await this.nonceCache.add(proof.nonce, ttlSeconds, proof.did);
1061
+ await this.nonceCache.add(nonce, ttlSeconds, did);
927
1062
  }
928
1063
  return isValid;
929
1064
  }
@@ -1293,13 +1428,18 @@ class MCPIRuntimeBase {
1293
1428
  };
1294
1429
  }
1295
1430
  extractPublicKey(didDoc) {
1296
- const method = didDoc.verificationMethod?.[0];
1431
+ const verificationMethods = isRecord(didDoc)
1432
+ ? didDoc.verificationMethod
1433
+ : undefined;
1434
+ const method = Array.isArray(verificationMethods)
1435
+ ? verificationMethods[0]
1436
+ : undefined;
1297
1437
  if (method?.publicKeyBase64) {
1298
- return method.publicKeyBase64;
1438
+ return String(method.publicKeyBase64);
1299
1439
  }
1300
1440
  if (method?.publicKeyMultibase) {
1301
1441
  // Convert multibase to base64
1302
- return method.publicKeyMultibase; // Simplified
1442
+ return String(method.publicKeyMultibase); // Simplified
1303
1443
  }
1304
1444
  throw new Error("Public key not found in DID document");
1305
1445
  }
@@ -1307,14 +1447,22 @@ class MCPIRuntimeBase {
1307
1447
  * Extract public key JWK from DID document
1308
1448
  */
1309
1449
  extractPublicKeyJwk(didDoc, kid) {
1450
+ const verificationMethods = isRecord(didDoc)
1451
+ ? didDoc.verificationMethod
1452
+ : undefined;
1453
+ const verificationMethodList = Array.isArray(verificationMethods)
1454
+ ? verificationMethods
1455
+ : [];
1310
1456
  // Try to find Ed25519 public key matching kid if provided
1311
- const verificationMethod = didDoc.verificationMethod?.find((vm) => {
1312
- const matchesType = vm.type === "Ed25519VerificationKey2020" ||
1313
- vm.type === "JsonWebKey2020";
1314
- const matchesKid = !kid || vm.id === kid || vm.id.endsWith(`#${kid}`);
1457
+ const verificationMethod = verificationMethodList.find((vm) => {
1458
+ const vmType = typeof vm.type === "string" ? vm.type : undefined;
1459
+ const vmId = typeof vm.id === "string" ? vm.id : undefined;
1460
+ const matchesType = vmType === "Ed25519VerificationKey2020" ||
1461
+ vmType === "JsonWebKey2020";
1462
+ const matchesKid = !kid || vmId === kid || vmId?.endsWith(`#${kid}`);
1315
1463
  return matchesType && matchesKid;
1316
- }) || didDoc.verificationMethod?.[0]; // Fallback to first method
1317
- if (verificationMethod?.publicKeyJwk) {
1464
+ }) || verificationMethodList[0]; // Fallback to first method
1465
+ if (verificationMethod && isRecord(verificationMethod.publicKeyJwk)) {
1318
1466
  const jwk = verificationMethod.publicKeyJwk;
1319
1467
  // Ensure it's Ed25519 format
1320
1468
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {