# @legislation/mcp

An [MCP](https://modelcontextprotocol.io) server that gives AI assistants real
U.S. legislative data from the [Delilah](https://delilahusa.com) API — all 50
states, D.C., and the federal Congress. Bills, sponsors, roll-call votes with
individual member positions, action history, bill texts, committees, hearings, a
change feed, and per-state coverage.

Read-only: it never writes anything. There are **two ways to use it** — a
remote hosted server (recommended, nothing to install) and a local stdio
package. Both expose the same 35 tools. The hosted server works without a key on a shared, rate-limited free tier; for your own cap use a Delilah API key
(request one at **<https://delilahusa.com/developers/request>**).

## Remote hosted server (recommended)

The server runs on Delilah's own infrastructure at **`https://mcp.delilahusa.com`** —
you just point your client at the URL and pass your key. Nothing to install, and
the implementation stays server-side.

**Cursor** — `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "legislation": {
      "url": "https://mcp.delilahusa.com/",
      "headers": { "Authorization": "Bearer dk_live_YOURKEY" }
    }
  }
}
```

**Claude** — Settings → Connectors → Add custom connector, URL:

```
https://mcp.delilahusa.com/?key=dk_live_YOURKEY
```

**Any stdio-only client** (bridge): `npx mcp-remote https://mcp.delilahusa.com/ --header "Authorization: Bearer dk_live_YOURKEY"`

See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the hosted server works
(a Cloudflare Worker fronting `api.delilahusa.com`), and where it deploys from.

## Local install (stdio)

One command, nothing to configure:

```bash
claude mcp add legislation -- npx @legislation/mcp
```

With no key it runs on the shared free tier: the hosted server supplies a key
of its own, so tools work immediately. That tier is read-only and rate-limited
per client. Set `DELILAH_API_KEY` for your own monthly cap and for writes:

```bash
claude mcp add legislation -e DELILAH_API_KEY=dk_live_… -- npx @legislation/mcp
```

## Claude Desktop

Add this to `claude_desktop_config.json` (macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`, Windows:
`%APPDATA%\Claude\claude_desktop_config.json`), then restart Claude Desktop:

```json
{
  "mcpServers": {
    "legislation": {
      "command": "npx",
      "args": ["-y", "@legislation/mcp"],
      "env": {
        "DELILAH_API_KEY": "dk_live_..."
      }
    }
  }
}
```

## Claude Code

**Plugin (recommended)** — adds the hosted MCP server and a `/legislation`
skill that tells Claude how to answer legislative questions well (cite bill
numbers, pick the right tool, never call a race). No API key needed to start:
the hosted server has a shared, rate-limited free tier.

```bash
/plugin marketplace add https://mcp.delilahusa.com/marketplace.json
/plugin install legislation@legislation
```

Then, in any session: `/legislation what passed in Florida this week`
— or just ask; Claude reaches for the tools on its own. For your own monthly
cap, register the hosted server with your key:

```bash
claude mcp add --transport http legislation https://mcp.delilahusa.com/ -H "Authorization: Bearer dk_live_..."
```

**MCP server only:**

```bash
claude mcp add legislation --env DELILAH_API_KEY=dk_live_... -- npx -y @legislation/mcp
```

## Tools

<!-- tools:start -->
36 tools, all read-only except `manage_webhooks` (whose writes require `confirm:true`).

### Bills

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `search_bills` | `/v1/bills` | Search or list legislation with structured filters. Use this when you know the jurisdiction, sponsor, topic, or status you want. |
| `semantic_search_bills` | `/v1/search/semantic` | Vector search over the full text of every bill: describe what you want in plain English — 'protecting kids from social media', 'limits on rent increases' — and get the closest bills with a match_score. Prefer this over search_bills for questions and topics; prefer search_bills for a known bill number, sponsor, or status filter, and keyword_search_bills for exact terms that must all appear. |
| `keyword_search_bills` | `/v1/search/bills` | Keyword search where EVERY term must appear. Use when the user gives specific words that all have to be present, e.g. ['short-term rental', 'tax']. |
| `get_bill` | `/v1/bills/{bill_id}` | Fetch a single bill by its numeric bill_id, optionally merging in sub-resources: votes (roll calls with tallies), sponsors, history (action timeline), texts (version metadata), amendments, subjects, analyses (staff analyses), calendar (scheduled events), citations (statutes the bill touches), referrals (committee referrals), sast (same-as / similar-to companion bills in the other chamber or other states), supplements (fiscal notes and summaries). analyses and citations are populated only for the deeper-coverage states. Each include is a separate API call, so ask only for what you need. |
| `get_bill_text` | `/v1/texts/{text_id}` | The cleaned plain text of one bill-text version, entities decoded, ready to quote or summarize. Get text_id from get_bill with include:['texts'] (each version has a date and type). |
| `get_bill_text_history` | `/v1/bills/{bill_id}/changes` | Timeline of all computed diffs between successive text versions of one bill: what changed, when, and how much. Use get_bill_diff for one specific pair of versions. |
| `get_bill_diff` | `/v1/bills/{bill_id}/diff` | Compare two versions of a bill's text: a unified diff plus similarity, added/removed line and character counts, and changed-section highlights. Get the two version ids from get_bill with include:['texts'] — each text row's `text_id`. |

### People and votes

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `list_legislators` | `/v1/legislators` | Search or list legislators by jurisdiction, chamber, party, district, or name. This is how you turn a name into the people_id that get_legislator needs. |
| `get_legislator` | `/v1/legislators/{people_id}` | Fetch a legislator's profile by people_id, optionally merging in votes (every roll call they are recorded on, newest first, with their individual position: 1=Yea, 2=Nay, 3=Not Voting, 4=Absent) and bills (legislation they sponsor, with sponsor_type_id 1=primary, 2=cosponsor). Find a people_id with list_legislators or from a bill's sponsors. |
| `get_legislator_scorecard` | `/v1/legislators/{people_id}/scorecard` | One-call voting and sponsorship profile for a legislator: participation, party-line alignment, bills sponsored and passed, notable votes. This is the tool for 'how does X vote' or 'is X effective'. |
| `get_roll_call` | `/v1/votes/{roll_call_id}` | Fetch a roll call by roll_call_id: the motion, chamber, date and yea/nay/absent tallies. Add include:['details'] to also pull how every individual member voted (vote_id 1=Yea, 2=Nay, 3=Not Voting, 4=Absent) — this is the tool for 'how did each legislator vote on this'. |

### Committees and hearings

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `list_committees` | `/v1/committees` | List legislative committees, filterable by jurisdiction, chamber, or name search. Returns the committee_id values that get_committee needs. |
| `get_committee` | `/v1/committees/{committee_id}` | Fetch a committee by committee_id, optionally merging in members (its roster, with roles) and hearings (its scheduled meetings). Find a committee_id with list_committees. |
| `list_hearings` | `/v1/hearings` | Scheduled committee hearings across committees, filterable by jurisdiction and start date. Use this to answer 'what is being heard this week' or to track when a committee next meets. |

### Sessions and analytics

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `list_sessions` | `/v1/sessions` | Legislative sessions: regular and special sessions with their years, start/sine-die dates and a session_id. Use this to resolve which session a bill belongs to, to find the current session for a state, or to pass session_id as a filter to search_bills. |
| `get_session_dashboard` | `/v1/sessions/{id}/dashboard` | Headline counts for one legislative session: bills filed, passed, signed, vetoed, roll calls, active legislators. Start here for 'how is the 2026 Florida session going'. |
| `get_session_sponsor_stats` | `/v1/sessions/{id}/sponsor-stats` | Legislators ranked by bills sponsored and passed in one session. Each row has bills_sponsored, bills_primary, bills_passed, bills_primary_passed and pass_rate (passed = status 4, 5 or 8). |
| `get_session_vote_participation` | `/v1/sessions/{id}/vote-participation` | How many roll calls each legislator voted in versus missed during one session. Use for attendance questions. |
| `list_session_legislators` | `/v1/sessions/{id}/legislators` | Every legislator who served in one session, with people_id. Use list_legislators for the current roster by state; use this for a specific past or special session. |
| `get_session_breakdown` | `/v1/sessions/{id}/status-counts` | Bill count per status (by:'status', on the status_id ladder) or per subject (by:'subject') for one session. Get session_id from list_sessions. status_id values: 1=Introduced, 2=In Committee, 3=Passed One Chamber, 4=Passed Both Chambers, 5=Signed into Law, 6=Vetoed, 7=Failed/Dead, 8=Chaptered. |
| `get_trending_bills` | `/v1/trending` | Bills with the most news coverage in the window, optionally for one state. Use for 'what's in the news' or 'hot bills right now'. |
| `list_bills_by_stage` | `/v1/bills-by-stage` | Bills whose LATEST action in the window falls in a stage: introduced, committee, passed, executive (with the governor, signed, or vetoed), amendments, other. Fleet-wide across all jurisdictions — there is no state filter, so filter the rows by state_id or state_name yourself. |
| `get_weekly_stage_breakdown` | `/v1/bills-weekly-breakdown` | One row per stage with the number of bills whose latest action landed there in the window. All jurisdictions combined. |

### Elections and geography

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `list_candidates` | `/v1/candidates` | Search the election-candidate directory: people running for office. Filters combine with AND. |
| `get_candidate` | `/v1/candidates/{id}` | Fetch one election candidate by id: their party, office, district and cycle, plus an FEC filer id where the campaign has filed and a Bioguide id for sitting members of Congress. When people_id is set, the candidate has a legislative voting record — pass it to get_legislator or get_legislator_scorecard to go from candidate to voting history in one hop. |
| `get_midterms` | `/v1/midterms` | The election slate for 2026-11-03: races, candidates and ballot measures per state, with official results where available. All data is official state election-office and FEC content collected by Delilah — no third-party attribution is required. live_results carries ONE ENTRY PER CONTEST, each with `contest` (e.g. primary:DEM, runoff:REP) and `party`: a closed primary returns each party separately and vote_pct is the share of THAT party's ballot, so never compare or rank candidates across two contests. |
| `get_ballot_measure` | `/v1/measures` | Ballot measures: the list for a state, or one measure by its number (e.g. 'Amendment 1') with full text, summary and official source URL. Use for 'what's on the ballot in X' and 'what does Amendment N do'. |
| `get_candidate_photo` | `/v1/photos/{candidate_id}` | The URL of a candidate's official portrait. Sources are official state-legislature and congressional portraits, public-domain and licensed images only — never a third party's photo library — and each carries a license and attribution string that MUST be shown when the license requires it (Wikimedia CC BY). |
| `list_districts` | `/v1/districts` | District metadata by state and type: congressional, state senate, state house. Pass district_id with boundary:true to get the polygon as GeoJSON — it is large, so set simplify (degrees, e.g. 0.01) to shrink it. |

### Monitoring and reference

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `get_changes` | `/v1/changes` | Near-real-time feed of what changed. Poll with since=<ISO timestamp> to get everything newer than your cursor; with no since it defaults to the last hour. |
| `list_topics` | `/v1/topics` | The canonical topic taxonomy with per-topic bill counts. Call this first to find the right slug for the topic filter on search_bills, instead of guessing one. |
| `list_subjects` | `/v1/subjects` | The raw subject taxonomy with per-subject bill counts. Subjects are the fine-grained categories the legislatures themselves assign to bills — narrower and more numerous than the curated topic slugs from list_topics. |
| `list_states` | `/v1/states` | The jurisdiction directory: the 50 states, D.C. and the U.S. Congress with their state_id, 2-letter code and full name. Returns the full list (52 rows) so you can resolve a name to a code or enumerate everything the corpus covers. |
| `get_coverage` | `/v1/coverage` | Per-state depth snapshot: how COMPLETE each jurisdiction's data is — bill counts, the share of bills holding at least one vote, text or sponsor, and whether the legislature is currently in session. Use it before drawing conclusions from a thin result set, to tell 'no such bills' apart from 'this state is thinly covered'. |

### Account

| Tool | Endpoint | What it does |
| --- | --- | --- |
| `get_usage` | `/v1/_meta/usage` | The current key's tier, monthly cap, requests used and remaining, and when the period resets. Call this when a request is rate-limited (HTTP 429) or the user asks about their plan. |
| `manage_webhooks` | `/v1/webhooks` | Manage the key's webhook subscriptions. action:'list' is read-only. 'create' REGISTERS a URL that Delilah will POST to on the chosen events — it changes the account, so it requires confirm:true and the user's explicit go-ahead. |
<!-- tools:end -->

### On `semantic_search_bills`

True vector search over the full text of every bill (`GET /v1/search/semantic`):
ask a question in plain English and each result carries a `match_score`. Use
`search_bills` for a known bill number, sponsor or status filter, and
`keyword_search_bills` when specific terms must all appear.

### Status IDs

| id | Meaning |
| --- | --- |
| 1 | Introduced |
| 2 | In Committee |
| 3 | Passed One Chamber |
| 4 | Passed Both Chambers |
| 5 | Signed into Law |
| 6 | Vetoed |
| 7 | Failed / Dead |
| 8 | Chaptered (law) |

Aggregate buckets: passed = {4, 5, 8}, failed = {6, 7}, alive = {1, 2, 3}. The
`status_id` filter on `search_bills` and the `status` field on every bill object
use this scheme.

## Notes

- **Coverage window:** the corpus is 2026-forward only.
- **Response size:** long arrays are capped and long strings clipped, each with
  an explicit truncation marker. Page with `limit`/`offset` for the rest.
- **Errors:** API error bodies are passed through verbatim, including
  `request_id` and any `X-RateLimit-*` / `Retry-After` headers.
- **Includes:** each entry in an `include` array is a separate API call against
  your rate limit, so request only what you need.

## Configuration

| Variable | Required | Default |
| --- | --- | --- |
| `DELILAH_API_KEY` | yes, for tool calls | — |
| `DELILAH_API_BASE` | no | `https://api.delilahusa.com` |

Requests time out after 30 seconds.

## Development

```bash
npm install
npm run build          # tsc -> dist/
npm test               # vitest: catalog, client, errors, worker-catalog parity
npm run docs           # regenerate the README tool tables from the catalog
npm run emit-worker    # bundle the catalog into ../delilah-infrastructure/cloudflare-workers/mcp/tools.generated.js
DELILAH_API_KEY=… npm run contract   # call every tool once against the live API
```

Tools live in `src/tools/*.ts` — one `defineTool({...})` per tool, written
against the tiny `Api` interface in `src/tools/types.ts`. The stdio server
(`src/index.ts`) registers that catalog directly; the hosted worker imports a
bundle generated from the same files, so the two transports cannot drift.
Add a tool by adding it to a module and to the ordered list in
`src/tools/index.ts`, then run `npm test`, `npm run docs` and `npm run emit-worker`.

Requires Node 18 or newer.

## License

MIT
