@mcp-i/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -0
  3. package/dist/auth/handshake.d.ts +104 -0
  4. package/dist/auth/handshake.d.ts.map +1 -0
  5. package/dist/auth/handshake.js +230 -0
  6. package/dist/auth/handshake.js.map +1 -0
  7. package/dist/auth/index.d.ts +3 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +2 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/types.d.ts +31 -0
  12. package/dist/auth/types.d.ts.map +1 -0
  13. package/dist/auth/types.js +7 -0
  14. package/dist/auth/types.js.map +1 -0
  15. package/dist/delegation/audience-validator.d.ts +9 -0
  16. package/dist/delegation/audience-validator.d.ts.map +1 -0
  17. package/dist/delegation/audience-validator.js +17 -0
  18. package/dist/delegation/audience-validator.js.map +1 -0
  19. package/dist/delegation/bitstring.d.ts +37 -0
  20. package/dist/delegation/bitstring.d.ts.map +1 -0
  21. package/dist/delegation/bitstring.js +117 -0
  22. package/dist/delegation/bitstring.js.map +1 -0
  23. package/dist/delegation/cascading-revocation.d.ts +45 -0
  24. package/dist/delegation/cascading-revocation.d.ts.map +1 -0
  25. package/dist/delegation/cascading-revocation.js +148 -0
  26. package/dist/delegation/cascading-revocation.js.map +1 -0
  27. package/dist/delegation/delegation-graph.d.ts +49 -0
  28. package/dist/delegation/delegation-graph.d.ts.map +1 -0
  29. package/dist/delegation/delegation-graph.js +99 -0
  30. package/dist/delegation/delegation-graph.js.map +1 -0
  31. package/dist/delegation/did-key-resolver.d.ts +64 -0
  32. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  33. package/dist/delegation/did-key-resolver.js +154 -0
  34. package/dist/delegation/did-key-resolver.js.map +1 -0
  35. package/dist/delegation/did-web-resolver.d.ts +83 -0
  36. package/dist/delegation/did-web-resolver.d.ts.map +1 -0
  37. package/dist/delegation/did-web-resolver.js +218 -0
  38. package/dist/delegation/did-web-resolver.js.map +1 -0
  39. package/dist/delegation/index.d.ts +21 -0
  40. package/dist/delegation/index.d.ts.map +1 -0
  41. package/dist/delegation/index.js +21 -0
  42. package/dist/delegation/index.js.map +1 -0
  43. package/dist/delegation/outbound-headers.d.ts +81 -0
  44. package/dist/delegation/outbound-headers.d.ts.map +1 -0
  45. package/dist/delegation/outbound-headers.js +139 -0
  46. package/dist/delegation/outbound-headers.js.map +1 -0
  47. package/dist/delegation/outbound-proof.d.ts +43 -0
  48. package/dist/delegation/outbound-proof.d.ts.map +1 -0
  49. package/dist/delegation/outbound-proof.js +52 -0
  50. package/dist/delegation/outbound-proof.js.map +1 -0
  51. package/dist/delegation/statuslist-manager.d.ts +44 -0
  52. package/dist/delegation/statuslist-manager.d.ts.map +1 -0
  53. package/dist/delegation/statuslist-manager.js +126 -0
  54. package/dist/delegation/statuslist-manager.js.map +1 -0
  55. package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
  56. package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
  57. package/dist/delegation/storage/memory-graph-storage.js +145 -0
  58. package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
  59. package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
  60. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
  61. package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
  62. package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
  63. package/dist/delegation/utils.d.ts +49 -0
  64. package/dist/delegation/utils.d.ts.map +1 -0
  65. package/dist/delegation/utils.js +131 -0
  66. package/dist/delegation/utils.js.map +1 -0
  67. package/dist/delegation/vc-issuer.d.ts +56 -0
  68. package/dist/delegation/vc-issuer.d.ts.map +1 -0
  69. package/dist/delegation/vc-issuer.js +80 -0
  70. package/dist/delegation/vc-issuer.js.map +1 -0
  71. package/dist/delegation/vc-verifier.d.ts +112 -0
  72. package/dist/delegation/vc-verifier.d.ts.map +1 -0
  73. package/dist/delegation/vc-verifier.js +280 -0
  74. package/dist/delegation/vc-verifier.js.map +1 -0
  75. package/dist/index.d.ts +45 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +53 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/logging/index.d.ts +2 -0
  80. package/dist/logging/index.d.ts.map +1 -0
  81. package/dist/logging/index.js +2 -0
  82. package/dist/logging/index.js.map +1 -0
  83. package/dist/logging/logger.d.ts +23 -0
  84. package/dist/logging/logger.d.ts.map +1 -0
  85. package/dist/logging/logger.js +82 -0
  86. package/dist/logging/logger.js.map +1 -0
  87. package/dist/middleware/index.d.ts +7 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +7 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/with-mcpi.d.ts +152 -0
  92. package/dist/middleware/with-mcpi.d.ts.map +1 -0
  93. package/dist/middleware/with-mcpi.js +472 -0
  94. package/dist/middleware/with-mcpi.js.map +1 -0
  95. package/dist/proof/errors.d.ts +49 -0
  96. package/dist/proof/errors.d.ts.map +1 -0
  97. package/dist/proof/errors.js +61 -0
  98. package/dist/proof/errors.js.map +1 -0
  99. package/dist/proof/generator.d.ts +65 -0
  100. package/dist/proof/generator.d.ts.map +1 -0
  101. package/dist/proof/generator.js +163 -0
  102. package/dist/proof/generator.js.map +1 -0
  103. package/dist/proof/index.d.ts +4 -0
  104. package/dist/proof/index.d.ts.map +1 -0
  105. package/dist/proof/index.js +4 -0
  106. package/dist/proof/index.js.map +1 -0
  107. package/dist/proof/verifier.d.ts +108 -0
  108. package/dist/proof/verifier.d.ts.map +1 -0
  109. package/dist/proof/verifier.js +299 -0
  110. package/dist/proof/verifier.js.map +1 -0
  111. package/dist/providers/base.d.ts +64 -0
  112. package/dist/providers/base.d.ts.map +1 -0
  113. package/dist/providers/base.js +19 -0
  114. package/dist/providers/base.js.map +1 -0
  115. package/dist/providers/index.d.ts +3 -0
  116. package/dist/providers/index.d.ts.map +1 -0
  117. package/dist/providers/index.js +3 -0
  118. package/dist/providers/index.js.map +1 -0
  119. package/dist/providers/memory.d.ts +33 -0
  120. package/dist/providers/memory.d.ts.map +1 -0
  121. package/dist/providers/memory.js +102 -0
  122. package/dist/providers/memory.js.map +1 -0
  123. package/dist/session/index.d.ts +2 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +2 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/session/manager.d.ts +77 -0
  128. package/dist/session/manager.d.ts.map +1 -0
  129. package/dist/session/manager.js +251 -0
  130. package/dist/session/manager.js.map +1 -0
  131. package/dist/types/protocol.d.ts +320 -0
  132. package/dist/types/protocol.d.ts.map +1 -0
  133. package/dist/types/protocol.js +229 -0
  134. package/dist/types/protocol.js.map +1 -0
  135. package/dist/utils/base58.d.ts +31 -0
  136. package/dist/utils/base58.d.ts.map +1 -0
  137. package/dist/utils/base58.js +104 -0
  138. package/dist/utils/base58.js.map +1 -0
  139. package/dist/utils/base64.d.ts +13 -0
  140. package/dist/utils/base64.d.ts.map +1 -0
  141. package/dist/utils/base64.js +99 -0
  142. package/dist/utils/base64.js.map +1 -0
  143. package/dist/utils/crypto-service.d.ts +37 -0
  144. package/dist/utils/crypto-service.d.ts.map +1 -0
  145. package/dist/utils/crypto-service.js +153 -0
  146. package/dist/utils/crypto-service.js.map +1 -0
  147. package/dist/utils/did-helpers.d.ts +156 -0
  148. package/dist/utils/did-helpers.d.ts.map +1 -0
  149. package/dist/utils/did-helpers.js +193 -0
  150. package/dist/utils/did-helpers.js.map +1 -0
  151. package/dist/utils/ed25519-constants.d.ts +18 -0
  152. package/dist/utils/ed25519-constants.d.ts.map +1 -0
  153. package/dist/utils/ed25519-constants.js +21 -0
  154. package/dist/utils/ed25519-constants.js.map +1 -0
  155. package/dist/utils/index.d.ts +5 -0
  156. package/dist/utils/index.d.ts.map +1 -0
  157. package/dist/utils/index.js +5 -0
  158. package/dist/utils/index.js.map +1 -0
  159. package/package.json +105 -0
  160. package/src/__tests__/integration/full-flow.test.ts +362 -0
  161. package/src/__tests__/providers/base.test.ts +173 -0
  162. package/src/__tests__/providers/memory.test.ts +332 -0
  163. package/src/__tests__/utils/mock-providers.ts +319 -0
  164. package/src/__tests__/utils/node-crypto-provider.ts +93 -0
  165. package/src/auth/handshake.ts +411 -0
  166. package/src/auth/index.ts +11 -0
  167. package/src/auth/types.ts +40 -0
  168. package/src/delegation/__tests__/audience-validator.test.ts +110 -0
  169. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  170. package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
  171. package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
  172. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  173. package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
  174. package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
  175. package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
  176. package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
  177. package/src/delegation/__tests__/utils.test.ts +185 -0
  178. package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
  179. package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
  180. package/src/delegation/audience-validator.ts +24 -0
  181. package/src/delegation/bitstring.ts +160 -0
  182. package/src/delegation/cascading-revocation.ts +224 -0
  183. package/src/delegation/delegation-graph.ts +143 -0
  184. package/src/delegation/did-key-resolver.ts +181 -0
  185. package/src/delegation/did-web-resolver.ts +270 -0
  186. package/src/delegation/index.ts +33 -0
  187. package/src/delegation/outbound-headers.ts +193 -0
  188. package/src/delegation/outbound-proof.ts +90 -0
  189. package/src/delegation/statuslist-manager.ts +219 -0
  190. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  191. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  192. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  193. package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
  194. package/src/delegation/utils.ts +189 -0
  195. package/src/delegation/vc-issuer.ts +137 -0
  196. package/src/delegation/vc-verifier.ts +440 -0
  197. package/src/index.ts +264 -0
  198. package/src/logging/__tests__/logger.test.ts +366 -0
  199. package/src/logging/index.ts +6 -0
  200. package/src/logging/logger.ts +91 -0
  201. package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
  202. package/src/middleware/index.ts +16 -0
  203. package/src/middleware/with-mcpi.ts +766 -0
  204. package/src/proof/__tests__/proof-generator.test.ts +483 -0
  205. package/src/proof/__tests__/verifier.test.ts +488 -0
  206. package/src/proof/errors.ts +75 -0
  207. package/src/proof/generator.ts +255 -0
  208. package/src/proof/index.ts +22 -0
  209. package/src/proof/verifier.ts +449 -0
  210. package/src/providers/base.ts +68 -0
  211. package/src/providers/index.ts +15 -0
  212. package/src/providers/memory.ts +130 -0
  213. package/src/session/__tests__/session-manager.test.ts +342 -0
  214. package/src/session/index.ts +7 -0
  215. package/src/session/manager.ts +332 -0
  216. package/src/types/protocol.ts +596 -0
  217. package/src/utils/__tests__/base58.test.ts +281 -0
  218. package/src/utils/__tests__/base64.test.ts +239 -0
  219. package/src/utils/__tests__/crypto-service.test.ts +530 -0
  220. package/src/utils/__tests__/did-helpers.test.ts +156 -0
  221. package/src/utils/base58.ts +115 -0
  222. package/src/utils/base64.ts +116 -0
  223. package/src/utils/crypto-service.ts +209 -0
  224. package/src/utils/did-helpers.ts +210 -0
  225. package/src/utils/ed25519-constants.ts +23 -0
  226. package/src/utils/index.ts +9 -0
