citation-mcp
by rafael6104
README.md
# citation-mcp
Remote MCP server exposing the CounselStack citation engine as nine legal
research tools. This is a thin consumer of **citation-api** — it owns no data
and no extraction logic; it speaks HTTP to the API and packages the proven
citator tool layer (schemas, orchestration, honesty guards) in MCP form so any
MCP client — Claude first — can drive the engine.
```
Claude (web/desktop/mobile) ──MCP (Streamable HTTP)──▶ citation-mcp ──HTTP──▶ citation-api ──▶ mirror-db
```
## The nine tools
| Tool | What it does | citation-api endpoints |
|---|---|---|
| `citation_lookup` | Resolve one cite or bare case name → canonical authority + precedent footprint | `/resolve` (+ `/mentions`) |
| `find_statutes` | Topic → governing statute/reg sections (multi-phrasing FTS, merged + reranked) | `/search-statutes` |
| `citation_mentions` | The opinions citing ONE authority, with treatment snippets; the workhorse | `/resolve` + `/mentions` |
| `find_case_text` | Full-text boolean keyword search over opinion bodies | `/search-cases` |
| `find_cases` | Parenthetical proposition search (slim index, fallback) | `/search-parentheticals` |
| `grep_opinion` | Search/verify text INSIDE one authority: quotes, pin cites, keywords | `/quote` |
| `read_subsection` | One pinpoint provision of a statute/reg + breadcrumb | `/subsection` |
| `citation_read` | Full text of one authority (every opinion in a cluster, labeled) | `/resolve` + `/read` |
| `citation_check` | Scan a draft, resolve every distinct authority, report found/not-found | `/scan` + `/resolve` |
All nine are read-only over immutable reference data and are annotated as such
(`readOnlyHint`), with titles, for connector-directory readiness. Research
methodology (tool order, the statutes→mentions front door, treatment honesty
rules) ships in the server's `instructions` field.
## Run it
```bash
npm install
cp .env.example .env # point CITATION_API_BASE_URL at a running citation-api
npm run dev # tsx watch, listens on :3300
# in another terminal:
npm run smoke # connect, list tools, verify all nine
SMOKE_LIVE=1 npm run smoke # also exercise citation_lookup against the live API
```
`npm run build && npm start` for production (compiles to `dist/`). The server
is **stateless** (fresh MCP server + transport per request, no sessions), so it
deploys as a plain web service and scales horizontally with no sticky routing.
`GET /healthz` for health checks.
## Connecting it to Claude
Deploy behind HTTPS, then in Claude: **Settings → Connectors → Add custom
connector**, URL `https://<your-host>/mcp`. Authless V1 needs no credentials.
Requests from allowed browser origins (`MCP_ALLOWED_ORIGINS`, default
claude.ai + claude.com) and server-to-server requests (no Origin header) are
accepted; anything else is rejected 403.
## Auth: authless now, OAuth-shaped already (Path C)
V1 runs **authless** (`AUTH_MODE=none`): no token required, and — by design —
no user identity, so no per-user metering. The OAuth 2.1 resource-server shape
is already in place so flipping auth on later is additive:
- Every `/mcp` request crosses `authMiddleware` (src/auth/auth.ts) — the single
boundary. Authless mode threads a `null` auth context into tool registration.
- `AUTH_MODE=oauth` activates the RFC 9728 discovery handshake: `401` +
`WWW-Authenticate: Bearer resource_metadata=...` and the
`/.well-known/oauth-protected-resource` document (404 while authless).
- `verifyBearerToken` is a **stub that always rejects** — real validation
(signature/expiry against the issuer, audience = this server per RFC 8707,
subject + scopes extraction) lands when an authorization server exists. Do
not enable oauth mode in production until then.
- Outbound, every citation-api call carries `CITATION_API_TOKEN` as a Bearer
when set — a no-op today, this server's API key when the API's key layer
ships.
## Layout
```
src/
index.ts Express app: origin validation, /healthz, well-known, stateless /mcp
config.ts env parsing (AUTH_MODE switch, cache knobs, origins)
http.ts HTTP client: result cache, 60s timeouts, __httpError sentinel
instructions.ts research methodology → MCP instructions field
api/client.ts one function per citation-api endpoint
orchestration/ the composites: lookup, mentions, read, check, grep, subsection, search
tools/index.ts the nine registrations (zod schemas, descriptions, annotations, handlers)
tools/format.ts result → model-facing text, incl. the honesty language
auth/auth.ts Path C skeleton: middleware, RFC 9728 metadata, stubbed verifier
scripts/smoke.ts MCP client: list tools, optional live call
```
Design notes worth knowing:
- **Error honesty is a contract.** Upstream failures surface as
`__httpError` sentinels and reach the model as "the engine failed — nothing
was learned; treat as unverified, not as not-found." A failed scan chunk is
counted and the check report says INCOMPLETE. A mentions filter that misses
says "not in the top page", never "no case discusses this."
- **Result cache** (10 min TTL / 500 entries, env-tunable): every endpoint is
an idempotent read over immutable law, so identical calls skip the network.
Per-instance; correct under horizontal scaling, just less shared.
- **CFR mentions works here.** The original agent integration had a gap where
CFR cites silently returned no mentions; the mapping in
`orchestration/mentions.ts` includes the CFR branch.
## Future work (deliberately out of scope for V1)
- **CFR topic search tool** — the API's `/cfr-search` exists but has no tool
yet (an agent can topic-search statutes, not regulations).
- OAuth authorization server + real token validation + per-user metering.
- Structured (`structuredContent`) outputs alongside the text blocks.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues