@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.
- package/LICENSE +21 -0
- package/README.md +14 -0
- package/dist/auth/handshake.d.ts +119 -0
- package/dist/auth/handshake.js +250 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +11 -0
- package/dist/auth/types.d.ts +46 -0
- package/dist/auth/types.js +10 -0
- package/dist/delegation/index.d.ts +1 -0
- package/dist/delegation/index.js +1 -0
- package/dist/delegation/outbound-proof.d.ts +70 -0
- package/dist/delegation/outbound-proof.js +67 -0
- package/dist/identity/user-did-manager.js +5 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -2
- package/dist/proof/generator.d.ts +109 -0
- package/dist/proof/generator.js +236 -0
- package/dist/proof/index.d.ts +5 -0
- package/dist/proof/index.js +11 -0
- package/dist/providers/base.d.ts +5 -1
- package/dist/runtime/base.d.ts +127 -13
- package/dist/runtime/base.js +195 -50
- package/dist/runtime/ext-apps-constants.d.ts +14 -0
- package/dist/runtime/ext-apps-constants.js +17 -0
- package/dist/services/batch-delegation.service.d.ts +1 -1
- package/dist/services/batch-delegation.service.js +4 -4
- package/dist/services/proof-verifier.js +1 -1
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.js +11 -0
- package/dist/session/manager.d.ts +113 -0
- package/dist/session/manager.js +273 -0
- package/docs/API_REFERENCE.md +76 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +21 -5
- package/vitest.config.mts +8 -7
package/dist/runtime/base.d.ts
CHANGED
|
@@ -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:
|
|
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):
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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<
|
|
296
|
+
getCurrentSession(): Promise<StoredRuntimeSession | null>;
|
|
184
297
|
/**
|
|
185
298
|
* Get the last generated proof for out-of-band transport
|
|
186
299
|
*/
|
|
187
|
-
getLastProof():
|
|
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():
|
|
308
|
+
createDebugEndpoint(): (() => Promise<RuntimeRecord>) | null;
|
|
196
309
|
/**
|
|
197
310
|
* Get audit logger
|
|
198
311
|
*/
|
|
199
|
-
getAuditLogger():
|
|
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
|
package/dist/runtime/base.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
712
|
-
|
|
713
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
918
|
-
nonce
|
|
919
|
-
did
|
|
920
|
-
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(
|
|
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(
|
|
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
|
|
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 =
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
const
|
|
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
|
-
}) ||
|
|
1320
|
-
if (verificationMethod
|
|
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
|