@mcp-i/core 0.1.0 → 1.1.0-canary.1

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.
@@ -0,0 +1,390 @@
1
+ /**
2
+ * End-to-End MCP Transport Integration Test
3
+ *
4
+ * Exercises the MCP-I middleware through a real MCP SDK Client→Server
5
+ * transport (InMemoryTransport). Unlike unit tests that call handlers
6
+ * directly, these tests go through full JSON-RPC serialization and
7
+ * MCP protocol framing.
8
+ */
9
+
10
+ import { describe, it, expect, afterEach } from 'vitest';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
13
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+ import { createMCPIMiddleware } from '../../middleware/with-mcpi.js';
19
+ import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
20
+ import { generateDidKeyFromBase64 } from '../../utils/did-helpers.js';
21
+ import { DelegationCredentialIssuer } from '../../delegation/vc-issuer.js';
22
+ import { ProofVerifier } from '../../proof/verifier.js';
23
+ import { MemoryNonceCacheProvider } from '../../providers/memory.js';
24
+ import { ClockProvider, FetchProvider } from '../../providers/base.js';
25
+ import {
26
+ createDidKeyResolver,
27
+ extractPublicKeyFromDidKey,
28
+ publicKeyToJwk,
29
+ } from '../../delegation/did-key-resolver.js';
30
+ import { base64urlEncodeFromBytes } from '../../utils/base64.js';
31
+ import type {
32
+ DelegationCredential,
33
+ DIDDocument,
34
+ StatusList2021Credential,
35
+ DelegationRecord,
36
+ Proof,
37
+ } from '../../types/protocol.js';
38
+
39
+ // ── Test providers for ProofVerifier ──────────────────────────────
40
+
41
+ class TestClockProvider extends ClockProvider {
42
+ now(): number { return Date.now(); }
43
+ isWithinSkew(timestampMs: number, skewSeconds: number): boolean {
44
+ return Math.abs(Date.now() - timestampMs) <= skewSeconds * 1000;
45
+ }
46
+ hasExpired(expiresAt: number): boolean { return Date.now() > expiresAt; }
47
+ calculateExpiry(ttlSeconds: number): number { return Date.now() + ttlSeconds * 1000; }
48
+ format(timestamp: number): string { return new Date(timestamp).toISOString(); }
49
+ }
50
+
51
+ class TestFetchProvider extends FetchProvider {
52
+ private didResolver = createDidKeyResolver();
53
+ async resolveDID(did: string): Promise<DIDDocument | null> {
54
+ return this.didResolver.resolve(did);
55
+ }
56
+ async fetchStatusList(): Promise<StatusList2021Credential | null> { return null; }
57
+ async fetchDelegationChain(): Promise<DelegationRecord[]> { return []; }
58
+ async fetch(): Promise<Response> { throw new Error('Not implemented'); }
59
+ }
60
+
61
+ // ── Helpers ──────────────────────────────────────────────────────
62
+
63
+ async function setupMcpPair(options?: { autoSession?: boolean }) {
64
+ const crypto = new NodeCryptoProvider();
65
+ const keyPair = await crypto.generateKeyPair();
66
+ const did = generateDidKeyFromBase64(keyPair.publicKey);
67
+ const kid = `${did}#keys-1`;
68
+
69
+ const mcpi = createMCPIMiddleware(
70
+ {
71
+ identity: { did, kid, privateKey: keyPair.privateKey, publicKey: keyPair.publicKey },
72
+ session: { sessionTtlMinutes: 60 },
73
+ autoSession: options?.autoSession,
74
+ },
75
+ crypto,
76
+ );
77
+
78
+ // ── Tool handlers (mirrors examples/node-server/server.ts) ──
79
+
80
+ const greetHandler = mcpi.wrapWithProof('greet', async (args) => ({
81
+ content: [{ type: 'text', text: `Hello, ${args['name'] ?? 'world'}!` }],
82
+ }));
83
+
84
+ const restrictedGreetHandler = mcpi.wrapWithDelegation(
85
+ 'restricted_greet',
86
+ {
87
+ scopeId: 'greeting:restricted',
88
+ consentUrl: 'https://example.com/consent?scope=greeting:restricted',
89
+ },
90
+ mcpi.wrapWithProof('restricted_greet', async (args) => ({
91
+ content: [{ type: 'text', text: `Hello, ${args['name'] ?? 'world'}! (delegation verified)` }],
92
+ })),
93
+ );
94
+
95
+ // ── MCP Server ──
96
+
97
+ const server = new Server(
98
+ { name: 'mcpi-transport-test', version: '1.0.0' },
99
+ { capabilities: { tools: {} } },
100
+ );
101
+
102
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
103
+ tools: [
104
+ mcpi.handshakeTool,
105
+ {
106
+ name: 'greet',
107
+ description: 'Returns a greeting with proof',
108
+ inputSchema: {
109
+ type: 'object' as const,
110
+ properties: { name: { type: 'string' } },
111
+ },
112
+ },
113
+ {
114
+ name: 'restricted_greet',
115
+ description: 'Protected greeting requiring delegation',
116
+ inputSchema: {
117
+ type: 'object' as const,
118
+ properties: {
119
+ name: { type: 'string' },
120
+ _mcpi_delegation: { type: 'object' },
121
+ },
122
+ },
123
+ },
124
+ ],
125
+ }));
126
+
127
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
128
+ const { name, arguments: args = {} } = request.params;
129
+
130
+ if (name === '_mcpi_handshake') {
131
+ return mcpi.handleHandshake(args as Record<string, unknown>);
132
+ }
133
+ if (name === 'greet') {
134
+ return greetHandler(args as Record<string, unknown>);
135
+ }
136
+ if (name === 'restricted_greet') {
137
+ return restrictedGreetHandler(args as Record<string, unknown>);
138
+ }
139
+
140
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
141
+ });
142
+
143
+ // ── Client + transport ──
144
+
145
+ const client = new Client(
146
+ { name: 'mcpi-transport-test-client', version: '1.0.0' },
147
+ );
148
+
149
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
150
+ await server.connect(serverTransport);
151
+ await client.connect(clientTransport);
152
+
153
+ return { client, server, did, kid, keyPair, crypto };
154
+ }
155
+
156
+ async function issueDelegationVC(scopes: string[]): Promise<DelegationCredential> {
157
+ const crypto = new NodeCryptoProvider();
158
+ const keyPair = await crypto.generateKeyPair();
159
+ const did = generateDidKeyFromBase64(keyPair.publicKey);
160
+ const kid = `${did}#keys-1`;
161
+
162
+ const signingFn = async (
163
+ canonicalVC: string,
164
+ _issuerDid: string,
165
+ kidArg: string,
166
+ ): Promise<Proof> => {
167
+ const data = new TextEncoder().encode(canonicalVC);
168
+ const sigBytes = await crypto.sign(data, keyPair.privateKey);
169
+ const proofValue = base64urlEncodeFromBytes(sigBytes);
170
+ return {
171
+ type: 'Ed25519Signature2020',
172
+ created: new Date().toISOString(),
173
+ verificationMethod: kidArg,
174
+ proofPurpose: 'assertionMethod',
175
+ proofValue,
176
+ };
177
+ };
178
+
179
+ const issuer = new DelegationCredentialIssuer(
180
+ { getDid: () => did, getKeyId: () => kid, getPrivateKey: () => keyPair.privateKey },
181
+ signingFn,
182
+ );
183
+
184
+ return issuer.createAndIssueDelegation({
185
+ id: `test-delegation-${Date.now()}-${Math.random().toString(16).slice(2)}`,
186
+ issuerDid: did,
187
+ subjectDid: did,
188
+ constraints: {
189
+ scopes,
190
+ notAfter: Math.floor(Date.now() / 1000) + 3600,
191
+ },
192
+ });
193
+ }
194
+
195
+ // ── Tests ──────────────────────────────────────────────────────
196
+
197
+ describe('MCP Transport Integration', () => {
198
+ const pairs: Array<{ client: Client; server: Server }> = [];
199
+
200
+ afterEach(async () => {
201
+ for (const pair of pairs) {
202
+ await pair.client.close();
203
+ await pair.server.close();
204
+ }
205
+ pairs.length = 0;
206
+ });
207
+
208
+ async function createPair(options?: { autoSession?: boolean }) {
209
+ const pair = await setupMcpPair(options);
210
+ pairs.push(pair);
211
+ return pair;
212
+ }
213
+
214
+ it('listTools returns handshake + app tools', async () => {
215
+ const { client } = await createPair();
216
+
217
+ const result = await client.listTools();
218
+ const toolNames = result.tools.map((t) => t.name);
219
+
220
+ expect(toolNames).toHaveLength(3);
221
+ expect(toolNames).toContain('_mcpi_handshake');
222
+ expect(toolNames).toContain('greet');
223
+ expect(toolNames).toContain('restricted_greet');
224
+ });
225
+
226
+ it('handshake establishes session via MCP transport', async () => {
227
+ const { client, did } = await createPair();
228
+
229
+ const result = await client.callTool({
230
+ name: '_mcpi_handshake',
231
+ arguments: {
232
+ nonce: `transport-test-${Date.now()}`,
233
+ audience: did,
234
+ timestamp: Math.floor(Date.now() / 1000),
235
+ },
236
+ });
237
+
238
+ expect(result.content).toHaveLength(1);
239
+ const first = result.content[0] as { type: string; text: string };
240
+ expect(first.type).toBe('text');
241
+
242
+ const parsed = JSON.parse(first.text);
243
+ expect(parsed.success).toBe(true);
244
+ expect(parsed.sessionId).toMatch(/^mcpi_/);
245
+ expect(parsed.serverDid).toBe(did);
246
+ });
247
+
248
+ it('greet returns proof in _meta after handshake', async () => {
249
+ const { client, did } = await createPair();
250
+
251
+ // Handshake first
252
+ await client.callTool({
253
+ name: '_mcpi_handshake',
254
+ arguments: {
255
+ nonce: `transport-test-${Date.now()}`,
256
+ audience: did,
257
+ timestamp: Math.floor(Date.now() / 1000),
258
+ },
259
+ });
260
+
261
+ // Call greet
262
+ const result = await client.callTool({
263
+ name: 'greet',
264
+ arguments: { name: 'MCP-I' },
265
+ });
266
+
267
+ // Verify tool output
268
+ const first = result.content[0] as { type: string; text: string };
269
+ expect(first.text).toBe('Hello, MCP-I!');
270
+
271
+ // Verify proof in _meta (top-level on the result)
272
+ expect(result._meta).toBeDefined();
273
+ const proof = (result._meta as Record<string, unknown>).proof as {
274
+ jws: string;
275
+ meta: Record<string, unknown>;
276
+ };
277
+ expect(proof).toBeDefined();
278
+ expect(proof.jws).toBeDefined();
279
+ expect(proof.meta.did).toMatch(/^did:key:/);
280
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
281
+ expect(proof.meta.requestHash).toMatch(/^sha256:[a-f0-9]{64}$/);
282
+ expect(proof.meta.responseHash).toMatch(/^sha256:[a-f0-9]{64}$/);
283
+ });
284
+
285
+ it('proof from greet verifies cryptographically', async () => {
286
+ const { client, did, kid } = await createPair();
287
+
288
+ // Handshake
289
+ await client.callTool({
290
+ name: '_mcpi_handshake',
291
+ arguments: {
292
+ nonce: `transport-test-${Date.now()}`,
293
+ audience: did,
294
+ timestamp: Math.floor(Date.now() / 1000),
295
+ },
296
+ });
297
+
298
+ // Call greet
299
+ const result = await client.callTool({
300
+ name: 'greet',
301
+ arguments: { name: 'Verifier' },
302
+ });
303
+
304
+ const proof = (result._meta as Record<string, unknown>).proof as {
305
+ jws: string;
306
+ meta: Record<string, unknown>;
307
+ };
308
+
309
+ // Resolve server's public key from did:key
310
+ const publicKeyBytes = extractPublicKeyFromDidKey(did);
311
+ expect(publicKeyBytes).not.toBeNull();
312
+ const publicKeyJwk = publicKeyToJwk(publicKeyBytes!);
313
+
314
+ // Create verifier and verify
315
+ const crypto = new NodeCryptoProvider();
316
+ const verifier = new ProofVerifier({
317
+ cryptoProvider: crypto,
318
+ clockProvider: new TestClockProvider(),
319
+ nonceCacheProvider: new MemoryNonceCacheProvider(),
320
+ fetchProvider: new TestFetchProvider(),
321
+ timestampSkewSeconds: 300,
322
+ });
323
+
324
+ const jwkWithKid = { ...publicKeyJwk, kid };
325
+ const verificationResult = await verifier.verifyProof(proof as any, jwkWithKid as any);
326
+ expect(verificationResult.valid).toBe(true);
327
+ });
328
+
329
+ it('autoSession attaches proof without handshake', async () => {
330
+ const { client } = await createPair({ autoSession: true });
331
+
332
+ // Call greet directly — no handshake
333
+ const result = await client.callTool({
334
+ name: 'greet',
335
+ arguments: { name: 'Auto' },
336
+ });
337
+
338
+ const first = result.content[0] as { type: string; text: string };
339
+ expect(first.text).toBe('Hello, Auto!');
340
+
341
+ // Proof should be present via auto-session
342
+ expect(result._meta).toBeDefined();
343
+ const proof = (result._meta as Record<string, unknown>).proof as {
344
+ jws: string;
345
+ meta: Record<string, unknown>;
346
+ };
347
+ expect(proof).toBeDefined();
348
+ expect(proof.jws).toBeDefined();
349
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
350
+ });
351
+
352
+ it('restricted_greet without delegation returns needs_authorization', async () => {
353
+ const { client } = await createPair();
354
+
355
+ const result = await client.callTool({
356
+ name: 'restricted_greet',
357
+ arguments: { name: 'Unauthorized' },
358
+ });
359
+
360
+ const first = result.content[0] as { type: string; text: string };
361
+ const parsed = JSON.parse(first.text);
362
+ expect(parsed.error).toBe('needs_authorization');
363
+ expect(parsed.authorizationUrl).toContain('consent');
364
+ expect(parsed.scopes).toContain('greeting:restricted');
365
+ expect(parsed.resumeToken).toBeDefined();
366
+ });
367
+
368
+ it('restricted_greet with valid delegation VC returns greeting with proof', async () => {
369
+ const { client, did } = await createPair({ autoSession: true });
370
+
371
+ const vc = await issueDelegationVC(['greeting:restricted']);
372
+
373
+ const result = await client.callTool({
374
+ name: 'restricted_greet',
375
+ arguments: { name: 'DIF', _mcpi_delegation: vc },
376
+ });
377
+
378
+ const first = result.content[0] as { type: string; text: string };
379
+ expect(first.text).toBe('Hello, DIF! (delegation verified)');
380
+
381
+ // Proof from inner wrapWithProof
382
+ expect(result._meta).toBeDefined();
383
+ const proof = (result._meta as Record<string, unknown>).proof as {
384
+ jws: string;
385
+ meta: Record<string, unknown>;
386
+ };
387
+ expect(proof).toBeDefined();
388
+ expect(proof.jws).toBeDefined();
389
+ });
390
+ });
package/src/index.ts CHANGED
@@ -246,6 +246,9 @@ export {
246
246
  type MCPIToolDefinition,
247
247
  type MCPIToolHandler,
248
248
  type MCPIServer,
249
+ withMCPI,
250
+ generateIdentity,
251
+ type WithMCPIOptions,
249
252
  } from './middleware/index.js';
