@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.
- 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 +199 -51
- 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 +22 -6
- 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) {
|
|
@@ -254,7 +261,10 @@ class MCPIRuntimeBase {
|
|
|
254
261
|
auth.type === "oauth2" ||
|
|
255
262
|
auth.type === "password" ||
|
|
256
263
|
auth.type === "idv") {
|
|
257
|
-
|
|
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
|
|
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
|
|
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(
|
|
709
|
-
|
|
710
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
915
|
-
nonce
|
|
916
|
-
did
|
|
917
|
-
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(
|
|
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(
|
|
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
|
|
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 =
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
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}`);
|
|
1315
1463
|
return matchesType && matchesKid;
|
|
1316
|
-
}) ||
|
|
1317
|
-
if (verificationMethod
|
|
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") {
|