import { describe, expect, test } from 'bun:test'; import { join } from 'node:path'; import { eraOf, handle, LEGACY, LEGACY_PROTOCOL, META, PROTOCOL, SUPPORTED, TOOLS_TTL_MS, UNSUPPORTED_PROTOCOL_VERSION } from '../cli/mcp'; import { TOOLS } from '../cli/mcp-tools'; const modernMeta = (v = PROTOCOL, caps: unknown = {}) => ({ [META.protocolVersion]: v, [META.clientCapabilities]: caps, [META.clientInfo]: { name: 'test', version: '1' } }); type R = Record; const res = (r: Record | null) => r!.result as R; const errOf = (r: Record | null) => r!.error as { code: number; message: string; data?: R }; describe('mcp 2026-07-28 (modern, stateless)', () => { test('server/discover: versions, capabilities, instructions, serverInfo in _meta, cacheable', async () => { const r = res(await handle({ jsonrpc: '2.0', id: 'd1', method: 'server/discover', params: { _meta: modernMeta() } })); expect(r.resultType).toBe('complete'); expect(r.supportedVersions).toEqual(SUPPORTED); expect((r.supportedVersions as string[])[0]).toBe('2026-07-28'); expect(r.capabilities).toEqual({ tools: {} }); expect(typeof r.instructions).toBe('string'); expect((r._meta as R)[META.serverInfo]).toMatchObject({ name: 'storebound' }); expect(r).toMatchObject({ ttlMs: TOOLS_TTL_MS, cacheScope: 'public' }); expect(r.protocolVersion).toBeUndefined(); }); test('tools/list and tools/call without any handshake', async () => { const list = res(await handle({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: modernMeta() } })); expect(list.resultType).toBe('complete'); expect((list.tools as unknown[]).length).toBe(TOOLS.length); expect(list).toMatchObject({ ttlMs: TOOLS_TTL_MS, cacheScope: 'public' }); let seen: string[] = []; const call = res(await handle({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'check', arguments: {}, _meta: modernMeta() } }, async argv => { seen = argv; return { code: 0, out: 'ok' }; })); expect(seen).toEqual(['check']); expect(call).toMatchObject({ resultType: 'complete', isError: false, structuredContent: { exitCode: 0, dryRun: false } }); expect((call._meta as R)[META.serverInfo]).toBeDefined(); }); test('version and _meta errors', async () => { const bad = errOf(await handle({ jsonrpc: '2.0', id: 3, method: 'tools/list', params: { _meta: modernMeta('2099-01-01') } })); expect(bad.code).toBe(UNSUPPORTED_PROTOCOL_VERSION); expect(bad.code).toBe(-32022); expect(bad.data).toEqual({ supported: SUPPORTED, requested: '2099-01-01' }); const noCaps = errOf(await handle({ jsonrpc: '2.0', id: 4, method: 'tools/list', params: { _meta: { [META.protocolVersion]: PROTOCOL } } })); expect(noCaps.code).toBe(-32602); expect(noCaps.message).toContain('clientCapabilities'); const probe = errOf(await handle({ jsonrpc: '2.0', id: 5, method: 'server/discover', params: {} })); expect(probe.code).toBe(-32602); expect(errOf(await handle({ jsonrpc: '2.0', id: 6, method: 'resources/list', params: { _meta: modernMeta() } })).code).toBe(-32601); const unknownTool = errOf(await handle({ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'apple_submit', _meta: modernMeta() } })); expect(unknownTool.code).toBe(-32602); }); test('era detection', () => { expect(eraOf({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: modernMeta() } })).toEqual({ era: 'modern', version: PROTOCOL }); expect(eraOf({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: modernMeta(LEGACY_PROTOCOL) } }).era).toBe('legacy'); expect(eraOf({ jsonrpc: '2.0', id: 1, method: 'tools/list' })).toEqual({ era: 'legacy', version: null }); }); }); describe('mcp 2025-11-25 (legacy handshake) stays compatible', () => { test('initialize negotiates a legacy version; results have no resultType', async () => { const init = res(await handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'old', version: '1' } } })); expect(init.protocolVersion).toBe('2025-11-25'); expect(init.capabilities).toEqual({ tools: { listChanged: false } }); expect(res(await handle({ jsonrpc: '2.0', id: 2, method: 'initialize', params: { protocolVersion: '2025-06-18' } })).protocolVersion).toBe('2025-06-18'); // a modern client that wrongly sends initialize with the modern version gets the newest legacy version back expect(res(await handle({ jsonrpc: '2.0', id: 3, method: 'initialize', params: { protocolVersion: PROTOCOL } })).protocolVersion).toBe(LEGACY_PROTOCOL); expect(await handle({ jsonrpc: '2.0', method: 'notifications/initialized' })).toBeNull(); expect(res(await handle({ jsonrpc: '2.0', id: 4, method: 'ping' }))).toEqual({}); const list = res(await handle({ jsonrpc: '2.0', id: 5, method: 'tools/list' })); expect(list.resultType).toBeUndefined(); expect(list.ttlMs).toBeUndefined(); expect(LEGACY).toEqual(['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05']); }); }); describe('stdio end to end', () => { test('one process answers a modern probe and a legacy handshake', async () => { const p = Bun.spawn([process.execPath, join(import.meta.dir, '..', 'cli', 'index.ts'), 'mcp'], { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' }); const lines = [ { jsonrpc: '2.0', id: 'probe', method: 'server/discover', params: { _meta: modernMeta() } }, { jsonrpc: '2.0', id: 'old', method: 'server/discover', params: { _meta: modernMeta('1999-01-01') } }, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'legacy', version: '1' } } }, { jsonrpc: '2.0', method: 'notifications/initialized' }, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, { jsonrpc: '2.0', id: 3, method: 'tools/list', params: { _meta: modernMeta() } }, ]; p.stdin.write(lines.map(l => JSON.stringify(l)).join('\n') + '\n'); p.stdin.end(); const out = await new Response(p.stdout).text(); await p.exited; const byId = new Map(out.trim().split('\n').map(l => JSON.parse(l) as R).map(r => [r.id, r] as const)); expect((byId.get('probe')!.result as R).supportedVersions).toContain('2026-07-28'); expect((byId.get('old')!.error as R).code).toBe(-32022); expect((byId.get(1)!.result as R).protocolVersion).toBe('2025-11-25'); expect((byId.get(2)!.result as R).resultType).toBeUndefined(); expect((byId.get(3)!.result as R).resultType).toBe('complete'); expect(byId.size).toBe(5); }); });