@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.
Files changed (35) hide show
  1. package/README.md +43 -20
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/middleware/index.d.ts +5 -1
  7. package/dist/middleware/index.d.ts.map +1 -1
  8. package/dist/middleware/index.js +5 -1
  9. package/dist/middleware/index.js.map +1 -1
  10. package/dist/middleware/with-mcpi-server.d.ts +62 -0
  11. package/dist/middleware/with-mcpi-server.d.ts.map +1 -0
  12. package/dist/middleware/with-mcpi-server.js +94 -0
  13. package/dist/middleware/with-mcpi-server.js.map +1 -0
  14. package/dist/middleware/with-mcpi.d.ts +25 -10
  15. package/dist/middleware/with-mcpi.d.ts.map +1 -1
  16. package/dist/middleware/with-mcpi.js +21 -7
  17. package/dist/middleware/with-mcpi.js.map +1 -1
  18. package/dist/providers/index.d.ts +1 -0
  19. package/dist/providers/index.d.ts.map +1 -1
  20. package/dist/providers/index.js +1 -0
  21. package/dist/providers/index.js.map +1 -1
  22. package/dist/providers/node-crypto.d.ts +26 -0
  23. package/dist/providers/node-crypto.d.ts.map +1 -0
  24. package/dist/providers/node-crypto.js +69 -0
  25. package/dist/providers/node-crypto.js.map +1 -0
  26. package/package.json +8 -3
  27. package/src/__tests__/integration/mcp-enhance-server.test.ts +311 -0
  28. package/src/__tests__/integration/mcp-transport-context7.test.ts +413 -0
  29. package/src/__tests__/integration/mcp-transport.test.ts +390 -0
  30. package/src/index.ts +5 -0
  31. package/src/middleware/index.ts +10 -1
  32. package/src/middleware/with-mcpi-server.ts +185 -0
  33. package/src/middleware/with-mcpi.ts +35 -13
  34. package/src/providers/index.ts +2 -0
  35. package/src/providers/node-crypto.ts +107 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * McpServer (High-Level API) + MCP-I Integration Test
