Skip to main content
Glama
cstillick

statefinance-mcp

by cstillick
README.md
# 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

B3.2/5.0

Scored across 9 tools

Disambiguation5/5

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.

Naming Consistency2/5

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.

Tool Count5/5

With 9 tools, the set is well-scoped for a state finance data server. It covers essential queries without being overwhelming or too sparse.

Completeness4/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues