@kya-os/mcp-i-core 1.4.19 → 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 +195 -50
  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 +21 -5
  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) {
@@ -301,7 +308,7 @@ class MCPIRuntimeBase {
301
308
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
302
309
  };
303
310
  // Generate resume token
304
- const resumeToken = this.generateResumeToken(interceptedCall);
311
+ const resumeToken = await this.generateResumeToken(interceptedCall);
305
312
  // Build consent URL with resume token
306
313
  // Note: projectId is not available in base class - subclasses should override buildConsentUrl
307
314
  // Pass oauthProvider to ensure correct auth method is selected (e.g., "credentials" vs "github")
@@ -323,6 +330,16 @@ class MCPIRuntimeBase {
323
330
  consentUrl,
324
331
  });
325
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
+ }
326
343
  throw error;
327
344
  }
328
345
  // Delegation provided - verify it with AccessControlApiService
@@ -414,7 +431,7 @@ class MCPIRuntimeBase {
414
431
  timestamp: this.clock.now(),
415
432
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
416
433
  };
417
- const resumeToken = this.generateResumeToken(interceptedCall);
434
+ const resumeToken = await this.generateResumeToken(interceptedCall);
418
435
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
419
436
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
420
437
  );
@@ -464,7 +481,7 @@ class MCPIRuntimeBase {
464
481
  timestamp: this.clock.now(),
465
482
  expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
466
483
  };
467
- const resumeToken = this.generateResumeToken(interceptedCall);
484
+ const resumeToken = await this.generateResumeToken(interceptedCall);
468
485
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
469
486
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
470
487
  );
@@ -528,7 +545,7 @@ class MCPIRuntimeBase {
528
545
  timestamp: this.clock.now(),
529
546
  expiresAt: this.clock.calculateExpiry(1800),
530
547
  };
531
- const resumeToken = this.generateResumeToken(interceptedCall);
548
+ const resumeToken = await this.generateResumeToken(interceptedCall);
532
549
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
533
550
  this.interceptedCalls.set(resumeToken, interceptedCall);
534
551
  this.cleanupExpiredInterceptedCalls();
@@ -536,7 +553,14 @@ class MCPIRuntimeBase {
536
553
  }
537
554
  // Both tool and delegation have authorization - compare them
538
555
  if (!(0, access_control_service_js_1.authorizationMatches)(delegationAuth, toolAuth)) {
539
- 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}`;
540
564
  if (this.config.audit?.enabled) {
541
565
  console.error("[MCP-I] ❌ Authorization method validation FAILED", {
542
566
  tool: toolName,
@@ -554,7 +578,7 @@ class MCPIRuntimeBase {
554
578
  timestamp: this.clock.now(),
555
579
  expiresAt: this.clock.calculateExpiry(1800),
556
580
  };
557
- const resumeToken = this.generateResumeToken(interceptedCall);
581
+ const resumeToken = await this.generateResumeToken(interceptedCall);
558
582
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
559
583
  this.interceptedCalls.set(resumeToken, interceptedCall);
560
584
  this.cleanupExpiredInterceptedCalls();
@@ -608,7 +632,7 @@ class MCPIRuntimeBase {
608
632
  timestamp: this.clock.now(),
609
633
  expiresAt: this.clock.calculateExpiry(1800),
610
634
  };
611
- const resumeToken = this.generateResumeToken(interceptedCall);
635
+ const resumeToken = await this.generateResumeToken(interceptedCall);
612
636
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
613
637
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
614
638
  );
@@ -621,8 +645,8 @@ class MCPIRuntimeBase {
621
645
  console.error("[MCP-I] ❌ Unexpected error during delegation verification", {
622
646
  tool: toolName,
623
647
  agentDid: identity.did.slice(0, 20) + "...",
624
- error: error.message || String(error),
625
- errorStack: error.stack,
648
+ error: error instanceof Error ? error.message : String(error),
649
+ errorStack: error instanceof Error ? error.stack : undefined,
626
650
  });
627
651
  }
628
652
  // Fail securely - require delegation on unexpected errors
@@ -633,7 +657,7 @@ class MCPIRuntimeBase {
633
657
  timestamp: this.clock.now(),
634
658
  expiresAt: this.clock.calculateExpiry(1800),
635
659
  };
636
- const resumeToken = this.generateResumeToken(interceptedCall);
660
+ const resumeToken = await this.generateResumeToken(interceptedCall);
637
661
  const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
638
662
  this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
639
663
  );
@@ -707,23 +731,13 @@ class MCPIRuntimeBase {
707
731
  }
708
732
  /**
709
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.
710
737
  */
711
- generateResumeToken(call) {
712
- // Create a deterministic token from the call context
713
- const tokenData = JSON.stringify({
714
- tool: call.toolName,
715
- args: call.args,
716
- sessionId: call.sessionId,
717
- timestamp: call.timestamp,
718
- });
719
- // Simple hash-based token (in production, use proper crypto)
720
- let hash = 0;
721
- for (let i = 0; i < tokenData.length; i++) {
722
- const char = tokenData.charCodeAt(i);
723
- hash = (hash << 5) - hash + char;
724
- hash = hash & hash; // Convert to 32bit integer
725
- }
726
- 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)}`;
727
741
  }
