Skip to main content
Glama
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.

Maintenance

ActivitySlowing
ResponsivenessNo issues