250
253
 
251
254
  // Logging
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * MCP-I Middleware
3
3
  *
4
- * Provides identity-aware middleware for @modelcontextprotocol/sdk Server.
4
+ * Primary entry point: `withMCPI(server, { crypto })` — adds identity,
5
+ * handshake, and auto-proofs to any McpServer instance in one call.
6
+ *
7
+ * For the low-level `Server` API or custom patterns, use `createMCPIMiddleware` directly.
5
8
  */
6
9
 
7
10
  export {
@@ -14,3 +17,9 @@ export {
14
17
  type MCPIToolHandler,
15
18
  type MCPIServer,
16
19
  } from './with-mcpi.js';
20
+
21
+ export {
22
+ withMCPI,
23
+ generateIdentity,
24
+ type WithMCPIOptions,
25
+ } from './with-mcpi-server.js';
@@ -0,0 +1,185 @@
1
+ /**
2
+ * McpServer Adapter for MCP-I
3
+ *
4
+ * Adds MCP-I identity, session management, and proof generation to a
5
+ * standard McpServer instance with a single function call.
6
+ *
7
+ * Usage:
8
+ * import { withMCPI } from '@mcp-i/core/middleware';
9
+ * const mcpi = await withMCPI(server, { crypto: new NodeCryptoProvider() });
10
+ * // All tools registered on `server` now get proofs automatically.
11
+ */
12
+
13
+ import type { CryptoProvider } from "../providers/base.js";
14
+ import { generateDidKeyFromBase64 } from "../utils/did-helpers.js";
15
+ import {
16
+ createMCPIMiddleware,
17
+ type MCPIIdentityConfig,
18
+ type MCPIDelegationConfig,
19
+ type MCPIMiddleware,
20
+ } from "./with-mcpi.js";
21
+ import { z } from "zod";
22
+
23
+ export interface WithMCPIOptions {
24
+ /** Platform-specific crypto implementation (required) */
25
+ crypto: CryptoProvider;
26
+ /** Identity config — auto-generated if omitted */
27
+ identity?: MCPIIdentityConfig;
28
+ /** Session configuration */
29
+ session?: { sessionTtlMinutes?: number };
30
+ /** Auto-create sessions for non-MCP-I clients (default: true) */
31
+ autoSession?: boolean;
32
+ /** Attach proofs to all tool responses (default: true) */
33
+ proofAllTools?: boolean;
34
+ /** Tools to skip proof generation for */
35
+ excludeTools?: string[];
36
+ /** Delegation verification config */
37
+ delegation?: MCPIDelegationConfig;
38
+ }
39
+
40
+ /**
41
+ * Generate a fresh Ed25519 identity for MCP-I.
42
+ *
43
+ * @param crypto - Platform-specific crypto provider
44
+ * @returns Identity config with DID, kid, and key material
45
+ */
46
+ export async function generateIdentity(
47
+ crypto: CryptoProvider,
48
+ ): Promise<MCPIIdentityConfig> {
49
+ const keyPair = await crypto.generateKeyPair();
50
+ const did = generateDidKeyFromBase64(keyPair.publicKey);
51
+ return {
52
+ did,
53
+ kid: `${did}#keys-1`,
54
+ privateKey: keyPair.privateKey,
55
+ publicKey: keyPair.publicKey,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * McpServer type — minimal interface to avoid hard dependency on the SDK.
61
+ * Matches the public API of @modelcontextprotocol/sdk McpServer.
62
+ */
63
+ interface McpServerLike {
64
+ server: {
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ [key: string]: any;
67
+ };
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ registerTool(...args: any[]): void;
70
+ }
71
+
72
+ /**
73
+ * Add MCP-I to a McpServer instance.
74
+ *
75
+ * 1. Auto-generates Ed25519 identity (or uses provided one)
76
+ * 2. Registers `_mcpi_handshake` tool
77
+ * 3. Intercepts the `tools/call` request handler to auto-attach proofs
78
+ *
79
+ * @param server - McpServer instance
80
+ * @param options - Configuration
81
+ * @returns The MCPIMiddleware instance for advanced usage (wrapWithDelegation, etc.)
82
+ */
83
+ export async function withMCPI(
84
+ server: McpServerLike,
85
+ options: WithMCPIOptions,
86
+ ): Promise<MCPIMiddleware> {
87
+ const identity =
88
+ options.identity ?? (await generateIdentity(options.crypto));
89
+
90
+ const mcpi = createMCPIMiddleware(
91
+ {
92
+ identity,
93
+ session: options.session,
94
+ delegation: options.delegation,
95
+ autoSession: options.autoSession ?? true,
96
+ },
97
+ options.crypto,
98
+ );
99
+
100
+ // Register _mcpi_handshake tool
101
+ server.registerTool(
102
+ "_mcpi_handshake",
103
+ {
104
+ description:
105
+ "MCP-I identity handshake — establishes a cryptographic session",
106
+ inputSchema: {
107
+ nonce: z.string().describe("Client-generated unique nonce"),
108
+ audience: z
109
+ .string()
110
+ .describe("Intended audience (server DID or URL)"),
111
+ timestamp: z.number().describe("Unix epoch seconds"),
112
+ },
113
+ },
114
+ async (args: unknown) => {
115
+ const result = await mcpi.handleHandshake(args as Record<string, unknown>);
116
+ return {
117
+ ...result,
118
+ content: result.content.map((c) => ({ ...c, type: "text" as const })),
119
+ };
120
+ },
121
+ );
122
+
123
+ // Auto-proof interception: wrap the tools/call handler
124
+ const proofAllTools = options.proofAllTools ?? true;
125
+
126
+ if (proofAllTools) {
127
+ const lowLevel = server.server;
128
+ const handlers: Map<string, Function> =
129
+ lowLevel._requestHandlers;
130
+ const original = handlers.get("tools/call");
131
+
132
+ if (original) {
133
+ handlers.set(
134
+ "tools/call",
135
+ async (request: Record<string, unknown>, extra: unknown) => {
136
+ const result = (await (
137
+ original as (
138
+ req: Record<string, unknown>,
139
+ ext: unknown,
140
+ ) => Promise<Record<string, unknown>>
141
+ )(request, extra)) as {
142
+ content?: Array<{
143
+ type: string;
144
+ text: string;
145
+ [key: string]: unknown;
146
+ }>;
147
+ isError?: boolean;
148
+ _meta?: Record<string, unknown>;
149
+ [key: string]: unknown;
150
+ };
151
+
152
+ const params = request.params as
153
+ | { name?: string; arguments?: Record<string, unknown> }
154
+ | undefined;
155
+ const toolName = params?.name;
156
+
157
+ if (
158
+ !toolName ||
159
+ toolName === "_mcpi_handshake" ||
160
+ result.isError
161
+ ) {
162
+ return result;
163
+ }
164
+
165
+ if (options.excludeTools?.includes(toolName)) {
166
+ return result;
167
+ }
168
+
169
+ // Use wrapWithProof to add proof — it handles session management
170
+ const addProof = mcpi.wrapWithProof(
171
+ toolName,
172
+ async () => result as {
173
+ content: Array<{ type: string; text: string; [key: string]: unknown }>;
174
+ isError?: boolean;
175
+ [key: string]: unknown;
176
+ },
177
+ );
178
+ return addProof(params?.arguments ?? {});
179
+ },
180
+ );
181
+ }
182
+ }
183
+
184
+ return mcpi;
185
+ }
@@ -1,13 +1,18 @@
1
1
  /**
2
- * MCP-I Middleware for @modelcontextprotocol/sdk Server
2
+ * MCP-I Middleware Core Implementation
3
3
  *
4
- * Adds identity, session management, and proof generation to a standard
5
- * MCP SDK Server.
4
+ * Adds identity, session management, and proof generation to MCP servers.
6
5
  *
7
- * Usage:
8
- * const { handshakeTool, registerToolWithProof } = createMCPIMiddleware(config, crypto);
9
- * server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [handshakeTool, ...] }));
10
- * registerToolWithProof(server, myToolDef, myHandler);
6
+ * For most use cases, prefer the high-level `withMCPI()` adapter from
7
+ * `./with-mcpi-server.ts` which auto-registers the handshake tool and
8
+ * auto-attaches proofs to all tool responses:
9
+ *
10
+ * import { withMCPI } from '@mcp-i/core';
11
+ * await withMCPI(server, { crypto: new NodeCryptoProvider() });
12
+ *
13
+ * `createMCPIMiddleware()` in this file is the lower-level API used
14
+ * internally by `withMCPI()` and for advanced use cases like the
15
+ * low-level `Server` API or custom request handler patterns.
11
16
  */
12
17
 
13
18
  import {
@@ -115,9 +120,11 @@ export interface MCPIToolDefinition {
115
120
  };
116
121
  }
117
122
 
118
- export interface MCPIToolHandler {
123
+ export interface MCPIToolHandler<
124
+ T extends Record<string, unknown> = Record<string, unknown>,
125
+ > {
119
126
  (
120
- args: Record<string, unknown>,
127
+ args: T,
121
128
  sessionId?: string,
122
129
  ): Promise<{
123
130
  content: Array<{ type: string; text: string; [key: string]: unknown }>;
@@ -138,6 +145,9 @@ export interface MCPIServer {
138
145
  }
139
146
 
140
147
  export interface MCPIMiddleware {
148
+ /** The identity config used by this middleware instance */
149
+ identity: MCPIIdentityConfig;
150
+
141
151
  /** The SessionManager instance for manual session operations */
142
152
  sessionManager: SessionManager;
143
153
 
@@ -163,7 +173,10 @@ export interface MCPIMiddleware {
163
173
  * Wrap a tool handler to automatically generate proofs.
164
174
  * Returns a new handler that appends proof metadata to the response.
165
175
  */
166
- wrapWithProof(toolName: string, handler: MCPIToolHandler): MCPIToolHandler;
176
+ wrapWithProof<T extends Record<string, unknown> = Record<string, unknown>>(
177
+ toolName: string,
178
+ handler: MCPIToolHandler<T>,
179
+ ): MCPIToolHandler;
167
180
 
168
181
  /**
169
182
  * Wrap a tool handler to require a valid W3C Delegation Credential.
@@ -253,6 +266,14 @@ function validateScopeAttenuation(
253
266
  /**
254
267
  * Create MCP-I middleware for a standard MCP SDK Server.
255
268
  *
269
+ * For most use cases, prefer {@link withMCPI} from `./with-mcpi-server.ts`
270
+ * which wraps this function and auto-registers handshake + auto-attaches proofs.
271
+ *
272
+ * Use `createMCPIMiddleware` directly when:
273
+ * - You use the low-level `Server` API (not `McpServer`)
274
+ * - You need custom request handler patterns
275
+ * - You want per-tool control over proof/delegation wrapping
276
+ *
256
277
  * @param config - Agent identity and session configuration
257
278
  * @param cryptoProvider - Platform-specific crypto implementation
258
279
  * @returns Middleware components for session management and proof generation
@@ -394,12 +415,12 @@ export function createMCPIMiddleware(
394
415
  return undefined;
395
416
  }
396
417
 
397
- function wrapWithProof(
418
+ function wrapWithProof<T extends Record<string, unknown> = Record<string, unknown>>(
398
419
  toolName: string,
399
- handler: MCPIToolHandler,
420
+ handler: MCPIToolHandler<T>,
400
421
  ): MCPIToolHandler {
401
422
  return async (args: Record<string, unknown>, sessionId?: string) => {
402
- const result = await handler(args, sessionId);
423
+ const result = await handler(args as T, sessionId);
403
424
 
404
425
  if (result.isError) {
405
426
  return result;
@@ -756,6 +777,7 @@ export function createMCPIMiddleware(
756
777
  }
757
778
 
758
779
  return {
780
+ identity: config.identity,
759
781
  sessionManager,
760
782
  proofGenerator,
761
783
  handshakeTool,