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 { basename, 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, or a folder name under the roots' }); expect(projectFor(basename(app), { roots: [root], project: null })).toEqual({ dir: app }); expect('error' in projectFor('nope', { roots: [root], project: null })).toBe(true); expect(projectFor(app.split(/[\/]/).pop(), { roots: [root], project: null })).toEqual({ dir: app }); expect('error' in projectFor('nope', { roots: [root], project: null })).toBe(true); 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(); } }); }); describe('read-only and broker profiles', () => { test('read-only lists only read tools and refuses a write tool', async () => { const list = await answer({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, { roots: [root], project: app, readOnly: true }); const tools = (list!.result as { tools: { name: string; annotations: { readOnlyHint: boolean } }[] }).tools; expect(tools.length).toBeGreaterThan(5); expect(tools.every(t => t.annotations.readOnlyHint)).toBe(true); const w = await answer({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'play_listing', arguments: {} } }, { roots: [root], project: app, readOnly: true }, async () => { throw new Error('must not run'); }); expect((w as { error: { message: string } }).error.message).toContain('read-only'); }); test('broker store credentials become STOREBOUND_PROFILE__* env', async () => { const { brokerProfileEnv } = await import('../cli/lib/broker'); const srv = createServer((req, res) => { res.setHeader('content-type', 'application/json'); const m = /^\/api\/store-credentials\/([^/]+)\/([^/]+)$/.exec(req.url ?? ''); if (req.headers.authorization !== 'Bearer kkkkkkkk') { res.statusCode = 401; return res.end('{}'); } if (m?.[1] === 'teamA' && m[2] === 'asc-api-key') return res.end(JSON.stringify({ secret: '-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----', metadata: { issuerId: 'iss', keyId: 'KEY' } })); if (m?.[1] === 'teamA' && m[2] === 'play-service-account') return res.end(JSON.stringify({ secret: '{"client_email":"a@example.com"}', metadata: {} })); res.statusCode = 404; res.end('{"error":"err_not_found"}'); }); await new Promise(r => srv.listen(0, '127.0.0.1', () => r())); const port = (srv.address() as { port: number }).port; const keyFile = join(root, 'broker.key'); (await import('node:fs')).writeFileSync(keyFile, 'kkkkkkkk', { mode: 0o600 }); try { const env = await brokerProfileEnv({ url: `http://127.0.0.1:${port}`, keyFile, account: null, from: 'test' }, ['teamA', 'other']); expect(Object.keys(env).sort()).toEqual(['STOREBOUND_PROFILE_TEAMA_ASC_ISSUER_ID', 'STOREBOUND_PROFILE_TEAMA_ASC_KEY_ID', 'STOREBOUND_PROFILE_TEAMA_ASC_P8', 'STOREBOUND_PROFILE_TEAMA_PLAY_SERVICE_ACCOUNT']); expect(env.STOREBOUND_PROFILE_TEAMA_ASC_KEY_ID).toBe('KEY'); } finally { srv.close(); } }); }); describe('long calls over SSE', () => { test('a tools/call that accepts text/event-stream gets keepalive comments, then the answer as one event', async () => { const { setKeepaliveMs } = await import('../cli/mcp-http'); setKeepaliveMs(50); const s = await startHttp({ host: '127.0.0.1', port: 0, roots: [root], project: app, allowOrigins: [], auth: { kind: 'token', token: 'b'.repeat(64) } }); try { const r = await fetch(`http://127.0.0.1:${s.port}/mcp`, { method: 'POST', headers: { authorization: `Bearer ${'b'.repeat(64)}`, 'content-type': 'application/json', accept: 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: 9, method: 'tools/call', params: { name: 'check', arguments: { project: '/nope' } } }) }); expect(r.headers.get('content-type')).toBe('text/event-stream'); const text = await r.text(); expect(text.startsWith(': started')).toBe(true); const data = /event: message\ndata: (.*)\n\n$/.exec(text)!; expect(JSON.parse(data[1]!)).toMatchObject({ id: 9, result: { isError: true } }); // JSON for everything else const p = await fetch(`http://127.0.0.1:${s.port}/mcp`, { method: 'POST', headers: { authorization: `Bearer ${'b'.repeat(64)}`, 'content-type': 'application/json', accept: 'application/json, text/event-stream' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' }) }); expect(p.headers.get('content-type')).toBe('application/json'); } finally { s.close(); setKeepaliveMs(20_000); } }); });