3
+ *
4
+ * Proves that MCP-I middleware works with the high-level McpServer API
5
+ * used by most real-world MCP servers (including Context7).
6
+ *
7
+ * This is distinct from mcp-transport.test.ts which uses the low-level
8
+ * Server API. The key difference: McpServer uses registerTool() with
9
+ * zod schemas, not setRequestHandler(CallToolRequestSchema, ...).
10
+ */
11
+
12
+ import { describe, it, expect, afterEach } from 'vitest';
13
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
16
+ import { z } from 'zod';
17
+ import { createMCPIMiddleware } from '../../middleware/with-mcpi.js';
18
+ import { withMCPI } from '../../middleware/with-mcpi-server.js';
19
+ import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
20
+ import { generateDidKeyFromBase64 } from '../../utils/did-helpers.js';
21
+
22
+ // ── Helpers ──────────────────────────────────────────────────────
23
+
24
+ async function setupMcpServerPair(options?: { autoSession?: boolean }) {
25
+ const crypto = new NodeCryptoProvider();
26
+ const keyPair = await crypto.generateKeyPair();
27
+ const did = generateDidKeyFromBase64(keyPair.publicKey);
28
+ const kid = `${did}#keys-1`;
29
+
30
+ const mcpi = createMCPIMiddleware(
31
+ {
32
+ identity: { did, kid, privateKey: keyPair.privateKey, publicKey: keyPair.publicKey },
33
+ session: { sessionTtlMinutes: 60 },
34
+ autoSession: options?.autoSession,
35
+ },
36
+ crypto,
37
+ );
38
+
39
+ // ── McpServer (high-level API, same as Context7) ──
40
+
41
+ const server = new McpServer(
42
+ { name: 'mcpi-mcpserver-test', version: '1.0.0' },
43
+ { instructions: 'Test server for McpServer + MCP-I integration' },
44
+ );
45
+
46
+ // Register _mcpi_handshake tool (same pattern as Context7 integration)
47
+ server.registerTool(
48
+ '_mcpi_handshake',
49
+ {
50
+ description: 'MCP-I identity handshake',
51
+ inputSchema: {
52
+ nonce: z.string(),
53
+ audience: z.string(),
54
+ timestamp: z.number(),
55
+ },
56
+ },
57
+ async (args) => mcpi.handleHandshake(args as Record<string, unknown>),
58
+ );
59
+
60
+ // Wrap a test tool with proof (simulates Context7's resolve-library-id)
61
+ const searchHandler = mcpi.wrapWithProof(
62
+ 'search',
63
+ async (args) => ({
64
+ content: [{
65
+ type: 'text',
66
+ text: `Found results for: ${(args as { query: string }).query}`,
67
+ }],
68
+ }),
69
+ );
70
+
71
+ server.registerTool(
72
+ 'search',
73
+ {
74
+ description: 'Search for something',
75
+ inputSchema: {
76
+ query: z.string().describe('Search query'),
77
+ },
78
+ annotations: { readOnlyHint: true },
79
+ },
80
+ async (args) => searchHandler(args as Record<string, unknown>),
81
+ );
82
+
83
+ // Wrap a second tool (simulates Context7's query-docs)
84
+ const fetchHandler = mcpi.wrapWithProof(
85
+ 'fetch-docs',
86
+ async (args) => ({
87
+ content: [{
88
+ type: 'text',
89
+ text: `Docs for ${(args as { libraryId: string }).libraryId}: example content`,
90
+ }],
91
+ }),
92
+ );
93
+
94
+ server.registerTool(
95
+ 'fetch-docs',
96
+ {
97
+ description: 'Fetch documentation for a library',
98
+ inputSchema: {
99
+ libraryId: z.string().describe('Library identifier'),
100
+ query: z.string().describe('What to search for in docs'),
101
+ },
102
+ },
103
+ async (args) => fetchHandler(args as Record<string, unknown>),
104
+ );
105
+
106
+ // ── Client + transport ──
107
+
108
+ const client = new Client(
109
+ { name: 'mcpi-mcpserver-test-client', version: '1.0.0' },
110
+ );
111
+
112
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
113
+ await server.connect(serverTransport);
114
+ await client.connect(clientTransport);
115
+
116
+ return { client, server, did, kid, keyPair, crypto };
117
+ }
118
+
119
+ // ── Tests ──────────────────────────────────────────────────────
120
+
121
+ describe('McpServer (High-Level API) + MCP-I Integration', () => {
122
+ const pairs: Array<{ client: Client; server: McpServer }> = [];
123
+
124
+ afterEach(async () => {
125
+ for (const pair of pairs) {
126
+ await pair.client.close();
127
+ await pair.server.close();
128
+ }
129
+ pairs.length = 0;
130
+ });
131
+
132
+ async function createPair(options?: { autoSession?: boolean }) {
133
+ const pair = await setupMcpServerPair(options);
134
+ pairs.push(pair);
135
+ return pair;
136
+ }
137
+
138
+ it('listTools returns handshake + registered tools', async () => {
139
+ const { client } = await createPair();
140
+
141
+ const result = await client.listTools();
142
+ const toolNames = result.tools.map((t) => t.name);
143
+
144
+ expect(toolNames).toContain('_mcpi_handshake');
145
+ expect(toolNames).toContain('search');
146
+ expect(toolNames).toContain('fetch-docs');
147
+ });
148
+
149
+ it('handshake establishes session via McpServer', async () => {
150
+ const { client, did } = await createPair();
151
+
152
+ const result = await client.callTool({
153
+ name: '_mcpi_handshake',
154
+ arguments: {
155
+ nonce: `mcpserver-test-${Date.now()}`,
156
+ audience: did,
157
+ timestamp: Math.floor(Date.now() / 1000),
158
+ },
159
+ });
160
+
161
+ expect(result.content).toHaveLength(1);
162
+ const first = result.content[0] as { type: string; text: string };
163
+ expect(first.type).toBe('text');
164
+
165
+ const parsed = JSON.parse(first.text);
166
+ expect(parsed.success).toBe(true);
167
+ expect(parsed.sessionId).toMatch(/^mcpi_/);
168
+ expect(parsed.serverDid).toBe(did);
169
+ });
170
+
171
+ it('tool returns proof in _meta after handshake', async () => {
172
+ const { client, did } = await createPair();
173
+
174
+ // Handshake first
175
+ await client.callTool({
176
+ name: '_mcpi_handshake',
177
+ arguments: {
178
+ nonce: `mcpserver-test-${Date.now()}`,
179
+ audience: did,
180
+ timestamp: Math.floor(Date.now() / 1000),
181
+ },
182
+ });
183
+
184
+ // Call search tool
185
+ const result = await client.callTool({
186
+ name: 'search',
187
+ arguments: { query: 'react hooks' },
188
+ });
189
+
190
+ // Verify tool output
191
+ const first = result.content[0] as { type: string; text: string };
192
+ expect(first.text).toBe('Found results for: react hooks');
193
+
194
+ // Verify proof in _meta
195
+ expect(result._meta).toBeDefined();
196
+ const proof = (result._meta as Record<string, unknown>).proof as {
197
+ jws: string;
198
+ meta: Record<string, unknown>;
199
+ };
200
+ expect(proof).toBeDefined();
201
+ expect(proof.jws).toBeDefined();
202
+ expect(proof.meta.did).toMatch(/^did:key:/);
203
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
204
+ expect(proof.meta.requestHash).toMatch(/^sha256:[a-f0-9]{64}$/);
205
+ expect(proof.meta.responseHash).toMatch(/^sha256:[a-f0-9]{64}$/);
206
+ });
207
+
208
+ it('autoSession attaches proof without handshake (McpServer)', async () => {
209
+ const { client } = await createPair({ autoSession: true });
210
+
211
+ // Call tool directly — no handshake
212
+ const result = await client.callTool({
213
+ name: 'search',
214
+ arguments: { query: 'next.js routing' },
215
+ });
216
+
217
+ const first = result.content[0] as { type: string; text: string };
218
+ expect(first.text).toBe('Found results for: next.js routing');
219
+
220
+ // Proof should be present via auto-session
221
+ expect(result._meta).toBeDefined();
222
+ const proof = (result._meta as Record<string, unknown>).proof as {
223
+ jws: string;
224
+ meta: Record<string, unknown>;
225
+ };
226
+ expect(proof).toBeDefined();
227
+ expect(proof.jws).toBeDefined();
228
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
229
+ });
230
+
231
+ it('second tool also gets proof (McpServer)', async () => {
232
+ const { client } = await createPair({ autoSession: true });
233
+
234
+ const result = await client.callTool({
235
+ name: 'fetch-docs',
236
+ arguments: { libraryId: '/vercel/next.js', query: 'app router' },
237
+ });
238
+
239
+ const first = result.content[0] as { type: string; text: string };
240
+ expect(first.text).toBe('Docs for /vercel/next.js: example content');
241
+
242
+ // Verify proof is attached
243
+ expect(result._meta).toBeDefined();
244
+ const proof = (result._meta as Record<string, unknown>).proof as {
245
+ jws: string;
246
+ meta: Record<string, unknown>;
247
+ };
248
+ expect(proof).toBeDefined();
249
+ expect(proof.jws).toBeDefined();
250
+ });
251
+
252
+ it('multiple tools share the same session', async () => {
253
+ const { client } = await createPair({ autoSession: true });
254
+
255
+ const result1 = await client.callTool({
256
+ name: 'search',
257
+ arguments: { query: 'express middleware' },
258
+ });
259
+ const result2 = await client.callTool({
260
+ name: 'fetch-docs',
261
+ arguments: { libraryId: '/expressjs/express', query: 'middleware' },
262
+ });
263
+
264
+ const proof1 = (result1._meta as Record<string, unknown>).proof as {
265
+ meta: Record<string, unknown>;
266
+ };
267
+ const proof2 = (result2._meta as Record<string, unknown>).proof as {
268
+ meta: Record<string, unknown>;
269
+ };
270
+
271
+ // Same session ID across tools
272
+ expect(proof1.meta.sessionId).toBe(proof2.meta.sessionId);
273
+ });
274
+ });
275
+
276
+ // ── withMCPI() path — same tests, dream API ───────────────────
277
+
278
+ describe('McpServer + withMCPI() (Dream API)', () => {
279
+ const pairs: Array<{ client: Client; server: McpServer }> = [];
280
+
281
+ afterEach(async () => {
282
+ for (const pair of pairs) {
283
+ await pair.client.close();
284
+ await pair.server.close();
285
+ }
286
+ pairs.length = 0;
287
+ });
288
+
289
+ async function createWithMCPIPair(options?: { autoSession?: boolean }) {
290
+ const crypto = new NodeCryptoProvider();
291
+ const server = new McpServer(
292
+ { name: 'mcpi-withmcpi-test', version: '1.0.0' },
293
+ { instructions: 'Test server for withMCPI integration' },
294
+ );
295
+
296
+ const mcpi = await withMCPI(server, {
297
+ crypto,
298
+ autoSession: options?.autoSession ?? true,
299
+ });
300
+
301
+ // Register tools AFTER withMCPI — they should still get proofs
302
+ server.registerTool(
303
+ 'search',
304
+ {
305
+ description: 'Search for something',
306
+ inputSchema: { query: z.string().describe('Search query') },
307
+ annotations: { readOnlyHint: true },
308
+ },
309
+ async ({ query }) => ({
310
+ content: [{ type: 'text', text: `Found results for: ${query}` }],
311
+ }),
312
+ );
313
+
314
+ server.registerTool(
315
+ 'fetch-docs',
316
+ {
317
+ description: 'Fetch documentation for a library',
318
+ inputSchema: {
319
+ libraryId: z.string().describe('Library identifier'),
320
+ query: z.string().describe('What to search for in docs'),
321
+ },
322
+ },
323
+ async ({ libraryId }) => ({
324
+ content: [{ type: 'text', text: `Docs for ${libraryId}: example content` }],
325
+ }),
326
+ );
327
+
328
+ const client = new Client(
329
+ { name: 'mcpi-withmcpi-test-client', version: '1.0.0' },
330
+ );
331
+
332
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
333
+ await server.connect(serverTransport);
334
+ await client.connect(clientTransport);
335
+
336
+ pairs.push({ client, server });
337
+ return { client, server, mcpi };
338
+ }
339
+
340
+ it('listTools returns handshake + registered tools (withMCPI)', async () => {
341
+ const { client } = await createWithMCPIPair();
342
+
343
+ const result = await client.listTools();
344
+ const toolNames = result.tools.map((t) => t.name);
345
+
346
+ expect(toolNames).toContain('_mcpi_handshake');
347
+ expect(toolNames).toContain('search');
348
+ expect(toolNames).toContain('fetch-docs');
349
+ });
350
+
351
+ it('autoSession attaches proof without handshake (withMCPI)', async () => {
352
+ const { client } = await createWithMCPIPair();
353
+
354
+ const result = await client.callTool({
355
+ name: 'search',
356
+ arguments: { query: 'next.js routing' },
357
+ });
358
+
359
+ const first = result.content[0] as { type: string; text: string };
360
+ expect(first.text).toBe('Found results for: next.js routing');
361
+
362
+ expect(result._meta).toBeDefined();
363
+ const proof = (result._meta as Record<string, unknown>).proof as {
364
+ jws: string;
365
+ meta: Record<string, unknown>;
366
+ };
367
+ expect(proof).toBeDefined();
368
+ expect(proof.jws).toBeDefined();
369
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
370
+ });
371
+
372
+ it('handshake works through withMCPI', async () => {
373
+ const { client, mcpi } = await createWithMCPIPair({ autoSession: false });
374
+
375
+ const result = await client.callTool({
376
+ name: '_mcpi_handshake',
377
+ arguments: {
378
+ nonce: `withmcpi-test-${Date.now()}`,
379
+ audience: mcpi.identity.did,
380
+ timestamp: Math.floor(Date.now() / 1000),
381
+ },
382
+ });
383
+
384
+ const first = result.content[0] as { type: string; text: string };
385
+ const parsed = JSON.parse(first.text);
386
+ expect(parsed.success).toBe(true);
387
+ expect(parsed.sessionId).toMatch(/^mcpi_/);
388
+ expect(parsed.serverDid).toBe(mcpi.identity.did);
389
+ });
390
+
391
+ it('no per-tool wrapping needed — proofs are automatic (withMCPI)', async () => {
392
+ const { client } = await createWithMCPIPair();
393
+
394
+ // Both tools should get proofs without any manual wrapping
395
+ const searchResult = await client.callTool({
396
+ name: 'search',
397
+ arguments: { query: 'react hooks' },
398
+ });
399
+ const docsResult = await client.callTool({
400
+ name: 'fetch-docs',
401
+ arguments: { libraryId: '/vercel/next.js', query: 'app router' },
402
+ });
403
+
404
+ for (const result of [searchResult, docsResult]) {
405
+ expect(result._meta).toBeDefined();
406
+ const proof = (result._meta as Record<string, unknown>).proof as {
407
+ jws: string;
408
+ };
409
+ expect(proof).toBeDefined();
410
+ expect(proof.jws).toBeDefined();
411
+ }
412
+ });
413
+ });