@kya-os/mcp-i-core 1.8.0 → 1.8.2
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/dist/conformance/capability-verifier-adapter.d.ts +66 -0
- package/dist/conformance/capability-verifier-adapter.js +61 -0
- package/dist/conformance/index.d.ts +3 -1
- package/dist/conformance/index.js +7 -1
- package/dist/conformance/validate-level2.d.ts +13 -3
- package/dist/conformance/validate-level2.js +15 -5
- package/dist/conformance/vc-jwt-verifier.d.ts +55 -0
- package/dist/conformance/vc-jwt-verifier.js +147 -0
- package/dist/delegation/did-web-resolver.d.ts +60 -0
- package/dist/delegation/did-web-resolver.js +129 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/package.json +3 -3
- package/dist/auth/handshake.d.ts +0 -119
- package/dist/auth/handshake.js +0 -250
- package/dist/auth/index.d.ts +0 -6
- package/dist/auth/index.js +0 -11
- package/dist/auth/types.d.ts +0 -46
- package/dist/auth/types.js +0 -10
- package/dist/cache/oauth-config-cache.d.ts +0 -69
- package/dist/cache/oauth-config-cache.js +0 -76
- package/dist/cache/tool-protection-cache.d.ts +0 -89
- package/dist/cache/tool-protection-cache.js +0 -118
- package/dist/compliance/index.d.ts +0 -8
- package/dist/compliance/index.js +0 -24
- package/dist/compliance/schema-registry.d.ts +0 -50
- package/dist/compliance/schema-registry.js +0 -438
- package/dist/compliance/schema-verifier.d.ts +0 -110
- package/dist/compliance/schema-verifier.js +0 -518
- package/dist/config/remote-config.d.ts +0 -173
- package/dist/config/remote-config.js +0 -253
- package/dist/config.d.ts +0 -182
- package/dist/config.js +0 -148
- package/dist/identity/idp-token-resolver.d.ts +0 -69
- package/dist/identity/idp-token-resolver.js +0 -136
- package/dist/identity/idp-token-storage.interface.d.ts +0 -80
- package/dist/identity/idp-token-storage.interface.js +0 -14
- package/dist/identity/user-did-manager.d.ts +0 -228
- package/dist/identity/user-did-manager.js +0 -356
- package/dist/logging/index.d.ts +0 -5
- package/dist/logging/index.js +0 -10
- package/dist/logging/logger.d.ts +0 -33
- package/dist/logging/logger.js +0 -104
- package/dist/proof/generator.d.ts +0 -109
- package/dist/proof/generator.js +0 -236
- package/dist/proof/index.d.ts +0 -5
- package/dist/proof/index.js +0 -11
- package/dist/providers/base.d.ts +0 -95
- package/dist/providers/base.js +0 -47
- package/dist/providers/memory.d.ts +0 -42
- package/dist/providers/memory.js +0 -121
- package/dist/runtime/audit-logger.d.ts +0 -37
- package/dist/runtime/audit-logger.js +0 -9
- package/dist/runtime/base.d.ts +0 -375
- package/dist/runtime/base.js +0 -1491
- package/dist/runtime/ext-apps-constants.d.ts +0 -14
- package/dist/runtime/ext-apps-constants.js +0 -17
- package/dist/services/access-control.service.d.ts +0 -138
- package/dist/services/access-control.service.js +0 -721
- package/dist/services/authorization/authorization-registry.d.ts +0 -29
- package/dist/services/authorization/authorization-registry.js +0 -58
- package/dist/services/authorization/types.d.ts +0 -53
- package/dist/services/authorization/types.js +0 -10
- package/dist/services/batch-delegation.service.d.ts +0 -53
- package/dist/services/batch-delegation.service.js +0 -95
- package/dist/services/crypto.service.d.ts +0 -69
- package/dist/services/crypto.service.js +0 -225
- package/dist/services/delegation-error-formatter.d.ts +0 -124
- package/dist/services/delegation-error-formatter.js +0 -214
- package/dist/services/errors.d.ts +0 -49
- package/dist/services/errors.js +0 -66
- package/dist/services/index.d.ts +0 -9
- package/dist/services/index.js +0 -15
- package/dist/services/oauth-config.service.d.ts +0 -53
- package/dist/services/oauth-config.service.js +0 -141
- package/dist/services/oauth-provider-registry.d.ts +0 -88
- package/dist/services/oauth-provider-registry.js +0 -128
- package/dist/services/oauth-service.d.ts +0 -122
- package/dist/services/oauth-service.js +0 -442
- package/dist/services/oauth-token-retrieval.service.d.ts +0 -49
- package/dist/services/oauth-token-retrieval.service.js +0 -150
- package/dist/services/policy.service.d.ts +0 -130
- package/dist/services/policy.service.js +0 -399
- package/dist/services/proof-verifier.d.ts +0 -98
- package/dist/services/proof-verifier.js +0 -319
- package/dist/services/provider-resolver.d.ts +0 -89
- package/dist/services/provider-resolver.js +0 -196
- package/dist/services/provider-validator.d.ts +0 -55
- package/dist/services/provider-validator.js +0 -135
- package/dist/services/session-registration.service.d.ts +0 -80
- package/dist/services/session-registration.service.js +0 -174
- package/dist/services/storage.service.d.ts +0 -116
- package/dist/services/storage.service.js +0 -411
- package/dist/services/tool-context-builder.d.ts +0 -74
- package/dist/services/tool-context-builder.js +0 -215
- package/dist/services/tool-protection.service.d.ts +0 -193
- package/dist/services/tool-protection.service.js +0 -736
- package/dist/session/index.d.ts +0 -5
- package/dist/session/index.js +0 -11
- package/dist/session/manager.d.ts +0 -113
- package/dist/session/manager.js +0 -273
- package/dist/types/oauth-required-error.d.ts +0 -46
- package/dist/types/oauth-required-error.js +0 -42
- package/dist/types/tool-protection.d.ts +0 -144
- package/dist/types/tool-protection.js +0 -48
- package/dist/utils/cors.d.ts +0 -44
- package/dist/utils/cors.js +0 -71
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.js +0 -24
- package/dist/utils/storage-keys.d.ts +0 -120
- package/dist/utils/storage-keys.js +0 -217
package/dist/runtime/base.js
DELETED
|
@@ -1,1491 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* MCPIRuntimeBase - Provider-based runtime
|
|
4
|
-
*
|
|
5
|
-
* Core runtime that accepts providers for all platform-specific operations.
|
|
6
|
-
* This enables the same runtime logic to work across Node.js, Cloudflare Workers,
|
|
7
|
-
* and other platforms.
|
|
8
|
-
*/
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.MCPIRuntimeBase = void 0;
|
|
11
|
-
const tool_protection_js_1 = require("../types/tool-protection.js");
|
|
12
|
-
const crypto_service_js_1 = require("../services/crypto.service.js");
|
|
13
|
-
const access_control_service_js_1 = require("../services/access-control.service.js");
|
|
14
|
-
const agentshield_api_1 = require("@kya-os/contracts/agentshield-api");
|
|
15
|
-
const user_did_manager_1 = require("../identity/user-did-manager");
|
|
16
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
17
|
-
class MCPIRuntimeBase {
|
|
18
|
-
crypto;
|
|
19
|
-
clock;
|
|
20
|
-
fetch;
|
|
21
|
-
storage;
|
|
22
|
-
nonceCache;
|
|
23
|
-
identity;
|
|
24
|
-
config;
|
|
25
|
-
cachedIdentity;
|
|
26
|
-
sessions = new Map();
|
|
27
|
-
lastProof;
|
|
28
|
-
userDidManager;
|
|
29
|
-
interceptedCalls = new Map(); // Store intercepted tool calls by resume token
|
|
30
|
-
cryptoService;
|
|
31
|
-
proofVerifier; // Optional ProofVerifier (injected by subclasses)
|
|
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;
|
|
39
|
-
constructor(config) {
|
|
40
|
-
this.config = config;
|
|
41
|
-
this.crypto = config.cryptoProvider;
|
|
42
|
-
this.clock = config.clockProvider;
|
|
43
|
-
this.fetch = config.fetchProvider;
|
|
44
|
-
this.storage = config.storageProvider;
|
|
45
|
-
this.nonceCache = config.nonceCacheProvider;
|
|
46
|
-
this.identity = config.identityProvider;
|
|
47
|
-
// Initialize CryptoService for JWS verification
|
|
48
|
-
this.cryptoService = new crypto_service_js_1.CryptoService(this.crypto);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Initialize the runtime
|
|
52
|
-
*/
|
|
53
|
-
async initialize() {
|
|
54
|
-
// Load or generate identity
|
|
55
|
-
this.cachedIdentity = await this.identity.getIdentity();
|
|
56
|
-
// Initialize user DID manager if identity config enables it
|
|
57
|
-
if (this.config.identity?.generateUserDids) {
|
|
58
|
-
this.userDidManager = new user_did_manager_1.UserDidManager({
|
|
59
|
-
crypto: this.crypto,
|
|
60
|
-
storage: this.config.identity?.userDidStorage
|
|
61
|
-
? {
|
|
62
|
-
get: async (key) => await this.storage.get(`userDid:${key}`),
|
|
63
|
-
set: async (key, value, ttl) => {
|
|
64
|
-
await this.storage.set(`userDid:${key}`, value);
|
|
65
|
-
},
|
|
66
|
-
delete: async (key) => await this.storage.delete(`userDid:${key}`),
|
|
67
|
-
}
|
|
68
|
-
: undefined,
|
|
69
|
-
useDidWeb: this.config.identity?.userDidStorage === "persistent",
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
// Initialize nonce cache if it has an initialize method
|
|
73
|
-
const nonceCacheWithInitialize = this.nonceCache;
|
|
74
|
-
if (typeof nonceCacheWithInitialize.initialize === "function") {
|
|
75
|
-
await nonceCacheWithInitialize.initialize();
|
|
76
|
-
}
|
|
77
|
-
// Log initialization if audit is enabled
|
|
78
|
-
if (this.config.audit?.enabled) {
|
|
79
|
-
this.logAudit("runtime_initialized", {
|
|
80
|
-
did: this.cachedIdentity.did,
|
|
81
|
-
environment: this.config.environment || "development",
|
|
82
|
-
userDidGeneration: this.config.identity?.generateUserDids
|
|
83
|
-
? "enabled"
|
|
84
|
-
: "disabled",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Get the current agent identity
|
|
90
|
-
*/
|
|
91
|
-
async getIdentity() {
|
|
92
|
-
if (!this.cachedIdentity) {
|
|
93
|
-
this.cachedIdentity = await this.identity.getIdentity();
|
|
94
|
-
}
|
|
95
|
-
return this.cachedIdentity;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Handle MCP handshake request
|
|
99
|
-
*
|
|
100
|
-
* Phase 5: Anonymous Sessions Until OAuth
|
|
101
|
-
* - Sessions start anonymous (no userDid) unless OAuth identity provided
|
|
102
|
-
* - User DID is resolved via AgentShield after OAuth completes
|
|
103
|
-
* - Eliminates DID fragmentation (same user = same DID across sessions)
|
|
104
|
-
*
|
|
105
|
-
* @param request - Handshake request object (may include oauthIdentity for persistent user DID lookup)
|
|
106
|
-
* @returns Handshake response with session ID and agent DID
|
|
107
|
-
*/
|
|
108
|
-
async handleHandshake(request) {
|
|
109
|
-
const identity = await this.getIdentity();
|
|
110
|
-
const timestamp = this.clock.now();
|
|
111
|
-
const sessionId = await this.generateSessionId();
|
|
112
|
-
// Phase 5: Try to resolve user DID from existing OAuth mapping
|
|
113
|
-
// Sessions start anonymous - no ephemeral generation
|
|
114
|
-
let userDid;
|
|
115
|
-
if (this.userDidManager) {
|
|
116
|
-
try {
|
|
117
|
-
const oauthIdentity = request.oauthIdentity;
|
|
118
|
-
const resolvedDid = await this.userDidManager.getOrCreateUserDid(sessionId, oauthIdentity);
|
|
119
|
-
// Convert null to undefined for session storage
|
|
120
|
-
userDid = resolvedDid ?? undefined;
|
|
121
|
-
if (this.config.audit?.enabled) {
|
|
122
|
-
if (userDid) {
|
|
123
|
-
console.error("[MCP-I] Resolved existing user DID for session:", {
|
|
124
|
-
userDid: userDid.substring(0, 20) + "...",
|
|
125
|
-
hasOAuth: !!oauthIdentity,
|
|
126
|
-
provider: oauthIdentity?.provider,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
console.error("[MCP-I] Session started anonymous (no userDid):", {
|
|
131
|
-
sessionId: sessionId.substring(0, 8) + "...",
|
|
132
|
-
hasOAuth: !!oauthIdentity,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
console.warn("[MCP-I] Failed to resolve user DID:", error);
|
|
139
|
-
// Continue without user DID - session is anonymous
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Extract client info if available
|
|
143
|
-
const normalizeString = (value) => {
|
|
144
|
-
if (typeof value !== "string") {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
const trimmed = value.trim();
|
|
148
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
149
|
-
};
|
|
150
|
-
const protocolVersion = normalizeString(request.clientProtocolVersion);
|
|
151
|
-
const clientCapabilities = request.clientCapabilities;
|
|
152
|
-
const requestClientInfo = request.clientInfo;
|
|
153
|
-
const shouldPersistClientInfo = requestClientInfo ||
|
|
154
|
-
typeof protocolVersion === "string" ||
|
|
155
|
-
typeof clientCapabilities !== "undefined";
|
|
156
|
-
const clientInfo = shouldPersistClientInfo
|
|
157
|
-
? {
|
|
158
|
-
name: normalizeString(requestClientInfo?.name) ?? "unknown",
|
|
159
|
-
title: normalizeString(requestClientInfo?.title),
|
|
160
|
-
version: normalizeString(requestClientInfo?.version),
|
|
161
|
-
platform: normalizeString(requestClientInfo?.platform),
|
|
162
|
-
vendor: normalizeString(requestClientInfo?.vendor),
|
|
163
|
-
persistentId: normalizeString(requestClientInfo?.persistentId),
|
|
164
|
-
clientId: normalizeString(requestClientInfo?.clientId) ?? crypto.randomUUID(),
|
|
165
|
-
protocolVersion,
|
|
166
|
-
capabilities: clientCapabilities,
|
|
167
|
-
}
|
|
168
|
-
: undefined;
|
|
169
|
-
// Create session with Phase 5 identity state
|
|
170
|
-
const session = {
|
|
171
|
-
id: sessionId,
|
|
172
|
-
clientDid: request.clientDid || userDid, // Use provided clientDid or generated userDid
|
|
173
|
-
userDid: userDid, // Store user DID (may be undefined for anonymous sessions)
|
|
174
|
-
agentDid: request.agentDid, // ✅ FIXED: Only agent DID, no fallback
|
|
175
|
-
serverDid: identity.did, // ✅ NEW: Server's DID (for clarity)
|
|
176
|
-
audience: request.audience,
|
|
177
|
-
createdAt: timestamp,
|
|
178
|
-
expiresAt: this.clock.calculateExpiry((this.config.session?.ttlMinutes || 30) * 60),
|
|
179
|
-
clientInfo, // Store client information
|
|
180
|
-
// Phase 5: Identity state tracking
|
|
181
|
-
identityState: userDid ? "authenticated" : "anonymous",
|
|
182
|
-
oauthIdentity: request.oauthIdentity ?? undefined,
|
|
183
|
-
};
|
|
184
|
-
this.sessions.set(sessionId, session);
|
|
185
|
-
// Create handshake response
|
|
186
|
-
const response = {
|
|
187
|
-
sessionId,
|
|
188
|
-
agentDid: identity.did,
|
|
189
|
-
timestamp,
|
|
190
|
-
capabilities: ["identity", "proof", "audit"],
|
|
191
|
-
...(userDid && { userDid }), // Include user DID in response if generated
|
|
192
|
-
};
|
|
193
|
-
// Sign the response
|
|
194
|
-
const signature = await this.signData(response);
|
|
195
|
-
return {
|
|
196
|
-
...response,
|
|
197
|
-
signature,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Update session identity after OAuth resolution (Phase 5)
|
|
202
|
-
*
|
|
203
|
-
* Called after AgentShield identity/resolve returns a persistent user DID.
|
|
204
|
-
* Updates the session to authenticated state with the resolved DID.
|
|
205
|
-
*
|
|
206
|
-
* @param sessionId - MCP session ID
|
|
207
|
-
* @param userDid - Persistent user DID from AgentShield
|
|
208
|
-
* @param oauthIdentity - OAuth identity information
|
|
209
|
-
* @throws Error if session not found
|
|
210
|
-
*/
|
|
211
|
-
async updateSessionIdentity(sessionId, userDid, oauthIdentity) {
|
|
212
|
-
const session = this.sessions.get(sessionId);
|
|
213
|
-
if (!session) {
|
|
214
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
215
|
-
}
|
|
216
|
-
// Update session with resolved identity
|
|
217
|
-
session.userDid = userDid;
|
|
218
|
-
session.identityState = "authenticated";
|
|
219
|
-
if (oauthIdentity) {
|
|
220
|
-
session.oauthIdentity = oauthIdentity;
|
|
221
|
-
}
|
|
222
|
-
// Update the sessions map
|
|
223
|
-
this.sessions.set(sessionId, session);
|
|
224
|
-
// Also update UserDidManager cache if available
|
|
225
|
-
if (this.userDidManager) {
|
|
226
|
-
await this.userDidManager.setUserDidForSession(sessionId, userDid, oauthIdentity);
|
|
227
|
-
}
|
|
228
|
-
if (this.config.audit?.enabled) {
|
|
229
|
-
console.error("[MCP-I] Session identity updated (Phase 5):", {
|
|
230
|
-
sessionId: sessionId.substring(0, 8) + "...",
|
|
231
|
-
userDid: userDid.substring(0, 20) + "...",
|
|
232
|
-
provider: oauthIdentity?.provider,
|
|
233
|
-
identityState: "authenticated",
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Get session by ID
|
|
239
|
-
*/
|
|
240
|
-
getSession(sessionId) {
|
|
241
|
-
return this.sessions.get(sessionId);
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Extract the correct provider for consent URL from tool protection config
|
|
245
|
-
*
|
|
246
|
-
* For password auth tools, the provider is in authorization.provider.
|
|
247
|
-
* For OAuth auth tools, the provider is in oauthProvider (legacy) or authorization.provider.
|
|
248
|
-
*
|
|
249
|
-
* Note: Not all authorization types have a provider field (e.g., 'none', 'mdl', 'webauthn').
|
|
250
|
-
* Only 'oauth', 'oauth2', 'password', and 'idv' types have provider.
|
|
251
|
-
*
|
|
252
|
-
* @param protection - Tool protection configuration
|
|
253
|
-
* @returns Provider name to use in consent URL, or undefined
|
|
254
|
-
*/
|
|
255
|
-
getConsentProvider(protection) {
|
|
256
|
-
// Check authorization field - only some types have provider
|
|
257
|
-
if (protection.authorization) {
|
|
258
|
-
const auth = protection.authorization;
|
|
259
|
-
// Types with provider: oauth, oauth2, password, idv
|
|
260
|
-
if (auth.type === "oauth" ||
|
|
261
|
-
auth.type === "oauth2" ||
|
|
262
|
-
auth.type === "password" ||
|
|
263
|
-
auth.type === "idv") {
|
|
264
|
-
// Return auth.provider if set, otherwise fall through to legacy oauthProvider
|
|
265
|
-
if (auth.provider) {
|
|
266
|
-
return auth.provider;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
// Other types don't have provider (none, mdl, verifiable_credential, webauthn, siwe)
|
|
270
|
-
// Fall through to legacy oauthProvider
|
|
271
|
-
}
|
|
272
|
-
// Fall back to legacy oauthProvider (deprecated)
|
|
273
|
-
return protection.oauthProvider;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Process tool call with automatic proof generation
|
|
277
|
-
* Returns clean result only - proof is stored for out-of-band retrieval
|
|
278
|
-
*
|
|
279
|
-
* @param toolName - Name of the tool being called
|
|
280
|
-
* @param args - Tool arguments
|
|
281
|
-
* @param handler - Tool execution handler
|
|
282
|
-
* @param session - Session context (expected fields: id, audience, nonce?, delegationToken?, consentProof?)
|
|
283
|
-
*/
|
|
284
|
-
async processToolCall(toolName, args, handler, session) {
|
|
285
|
-
// Check tool protection (delegation requirement)
|
|
286
|
-
if (this.config.toolProtectionService) {
|
|
287
|
-
// Get agent identity to check protection
|
|
288
|
-
const identity = await this.getIdentity();
|
|
289
|
-
if (this.config.audit?.enabled) {
|
|
290
|
-
console.error("[MCP-I] Checking tool protection:", {
|
|
291
|
-
tool: toolName,
|
|
292
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
293
|
-
hasDelegation: !!(session?.delegationToken || session?.consentProof),
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const protection = await this.config.toolProtectionService.checkToolProtection(toolName, identity.did);
|
|
297
|
-
if (protection) {
|
|
298
|
-
// Tool requires delegation
|
|
299
|
-
const hasDelegation = session?.delegationToken || session?.consentProof;
|
|
300
|
-
if (!hasDelegation) {
|
|
301
|
-
// No delegation provided - intercept the tool call and store context
|
|
302
|
-
// Store intercepted tool call context for resumption
|
|
303
|
-
const interceptedCall = {
|
|
304
|
-
toolName,
|
|
305
|
-
args,
|
|
306
|
-
sessionId: session?.id || "unknown",
|
|
307
|
-
timestamp: this.clock.now(),
|
|
308
|
-
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
309
|
-
};
|
|
310
|
-
// Generate resume token
|
|
311
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
312
|
-
// Build consent URL with resume token
|
|
313
|
-
// Note: projectId is not available in base class - subclasses should override buildConsentUrl
|
|
314
|
-
// Pass oauthProvider to ensure correct auth method is selected (e.g., "credentials" vs "github")
|
|
315
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
|
|
316
|
-
this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
|
|
317
|
-
);
|
|
318
|
-
// Create error with intercepted call context and pre-generated resume token
|
|
319
|
-
const error = new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
320
|
-
// Store intercepted call for resumption
|
|
321
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
322
|
-
// Clean up expired intercepted calls periodically
|
|
323
|
-
this.cleanupExpiredInterceptedCalls();
|
|
324
|
-
if (this.config.audit?.enabled) {
|
|
325
|
-
console.warn("[MCP-I] BLOCKED: Tool requires delegation but none provided", {
|
|
326
|
-
tool: toolName,
|
|
327
|
-
requiredScopes: protection.requiredScopes,
|
|
328
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
329
|
-
resumeToken: error.resumeToken,
|
|
330
|
-
consentUrl,
|
|
331
|
-
});
|
|
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
|
-
}
|
|
343
|
-
throw error;
|
|
344
|
-
}
|
|
345
|
-
// Delegation provided - verify it with AccessControlApiService
|
|
346
|
-
const delegationToken = session?.delegationToken;
|
|
347
|
-
const consentProof = session?.consentProof;
|
|
348
|
-
if (!this.accessControlService) {
|
|
349
|
-
// Access control service not available - log warning but allow execution
|
|
350
|
-
// This enables graceful degradation when service is not configured
|
|
351
|
-
if (this.config.audit?.enabled) {
|
|
352
|
-
console.warn("[MCP-I] ⚠️ Delegation token provided but AccessControlApiService not configured - skipping verification", {
|
|
353
|
-
tool: toolName,
|
|
354
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
355
|
-
hasDelegationToken: !!delegationToken,
|
|
356
|
-
hasConsentProof: !!consentProof,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
// Verify delegation token with AccessControlApiService
|
|
362
|
-
try {
|
|
363
|
-
if (this.config.audit?.enabled) {
|
|
364
|
-
console.error("[MCP-I] 🔐 Verifying delegation token with AccessControlApiService", {
|
|
365
|
-
tool: toolName,
|
|
366
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
367
|
-
hasDelegationToken: !!delegationToken,
|
|
368
|
-
hasConsentProof: !!consentProof,
|
|
369
|
-
requiredScopes: protection.requiredScopes,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
// Build verification request
|
|
373
|
-
const verifyRequest = {
|
|
374
|
-
agent_did: identity.did,
|
|
375
|
-
scopes: protection.requiredScopes,
|
|
376
|
-
};
|
|
377
|
-
// ✅ CRITICAL: Include user_did for user-scoped delegation verification
|
|
378
|
-
// This prevents cross-user delegation leakage (Priority 3 fallback issue)
|
|
379
|
-
// Without user_did, AgentShield cannot validate user isolation
|
|
380
|
-
if (session?.userDid) {
|
|
381
|
-
verifyRequest.user_did = session.userDid;
|
|
382
|
-
if (this.config.audit?.enabled) {
|
|
383
|
-
console.error("[MCP-I] 🔐 Including user_did in verification request", {
|
|
384
|
-
userDid: session.userDid.slice(0, 20) + "...",
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// Add delegation token if available (preferred over consent proof)
|
|
389
|
-
if (delegationToken) {
|
|
390
|
-
verifyRequest.delegation_token = delegationToken;
|
|
391
|
-
}
|
|
392
|
-
else if (consentProof) {
|
|
393
|
-
// Consent proof is a JWT credential - use as credential_jwt
|
|
394
|
-
verifyRequest.credential_jwt = consentProof;
|
|
395
|
-
}
|
|
396
|
-
// Add optional timestamp for verification
|
|
397
|
-
verifyRequest.timestamp = this.clock.now();
|
|
398
|
-
// Add client info from session if available
|
|
399
|
-
if (session?.clientDid || session?.clientId) {
|
|
400
|
-
verifyRequest.client_info = {
|
|
401
|
-
origin: session?.serverOrigin,
|
|
402
|
-
user_agent: session?.userAgent,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
// Perform verification
|
|
406
|
-
const verificationResult = await this.accessControlService.verifyDelegation(verifyRequest, {
|
|
407
|
-
delegationToken: delegationToken || undefined,
|
|
408
|
-
credentialJwt: consentProof || undefined,
|
|
409
|
-
});
|
|
410
|
-
// Check verification result
|
|
411
|
-
if (!verificationResult.data.valid) {
|
|
412
|
-
// Delegation verification failed
|
|
413
|
-
const reason = verificationResult.data.reason ||
|
|
414
|
-
"Delegation token invalid or expired";
|
|
415
|
-
const errorDetails = verificationResult.data.error;
|
|
416
|
-
if (this.config.audit?.enabled) {
|
|
417
|
-
console.error("[MCP-I] ❌ Delegation verification FAILED", {
|
|
418
|
-
tool: toolName,
|
|
419
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
420
|
-
reason,
|
|
421
|
-
errorCode: errorDetails?.code,
|
|
422
|
-
errorMessage: errorDetails?.message,
|
|
423
|
-
requiredScopes: protection.requiredScopes,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
// Throw DelegationRequiredError to trigger consent flow
|
|
427
|
-
const interceptedCall = {
|
|
428
|
-
toolName,
|
|
429
|
-
args,
|
|
430
|
-
sessionId: session?.id || "unknown",
|
|
431
|
-
timestamp: this.clock.now(),
|
|
432
|
-
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
433
|
-
};
|
|
434
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
435
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
|
|
436
|
-
this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
|
|
437
|
-
);
|
|
438
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
439
|
-
this.cleanupExpiredInterceptedCalls();
|
|
440
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
441
|
-
}
|
|
442
|
-
// ✅ SECURITY: Validate user_identifier matches session userDid
|
|
443
|
-
// This ensures delegations are user-specific and prevents user isolation bypass
|
|
444
|
-
const credential = verificationResult.data.credential;
|
|
445
|
-
const delegationUserIdentifier = credential?.user_identifier;
|
|
446
|
-
// Also check for user_id field (AgentShield may return DID or ID directly)
|
|
447
|
-
const delegationUserId = credential?.user_id;
|
|
448
|
-
const sessionUserDid = session?.userDid;
|
|
449
|
-
if (delegationUserIdentifier && sessionUserDid) {
|
|
450
|
-
// Check if identifiers match (direct match or via user_id field)
|
|
451
|
-
const identifiersMatch = delegationUserIdentifier === sessionUserDid ||
|
|
452
|
-
delegationUserId === sessionUserDid;
|
|
453
|
-
// Special case: If delegationUserIdentifier is NOT a DID (e.g., email),
|
|
454
|
-
// but AgentShield verified the delegation, trust it.
|
|
455
|
-
// AgentShield internally links email → DID, so the verification is valid.
|
|
456
|
-
// Only fail if BOTH are DIDs and they don't match.
|
|
457
|
-
const isDelegationIdentifierDid = delegationUserIdentifier.startsWith("did:");
|
|
458
|
-
const shouldEnforceMatch = isDelegationIdentifierDid || delegationUserId;
|
|
459
|
-
if (shouldEnforceMatch && !identifiersMatch) {
|
|
460
|
-
// User identifier mismatch - potential security issue
|
|
461
|
-
const securityError = `Delegation user_identifier mismatch: delegation has "${delegationUserIdentifier.substring(0, 20)}..." but session has "${sessionUserDid.substring(0, 20)}..."`;
|
|
462
|
-
if (this.config.audit?.enabled) {
|
|
463
|
-
console.error("[MCP-I] 🔒 SECURITY: User identifier validation FAILED", {
|
|
464
|
-
tool: toolName,
|
|
465
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
466
|
-
delegationUserIdentifier: delegationUserIdentifier.substring(0, 20) + "...",
|
|
467
|
-
delegationUserId: delegationUserId
|
|
468
|
-
? delegationUserId.substring(0, 20) + "..."
|
|
469
|
-
: "N/A",
|
|
470
|
-
sessionUserDid: sessionUserDid.substring(0, 20) + "...",
|
|
471
|
-
sessionId: session?.id?.substring(0, 20) + "...",
|
|
472
|
-
reason: "user_identifier_mismatch",
|
|
473
|
-
severity: "high",
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
// Throw DelegationRequiredError to force re-authentication
|
|
477
|
-
const interceptedCall = {
|
|
478
|
-
toolName,
|
|
479
|
-
args,
|
|
480
|
-
sessionId: session?.id || "unknown",
|
|
481
|
-
timestamp: this.clock.now(),
|
|
482
|
-
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
483
|
-
};
|
|
484
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
485
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
|
|
486
|
-
this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
|
|
487
|
-
);
|
|
488
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
489
|
-
this.cleanupExpiredInterceptedCalls();
|
|
490
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
491
|
-
}
|
|
492
|
-
// User identifier validation passed (direct match or trusted AgentShield verification)
|
|
493
|
-
if (this.config.audit?.enabled) {
|
|
494
|
-
console.error("[MCP-I] ✅ User identifier validation PASSED", {
|
|
495
|
-
tool: toolName,
|
|
496
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
497
|
-
userDid: sessionUserDid.substring(0, 20) + "...",
|
|
498
|
-
sessionId: session?.id?.substring(0, 20) + "...",
|
|
499
|
-
matchType: identifiersMatch
|
|
500
|
-
? "direct"
|
|
501
|
-
: "trusted_agentshield_verification",
|
|
502
|
-
delegationIdentifierFormat: isDelegationIdentifierDid
|
|
503
|
-
? "did"
|
|
504
|
-
: "email_or_other",
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
else if (delegationUserIdentifier && !sessionUserDid) {
|
|
509
|
-
// Delegation has user_identifier but session doesn't - log warning
|
|
510
|
-
if (this.config.audit?.enabled) {
|
|
511
|
-
console.warn("[MCP-I] ⚠️ Delegation has user_identifier but session missing userDid", {
|
|
512
|
-
tool: toolName,
|
|
513
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
514
|
-
delegationUserIdentifier: delegationUserIdentifier.substring(0, 20) + "...",
|
|
515
|
-
sessionId: session?.id?.substring(0, 20) + "...",
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// ✅ SECURITY: Validate authorization method matches tool requirements
|
|
520
|
-
// This prevents stale delegations from working after tool auth method changes
|
|
521
|
-
// NOTE: Authorization validation only applies when credential is present (AgentShield API verifier).
|
|
522
|
-
// KV/Memory verifiers don't return credential data, so we skip auth method validation for them.
|
|
523
|
-
const toolAuth = protection.authorization;
|
|
524
|
-
const delegationAuth = credential?.authorization;
|
|
525
|
-
// Only validate when:
|
|
526
|
-
// 1. Tool has authorization requirement defined
|
|
527
|
-
// 2. Credential is present (AgentShield API verifier)
|
|
528
|
-
if (toolAuth && credential) {
|
|
529
|
-
// If tool requires authorization but credential doesn't have auth data, reject
|
|
530
|
-
// This prevents bypassing auth checks with incomplete credential data
|
|
531
|
-
if (!delegationAuth) {
|
|
532
|
-
if (this.config.audit?.enabled) {
|
|
533
|
-
console.error("[MCP-I] ❌ Tool requires authorization but credential missing auth data", {
|
|
534
|
-
tool: toolName,
|
|
535
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
536
|
-
toolAuth,
|
|
537
|
-
note: "Credential exists but missing authorization metadata - rejecting for security",
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
// Throw DelegationRequiredError to force re-authorization
|
|
541
|
-
const interceptedCall = {
|
|
542
|
-
toolName,
|
|
543
|
-
args,
|
|
544
|
-
sessionId: session?.id || "unknown",
|
|
545
|
-
timestamp: this.clock.now(),
|
|
546
|
-
expiresAt: this.clock.calculateExpiry(1800),
|
|
547
|
-
};
|
|
548
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
549
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
|
|
550
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
551
|
-
this.cleanupExpiredInterceptedCalls();
|
|
552
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
553
|
-
}
|
|
554
|
-
// Both tool and delegation have authorization - compare them
|
|
555
|
-
if (!(0, access_control_service_js_1.authorizationMatches)(delegationAuth, toolAuth)) {
|
|
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}`;
|
|
564
|
-
if (this.config.audit?.enabled) {
|
|
565
|
-
console.error("[MCP-I] ❌ Authorization method validation FAILED", {
|
|
566
|
-
tool: toolName,
|
|
567
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
568
|
-
reason: authMismatchReason,
|
|
569
|
-
delegationAuth,
|
|
570
|
-
toolAuth,
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
// Throw DelegationRequiredError to force re-authorization with correct method
|
|
574
|
-
const interceptedCall = {
|
|
575
|
-
toolName,
|
|
576
|
-
args,
|
|
577
|
-
sessionId: session?.id || "unknown",
|
|
578
|
-
timestamp: this.clock.now(),
|
|
579
|
-
expiresAt: this.clock.calculateExpiry(1800),
|
|
580
|
-
};
|
|
581
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
582
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
|
|
583
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
584
|
-
this.cleanupExpiredInterceptedCalls();
|
|
585
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
586
|
-
}
|
|
587
|
-
// Authorization method validation passed
|
|
588
|
-
if (this.config.audit?.enabled) {
|
|
589
|
-
console.error("[MCP-I] ✅ Authorization method validation PASSED", {
|
|
590
|
-
tool: toolName,
|
|
591
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
592
|
-
authType: delegationAuth.type,
|
|
593
|
-
provider: delegationAuth.provider || "N/A",
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
// If credential is missing entirely (KV/Memory verifiers), skip validation for backward compatibility
|
|
598
|
-
// Verification succeeded
|
|
599
|
-
if (this.config.audit?.enabled) {
|
|
600
|
-
console.error("[MCP-I] ✅ Delegation verification SUCCEEDED", {
|
|
601
|
-
tool: toolName,
|
|
602
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
603
|
-
delegationId: verificationResult.data.delegation_id,
|
|
604
|
-
credentialScopes: verificationResult.data.credential?.scopes,
|
|
605
|
-
requiredScopes: protection.requiredScopes,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
// Handle verification errors
|
|
611
|
-
if (error instanceof tool_protection_js_1.DelegationRequiredError) {
|
|
612
|
-
// Re-throw DelegationRequiredError as-is (already handled above)
|
|
613
|
-
throw error;
|
|
614
|
-
}
|
|
615
|
-
// Handle AgentShieldAPIError (network errors, API errors, etc.)
|
|
616
|
-
if (error instanceof agentshield_api_1.AgentShieldAPIError) {
|
|
617
|
-
if (this.config.audit?.enabled) {
|
|
618
|
-
console.error("[MCP-I] ❌ Delegation verification error (API failure)", {
|
|
619
|
-
tool: toolName,
|
|
620
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
621
|
-
errorCode: error.code,
|
|
622
|
-
errorMessage: error.message,
|
|
623
|
-
errorDetails: error.details,
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
// On API errors, fail securely by requiring delegation
|
|
627
|
-
// This prevents unauthorized access when verification service is unavailable
|
|
628
|
-
const interceptedCall = {
|
|
629
|
-
toolName,
|
|
630
|
-
args,
|
|
631
|
-
sessionId: session?.id || "unknown",
|
|
632
|
-
timestamp: this.clock.now(),
|
|
633
|
-
expiresAt: this.clock.calculateExpiry(1800),
|
|
634
|
-
};
|
|
635
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
636
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
|
|
637
|
-
this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
|
|
638
|
-
);
|
|
639
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
640
|
-
this.cleanupExpiredInterceptedCalls();
|
|
641
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
642
|
-
}
|
|
643
|
-
// Unexpected error - log and fail securely
|
|
644
|
-
if (this.config.audit?.enabled) {
|
|
645
|
-
console.error("[MCP-I] ❌ Unexpected error during delegation verification", {
|
|
646
|
-
tool: toolName,
|
|
647
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
648
|
-
error: error instanceof Error ? error.message : String(error),
|
|
649
|
-
errorStack: error instanceof Error ? error.stack : undefined,
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
// Fail securely - require delegation on unexpected errors
|
|
653
|
-
const interceptedCall = {
|
|
654
|
-
toolName,
|
|
655
|
-
args,
|
|
656
|
-
sessionId: session?.id || "unknown",
|
|
657
|
-
timestamp: this.clock.now(),
|
|
658
|
-
expiresAt: this.clock.calculateExpiry(1800),
|
|
659
|
-
};
|
|
660
|
-
const resumeToken = await this.generateResumeToken(interceptedCall);
|
|
661
|
-
const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
|
|
662
|
-
this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
|
|
663
|
-
);
|
|
664
|
-
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
665
|
-
this.cleanupExpiredInterceptedCalls();
|
|
666
|
-
throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
// No protection required - tool can be executed freely
|
|
672
|
-
if (this.config.audit?.enabled) {
|
|
673
|
-
console.error("[MCP-I] Tool protection check passed (no delegation required)", {
|
|
674
|
-
tool: toolName,
|
|
675
|
-
agentDid: identity.did.slice(0, 20) + "...",
|
|
676
|
-
reason: "Tool not configured to require delegation",
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// Execute the tool
|
|
682
|
-
const result = await handler(args);
|
|
683
|
-
// Create proof
|
|
684
|
-
const proof = await this.createProof(result, session);
|
|
685
|
-
// Store proof for out-of-band retrieval
|
|
686
|
-
this.lastProof = proof;
|
|
687
|
-
// Log if audit is enabled
|
|
688
|
-
if (this.config.audit?.enabled) {
|
|
689
|
-
this.logAudit("tool_executed", {
|
|
690
|
-
tool: toolName,
|
|
691
|
-
sessionId: session?.id,
|
|
692
|
-
timestamp: this.clock.now(),
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
// Return clean result only (no proof in LLM context)
|
|
696
|
-
return result;
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Resume a tool call after authorization
|
|
700
|
-
*
|
|
701
|
-
* @param resumeToken - Token from DelegationRequiredError
|
|
702
|
-
* @param handler - Tool execution handler
|
|
703
|
-
* @param delegationToken - Delegation token from authorization
|
|
704
|
-
* @returns Tool execution result
|
|
705
|
-
*/
|
|
706
|
-
async resumeToolCall(resumeToken, handler, delegationToken) {
|
|
707
|
-
const interceptedCall = this.interceptedCalls.get(resumeToken);
|
|
708
|
-
if (!interceptedCall) {
|
|
709
|
-
throw new Error(`Invalid or expired resume token: ${resumeToken}`);
|
|
710
|
-
}
|
|
711
|
-
// Check if call has expired
|
|
712
|
-
if (this.clock.hasExpired(interceptedCall.expiresAt)) {
|
|
713
|
-
this.interceptedCalls.delete(resumeToken);
|
|
714
|
-
throw new Error(`Resume token expired: ${resumeToken}`);
|
|
715
|
-
}
|
|
716
|
-
// Get session for the intercepted call
|
|
717
|
-
const session = this.sessions.get(interceptedCall.sessionId);
|
|
718
|
-
if (!session) {
|
|
719
|
-
throw new Error(`Session not found for intercepted call: ${interceptedCall.sessionId}`);
|
|
720
|
-
}
|
|
721
|
-
// Add delegation token to session
|
|
722
|
-
const enhancedSession = {
|
|
723
|
-
...session,
|
|
724
|
-
delegationToken,
|
|
725
|
-
};
|
|
726
|
-
// Resume the tool call with delegation
|
|
727
|
-
const result = await this.processToolCall(interceptedCall.toolName, interceptedCall.args, handler, enhancedSession);
|
|
728
|
-
// Clean up intercepted call after successful resumption to prevent memory leak
|
|
729
|
-
this.interceptedCalls.delete(resumeToken);
|
|
730
|
-
return result;
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
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.
|
|
737
|
-
*/
|
|
738
|
-
async generateResumeToken(_call) {
|
|
739
|
-
const bytes = await this.crypto.randomBytes(16);
|
|
740
|
-
return `resume_${this.bytesToHex(bytes)}`;
|
|
741
|
-
}
|
|
742
|
-
/**
|
|
743
|
-
* Clean up expired intercepted calls
|
|
744
|
-
*/
|
|
745
|
-
cleanupExpiredInterceptedCalls() {
|
|
746
|
-
const now = this.clock.now();
|
|
747
|
-
for (const [token, call] of this.interceptedCalls.entries()) {
|
|
748
|
-
if (this.clock.hasExpired(call.expiresAt)) {
|
|
749
|
-
this.interceptedCalls.delete(token);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Build consent URL for AgentShield delegation flow
|
|
755
|
-
*
|
|
756
|
-
* Note: Parameter names use snake_case for AgentShield API compatibility.
|
|
757
|
-
* This is documented in docs/API_PARITY_GUIDE.md under "Field Naming Conventions".
|
|
758
|
-
*
|
|
759
|
-
* AgentShield API requires snake_case in URL parameters:
|
|
760
|
-
* - session_id (not sessionId)
|
|
761
|
-
* - agent_did (not agentDid)
|
|
762
|
-
* - resume_token (not resumeToken)
|
|
763
|
-
* - agent_name (human-readable agent name for dashboard display)
|
|
764
|
-
*
|
|
765
|
-
* @param toolName - Tool that requires delegation
|
|
766
|
-
* @param scopes - Required scopes for the tool
|
|
767
|
-
* @param session - Current session context
|
|
768
|
-
* @param resumeToken - Token to resume after delegation
|
|
769
|
-
* @param projectId - Project ID for AgentShield API
|
|
770
|
-
* @param provider - Provider name (e.g., "github", "credentials") to select specific auth method
|
|
771
|
-
* @returns Full consent URL with snake_case parameters
|
|
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
|
-
}
|
|
881
|
-
buildConsentUrl(toolName, scopes, session, resumeToken, projectId, provider) {
|
|
882
|
-
// Default implementation - override in subclasses
|
|
883
|
-
// This URL should point to AgentShield's consent page
|
|
884
|
-
// Parameter names use snake_case for AgentShield API compatibility
|
|
885
|
-
const params = new URLSearchParams({
|
|
886
|
-
tool: toolName,
|
|
887
|
-
scopes: scopes.join(","),
|
|
888
|
-
session_id: session?.id || "",
|
|
889
|
-
agent_did: session?.agentDid || "",
|
|
890
|
-
});
|
|
891
|
-
// Add agent_name for AgentShield dashboard display
|
|
892
|
-
// Priority: session.agentName > "Unknown Agent"
|
|
893
|
-
const agentName = session?.agentName;
|
|
894
|
-
if (agentName) {
|
|
895
|
-
params.set("agent_name", agentName);
|
|
896
|
-
}
|
|
897
|
-
// Add project_id if provided (required for AgentShield consent endpoint)
|
|
898
|
-
if (projectId) {
|
|
899
|
-
params.set("project_id", projectId);
|
|
900
|
-
}
|
|
901
|
-
// Add resume token if provided
|
|
902
|
-
if (resumeToken) {
|
|
903
|
-
params.set("resume_token", resumeToken);
|
|
904
|
-
}
|
|
905
|
-
// Add provider if specified (allows selecting specific auth method like "credentials" or "github")
|
|
906
|
-
// This is critical when multiple providers are configured for a project
|
|
907
|
-
if (provider) {
|
|
908
|
-
params.set("provider", provider);
|
|
909
|
-
}
|
|
910
|
-
// Use AgentShield consent endpoint
|
|
911
|
-
return `https://kya.vouched.id/bouncer/consent?${params.toString()}`;
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Issue a new nonce and register it in the cache
|
|
915
|
-
* Use this to get a nonce for the session context before calling processToolCall
|
|
916
|
-
*/
|
|
917
|
-
async issueNonce(sessionId) {
|
|
918
|
-
const nonce = await this.generateNonce();
|
|
919
|
-
// Get session to extract agentDid for agent-scoped nonce caching
|
|
920
|
-
const session = this.sessions.get(sessionId);
|
|
921
|
-
const agentDid = session?.agentDid || (await this.getIdentity()).did;
|
|
922
|
-
await this.nonceCache.add(nonce, 300, // 5 minute expiry in seconds
|
|
923
|
-
agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
|
|
924
|
-
);
|
|
925
|
-
return nonce;
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Create cryptographic proof for data
|
|
929
|
-
*/
|
|
930
|
-
async createProof(data, session) {
|
|
931
|
-
const identity = await this.getIdentity();
|
|
932
|
-
const timestamp = this.clock.now();
|
|
933
|
-
// Use nonce from session if provided, otherwise generate new one
|
|
934
|
-
let nonce;
|
|
935
|
-
if (session?.nonce) {
|
|
936
|
-
nonce = session.nonce;
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
nonce = await this.generateNonce();
|
|
940
|
-
// Add nonce to cache to prevent replay (agent-scoped to prevent cross-agent replay attacks)
|
|
941
|
-
const agentDid = session?.agentDid || identity.did;
|
|
942
|
-
await this.nonceCache.add(nonce, 300, // 5 minute expiry in seconds
|
|
943
|
-
agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
const proofData = {
|
|
947
|
-
data,
|
|
948
|
-
timestamp,
|
|
949
|
-
nonce,
|
|
950
|
-
did: identity.did,
|
|
951
|
-
sessionId: session?.id,
|
|
952
|
-
audience: session?.audience,
|
|
953
|
-
};
|
|
954
|
-
const signature = await this.signData(proofData);
|
|
955
|
-
return {
|
|
956
|
-
timestamp,
|
|
957
|
-
nonce,
|
|
958
|
-
did: identity.did,
|
|
959
|
-
signature,
|
|
960
|
-
algorithm: "Ed25519",
|
|
961
|
-
sessionId: session?.id,
|
|
962
|
-
audience: session?.audience,
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Verify a proof
|
|
967
|
-
*
|
|
968
|
-
* Supports both old format (data, proof) and new DetachedProof format.
|
|
969
|
-
* When DetachedProof format is used, ProofVerifier is used if available.
|
|
970
|
-
*
|
|
971
|
-
* @param dataOrProof - Either raw data (old format) or DetachedProof (new format)
|
|
972
|
-
* @param proofOrSession - Either proof object (old format) or session context (new format)
|
|
973
|
-
* @returns true if proof is valid, false otherwise
|
|
974
|
-
*/
|
|
975
|
-
async verifyProof(dataOrProof, proofOrSession) {
|
|
976
|
-
// Check if first argument is DetachedProof format
|
|
977
|
-
if (dataOrProof &&
|
|
978
|
-
typeof dataOrProof === "object" &&
|
|
979
|
-
"jws" in dataOrProof &&
|
|
980
|
-
"meta" in dataOrProof) {
|
|
981
|
-
// New DetachedProof format
|
|
982
|
-
const detachedProof = dataOrProof;
|
|
983
|
-
const session = isRecord(proofOrSession)
|
|
984
|
-
? proofOrSession
|
|
985
|
-
: undefined;
|
|
986
|
-
// Use ProofVerifier if available
|
|
987
|
-
if (this.proofVerifier) {
|
|
988
|
-
try {
|
|
989
|
-
// Resolve DID to get public key
|
|
990
|
-
const didDoc = await this.fetch.resolveDID(detachedProof.meta.did);
|
|
991
|
-
const publicKeyJwk = this.extractPublicKeyJwk(didDoc, detachedProof.meta.kid);
|
|
992
|
-
if (!publicKeyJwk) {
|
|
993
|
-
console.error("[MCPIRuntimeBase] Failed to extract public key JWK");
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
// Verify proof using ProofVerifier
|
|
997
|
-
const result = await this.proofVerifier.verifyProof(detachedProof, publicKeyJwk);
|
|
998
|
-
if (result.valid && session) {
|
|
999
|
-
// Store canonical payload in session for detached JWS consumers
|
|
1000
|
-
const canonicalPayload = this.proofVerifier.buildCanonicalPayload(detachedProof.meta);
|
|
1001
|
-
session.canonicalPayload = canonicalPayload;
|
|
1002
|
-
}
|
|
1003
|
-
return result.valid;
|
|
1004
|
-
}
|
|
1005
|
-
catch (error) {
|
|
1006
|
-
console.error("[MCPIRuntimeBase] Proof verification failed:", error);
|
|
1007
|
-
return false;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
else {
|
|
1011
|
-
// Fallback to old verification if ProofVerifier not available
|
|
1012
|
-
console.warn("[MCPIRuntimeBase] ProofVerifier not available, using fallback verification");
|
|
1013
|
-
return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
// Old format (data, proof)
|
|
1018
|
-
return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Legacy proof verification (backward compatibility)
|
|
1023
|
-
* @internal
|
|
1024
|
-
*/
|
|
1025
|
-
async verifyProofLegacy(data, proof) {
|
|
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
|
-
}
|
|
1035
|
-
// Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
|
|
1036
|
-
if (await this.nonceCache.has(nonce, did)) {
|
|
1037
|
-
return false;
|
|
1038
|
-
}
|
|
1039
|
-
// Check timestamp is within skew
|
|
1040
|
-
if (!this.clock.isWithinSkew(timestamp, this.config.session?.timestampSkewSeconds || 300)) {
|
|
1041
|
-
return false;
|
|
1042
|
-
}
|
|
1043
|
-
// Resolve DID to get public key
|
|
1044
|
-
const didDoc = await this.fetch.resolveDID(did);
|
|
1045
|
-
const publicKey = this.extractPublicKey(didDoc);
|
|
1046
|
-
// Verify signature
|
|
1047
|
-
const proofData = {
|
|
1048
|
-
data,
|
|
1049
|
-
timestamp,
|
|
1050
|
-
nonce,
|
|
1051
|
-
did,
|
|
1052
|
-
sessionId,
|
|
1053
|
-
};
|
|
1054
|
-
const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
|
|
1055
|
-
const signatureBytes = this.base64ToBytes(signature);
|
|
1056
|
-
const isValid = await this.crypto.verify(dataBytes, signatureBytes, publicKey);
|
|
1057
|
-
// If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
|
|
1058
|
-
if (isValid) {
|
|
1059
|
-
// Pass TTL in seconds, not absolute timestamp
|
|
1060
|
-
const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
|
|
1061
|
-
await this.nonceCache.add(nonce, ttlSeconds, did);
|
|
1062
|
-
}
|
|
1063
|
-
return isValid;
|
|
1064
|
-
}
|
|
1065
|
-
catch (error) {
|
|
1066
|
-
console.error("Proof verification failed:", error);
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Verify a JWS proof (full compact JWS format: header.payload.signature)
|
|
1072
|
-
*
|
|
1073
|
-
* This method provides full cryptographic signature verification using CryptoService.
|
|
1074
|
-
* Use this when you have a DetachedProof with JWS instead of raw signature.
|
|
1075
|
-
*
|
|
1076
|
-
* @param jws - Full compact JWS string (header.payload.signature)
|
|
1077
|
-
* @param publicKeyJwk - Ed25519 public key in JWK format
|
|
1078
|
-
* @param detachedPayload - Optional detached payload for detached JWS format
|
|
1079
|
-
* @returns true if signature is valid, false otherwise
|
|
1080
|
-
*/
|
|
1081
|
-
async verifyProofJWS(jws, publicKeyJwk, detachedPayload) {
|
|
1082
|
-
if (!this.cryptoService) {
|
|
1083
|
-
console.error("[MCPIRuntimeBase] CryptoService not initialized");
|
|
1084
|
-
return false;
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
const options = detachedPayload !== undefined ? { detachedPayload } : undefined;
|
|
1088
|
-
return await this.cryptoService.verifyJWS(jws, publicKeyJwk, options);
|
|
1089
|
-
}
|
|
1090
|
-
catch (error) {
|
|
1091
|
-
console.error("[MCPIRuntimeBase] JWS verification failed:", error);
|
|
1092
|
-
return false;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
/**
|
|
1096
|
-
* Get current session
|
|
1097
|
-
*/
|
|
1098
|
-
async getCurrentSession() {
|
|
1099
|
-
// Find non-expired session
|
|
1100
|
-
for (const [id, session] of this.sessions) {
|
|
1101
|
-
if (!this.clock.hasExpired(session.expiresAt)) {
|
|
1102
|
-
return session;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* Get the last generated proof for out-of-band transport
|
|
1109
|
-
*/
|
|
1110
|
-
getLastProof() {
|
|
1111
|
-
return this.lastProof;
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* Create well-known handler for identity verification
|
|
1115
|
-
*/
|
|
1116
|
-
createWellKnownHandler(config) {
|
|
1117
|
-
return async (path) => {
|
|
1118
|
-
const identity = await this.getIdentity();
|
|
1119
|
-
if (path === "/.well-known/did.json") {
|
|
1120
|
-
const didDocument = this.createDIDDocument(identity);
|
|
1121
|
-
return {
|
|
1122
|
-
status: 200,
|
|
1123
|
-
headers: {
|
|
1124
|
-
"Content-Type": "application/did+json",
|
|
1125
|
-
"Cache-Control": this.config.environment === "production"
|
|
1126
|
-
? "public, max-age=300"
|
|
1127
|
-
: "no-store",
|
|
1128
|
-
},
|
|
1129
|
-
body: JSON.stringify(didDocument, null, 2),
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
if (path === "/.well-known/agent.json") {
|
|
1133
|
-
const agentDocument = {
|
|
1134
|
-
id: identity.did,
|
|
1135
|
-
capabilities: {
|
|
1136
|
-
"mcp-i": ["handshake", "signing", "verification"],
|
|
1137
|
-
},
|
|
1138
|
-
};
|
|
1139
|
-
if (config?.serviceName || config?.serviceEndpoint) {
|
|
1140
|
-
agentDocument.metadata = {
|
|
1141
|
-
...(config?.serviceName && { name: config.serviceName }),
|
|
1142
|
-
...(config?.serviceEndpoint && {
|
|
1143
|
-
serviceEndpoint: config.serviceEndpoint,
|
|
1144
|
-
}),
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
return {
|
|
1148
|
-
status: 200,
|
|
1149
|
-
headers: {
|
|
1150
|
-
"Content-Type": "application/json",
|
|
1151
|
-
"Cache-Control": this.config.environment === "production"
|
|
1152
|
-
? "public, max-age=300"
|
|
1153
|
-
: "no-store",
|
|
1154
|
-
},
|
|
1155
|
-
body: JSON.stringify(agentDocument, null, 2),
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
if (path === "/.well-known/mcp-identity") {
|
|
1159
|
-
return {
|
|
1160
|
-
did: identity.did,
|
|
1161
|
-
publicKey: identity.publicKey,
|
|
1162
|
-
serviceName: config?.serviceName || "MCP-I Service",
|
|
1163
|
-
serviceEndpoint: config?.serviceEndpoint || "https://example.com",
|
|
1164
|
-
timestamp: this.clock.now(),
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
// SEP-1649 MCP Server Card for client discovery
|
|
1168
|
-
// Support both paths during spec transition:
|
|
1169
|
-
// - /.well-known/mcp.json (current Cursor/Claude Desktop usage)
|
|
1170
|
-
// - /.well-known/mcp/server-card.json (SEP-1649 proposed spec)
|
|
1171
|
-
if (path === "/.well-known/mcp.json" || path === "/.well-known/mcp/server-card.json") {
|
|
1172
|
-
const serverName = config?.serviceName || "mcp-i-server";
|
|
1173
|
-
// SEP-1649 compliant MCP Server Card
|
|
1174
|
-
const mcpServerCard = {
|
|
1175
|
-
$schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
|
|
1176
|
-
version: "1.0",
|
|
1177
|
-
protocolVersion: config?.protocolVersion || "2025-06-18",
|
|
1178
|
-
serverInfo: {
|
|
1179
|
-
name: serverName,
|
|
1180
|
-
title: config?.serviceTitle || config?.serviceName || "MCP-I Server",
|
|
1181
|
-
version: config?.serverVersion || "1.0.0",
|
|
1182
|
-
},
|
|
1183
|
-
description: config?.description || "MCP-I enabled server with cryptographic identity",
|
|
1184
|
-
...(config?.iconUrl && { iconUrl: config.iconUrl }),
|
|
1185
|
-
...(config?.documentationUrl && { documentationUrl: config.documentationUrl }),
|
|
1186
|
-
transport: {
|
|
1187
|
-
type: "streamable-http",
|
|
1188
|
-
endpoint: "/mcp",
|
|
1189
|
-
},
|
|
1190
|
-
capabilities: {
|
|
1191
|
-
tools: { listChanged: true },
|
|
1192
|
-
},
|
|
1193
|
-
...(config?.requiresAuth && {
|
|
1194
|
-
authentication: {
|
|
1195
|
-
required: true,
|
|
1196
|
-
schemes: config.authSchemes || ["oauth2"],
|
|
1197
|
-
},
|
|
1198
|
-
}),
|
|
1199
|
-
...(config?.instructions && { instructions: config.instructions }),
|
|
1200
|
-
// Tools are dynamic - discovered via protocol
|
|
1201
|
-
tools: "dynamic",
|
|
1202
|
-
_meta: {
|
|
1203
|
-
...config?.metadata,
|
|
1204
|
-
"mcp-i": {
|
|
1205
|
-
agentDid: identity.did,
|
|
1206
|
-
publicKey: identity.publicKey,
|
|
1207
|
-
generatedAt: this.clock.now(),
|
|
1208
|
-
},
|
|
1209
|
-
},
|
|
1210
|
-
};
|
|
1211
|
-
const headers = {
|
|
1212
|
-
"Content-Type": "application/json",
|
|
1213
|
-
"Cache-Control": this.config.environment === "production"
|
|
1214
|
-
? "public, max-age=3600" // SEP-1649 recommends 1 hour
|
|
1215
|
-
: "no-store",
|
|
1216
|
-
// SEP-1649 CORS requirements for browser-based client discovery
|
|
1217
|
-
"Access-Control-Allow-Origin": "*",
|
|
1218
|
-
"Access-Control-Allow-Methods": "GET",
|
|
1219
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
1220
|
-
};
|
|
1221
|
-
return {
|
|
1222
|
-
status: 200,
|
|
1223
|
-
headers,
|
|
1224
|
-
body: JSON.stringify(mcpServerCard, null, 2),
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
// Client configuration helper - ready-to-use config for Cursor/Claude Desktop
|
|
1228
|
-
if (path === "/.well-known/mcp-client-config.json") {
|
|
1229
|
-
const serviceEndpoint = config?.serviceEndpoint || "https://example.com";
|
|
1230
|
-
const serverName = config?.serviceName || "mcp-i-server";
|
|
1231
|
-
const mcpUrl = `${serviceEndpoint}/mcp`;
|
|
1232
|
-
// Generate Cursor deep link
|
|
1233
|
-
// Note: Base64 can contain +, /, = which have special meaning in URLs
|
|
1234
|
-
// Must URL-encode the base64 string for safe transmission
|
|
1235
|
-
const cursorConfig = { [serverName]: { url: mcpUrl, transport: "sse" } };
|
|
1236
|
-
const encodedConfig = Buffer.from(JSON.stringify(cursorConfig)).toString("base64");
|
|
1237
|
-
const cursorDeepLink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(serverName)}&config=${encodeURIComponent(encodedConfig)}`;
|
|
1238
|
-
const clientConfig = {
|
|
1239
|
-
// For claude_desktop_config.json
|
|
1240
|
-
claudeDesktop: {
|
|
1241
|
-
mcpServers: {
|
|
1242
|
-
[serverName]: {
|
|
1243
|
-
url: mcpUrl,
|
|
1244
|
-
transport: "sse",
|
|
1245
|
-
},
|
|
1246
|
-
},
|
|
1247
|
-
},
|
|
1248
|
-
// For .cursor/mcp.json
|
|
1249
|
-
cursor: {
|
|
1250
|
-
mcpServers: {
|
|
1251
|
-
[serverName]: {
|
|
1252
|
-
url: mcpUrl,
|
|
1253
|
-
transport: "sse",
|
|
1254
|
-
},
|
|
1255
|
-
},
|
|
1256
|
-
},
|
|
1257
|
-
// One-click install for Cursor
|
|
1258
|
-
cursorDeepLink,
|
|
1259
|
-
// Additional info
|
|
1260
|
-
_meta: {
|
|
1261
|
-
serverName,
|
|
1262
|
-
serverUrl: serviceEndpoint,
|
|
1263
|
-
mcpEndpoint: mcpUrl,
|
|
1264
|
-
agentDid: identity.did,
|
|
1265
|
-
generatedAt: this.clock.now(),
|
|
1266
|
-
instructions: "Copy the appropriate config section to your client configuration file.",
|
|
1267
|
-
},
|
|
1268
|
-
};
|
|
1269
|
-
return {
|
|
1270
|
-
status: 200,
|
|
1271
|
-
headers: {
|
|
1272
|
-
"Content-Type": "application/json",
|
|
1273
|
-
"Cache-Control": this.config.environment === "production"
|
|
1274
|
-
? "public, max-age=300"
|
|
1275
|
-
: "no-store",
|
|
1276
|
-
},
|
|
1277
|
-
body: JSON.stringify(clientConfig, null, 2),
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
return null;
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* Create debug endpoint (development only)
|
|
1285
|
-
*/
|
|
1286
|
-
createDebugEndpoint() {
|
|
1287
|
-
if (this.config.environment === "production") {
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
return async () => {
|
|
1291
|
-
const identity = await this.getIdentity();
|
|
1292
|
-
const session = await this.getCurrentSession();
|
|
1293
|
-
return {
|
|
1294
|
-
identity: {
|
|
1295
|
-
did: identity.did,
|
|
1296
|
-
publicKey: identity.publicKey,
|
|
1297
|
-
},
|
|
1298
|
-
session,
|
|
1299
|
-
config: {
|
|
1300
|
-
environment: this.config.environment,
|
|
1301
|
-
timestampSkewSeconds: this.config.session?.timestampSkewSeconds,
|
|
1302
|
-
sessionTtlMinutes: this.config.session?.ttlMinutes,
|
|
1303
|
-
},
|
|
1304
|
-
timestamp: this.clock.now(),
|
|
1305
|
-
};
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Get audit logger
|
|
1310
|
-
*/
|
|
1311
|
-
getAuditLogger() {
|
|
1312
|
-
return {
|
|
1313
|
-
log: (event, data) => this.logAudit(event, data),
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Rotate keys
|
|
1318
|
-
*/
|
|
1319
|
-
async rotateKeys() {
|
|
1320
|
-
const oldDid = this.cachedIdentity?.did;
|
|
1321
|
-
const newIdentity = await this.identity.rotateKeys();
|
|
1322
|
-
this.cachedIdentity = newIdentity;
|
|
1323
|
-
if (this.config.audit?.enabled) {
|
|
1324
|
-
this.logAudit("keys_rotated", {
|
|
1325
|
-
oldDid: oldDid,
|
|
1326
|
-
newDid: newIdentity.did,
|
|
1327
|
-
timestamp: this.clock.now(),
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
return newIdentity;
|
|
1331
|
-
}
|
|
1332
|
-
// Helper methods
|
|
1333
|
-
async signData(data) {
|
|
1334
|
-
const identity = await this.getIdentity();
|
|
1335
|
-
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
1336
|
-
const signature = await this.crypto.sign(dataBytes, identity.privateKey);
|
|
1337
|
-
return this.bytesToBase64(signature);
|
|
1338
|
-
}
|
|
1339
|
-
async generateNonce() {
|
|
1340
|
-
const bytes = await this.crypto.randomBytes(32);
|
|
1341
|
-
return this.bytesToBase64(bytes);
|
|
1342
|
-
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Generate a unique session ID
|
|
1345
|
-
* Uses mcpi_{uuid} format for MCP protocol compliance and traceability
|
|
1346
|
-
*/
|
|
1347
|
-
async generateSessionId() {
|
|
1348
|
-
const bytes = await this.crypto.randomBytes(16);
|
|
1349
|
-
// Set version (4) and variant (RFC4122)
|
|
1350
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
1351
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
1352
|
-
const hex = this.bytesToHex(bytes);
|
|
1353
|
-
const uuid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
1354
|
-
return `mcpi_${uuid}`;
|
|
1355
|
-
}
|
|
1356
|
-
/**
|
|
1357
|
-
* Log structured events in JSON format (NOT frozen audit format).
|
|
1358
|
-
*
|
|
1359
|
-
* **Important:** This method logs events in JSON format for general runtime events
|
|
1360
|
-
* (e.g., "runtime_initialized", "tool_executed", "keys_rotated").
|
|
1361
|
-
*
|
|
1362
|
-
* **For frozen format audit logs** (MCP-I spec compliance), use `AuditLogger.logAuditRecord()`
|
|
1363
|
-
* from `@kya-os/mcp-i/runtime` instead. The frozen format is:
|
|
1364
|
-
* ```
|
|
1365
|
-
* audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
|
|
1366
|
-
* ```
|
|
1367
|
-
*
|
|
1368
|
-
* **Format:** JSON object with `event`, `data`, `timestamp`, and `timestampFormatted` fields.
|
|
1369
|
-
*
|
|
1370
|
-
* **Privacy:** If `includePayloads` is false (default), the `data` field is omitted.
|
|
1371
|
-
*
|
|
1372
|
-
* **Use Cases:**
|
|
1373
|
-
* - Developer debugging and local logging
|
|
1374
|
-
* - Runtime initialization events
|
|
1375
|
-
* - Tool execution tracking (non-spec-compliant)
|
|
1376
|
-
* - Key rotation events
|
|
1377
|
-
*
|
|
1378
|
-
* **NOT for:**
|
|
1379
|
-
* - MCP-I spec-compliant audit logs (use `AuditLogger`)
|
|
1380
|
-
* - Production audit trails (use `AuditLogger`)
|
|
1381
|
-
* - Compliance requirements (use `AuditLogger`)
|
|
1382
|
-
*
|
|
1383
|
-
* @param event - Event name (e.g., "runtime_initialized", "tool_executed")
|
|
1384
|
-
* @param data - Event data (only included if `includePayloads` is true)
|
|
1385
|
-
*
|
|
1386
|
-
* @example
|
|
1387
|
-
* ```typescript
|
|
1388
|
-
* // Logs: {"event":"runtime_initialized","timestamp":1234567890,"timestampFormatted":"2024-01-01T00:00:00Z"}
|
|
1389
|
-
* this.logAudit("runtime_initialized", { did: "did:key:..." });
|
|
1390
|
-
* ```
|
|
1391
|
-
*
|
|
1392
|
-
* @internal This is a private method for internal runtime events.
|
|
1393
|
-
* Use AuditLogger for spec-compliant frozen format audit logs.
|
|
1394
|
-
*/
|
|
1395
|
-
logAudit(event, data) {
|
|
1396
|
-
if (!this.config.audit?.enabled)
|
|
1397
|
-
return;
|
|
1398
|
-
const record = {
|
|
1399
|
-
event,
|
|
1400
|
-
data: this.config.audit.includePayloads ? data : undefined,
|
|
1401
|
-
timestamp: this.clock.now(),
|
|
1402
|
-
timestampFormatted: this.clock.format(this.clock.now()),
|
|
1403
|
-
};
|
|
1404
|
-
const logLine = JSON.stringify(record);
|
|
1405
|
-
if (this.config.audit.logFunction) {
|
|
1406
|
-
this.config.audit.logFunction(logLine);
|
|
1407
|
-
}
|
|
1408
|
-
else {
|
|
1409
|
-
console.error("[AUDIT]", logLine);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
createDIDDocument(identity) {
|
|
1413
|
-
return {
|
|
1414
|
-
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
1415
|
-
id: identity.did,
|
|
1416
|
-
verificationMethod: [
|
|
1417
|
-
{
|
|
1418
|
-
id: `${identity.did}#key-1`,
|
|
1419
|
-
type: "Ed25519VerificationKey2020",
|
|
1420
|
-
controller: identity.did,
|
|
1421
|
-
// Using base64 for cross-platform compatibility (Cloudflare Workers)
|
|
1422
|
-
// W3C DID spec supports both base64 and multibase formats
|
|
1423
|
-
publicKeyBase64: identity.publicKey,
|
|
1424
|
-
},
|
|
1425
|
-
],
|
|
1426
|
-
authentication: [`${identity.did}#key-1`],
|
|
1427
|
-
assertionMethod: [`${identity.did}#key-1`],
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
extractPublicKey(didDoc) {
|
|
1431
|
-
const verificationMethods = isRecord(didDoc)
|
|
1432
|
-
? didDoc.verificationMethod
|
|
1433
|
-
: undefined;
|
|
1434
|
-
const method = Array.isArray(verificationMethods)
|
|
1435
|
-
? verificationMethods[0]
|
|
1436
|
-
: undefined;
|
|
1437
|
-
if (method?.publicKeyBase64) {
|
|
1438
|
-
return String(method.publicKeyBase64);
|
|
1439
|
-
}
|
|
1440
|
-
if (method?.publicKeyMultibase) {
|
|
1441
|
-
// Convert multibase to base64
|
|
1442
|
-
return String(method.publicKeyMultibase); // Simplified
|
|
1443
|
-
}
|
|
1444
|
-
throw new Error("Public key not found in DID document");
|
|
1445
|
-
}
|
|
1446
|
-
/**
|
|
1447
|
-
* Extract public key JWK from DID document
|
|
1448
|
-
*/
|
|
1449
|
-
extractPublicKeyJwk(didDoc, kid) {
|
|
1450
|
-
const verificationMethods = isRecord(didDoc)
|
|
1451
|
-
? didDoc.verificationMethod
|
|
1452
|
-
: undefined;
|
|
1453
|
-
const verificationMethodList = Array.isArray(verificationMethods)
|
|
1454
|
-
? verificationMethods
|
|
1455
|
-
: [];
|
|
1456
|
-
// Try to find Ed25519 public key matching kid if provided
|
|
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}`);
|
|
1463
|
-
return matchesType && matchesKid;
|
|
1464
|
-
}) || verificationMethodList[0]; // Fallback to first method
|
|
1465
|
-
if (verificationMethod && isRecord(verificationMethod.publicKeyJwk)) {
|
|
1466
|
-
const jwk = verificationMethod.publicKeyJwk;
|
|
1467
|
-
// Ensure it's Ed25519 format
|
|
1468
|
-
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
|
|
1469
|
-
return jwk;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
// Fallback: try to convert multibase to JWK (simplified)
|
|
1473
|
-
if (verificationMethod?.publicKeyMultibase) {
|
|
1474
|
-
// This is a simplified conversion - in production, use proper multibase decoding
|
|
1475
|
-
console.warn("[MCPIRuntimeBase] Multibase to JWK conversion not fully implemented");
|
|
1476
|
-
return null;
|
|
1477
|
-
}
|
|
1478
|
-
return null;
|
|
1479
|
-
}
|
|
1480
|
-
bytesToBase64(bytes) {
|
|
1481
|
-
return Buffer.from(bytes).toString("base64");
|
|
1482
|
-
}
|
|
1483
|
-
base64ToBytes(base64) {
|
|
1484
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
1485
|
-
}
|
|
1486
|
-
bytesToHex(bytes) {
|
|
1487
|
-
return Buffer.from(bytes).toString("hex");
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
exports.MCPIRuntimeBase = MCPIRuntimeBase;
|
|
1491
|
-
//# sourceMappingURL=base.js.map
|