Commit 4ec51d8
authored
feat(serve): codemap serve — HTTP API exposing every MCP tool over POST /tool/{name} (#44)
* refactor(mcp): extract pure tool-handlers.ts (Tracer 1 of 7 — codemap serve prereq)
Pulls every MCP tool body (11 tools) into application/tool-handlers.ts — pure transport-agnostic functions returning a discriminated ToolResult ({ok: true, format: 'json'|'sarif'|'annotations', payload} | {ok: false, error}). MCP wrapper (mcp-server.ts) now slim: each registerXxxTool call is one wrapToolResult(handle*(args, opts.root)) line.
Why: prereq for the HTTP transport coming in Tracers 2-6. Keeping handlers pure means HTTP can dispatch the exact same logic without depending on the MCP SDK. Also moves the formatToolIncompatibility guard + runFormattedQuery sarif/annotations wiring + makeChangedFilesResolver + tryGetGitRefSafe into the pure module.
mcp-server.ts shrinks ~640 → ~370 LOC; tool-handlers.ts adds ~600 LOC. All 42 existing MCP server tests still pass — refactor is behavior-preserving (verified via 'bun test src/application/mcp-server.test.ts').
* feat(serve): cmd-serve.ts parser + http-server.ts skeleton (Tracer 2 of 7)
Adds the 'codemap serve' CLI verb + boots a long-running node:http listener. Skeleton routes:
- GET /health (auth-exempt liveness probe)
- GET /tools (catalog of 11 names)
- POST /tool/{name} → 501 stub (Tracer 3+ wires individual handlers)
- 404 for unknown routes / tools
- 401 when --token <secret> is set and the Authorization: Bearer header doesn't match (auth check plumbed; Tracer 5 will add tests + docs)
Defaults: 127.0.0.1:7878. SIGINT/SIGTERM → graceful drain.
Bare node:http (no Express/Fastify dep). 14 parser tests cover --port / --host / --token (both space and = forms), defaults, error paths, unknown flag rejection.
Smoke verified: 'bun src/index.ts serve --port 7879' boots; curl /health returns {ok, version} + X-Codemap-Version header; /tools returns the catalog; /tool/query 501s with the Tracer 3+ message; SIGTERM drains cleanly.
* feat(serve): POST /tool/query end-to-end (Tracer 3 of 7)
First wired tool. dispatchTool() reads JSON body (1 MiB cap to prevent trivial DoS), routes by name, calls the pure handler from tool-handlers.ts, then writeToolResult() translates ToolResult → HTTP response with the right Content-Type:
- format: 'json' → application/json
- format: 'sarif' → application/sarif+json (proper IANA media type)
- format: 'annotations' → text/plain
- !ok → 400 + {error}
Smoke verified all four shapes:
- POST /tool/query {sql:'SELECT ...'} → 200 [...rows]
- POST /tool/query {sql:'...', format:'sarif'} → 200 application/sarif+json + SARIF doc
- POST /tool/query {sql:'SELECT * FROM nonexistent'} → 400 {error: 'no such table: ...'}
- POST /tool/query (invalid JSON body) → 400 {error: 'invalid JSON body: ...'}
Other 10 tools still return 501 — Tracer 4 wires them in one batch.
* feat(serve): wire remaining 10 tools (audit, context, validate, show, snippet, query_recipe, query_batch, baseline trio) (Tracer 4 of 7)
Switch-dispatch from POST /tool/{name} → corresponding pure handler in tool-handlers.ts. Every tool now responds:
- audit (async — runs incremental index unless no_index: true)
- context, validate, show, snippet
- query_recipe, query_batch
- save_baseline, list_baselines, drop_baseline
handleRequest exported so tests can attach to their own createServer() (skipping runHttpServer's SIGINT-awaiting outer loop). 17 integration tests cover health + tools catalog + every wired tool + sarif/annotations Content-Type + 400 on bad SQL / invalid JSON / unknown recipe + 404 on unknown tool.
Smoke verified all 11 tools end-to-end: every CLI verb is reachable via POST /tool/{name} with the same envelope shape codemap query --json prints.
* test(serve): lock --token Bearer auth (Tracer 5 of 7)
Auth check was already plumbed in Tracer 2; Tracer 5 adds the integration tests that lock the contract:
- POST without Authorization → 401 + {error: "...Bearer..."}
- POST with wrong Bearer token → 401
- POST with correct Bearer token → 200 + payload
- GET /health is auth-exempt (liveness probes work without leaking the token)
- GET /tools requires the token (catalog-leak protection — agents shouldn't enumerate tools without auth)
5 new tests; 22 pass total.
* feat(serve): GET /resources/{uri} mirroring MCP resources (Tracer 6 of 7)
New application/resource-handlers.ts: pure transport-agnostic resource fetchers shared between MCP and HTTP. Same lazy-cache-on-first-read pattern (resources are constant for server-process lifetime so no invalidation needed); _resetResourceCachesForTests() escape hatch for temp-DB tests.
HTTP routes:
- GET /resources → catalog ({resources: [{uri, description}]})
- GET /resources/{encoded uri} → payload with the right mimeType
- 400 on invalid percent-encoding; 404 on unknown URI
mcp-server.ts's registerResources slimmed: 4 static URIs go through registerStaticResource() helper that delegates to readResource(); the recipe-template URI shares the same readResource lookup. ~150 LOC removed in mcp-server.ts; same observable behavior (42 MCP tests still pass).
6 new HTTP integration tests cover catalog + each resource type + 404 paths.
* docs: sync architecture / glossary / README / agents (Rule 10) + delete plan + changeset (Tracer 7 of 7)
- docs/architecture.md: new 'HTTP wiring' paragraph after MCP wiring; new 'Tool / resource handlers (transport-agnostic)' paragraph documenting the shared tool-handlers.ts + resource-handlers.ts modules; application/ table extended with the four new files (output-formatters, tool-handlers, resource-handlers, http-server).
- docs/glossary.md: new 'codemap serve / HTTP server' entry under ## S; 'codemap mcp' entry updated to include show + snippet tools and to reference the shared transport-agnostic handlers.
- docs/roadmap.md: 'codemap serve (HTTP API, v1.x)' line removed (shipped per Rule 2).
- README.md: 'Daily commands' stripe extended with codemap serve example (port + token + curl).
- .agents/rules/codemap.md + templates/agents/rules/codemap.md (Rule 10): new 'HTTP server (for non-MCP)' row in the CLI table.
- .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/SKILL.md: new 'HTTP server' paragraph next to 'MCP server'; documents loopback-default, --token, output-shape difference (no MCP {content: [...]} wrapper), shared-handlers note.
- .changeset/codemap-serve.md: minor changeset (new top-level CLI verb).
- docs/plans/codemap-serve.md: deleted on ship per docs-governance Rule 3.
- src/{application/http-server.ts, cli/cmd-serve.ts}: replaced dangling cross-refs to the deleted plan with cross-refs to architecture.md § HTTP wiring.
* fix(serve): CSRF + DNS-rebinding guard on every request (security audit)
Self-audit found a real attack vector: HTTP API on 127.0.0.1 is reachable from any locally-running browser tab via fetch. Without an Origin / Sec-Fetch-Site check, a malicious page at evil.com can:
- POST /tool/save_baseline / drop_baseline → CSRF (request reaches us, state mutates; CORS only blocks the response from being read, not the request itself).
- DNS-rebind to bypass the loopback-only bind (evil.com → 127.0.0.1 after page load; browser sends Host: evil.com:7878).
New csrfCheck() runs BEFORE every route (including auth-exempt /health so a malicious page can't even probe for liveness):
1. Sec-Fetch-Site = cross-site / same-site → 403 (modern browsers always send).
2. Host header mismatch on loopback bind → 403 (DNS rebinding).
3. Origin header set + non-null → 403 (older-browser fallback).
Non-browser clients (curl, fetch from Node, MCP hosts, CI scripts) don't send any of these headers and pass through. When --host 0.0.0.0 is set the user explicitly opted in to broader exposure; the Host check is skipped (Host could legitimately be any hostname/IP that resolves to the bound interface).
Bug found during testing: csrfCheck used opts.port for the allowed-Host set, but tests bind to port 0 (OS picks). Fixed by using req.socket.localPort (always the real listening port).
10 new tests cover every attack vector + every legit pass-through. End-to-end smoke verified with curl: Origin, Sec-Fetch-Site, Host all gate correctly.
Also: docs/architecture.md HTTP wiring + docs/glossary.md codemap serve entry + changeset all updated to call out the guard. bun audit clean (no new deps — bare node:http).
* fix(serve): Zod validation + 404/500 status + IPv6 host + 7 doc nits (CodeRabbit on #44)
10 CodeRabbit threads. All verified ✅ correct.
**Major bugs:**
- (#5) IPv6 host bracketing — new URL('/foo', 'http://::1:7878') threw because IPv6 literals need brackets per RFC 3986. Fixed by wrapping when host contains ':' and isn't already bracketed.
- (#6) HTTP path bypassed Zod validation — schemas were exported but never applied; handlers received unvalidated 'any'-cast args. Added per-tool validate() helper that wraps the ZodRawShape with z.object() and safeParse()s; failure → structured 400 with '<path>: <message>' joined error string. Mirrors what MCP gets for free via the SDK's inputSchema. 7 new tests cover missing required fields, type mismatches, and per-tool error message format.
- (#7) Error classification — ToolResult error arm gained an optional status field (400 default | 404 | 500). query_recipe + save_baseline 'unknown recipe' and drop_baseline 'no baseline named X' now return 404 instead of 400 (semantics matter for HTTP consumers branching on status). All catch-all engine throws (try/catch) marked 500. MCP transport ignores the field; HTTP transport reads it via writeToolResult.
**Doc nits (batch):**
- (#1) .agents/skills/codemap/SKILL.md — dropped 'v1.x backlog: codemap serve' (it shipped); replaced with pointer to tool-handlers.ts + resource-handlers.ts shared modules.
- (#2) docs/architecture.md — buildContextEnvelope/computeValidateRows location updated from src/cli/cmd-*.ts (pre-PR #41) to src/application/{context,validate}-engine.ts.
- (#3) docs/glossary.md — softened 'modern browsers always send' to 'send on cross-origin fetches (header presence varies by request type, browser, and privacy settings)'. Accurate without absolutism.
- (#4) README.md — TOKEN=$(openssl rand -hex 32) hoisted out of the codemap serve invocation so the curl command actually has a defined $TOKEN.
- (#8) bootstrap.ts printCliUsage() — added 'codemap serve' entry so it's discoverable from --help.
- (#9) cmd-serve.ts help text — added GET /resources catalog route + extended the error-status enumeration to 400/401/403/404/500 (was 400/401/404/500).
- (#10) templates/agents/rules/codemap.md + .agents/rules/codemap.md — added --host flag to the serve table row.
50 HTTP tests + 42 MCP tests still green. No new dependency vulnerabilities (bun audit clean — bare node:http).1 parent 4061ac3 commit 4ec51d8
18 files changed
Lines changed: 2529 additions & 770 deletions
File tree
- .agents
- rules
- skills/codemap
- .changeset
- docs
- src
- application
- cli
- templates/agents
- rules
- skills/codemap
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
| |||
77 | 79 | | |
78 | 80 | | |
79 | 81 | | |
80 | | - | |
| 82 | + | |
81 | 83 | | |
82 | 84 | | |
83 | 85 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
114 | 122 | | |
115 | 123 | | |
116 | 124 | | |
| |||
0 commit comments