728
742
  /**
729
743
  * Clean up expired intercepted calls
@@ -756,6 +770,114 @@ class MCPIRuntimeBase {
756
770
  * @param provider - Provider name (e.g., "github", "credentials") to select specific auth method
757
771
  * @returns Full consent URL with snake_case parameters
758
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
+ }
759
881
  buildConsentUrl(toolName, scopes, session, resumeToken, projectId, provider) {
760
882
  // Default implementation - override in subclasses
761
883
  // This URL should point to AgentShield's consent page
@@ -858,7 +980,9 @@ class MCPIRuntimeBase {
858
980
  "meta" in dataOrProof) {
859
981
  // New DetachedProof format
860
982
  const detachedProof = dataOrProof;
861
- const session = proofOrSession;
983
+ const session = isRecord(proofOrSession)
984
+ ? proofOrSession
985
+ : undefined;
862
986
  // Use ProofVerifier if available
863
987
  if (this.proofVerifier) {
864
988
  try {
@@ -886,12 +1010,12 @@ class MCPIRuntimeBase {
886
1010
  else {
887
1011
  // Fallback to old verification if ProofVerifier not available
888
1012
  console.warn("[MCPIRuntimeBase] ProofVerifier not available, using fallback verification");
889
- return this.verifyProofLegacy(dataOrProof, proofOrSession);
1013
+ return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
890
1014
  }
891
1015
  }
892
1016
  else {
893
1017
  // Old format (data, proof)
894
- return this.verifyProofLegacy(dataOrProof, proofOrSession);
1018
+ return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
895
1019
  }
896
1020
  }
897
1021
  /**
@@ -900,33 +1024,41 @@ class MCPIRuntimeBase {
900
1024
  */
901
1025
  async verifyProofLegacy(data, proof) {
902
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
+ }
903
1035
  // Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
904
- if (await this.nonceCache.has(proof.nonce, proof.did)) {
1036
+ if (await this.nonceCache.has(nonce, did)) {
905
1037
  return false;
906
1038
  }
907
1039
  // Check timestamp is within skew
908
- if (!this.clock.isWithinSkew(proof.timestamp, this.config.session?.timestampSkewSeconds || 120)) {
1040
+ if (!this.clock.isWithinSkew(timestamp, this.config.session?.timestampSkewSeconds || 300)) {
909
1041
  return false;
910
1042
  }
911
1043
  // Resolve DID to get public key
912
- const didDoc = await this.fetch.resolveDID(proof.did);
1044
+ const didDoc = await this.fetch.resolveDID(did);
913
1045
  const publicKey = this.extractPublicKey(didDoc);
914
1046
  // Verify signature
915
1047
  const proofData = {
916
1048
  data,
917
- timestamp: proof.timestamp,
918
- nonce: proof.nonce,
919
- did: proof.did,
920
- sessionId: proof.sessionId,
1049
+ timestamp,
1050
+ nonce,
1051
+ did,
1052
+ sessionId,
921
1053
  };
922
1054
  const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
923
- const signatureBytes = this.base64ToBytes(proof.signature);
1055
+ const signatureBytes = this.base64ToBytes(signature);
924
1056
  const isValid = await this.crypto.verify(dataBytes, signatureBytes, publicKey);
925
1057
  // If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
926
1058
  if (isValid) {
927
1059
  // Pass TTL in seconds, not absolute timestamp
928
1060
  const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
929
- await this.nonceCache.add(proof.nonce, ttlSeconds, proof.did);
1061
+ await this.nonceCache.add(nonce, ttlSeconds, did);
930
1062
  }
931
1063
  return isValid;
932
1064
  }
@@ -1296,13 +1428,18 @@ class MCPIRuntimeBase {
1296
1428
  };
1297
1429
  }
1298
1430
  extractPublicKey(didDoc) {
1299
- 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;
1300
1437
  if (method?.publicKeyBase64) {
1301
- return method.publicKeyBase64;
1438
+ return String(method.publicKeyBase64);
1302
1439
  }
1303
1440
  if (method?.publicKeyMultibase) {
1304
1441
  // Convert multibase to base64
1305
- return method.publicKeyMultibase; // Simplified
1442
+ return String(method.publicKeyMultibase); // Simplified
1306
1443
  }
1307
1444
  throw new Error("Public key not found in DID document");
1308
1445
  }
@@ -1310,14 +1447,22 @@ class MCPIRuntimeBase {
1310
1447
  * Extract public key JWK from DID document
1311
1448
  */
1312
1449
  extractPublicKeyJwk(didDoc, kid) {
1450
+ const verificationMethods = isRecord(didDoc)
1451
+ ? didDoc.verificationMethod
1452
+ : undefined;
1453
+ const verificationMethodList = Array.isArray(verificationMethods)
1454
+ ? verificationMethods
1455
+ : [];
1313
1456
  // Try to find Ed25519 public key matching kid if provided
1314
- const verificationMethod = didDoc.verificationMethod?.find((vm) => {
1315
- const matchesType = vm.type === "Ed25519VerificationKey2020" ||
1316
- vm.type === "JsonWebKey2020";
1317
- 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}`);
1318
1463
  return matchesType && matchesKid;
1319
- }) || didDoc.verificationMethod?.[0]; // Fallback to first method
1320
- if (verificationMethod?.publicKeyJwk) {
1464
+ }) || verificationMethodList[0]; // Fallback to first method
1465
+ if (verificationMethod && isRecord(verificationMethod.publicKeyJwk)) {
1321
1466
  const jwk = verificationMethod.publicKeyJwk;
1322
1467
  // Ensure it's Ed25519 format
1323
1468
  if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP Apps Extension Constants
3
+ *
4
+ * Shared constants for the MCP Apps consent UI integration.
5
+ * Kept separate from the runtime class so consumers can import
6
+ * without pulling in the full MCPIRuntimeBase dependency.
7
+ */
8
+ /**
9
+ * URI for the shared consent UI resource.
10
+ * All consent-requiring tools reference this single resource;
11
+ * tool-specific data is passed via structuredContent.
12
+ */
13
+ export declare const CONSENT_UI_RESOURCE_URI = "ui://mcpi-consent/authorize";
14
+ //# sourceMappingURL=ext-apps-constants.d.ts.map