@cyanheads/openfec-mcp-server
Access FEC campaign finance data on candidates, money trails, and election filings via MCP. STDIO or Streamable HTTP.
12 Tools • 5 FEC Resource Templates • 2 Prompts • 1 Skill
Overview
US federal campaign finance data from the FEC's OpenFEC API. Search candidates, committees, and filings, trace contributions (Schedule A), disbursements (Schedule B), and independent and coordinated party expenditures (Schedules E/F), and look up election races, legal documents, and filing deadlines.
Tools
Resources
The same data is reachable through openfec_search_candidates, openfec_search_committees, and openfec_lookup_elections for tool-only clients.
The openfec-guide skill covers campaign-finance investigations, historical committee attribution, and coverage checks. MCP clients can read it at skill://openfec-guide/SKILL.md.
Prompts
Capability reference
openfec_search_candidates tool
query name search filtered by state, district, office, party, cycle, election_year, incumbent_challenge, candidate_status, and has_raised_funds, up to 100 per page; or a candidate_id lookup (H/S/P + eight letters or digits) returning one full record
include_totals (default on for an ID lookup) adds per-cycle totals rows and caps a search page at 35 candidates when cycle or election_year scopes the totals, 5 when they span every cycle; candidates the totals fetch didn't reach are listed in missing_totals
- Typed errors:
candidate_not_found, inputs_not_applicable_to_id_lookup
openfec_search_committees tool
query name search filtered by candidate_id, state, party, committee_type, designation, cycle, and treasurer_name, up to 100 per page; or a committee_id lookup (C + eight digits)
- Typed errors:
committee_not_found, inputs_not_applicable_to_id_lookup
openfec_get_committee_totals tool
mode: "single" (default) takes a committee_id and returns one row per two-year cycle; mode: "by_entity_type" ranks every committee of one entity_type (presidential, pac, party, pac-party, house-senate, ie-only), filterable by state, type, designation, organization type, and receipts/disbursements bounds; up to 100 per page
- Rows carry receipts, disbursements, cash on hand, debts, and the itemized/unitemized split; the output
mode says whether rows are cycles or committees
- Typed errors:
committee_id_required_for_single_mode, entity_type_required_for_group_mode, committee_totals_not_found
openfec_search_contributions tool
itemized (default) needs a committee_id and filters by contributor name, employer, occupation, city, state, ZIP, date and amount ranges, and is_individual, 30 rows per page; by_size / by_state take a committee_id or candidate_id, by_employer / by_occupation a committee_id
- The output
mode becomes by_size_candidate or by_state_candidate when scoped by candidate, a different row shape; itemized responses carry next_cursor, count, and count_is_approximate
- Typed errors:
itemized_requires_committee_id, aggregate_requires_committee_id
openfec_search_disbursements tool
committee_id required in every mode: itemized (default; recipient name, state, city, and committee ID, description, purpose category, date and amount ranges, 30 rows per page), by_purpose, by_recipient, by_recipient_id
- Itemized responses carry
next_cursor, count, and count_is_approximate; aggregate modes page by number
openfec_search_expenditures tool
itemized (default) filters by committee, candidate, support_oppose, payee, targeted office/state/district, party, is_notice (24/48-hour notices), dates, and amounts; most_recent defaults to true; 60 rows per page when scoped by committee_id, 30 otherwise
by_candidate totals need a candidate_id or a full race scope (candidate_office=P alone, S plus candidate_office_state, H plus state and candidate_office_district), or fail as by_candidate_requires_scope
openfec_search_coordinated_expenditures tool
- Scope by spending party
committee_id, benefiting candidate_id, cycle, payee, dates, and amounts; unscoped queries span all years
- Page-based, 80 rows per page when scoped by
committee_id and 25 otherwise; rows carry subordinate_committee_id, the committee the spending is attributed to
openfec_search_filings tool
- Filters: committee_id, candidate_id, filer_name, form_type (F3, F3P, F3X, F24, F1, F2, F5), report_type, report_year, cycle, is_amended, and receipt date range; 65 rows per page
most_recent defaults to true, hiding superseded amendments; rows carry form and report type, financial totals, and pdf_url
openfec_lookup_elections tool
office and an even cycle are required; Senate also needs state and House state plus district, unless a zip scopes a search
mode: "search" (default) returns the race's candidates with totals and candidate_pcc_id / candidate_pcc_name, that cycle's principal campaign committee; mode: "summary" returns one aggregate race row
- Typed errors:
cycle_must_be_even, missing_state_for_office, missing_district_for_house, summary_does_not_support_zip
openfec_search_legal tool
type (advisory_opinions, murs, adrs, admin_fines, statutes), query, ao_number, case_number, respondent, citations, penalty bounds, or dates; at least one is required (missing_filter), and a date bound needs type plus a date_kind that type records
- Citations, penalty bounds, respondent, ao_number, and case_number each filter only some types; one sent with a
type it doesn't filter fails as filter_not_valid_for_type, and with type omitted the search returns only the types they all apply to, with a total over those types. On MURs and ADRs a citation can't be combined with case_number, respondent, a penalty bound, or an open/close date bound, which upstream would ignore; the two citation fields together match either one
- Citations take the form upstream parses —
52 U.S.C. 30104 or 11 CFR 110.1 (USC, C.F.R., §, and suffixes like 30104(g) are fine); a bare 30106 or 110.1 fails as invalid_citation instead of silently returning unfiltered results. One citation per field: upstream applies only the first citation in a value, so 52 U.S.C. 30104, 52 U.S.C. 30118 fails the same way
- Offset paging via
from_hit / hits_returned, up to 200 per type within a 10,000-result window
- Results are trimmed:
documents becomes document_count and document_categories, dispositions becomes disposition_count and disposition_categories, highlights stop at 3 with their <em> match markup removed and render one per line, and commission_votes shrink to a date and a 200-character action; openfec_get_legal_document returns the full record
- Each response stays under 100,000 bytes: whole results are admitted one type at a time until the next would not fit, and a page that held some back reports
truncated, shown, and nextFromHit — the from_hit that continues each type, passed back with that type
openfec_get_legal_document tool
doc_type is the plural of a search result's document_type (mur → murs); no is that result's no field
- Returns the untrimmed record, including
documents, commission_votes, and dispositions; fails as legal_document_not_found
- A record over 100,000 bytes returns its scalar fields, the arrays that fit (smallest first), and
withheld — each held-back array with its entry count and size; re-call with array and offset for that array's entries while they fit, plus next_offset. An array the record lacks fails as array_not_in_record
openfec_lookup_calendar tool
events (default; category, one of 18 codes, and description), filing_deadlines (report_type, report_year), or election_dates (state, office, district, election_year); min_date / max_date apply in every mode, up to 100 per page
- A
district filter never matches an at-large race; pair it with state
openfec://candidate/{candidate_id} resource
- Candidate record merged with its financial totals and
principal_committees (current designation P, not scoped to a cycle)
candidate_id comes from openfec_search_candidates; fails as candidate_not_found
openfec://committee/{committee_id} resource
- Committee record merged with its financial totals, which are absent for a committee that files no Form 3/3X/3P
committee_id comes from openfec_search_committees; fails as committee_not_found
openfec://election/{cycle}/{office} resource
- Presidential races (
office P), with totals for the full election period
- First page only: a longer race carries
truncation_notice pointing to openfec_lookup_elections, and an empty one empty_result_notice
openfec://election/{cycle}/{office}/{state} resource
- Senate races, or an at-large House race in a single-district state (
office S or H)
- Same first-page limit and notices as the presidential template
openfec://election/{cycle}/{office}/{state}/{district} resource
- House district races (
office H)
- Same first-page limit and notices as the presidential template
openfec_money_trail prompt
- Arguments:
candidate_name or candidate_id (one required), optional cycle
- Seven steps: identify the candidate, resolve the cycle's principal committee via
candidate_pcc_id, then direct fundraising, independent expenditures, coordinated party spending, disbursements, and a synthesis
openfec_campaign_analysis prompt
- Arguments:
candidate_name or candidate_id (one required), optional cycle
- Seven steps: candidate overview, principal committee and per-cycle trajectory, fundraising mix, burn rate, competitive position, outside money, and an assessment
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
OpenFEC-specific:
- Type-safe client for the OpenFEC REST API with retry and a configurable timeout; error messages have the API key stripped and HTTP failures carry recovery hints
- Cycles are even years covering two calendar years (2024 = Jan 2023 – Dec 2024); itemized Schedule A/B/E default to the current cycle when
cycle is omitted, and election_full (default true) widens a race total to the full election period (4yr president, 6yr senate, 2yr house)
- Itemized contributions and disbursements scope to a
committee_id, not a candidate_id. Committee designation and principal_committees reflect current designation only, so for a cycle's principal committee read candidate_pcc_id from openfec_lookup_elections
- Itemized Schedule A/B/E page with an opaque
next_cursor, valid only for an otherwise-identical call; other tools page by number, and legal search by offset
Agent-friendly output:
- Effective-query echo: every tool response carries
search_criteria with the post-default filters, and an empty result carries a notice on how to broaden it
- Response budget of 100,000 bytes per surface: high-volume tools cap rows per page and report
truncated, shown, and cap when a page falls below the requested per_page (page-based tools also echo the applied size in pagination.per_page); legal search holds back whole results and reports nextFromHit, and a large legal document pages its arrays by offset; a single-committee query lifts that committee into one top-level committee field instead of repeating it per row
- Typed contracts: multi-mode tools echo the resolved
mode and reject a filter the mode can't apply (itemized_only_filters_in_aggregate_mode, inputs_not_applicable_to_mode) instead of dropping it; every failure carries a typed reason and recovery text, and estimated totals are flagged count_is_approximate
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
An optional OpenFEC API key in FEC_API_KEY raises the limit from 30 to 1,000 requests per hour (-e FEC_API_KEY=your-key, or an env entry). For Docker or a source build, see Running the server.
Configuration
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
-
Build and run:
-
Run checks and tests:
Docker
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/openfec-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for logging, ctx.state for storage
- Register new tools and resources in the barrels at
src/mcp-server/*/definitions/index.ts
- Wrap the OpenFEC API: validate raw → normalize to domain type → return the output schema; never fabricate fields the upstream response omitted
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.