@mcp-i/core 0.1.0 → 1.1.0-canary.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/README.md +43 -20
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +5 -1
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/with-mcpi-server.d.ts +62 -0
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -0
- package/dist/middleware/with-mcpi-server.js +94 -0
- package/dist/middleware/with-mcpi-server.js.map +1 -0
- package/dist/middleware/with-mcpi.d.ts +25 -10
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +21 -7
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/node-crypto.d.ts +26 -0
- package/dist/providers/node-crypto.d.ts.map +1 -0
- package/dist/providers/node-crypto.js +69 -0
- package/dist/providers/node-crypto.js.map +1 -0
- package/package.json +8 -3
- package/src/__tests__/integration/mcp-enhance-server.test.ts +311 -0
- package/src/__tests__/integration/mcp-transport-context7.test.ts +413 -0
- package/src/__tests__/integration/mcp-transport.test.ts +390 -0
- package/src/index.ts +5 -0
- package/src/middleware/index.ts +10 -1
- package/src/middleware/with-mcpi-server.ts +185 -0
- package/src/middleware/with-mcpi.ts +35 -13
- package/src/providers/index.ts +2 -0
- package/src/providers/node-crypto.ts +107 -0
|
@@ -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
|
@@ -236,6 +236,8 @@ export {
|
|
|
236
236
|
MemoryIdentityProvider,
|
|
237
237
|
} from './providers/memory.js';
|
|
238
238
|
|
|
239
|
+
export { NodeCryptoProvider } from './providers/node-crypto.js';
|
|
240
|
+
|
|
239
241
|
// Middleware
|
|
240
242
|
export {
|
|
241
243
|
createMCPIMiddleware,
|
|
@@ -246,6 +248,9 @@ export {
|
|
|
246
248
|
type MCPIToolDefinition,
|
|
247
249
|
type MCPIToolHandler,
|
|
248
250
|
type MCPIServer,
|
|
251
|
+
withMCPI,
|
|
252
|
+
generateIdentity,
|
|
253
|
+
type WithMCPIOptions,
|
|
249
254
|
} from './middleware/index.js';
|
|
250
255
|
|
|
251
256
|
// Logging
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP-I Middleware
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
+
}
|