statefinance-mcp
# statefinance-mcp
**State-level campaign finance, as an MCP server.** Donors, expenditures,
committees, and candidates from state disclosure systems — the data the FEC
*doesn't* cover and that no MCP server currently exposes — through **one common
schema**. Oklahoma first; more states via adapters.
## Why this exists
Federal campaign finance (FEC) is wrapped to death. **State** campaign finance —
where most money-in-politics questions actually live — is fragmented across ~46
portals with no unified API, and the one historical normalizer (FollowTheMoney)
is winding down. The hard, valuable part isn't the MCP plumbing; it's the
**normalization layer** that maps a messy state portal into a clean, sourced
schema. That's the product.
## Quick start (offline, no keys)
The v1 demo runs on a committed **synthetic** Oklahoma extract (see the data note
below), so a fresh checkout works with no network and no keys.
```bash
uv sync
uv run statefinance ingest ok # load the OK 2024 sample into the store
uv run statefinance top-donors "Tallchief for Oklahoma"
# Redbud Ranch LLC: $10,000
# Acme Energy, LLC: $5,000 (spelling variants combined)
# Patterson, John Q.: $1,500
uv run statefinance summary cycle 2024
# raised $30,650, spent $76,200, top recipient: Tallchief for Oklahoma
uv run statefinance donor-history "Patterson, John Q." # all of a donor's giving
uv run statefinance serve # run the MCP server (stdio)
```
## Use it as an MCP server
```json
{
"mcpServers": {
"statefinance": {
"command": "uv",
"args": ["run", "statefinance", "serve"],
"cwd": "/path/to/statefinance-mcp"
}
}
}
```
### Tools
| Tool | What it does |
|---|---|
| `search_contributions` | Filter contributions by donor, recipient, candidate, amount/date range, state, cycle. Returns matches + full-match count and total. |
| `search_expenditures` | Filter expenditures by committee, payee, purpose, date, state, cycle. |
| `get_committee` | A committee plus its contribution and expenditure totals. |
| `get_candidate` | A candidate (by id or name), their committees, and money raised/spent. |
| `top_donors` | Top donors to a committee, grouped by a light-normalized donor key. |
| `donor_history` | Every contribution by a donor, with a per-recipient breakdown. |
| `summary` | Aggregate totals for a `committee`, `candidate`, or `cycle`. |
Every result carries each record's `source_url` + `as_of` — this is accountability
data, so a wrong number is worse than no number.
## Design: one schema, many adapters
```
state portal ─▶ StateAdapter.fetch() ─▶ raw snapshot (data/raw/<state>/)
StateAdapter.normalize() ─▶ common schema ─▶ DuckDB store
│
MCP server (reads the store only)
```
- **Ingestion and serving are decoupled.** Ingest populates a local normalized
store; the MCP server only reads it, so tool calls are fast and portals stay
un-hammered.
- **Adding a state** is implement `fetch` + `normalize`, ship a fixture, register
— core, store, and tools untouched. See [docs/adding-a-state.md](docs/adding-a-state.md)
(enforced by a test that adds a second state through the adapter contract alone).
- **Donor normalization is light and honest** (trim/case/whitespace, org suffixes).
Fuzzy entity resolution is explicitly deferred — see
[docs/donor-normalization.md](docs/donor-normalization.md).
## ⚠️ Data note
The committed Oklahoma sample is **synthetic** (fictional committees, candidates,
donors — clearly labeled), so the offline demo never asserts fabricated facts
about real people. Acquisition of *real* Oklahoma data and the ToS posture are
documented in [docs/sources/oklahoma.md](docs/sources/oklahoma.md); **live
ingestion is intentionally gated** — enable `ingest ok --live` only after
confirming the portal's current export method and terms of service.
## Development
```bash
uv run pytest # offline suite (committed synthetic extract)
uv run ruff check . # lint
```
If `uv run statefinance` ever reports `No module named 'statefinance'` (a known
editable-install quirk on some setups, e.g. paths with spaces), run with
`PYTHONPATH=src uv run statefinance ...`.
See [SPEC.md](SPEC.md) and [BUILD_PLAN.md](BUILD_PLAN.md) for the design.
## License
MIT.
TDQS
Scored across 9 tools
Each tool serves a distinct purpose: donor history, candidate info, committee info, health check, contribution search, expenditure search, store stats, summary aggregates, and top donors. There is no ambiguity between them.
Tool names mix verb-noun patterns (get_candidate, search_contributions) with noun phrases (donor_history, summary, ping) and inconsistent styles. This lack of a consistent pattern reduces predictability.
With 9 tools, the set is well-scoped for a state finance data server. It covers essential queries without being overwhelming or too sparse.
The tools cover core workflows: lookup candidates, committees, donors, contributions, expenditures, and summaries. Minor gaps exist (e.g., no listing of all candidates or committees), but the surface is largely complete for a read-oriented server.