import { afterAll, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync, statSync } from 'node:fs'; import { createServer } from 'node:http'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { answer, insideRoots, introspectionOk, metadataUrl, originAllowed, projectFor, startHttp, tokenFrom } from '../cli/mcp-http'; const root = mkdtempSync(join(tmpdir(), 'mcph-')); const app = mkdtempSync(join(root, 'app-')); afterAll(() => rmSync(root, { recursive: true, force: true })); describe('pure parts', () => { test('roots and project', () => { expect(insideRoots(app, [root])).toBe(true); expect(insideRoots(join(root, '..'), [root])).toBe(false); expect(projectFor(undefined, { roots: [root], project: null })).toEqual({ error: 'project: required (absolute path of the app folder)' }); expect(projectFor(undefined, { roots: [root], project: app })).toEqual({ dir: app }); expect(projectFor('rel/dir', { roots: [root], project: null })).toEqual({ error: 'project: must be an absolute path' }); expect('error' in projectFor(tmpdir(), { roots: [root], project: null })).toBe(true); expect('error' in projectFor(join(root, 'missing'), { roots: [root], project: null })).toBe(true); expect(projectFor(app, { roots: [root], project: null })).toEqual({ dir: app }); }); test('origin, metadata url, introspection', () => { expect(originAllowed(undefined, [])).toBe(true); expect(originAllowed('http://localhost:3000', [])).toBe(true); expect(originAllowed('https://evil.example.com', [])).toBe(false); expect(originAllowed('https://app.example.com', ['https://app.example.com'])).toBe(true); expect(metadataUrl('https://sb.example.com/mcp')).toBe('https://sb.example.com/.well-known/oauth-protected-resource/mcp'); const r = 'https://sb.example.com/mcp'; expect(introspectionOk({ active: true, scope: 'mcp apps:read', aud: [r] }, r)).toBe(true); expect(introspectionOk({ active: true, scope: 'apps:read', aud: [r] }, r)).toBe(false); expect(introspectionOk({ active: true, scope: 'mcp', aud: ['https://other.example.com/mcp'] }, r)).toBe(false); expect(introspectionOk({ active: false }, r)).toBe(false); }); test('token file is made once, mode 600 off Windows', () => { const f = join(root, 'tok', 'mcp-token'); const t = tokenFrom(f); expect(t).toMatch(/^[0-9a-f]{64}$/); expect(tokenFrom(f)).toBe(t); if (process.platform !== 'win32') expect(statSync(f).mode & 0o777).toBe(0o600); }); test('tools/list gains a required project; tools/call runs in it and drops it from the arguments', async () => { const list = await answer({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, { roots: [root], project: null }); const check = ((list!.result as { tools: { name: string; inputSchema: { properties: Record; required?: string[] } }[] }).tools).find(t => t.name === 'check')!; expect(check.inputSchema.properties.project).toBeDefined(); expect(check.inputSchema.required).toContain('project'); let seen = null as { argv: string[]; cwd: string } | null; const r = await answer({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'check', arguments: { project: app } } }, { roots: [root], project: null }, async (argv, cwd) => { seen = { argv, cwd }; return { code: 0, out: 'ok' }; }); expect(seen).toEqual({ argv: ['check'], cwd: app }); expect((r!.result as { isError: boolean }).isError).toBe(false); const bad = await answer({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'check', arguments: {} } }, { roots: [root], project: null }, async () => { throw new Error('must not run'); }); expect((bad!.result as { isError: boolean }).isError).toBe(true); }); }); describe('http server', () => { const post = (port: number, body: unknown, headers: Record = {}) => fetch(`http://127.0.0.1:${port}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...headers }, body: JSON.stringify(body) }); test('token auth: 401 without, initialize with, 202 for a notification, 405 for GET, 403 for a foreign origin', async () => { const s = await startHttp({ host: '127.0.0.1', port: 0, roots: [root], project: null, allowOrigins: [], auth: { kind: 'token', token: 'a'.repeat(64) } }); try { const auth = { authorization: `Bearer ${'a'.repeat(64)}` }; expect((await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' })).status).toBe(401); expect((await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' }, { authorization: 'Bearer nope' })).status).toBe(401); const init = await post(s.port, { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 't', version: '1' } } }, auth); expect(init.status).toBe(200); expect(((await init.json()) as { result: { protocolVersion: string } }).result.protocolVersion).toBe('2025-06-18'); expect((await post(s.port, { jsonrpc: '2.0', method: 'notifications/initialized' }, auth)).status).toBe(202); expect((await fetch(`http://127.0.0.1:${s.port}/mcp`, { headers: auth })).status).toBe(405); expect((await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' }, { ...auth, origin: 'https://evil.example.com' })).status).toBe(403); expect((await post(s.port, [{ jsonrpc: '2.0', id: 1, method: 'ping' }], auth)).status).toBe(400); } finally { s.close(); } }); test('introspect auth: metadata document, 401 names it, a valid token passes, a token without mcp scope does not', async () => { const resource = 'https://sb.example.com/mcp'; let calls = 0; const idp = createServer((req, res) => { calls++; let b = ''; req.on('data', c => (b += c)); req.on('end', () => { const tok = new URLSearchParams(b).get('token'); const ok = req.headers.authorization === 'Bearer server-key'; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(!ok ? { active: false } : tok === 'good' ? { active: true, scope: 'mcp', aud: [resource] } : tok === 'noscope' ? { active: true, scope: 'apps', aud: [resource] } : { active: false })); }); }); await new Promise(r => idp.listen(0, '127.0.0.1', () => r())); const ip = (idp.address() as { port: number }).port; const s = await startHttp({ host: '127.0.0.1', port: 0, roots: [root], project: app, allowOrigins: [], auth: { kind: 'introspect', url: `http://127.0.0.1:${ip}/introspect`, key: 'server-key', resource, issuer: 'https://idp.example.com' } }); try { const meta = await fetch(`http://127.0.0.1:${s.port}/.well-known/oauth-protected-resource/mcp`); expect(await meta.json()).toMatchObject({ resource, authorization_servers: ['https://idp.example.com'], scopes_supported: ['mcp'] }); const no = await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' }); expect(no.status).toBe(401); expect(no.headers.get('www-authenticate')).toBe('Bearer resource_metadata="https://sb.example.com/.well-known/oauth-protected-resource/mcp"'); expect((await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' }, { authorization: 'Bearer good' })).status).toBe(200); expect((await post(s.port, { jsonrpc: '2.0', id: 2, method: 'ping' }, { authorization: 'Bearer good' })).status).toBe(200); expect(calls).toBe(1); // cached expect((await post(s.port, { jsonrpc: '2.0', id: 1, method: 'ping' }, { authorization: 'Bearer noscope' })).status).toBe(401); } finally { s.close(); idp.close(); } }); });