@@ -0,0 +1,766 @@
1
+ /**
2
+ * MCP-I Middleware for @modelcontextprotocol/sdk Server
3
+ *
4
+ * Adds identity, session management, and proof generation to a standard
5
+ * MCP SDK Server.
6
+ *
7
+ * Usage:
8
+ * const { handshakeTool, registerToolWithProof } = createMCPIMiddleware(config, crypto);
9
+ * server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [handshakeTool, ...] }));
10
+ * registerToolWithProof(server, myToolDef, myHandler);
11
+ */
12
+
13
+ import {
14
+ type CryptoProvider,
15
+ FetchProvider,
16
+ } from "../providers/base.js";
17
+ import {
18
+ SessionManager,
19
+ type SessionConfig,
20
+ type HandshakeResult,
21
+ } from "../session/manager.js";
22
+ import {
23
+ ProofGenerator,
24
+ type ProofAgentIdentity,
25
+ type ToolRequest,
26
+ type ToolResponse,
27
+ } from "../proof/generator.js";
28
+ import { validateHandshakeFormat } from "../session/manager.js";
29
+ import {
30
+ DelegationCredentialVerifier,
31
+ type DIDResolver,
32
+ type SignatureVerificationFunction,
33
+ type StatusListResolver,
34
+ } from "../delegation/vc-verifier.js";
35
+ import { createDidKeyResolver } from "../delegation/did-key-resolver.js";
36
+ import { createDidWebResolver } from "../delegation/did-web-resolver.js";
37
+ import { verifyDelegationAudience } from "../delegation/audience-validator.js";
38
+ import {
39
+ createNeedsAuthorizationError,
40
+ extractDelegationFromVC,
41
+ type DelegationCredential,
42
+ type DelegationRecord,
43
+ } from "../types/protocol.js";
44
+ import { logger } from "../logging/index.js";
45
+ import { canonicalizeJSON } from "../delegation/utils.js";
46
+ import { base64urlDecodeToBytes, base64urlEncodeFromBytes, bytesToBase64 } from "../utils/base64.js";
47
+
48
+ export interface MCPIIdentityConfig {
49
+ did: string;
50
+ kid: string;
51
+ privateKey: string;
52
+ publicKey: string;
53
+ }
54
+
55
+ export interface MCPIDelegationConfig {
56
+ /**
57
+ * Optional custom DID resolver. If it returns null, middleware falls back to
58
+ * built-in did:key resolution and fetch-backed did:web resolution.
59
+ */
60
+ didResolver?: DIDResolver;
61
+ /**
62
+ * Optional fetch provider used for did:web resolution.
63
+ * If omitted, middleware falls back to the runtime global fetch when available.
64
+ */
65
+ fetchProvider?: FetchProvider;
66
+ /**
67
+ * Resolver for StatusList2021 checks. Credentials with credentialStatus are
68
+ * rejected when no resolver is configured.
69
+ */
70
+ statusListResolver?: StatusListResolver;
71
+ /**
72
+ * Resolve ancestor credentials for a delegated chain. The returned array may
73
+ * contain only ancestors (root -> parent) or the full chain (root -> leaf).
74
+ */
75
+ resolveDelegationChain?: (
76
+ leafCredential: DelegationCredential,
77
+ ) => Promise<DelegationCredential[]>;
78
+ /**
79
+ * Compatibility mode for legacy integrations that cannot yet provide
80
+ * full delegation-chain and status-list resolvers.
81
+ *
82
+ * WARNING: Enabling this weakens verification guarantees:
83
+ * - Parent-linked delegations are accepted without chain resolution
84
+ * - credentialStatus is accepted without StatusList checks
85
+ *
86
+ * Default is false (strict security behavior).
87
+ */
88
+ allowLegacyUnsafeDelegation?: boolean;
89
+ }
90
+
91
+ export interface MCPIConfig {
92
+ /** Agent identity (DID + key material) */
93
+ identity: MCPIIdentityConfig;
94
+ /** Session configuration overrides */
95
+ session?: Omit<SessionConfig, "nonceCache">;
96
+ /** Delegation verification overrides */
97
+ delegation?: MCPIDelegationConfig;
98
+ /**
99
+ * When true, automatically creates a session on the first tool call
100
+ * if no session exists. Useful for demos and development where
101
+ * MCP clients don't support the _mcpi_handshake flow.
102
+ * In production, MCP-I-aware clients handle handshake automatically.
103
+ */
104
+ autoSession?: boolean;
105
+ }
106
+
107
+ export interface MCPIToolDefinition {
108
+ name: string;
109
+ description?: string;
110
+ inputSchema: {
111
+ type: "object";
112
+ properties?: Record<string, unknown>;
113
+ required?: string[];
114
+ [key: string]: unknown;
115
+ };
116
+ }
117
+
118
+ export interface MCPIToolHandler {
119
+ (
120
+ args: Record<string, unknown>,
121
+ sessionId?: string,
122
+ ): Promise<{
123
+ content: Array<{ type: string; text: string; [key: string]: unknown }>;
124
+ isError?: boolean;
125
+ [key: string]: unknown;
126
+ }>;
127
+ }
128
+
129
+ /**
130
+ * Server interface — minimal subset of @modelcontextprotocol/sdk Server.
131
+ * This avoids a hard dependency on the SDK at the type level.
132
+ */
133
+ export interface MCPIServer {
134
+ setRequestHandler(
135
+ schema: unknown,
136
+ handler: (...args: unknown[]) => unknown,
137
+ ): void;
138
+ }
139
+
140
+ export interface MCPIMiddleware {
141
+ /** The SessionManager instance for manual session operations */
142
+ sessionManager: SessionManager;
143
+
144
+ /** The ProofGenerator instance for manual proof operations */
145
+ proofGenerator: ProofGenerator;
146
+
147
+ /**
148
+ * Tool definition for `_mcpi_handshake`.
149
+ * Include this in your ListToolsRequest handler's tool list.
150
+ */
151
+ handshakeTool: MCPIToolDefinition;
152
+
153
+ /**
154
+ * Handle a handshake call. Use this in your CallToolRequest handler
155
+ * when `request.params.name === '_mcpi_handshake'`.
156
+ */
157
+ handleHandshake(args: Record<string, unknown>): Promise<{
158
+ content: Array<{ type: string; text: string }>;
159
+ isError?: boolean;
160
+ }>;
161
+
162
+ /**
163
+ * Wrap a tool handler to automatically generate proofs.
164
+ * Returns a new handler that appends proof metadata to the response.
165
+ */
166
+ wrapWithProof(toolName: string, handler: MCPIToolHandler): MCPIToolHandler;
167
+
168
+ /**
169
+ * Wrap a tool handler to require a valid W3C Delegation Credential.
170
+ *
171
+ * Caller must pass the VC as `_mcpi_delegation` in the tool args.
172
+ * - If absent: returns a `needs_authorization` response with the consentUrl.
173
+ * - If present but invalid: returns a structured error with reason.
174
+ * - If valid with correct scope: strips `_mcpi_delegation` and calls the handler.
175
+ */
176
+ wrapWithDelegation(
177
+ toolName: string,
178
+ config: {
179
+ scopeId: string;
180
+ consentUrl: string;
181
+ },
182
+ handler: MCPIToolHandler,
183
+ ): MCPIToolHandler;
184
+ }
185
+
186
+ class RuntimeFetchProvider extends FetchProvider {
187
+ async resolveDID(): Promise<null> {
188
+ return null;
189
+ }
190
+
191
+ async fetchStatusList(): Promise<null> {
192
+ return null;
193
+ }
194
+
195
+ async fetchDelegationChain(): Promise<DelegationRecord[]> {
196
+ return [];
197
+ }
198
+
199
+ async fetch(url: string, options?: unknown): Promise<Response> {
200
+ if (typeof globalThis.fetch !== "function") {
201
+ throw new Error("Global fetch is not available in this runtime");
202
+ }
203
+
204
+ return globalThis.fetch(url, options as RequestInit);
205
+ }
206
+ }
207
+
208
+ function getDelegationScopes(credential: DelegationCredential): string[] {
209
+ const scopes = new Set<string>();
210
+
211
+ for (const scope of credential.credentialSubject.delegation.scopes ?? []) {
212
+ scopes.add(scope);
213
+ }
214
+
215
+ for (const scope of credential.credentialSubject.delegation.constraints.scopes ?? []) {
216
+ scopes.add(scope);
217
+ }
218
+
219
+ return Array.from(scopes);
220
+ }
221
+
222
+ function validateScopeAttenuation(
223
+ parentCredential: DelegationCredential,
224
+ childCredential: DelegationCredential,
225
+ ): { valid: boolean; reason?: string } {
226
+ const parentScopes = getDelegationScopes(parentCredential);
227
+ const childScopes = getDelegationScopes(childCredential);
228
+ const childDelegation = childCredential.credentialSubject.delegation;
229
+
230
+ if (parentScopes.length === 0) {
231
+ return { valid: true };
232
+ }
233
+
234
+ if (childScopes.length === 0) {
235
+ return {
236
+ valid: false,
237
+ reason: `Delegation ${childDelegation.id} omits scopes required to prove attenuation from parent ${parentCredential.credentialSubject.delegation.id}`,
238
+ };
239
+ }
240
+
241
+ const parentScopeSet = new Set(parentScopes);
242
+ const widenedScopes = childScopes.filter((scope) => !parentScopeSet.has(scope));
243
+ if (widenedScopes.length > 0) {
244
+ return {
245
+ valid: false,
246
+ reason: `Delegation ${childDelegation.id} widens scopes beyond parent ${parentCredential.credentialSubject.delegation.id}: ${widenedScopes.join(", ")}`,
247
+ };
248
+ }
249
+
250
+ return { valid: true };
251
+ }
252
+
253
+ /**
254
+ * Create MCP-I middleware for a standard MCP SDK Server.
255
+ *
256
+ * @param config - Agent identity and session configuration
257
+ * @param cryptoProvider - Platform-specific crypto implementation
258
+ * @returns Middleware components for session management and proof generation
259
+ *
260
+ * @remarks
261
+ * **Single-process only**: This middleware stores session state in memory using closure
262
+ * variables (`activeSessionId`, `sessionNonces`). It is NOT suitable for multi-instance
263
+ * deployments behind a load balancer. For distributed deployments, implement a custom
264
+ * `SessionStore` backed by Redis, DynamoDB, or similar and pass it via `config.session`.
265
+ */
266
+ export function createMCPIMiddleware(
267
+ config: MCPIConfig,
268
+ cryptoProvider: CryptoProvider,
269
+ ): MCPIMiddleware {
270
+ const identity: ProofAgentIdentity = {
271
+ did: config.identity.did,
272
+ kid: config.identity.kid,
273
+ privateKey: config.identity.privateKey,
274
+ publicKey: config.identity.publicKey,
275
+ };
276
+
277
+ const sessionManager = new SessionManager(cryptoProvider, {
278
+ ...config.session,
279
+ serverDid: identity.did,
280
+ });
281
+
282
+ const proofGenerator = new ProofGenerator(identity, cryptoProvider);
283
+ const delegationConfig = config.delegation;
284
+
285
+ // Session map: sessionId → last nonce (for proof generation)
286
+ const sessionNonces = new Map<string, string>();
287
+
288
+ // Active session tracking — set after handshake (manual or auto)
289
+ let activeSessionId: string | undefined;
290
+
291
+ const handshakeTool: MCPIToolDefinition = {
292
+ name: "_mcpi_handshake",
293
+ description:
294
+ "MCP-I identity handshake — establishes a cryptographic session",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ nonce: { type: "string", description: "Client-generated unique nonce" },
299
+ audience: {
300
+ type: "string",
301
+ description: "Intended audience (server DID or URL)",
302
+ },
303
+ timestamp: { type: "number", description: "Unix epoch seconds" },
304
+ agentDid: {
305
+ type: "string",
306
+ description: "Client agent DID (optional)",
307
+ },
308
+ },
309
+ required: ["nonce", "audience", "timestamp"],
310
+ },
311
+ };
312
+
313
+ async function handleHandshake(args: Record<string, unknown>): Promise<{
314
+ content: Array<{ type: string; text: string }>;
315
+ isError?: boolean;
316
+ }> {
317
+ if (!validateHandshakeFormat(args)) {
318
+ return {
319
+ content: [
320
+ {
321
+ type: "text",
322
+ text: JSON.stringify({
323
+ success: false,
324
+ error: {
325
+ code: "MCPI_INVALID_HANDSHAKE",
326
+ message:
327
+ "Invalid handshake format: requires nonce (string), audience (string), and timestamp (positive integer)",
328
+ },
329
+ }),
330
+ },
331
+ ],
332
+ isError: true,
333
+ };
334
+ }
335
+
336
+ const result: HandshakeResult =
337
+ await sessionManager.validateHandshake(args);
338
+
339
+ if (result.success && result.session) {
340
+ sessionNonces.set(result.session.sessionId, result.session.nonce);
341
+ activeSessionId = result.session.sessionId;
342
+ }
343
+
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: JSON.stringify({
349
+ success: result.success,
350
+ ...(result.session && {
351
+ sessionId: result.session.sessionId,
352
+ serverDid: identity.did,
353
+ serverKid: identity.kid,
354
+ }),
355
+ ...(result.error && { error: result.error }),
356
+ }),
357
+ },
358
+ ],
359
+ ...(result.error && { isError: true }),
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Auto-create a session for proof generation when no handshake has occurred.
365
+ * In production, MCP-I-aware clients handle the handshake automatically.
366
+ * This convenience mode allows non-MCP-I clients (like MCP Inspector) to
367
+ * still see proofs without manual handshake.
368
+ */
369
+ async function ensureSession(): Promise<string | undefined> {
370
+ if (activeSessionId) {
371
+ const existing = await sessionManager.getSession(activeSessionId);
372
+ if (existing) return activeSessionId;
373
+ }
374
+
375
+ if (!config.autoSession) return undefined;
376
+
377
+ // Generate a server-side session with cryptographically random nonce (SPEC.md §4)
378
+ const nonceBytes = await cryptoProvider.randomBytes(16);
379
+ const nonce = base64urlEncodeFromBytes(nonceBytes);
380
+ const timestamp = Math.floor(Date.now() / 1000);
381
+
382
+ const result = await sessionManager.validateHandshake({
383
+ nonce,
384
+ audience: identity.did,
385
+ timestamp,
386
+ });
387
+
388
+ if (result.success && result.session) {
389
+ activeSessionId = result.session.sessionId;
390
+ sessionNonces.set(result.session.sessionId, result.session.nonce);
391
+ return activeSessionId;
392
+ }
393
+
394
+ return undefined;
395
+ }
396
+
397
+ function wrapWithProof(
398
+ toolName: string,
399
+ handler: MCPIToolHandler,
400
+ ): MCPIToolHandler {
401
+ return async (args: Record<string, unknown>, sessionId?: string) => {
402
+ const result = await handler(args, sessionId);
403
+
404
+ if (result.isError) {
405
+ return result;
406
+ }
407
+
408
+ // Resolve session: explicit param → active session → auto-create
409
+ const resolvedSessionId = sessionId ?? await ensureSession();
410
+ if (!resolvedSessionId) {
411
+ return result;
412
+ }
413
+
414
+ const session = await sessionManager.getSession(resolvedSessionId);
415
+ if (!session) {
416
+ return result;
417
+ }
418
+
419
+ try {
420
+ const request: ToolRequest = { method: toolName, params: args };
421
+ const response: ToolResponse = { data: result.content };
422
+
423
+ const proof = await proofGenerator.generateProof(
424
+ request,
425
+ response,
426
+ session,
427
+ );
428
+
429
+ // Attach proof as _meta (rendered by MCP Inspector, invisible to LLMs)
430
+ result._meta = { proof };
431
+ } catch {
432
+ // Proof generation failure is non-fatal — the tool result is still valid
433
+ }
434
+
435
+ return result;
436
+ };
437
+ }
438
+
439
+ function wrapWithDelegation(
440
+ toolName: string,
441
+ config: { scopeId: string; consentUrl: string },
442
+ handler: MCPIToolHandler,
443
+ ): MCPIToolHandler {
444
+ const legacyUnsafeDelegationEnabled =
445
+ delegationConfig?.allowLegacyUnsafeDelegation === true;
446
+ const didKeyResolver = createDidKeyResolver();
447
+ const fetchProvider =
448
+ delegationConfig?.fetchProvider ??
449
+ (typeof globalThis.fetch === "function"
450
+ ? new RuntimeFetchProvider()
451
+ : undefined);
452
+ const didWebResolver = fetchProvider
453
+ ? createDidWebResolver(fetchProvider)
454
+ : undefined;
455
+ const didResolver: DIDResolver = {
456
+ async resolve(did: string) {
457
+ const customResolver = delegationConfig?.didResolver;
458
+ if (customResolver) {
459
+ const resolved = await customResolver.resolve(did);
460
+ if (resolved) {
461
+ return resolved;
462
+ }
463
+ }
464
+
465
+ if (did.startsWith("did:key:")) {
466
+ return didKeyResolver.resolve(did);
467
+ }
468
+
469
+ if (did.startsWith("did:web:")) {
470
+ return didWebResolver?.resolve(did) ?? null;
471
+ }
472
+
473
+ return null;
474
+ },
475
+ };
476
+
477
+ const signatureVerifier: SignatureVerificationFunction = async (
478
+ vc: DelegationCredential,
479
+ publicKeyJwk: unknown,
480
+ ): Promise<{ valid: boolean; reason?: string }> => {
481
+ const proof = vc.proof;
482
+ if (!proof) {
483
+ return { valid: false, reason: "Missing proof" };
484
+ }
485
+
486
+ const proofValue = proof["proofValue"] as string | undefined;
487
+ if (!proofValue) {
488
+ return { valid: false, reason: "Missing proofValue in proof" };
489
+ }
490
+
491
+ // Reconstruct the unsigned VC (without proof) for signature verification
492
+ const vcRecord = vc as Record<string, unknown>;
493
+ const vcWithoutProof: Record<string, unknown> = {};
494
+ for (const [k, v] of Object.entries(vcRecord)) {
495
+ if (k !== "proof") vcWithoutProof[k] = v;
496
+ }
497
+ const canonical = canonicalizeJSON(vcWithoutProof);
498
+ const data = new TextEncoder().encode(canonical);
499
+
500
+ // Decode signature from base64url proof value
501
+ const sigBytes = base64urlDecodeToBytes(proofValue);
502
+
503
+ // Get public key from JWK (x is base64url-encoded raw key bytes)
504
+ const jwk = publicKeyJwk as { x?: string };
505
+ if (!jwk.x) {
506
+ return { valid: false, reason: "No x field in publicKeyJwk" };
507
+ }
508
+
509
+ // Convert base64url key to standard base64 for the crypto provider
510
+ const pubKeyBytes = base64urlDecodeToBytes(jwk.x);
511
+ const pubKeyBase64 = bytesToBase64(pubKeyBytes);
512
+
513
+ const valid = await cryptoProvider.verify(data, sigBytes, pubKeyBase64);
514
+ return {
515
+ valid,
516
+ reason: valid ? undefined : "Signature verification failed",
517
+ };
518
+ };
519
+
520
+ const verifier = new DelegationCredentialVerifier({
521
+ didResolver,
522
+ signatureVerifier,
523
+ statusListResolver: delegationConfig?.statusListResolver,
524
+ });
525
+
526
+ const buildDelegationErrorResponse = (
527
+ error: string,
528
+ reason: string,
529
+ ): Awaited<ReturnType<MCPIToolHandler>> => ({
530
+ content: [
531
+ {
532
+ type: "text" as const,
533
+ text: JSON.stringify({ error, reason }),
534
+ },
535
+ ],
536
+ isError: true,
537
+ });
538
+
539
+ const validateDelegationChain = async (
540
+ leafCredential: DelegationCredential,
541
+ ): Promise<{ valid: boolean; reason?: string }> => {
542
+ const leafDelegation = extractDelegationFromVC(leafCredential);
543
+ let chain: DelegationCredential[] = [leafCredential];
544
+
545
+ if (leafDelegation.parentId) {
546
+ if (!delegationConfig?.resolveDelegationChain) {
547
+ if (legacyUnsafeDelegationEnabled) {
548
+ logger.warn(
549
+ `[mcpi] Legacy delegation mode enabled: accepting parent-linked credential ${leafDelegation.id} without resolveDelegationChain`,
550
+ );
551
+ return { valid: true };
552
+ }
553
+ return {
554
+ valid: false,
555
+ reason: `Delegation ${leafDelegation.id} references parent ${leafDelegation.parentId} but no resolveDelegationChain handler is configured`,
556
+ };
557
+ }
558
+
559
+ let resolvedChain: DelegationCredential[];
560
+ try {
561
+ resolvedChain =
562
+ await delegationConfig.resolveDelegationChain(leafCredential);
563
+ } catch (error) {
564
+ return {
565
+ valid: false,
566
+ reason: `Failed to resolve delegation chain: ${error instanceof Error ? error.message : "Unknown error"}`,
567
+ };
568
+ }
569
+
570
+ if (resolvedChain.length === 0) {
571
+ return {
572
+ valid: false,
573
+ reason: `Delegation ${leafDelegation.id} references parent ${leafDelegation.parentId} but the resolved chain is empty`,
574
+ };
575
+ }
576
+
577
+ const leafIndex = resolvedChain.findIndex(
578
+ (credential) =>
579
+ credential.credentialSubject.delegation.id === leafDelegation.id,
580
+ );
581
+ if (leafIndex !== -1 && leafIndex !== resolvedChain.length - 1) {
582
+ return {
583
+ valid: false,
584
+ reason: `Resolved delegation chain for ${leafDelegation.id} must end with the leaf credential`,
585
+ };
586
+ }
587
+
588
+ chain =
589
+ leafIndex === -1 ? [...resolvedChain, leafCredential] : resolvedChain;
590
+ }
591
+
592
+ const seenIds = new Set<string>();
593
+ let previousDelegation: DelegationRecord | undefined;
594
+ let previousCredential: DelegationCredential | undefined;
595
+
596
+ for (const credential of chain) {
597
+ const delegation = extractDelegationFromVC(credential);
598
+
599
+ if (seenIds.has(delegation.id)) {
600
+ return {
601
+ valid: false,
602
+ reason: `Delegation chain contains a circular reference at ${delegation.id}`,
603
+ };
604
+ }
605
+ seenIds.add(delegation.id);
606
+
607
+ if (credential.credentialStatus && !delegationConfig?.statusListResolver) {
608
+ if (legacyUnsafeDelegationEnabled) {
609
+ logger.warn(
610
+ `[mcpi] Legacy delegation mode enabled: skipping status-list verification for ${delegation.id}`,
611
+ );
612
+ } else {
613
+ return {
614
+ valid: false,
615
+ reason: `Delegation ${delegation.id} has credentialStatus but no statusListResolver is configured`,
616
+ };
617
+ }
618
+ }
619
+
620
+ const credentialVerification = await verifier.verifyDelegationCredential(
621
+ credential,
622
+ );
623
+ if (!credentialVerification.valid) {
624
+ return {
625
+ valid: false,
626
+ reason: `Delegation ${delegation.id} invalid: ${credentialVerification.reason}`,
627
+ };
628
+ }
629
+
630
+ if (!verifyDelegationAudience(delegation, identity.did)) {
631
+ return {
632
+ valid: false,
633
+ reason: `Delegation ${delegation.id} audience does not include server DID ${identity.did}`,
634
+ };
635
+ }
636
+
637
+ if (!previousDelegation || !previousCredential) {
638
+ if (delegation.parentId) {
639
+ return {
640
+ valid: false,
641
+ reason: `Resolved delegation chain is incomplete: root delegation ${delegation.id} still references parent ${delegation.parentId}`,
642
+ };
643
+ }
644
+
645
+ previousDelegation = delegation;
646
+ previousCredential = credential;
647
+ continue;
648
+ }
649
+
650
+ if (delegation.parentId !== previousDelegation.id) {
651
+ return {
652
+ valid: false,
653
+ reason: `Delegation ${delegation.id} references parent ${delegation.parentId} but expected ${previousDelegation.id}`,
654
+ };
655
+ }
656
+
657
+ if (delegation.issuerDid !== previousDelegation.subjectDid) {
658
+ return {
659
+ valid: false,
660
+ reason: `Delegation ${delegation.id} issued by ${delegation.issuerDid} but parent subject is ${previousDelegation.subjectDid}`,
661
+ };
662
+ }
663
+
664
+ const scopeValidation = validateScopeAttenuation(
665
+ previousCredential,
666
+ credential,
667
+ );
668
+ if (!scopeValidation.valid) {
669
+ return scopeValidation;
670
+ }
671
+
672
+ previousDelegation = delegation;
673
+ previousCredential = credential;
674
+ }
675
+
676
+ const finalDelegation = extractDelegationFromVC(chain[chain.length - 1]!);
677
+ if (finalDelegation.id !== leafDelegation.id) {
678
+ return {
679
+ valid: false,
680
+ reason: `Resolved delegation chain ended at ${finalDelegation.id} instead of leaf ${leafDelegation.id}`,
681
+ };
682
+ }
683
+
684
+ return { valid: true };
685
+ };
686
+
687
+ return async (
688
+ args: Record<string, unknown>,
689
+ sessionId?: string,
690
+ ) => {
691
+ const delegationArg = args["_mcpi_delegation"];
692
+
693
+ if (delegationArg === undefined || delegationArg === null) {
694
+ // No delegation provided — return needs_authorization response
695
+ const tokenBytes = await cryptoProvider.randomBytes(16);
696
+ const hex = Array.from(tokenBytes)
697
+ .map((b) => b.toString(16).padStart(2, "0"))
698
+ .join("");
699
+ const resumeToken = [
700
+ hex.slice(0, 8),
701
+ hex.slice(8, 12),
702
+ hex.slice(12, 16),
703
+ hex.slice(16, 20),
704
+ hex.slice(20),
705
+ ].join("-");
706
+ const expiresAt = Math.floor(Date.now() / 1000) + 300;
707
+
708
+ const authError = createNeedsAuthorizationError({
709
+ message: `Tool "${toolName}" requires delegation with scope: ${config.scopeId}`,
710
+ authorizationUrl: config.consentUrl,
711
+ resumeToken,
712
+ expiresAt,
713
+ scopes: [config.scopeId],
714
+ });
715
+
716
+ return {
717
+ content: [{ type: "text", text: JSON.stringify(authError) }],
718
+ };
719
+ }
720
+
721
+ const vc = delegationArg as DelegationCredential;
722
+ const verificationResult = await validateDelegationChain(vc);
723
+
724
+ if (!verificationResult.valid) {
725
+ logger.warn(
726
+ `[mcpi] Delegation verification failed for "${toolName}": ${verificationResult.reason}`,
727
+ );
728
+ return buildDelegationErrorResponse(
729
+ "delegation_invalid",
730
+ verificationResult.reason ?? "Unknown delegation validation error",
731
+ );
732
+ }
733
+
734
+ const scopes = getDelegationScopes(vc);
735
+ if (!scopes.includes(config.scopeId)) {
736
+ logger.warn(
737
+ `[mcpi] Delegation missing required scope "${config.scopeId}" for "${toolName}"`,
738
+ );
739
+ return buildDelegationErrorResponse(
740
+ "delegation_scope_missing",
741
+ `Required scope "${config.scopeId}" not in delegation scopes`,
742
+ );
743
+ }
744
+
745
+ // Strip _mcpi_delegation from args before passing to handler
746
+ const cleanArgs: Record<string, unknown> = {};
747
+ for (const [k, v] of Object.entries(args)) {
748
+ if (k !== "_mcpi_delegation") cleanArgs[k] = v;
749
+ }
750
+
751
+ logger.debug(
752
+ `[mcpi] Delegation verified for "${toolName}", scope "${config.scopeId}"`,
753
+ );
754
+ return handler(cleanArgs, sessionId);
755
+ };
756
+ }
757
+
758
+ return {
759
+ sessionManager,
760
+ proofGenerator,
761
+ handshakeTool,
762
+ handleHandshake,
763
+ wrapWithProof,
764
+ wrapWithDelegation,
765
+ };
766
+ }