openfec-mcp-server-22d18f — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited openfec-mcp-server-22d18f (Plugin) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<div align="center"> <h1>@cyanheads/openfec-mcp-server</h1> <p><b>Access FEC campaign finance data on candidates, money trails, and election filings via MCP. STDIO or Streamable HTTP.</b> <div>12 Tools • 5 FEC Resource Templates • 2 Prompts • 1 Skill</div> </p> </div>
<div align="center">
</div>
<div align="center">
</div>
<div align="center">
Public Hosted Server: https://openfec.caseyjhand.com/mcp
</div>
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.
| Tool | Description |
|---|---|
openfec_search_candidates | Find federal candidates by name, state, office, party, or cycle; fetch one by FEC ID with financial totals |
openfec_search_committees | Find political committees by name, type, candidate affiliation, or state; fetch one by FEC ID |
openfec_get_committee_totals | Pre-aggregated financial totals for one committee by cycle, or ranked across committees of one entity type |
openfec_search_contributions | Itemized contributions (Schedule A), or aggregates by size, state, employer, or occupation |
openfec_search_disbursements | Itemized committee spending (Schedule B), or aggregates by purpose or recipient |
openfec_search_expenditures | Independent expenditures (Schedule E) supporting or opposing candidates, itemized or totaled per candidate |
openfec_search_coordinated_expenditures | Coordinated party expenditures (Schedule F) made on behalf of a candidate |
openfec_search_filings | FEC filings and reports by committee, candidate, form type, or date range |
openfec_lookup_elections | Candidates in a federal race with their fundraising totals, or an aggregate race summary |
openfec_search_legal | Advisory opinions, enforcement cases, alternative dispute resolutions, administrative fines, and statutes |
openfec_get_legal_document | One legal document in full, including the arrays search trims away, paged by entry when too large for one response |
openfec_lookup_calendar | FEC calendar events, filing deadlines, and election dates |
| Resource | Description |
|---|---|
openfec://candidate/{candidate_id} | Candidate profile with financial totals and principal committees |
openfec://committee/{committee_id} | Committee profile with type, designation, and financial summary |
openfec://election/{cycle}/{office} | Presidential race with candidate financial totals |
openfec://election/{cycle}/{office}/{state} | Senate or at-large House race with candidate financial totals |
openfec://election/{cycle}/{office}/{state}/{district} | House district race with candidate financial totals |
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.
| Prompt | Description |
|---|---|
openfec_money_trail | Trace the money around a candidate: direct fundraising, PAC support, independent expenditures, and party spending |
openfec_campaign_analysis | Structured analysis of a candidate's financial position |
openfec_search_candidates <sub>tool</sub>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 recordinclude_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_totalscandidate_not_found, inputs_not_applicable_to_id_lookupopenfec_search_committees <sub>tool</sub>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)committee_not_found, inputs_not_applicable_to_id_lookupopenfec_get_committee_totals <sub>tool</sub>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 pagemode says whether rows are cycles or committeescommittee_id_required_for_single_mode, entity_type_required_for_group_mode, committee_totals_not_foundopenfec_search_contributions <sub>tool</sub>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_idmode 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_approximateitemized_requires_committee_id, aggregate_requires_committee_idopenfec_search_disbursements <sub>tool</sub>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_idnext_cursor, count, and count_is_approximate; aggregate modes page by numberopenfec_search_expenditures <sub>tool</sub>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 otherwiseby_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_scopeopenfec_search_coordinated_expenditures <sub>tool</sub>committee_id, benefiting candidate_id, cycle, payee, dates, and amounts; unscoped queries span all yearscommittee_id and 25 otherwise; rows carry subordinate_committee_id, the committee the spending is attributed toopenfec_search_filings <sub>tool</sub>most_recent defaults to true, hiding superseded amendments; rows carry form and report type, financial totals, and pdf_urlopenfec_lookup_elections <sub>tool</sub>office and an even cycle are required; Senate also needs state and House state plus district, unless a zip scopes a searchmode: "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 rowcycle_must_be_even, missing_state_for_office, missing_district_for_house, summary_does_not_support_zipopenfec_search_legal <sub>tool</sub>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 recordstype 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 one52 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 wayfrom_hit / hits_returned, up to 200 per type within a 10,000-result windowdocuments 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 recordtruncated, shown, and nextFromHit — the from_hit that continues each type, passed back with that typeopenfec_get_legal_document <sub>tool</sub>doc_type is the plural of a search result's document_type (mur → murs); no is that result's no fielddocuments, commission_votes, and dispositions; fails as legal_document_not_foundwithheld — 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_recordopenfec_lookup_calendar <sub>tool</sub>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 pagedistrict filter never matches an at-large race; pair it with stateopenfec://candidate/{candidate_id} <sub>resource</sub>principal_committees (current designation P, not scoped to a cycle)candidate_id comes from openfec_search_candidates; fails as candidate_not_foundopenfec://committee/{committee_id} <sub>resource</sub>committee_id comes from openfec_search_committees; fails as committee_not_foundopenfec://election/{cycle}/{office} <sub>resource</sub>office P), with totals for the full election periodtruncation_notice pointing to openfec_lookup_elections, and an empty one empty_result_noticeopenfec://election/{cycle}/{office}/{state} <sub>resource</sub>office S or H)openfec://election/{cycle}/{office}/{state}/{district} <sub>resource</sub>office H)openfec_money_trail <sub>prompt</sub>candidate_name or candidate_id (one required), optional cyclecandidate_pcc_id, then direct fundraising, independent expenditures, coordinated party spending, disbursements, and a synthesisopenfec_campaign_analysis <sub>prompt</sub>candidate_name or candidate_id (one required), optional cycleBuilt 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:
cycle is omitted, and election_full (default true) widens a race total to the full election period (4yr president, 6yr senate, 2yr house)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_electionsnext_cursor, valid only for an otherwise-identical call; other tools page by number, and legal search by offsetAgent-friendly output:
search_criteria with the post-default filters, and an empty result carries a notice on how to broaden ittruncated, 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 rowmode 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_approximateConnect to the hosted instance, no install:
claude mcp add --transport http openfec-mcp-server https://openfec.caseyjhand.com/mcpOr run it locally:
claude mcp add openfec-mcp-server -- npx -y @cyanheads/openfec-mcp-serverFor other clients, add this to the MCP config file:
{
"mcpServers": {
"openfec-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/openfec-mcp-server"]
}
}
}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.
| Variable | Description | Default |
|---|---|---|
FEC_API_KEY | OpenFEC API key from api.data.gov. DEMO_KEY allows 30 req/hr; your own key allows 1,000. | DEMO_KEY |
FEC_BASE_URL | OpenFEC API base URL. | https://api.open.fec.gov/v1 |
FEC_MAX_RETRIES | Retry attempts for failed API requests. | 3 |
FEC_REQUEST_TIMEOUT | Per-request timeout, in ms. | 30000 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. Overrides the stateless mode the server declares. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry. | false |
See .env.example for the full list of optional overrides.
Requires Bun v1.4.0 or higher (or Node.js v24+).
git clone https://github.com/cyanheads/openfec-mcp-server.git
cd openfec-mcp-server && bun install
cp .env.example .env # set FEC_API_KEY as needed bun run rebuild
bun run start:stdio # or start:http bun run devcheck # Lint, format, typecheck, security audit
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against specdocker build -t openfec-mcp-server .
docker run --rm -e FEC_API_KEY=your-key -p 3010:3010 openfec-mcp-serverThe 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.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources/prompts and inits the OpenFEC service. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts), with shared validators and formatters in definitions/utils/. |
src/mcp-server/resources | Resource definitions (*.resource.ts). Candidate, committee, and three election-race templates. |
src/mcp-server/prompts | Prompt definitions (*.prompt.ts). Money trail and campaign analysis. |
src/services/openfec | OpenFEC API client and domain types. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
scripts/ | Build, clean, devcheck, tree, changelog, and lint scripts. |
skills/ | Agent guide to campaign-finance workflows, identifiers, and coverage limits; also served over MCP. |
docs/ | Design notes and the OpenFEC OpenAPI spec. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagesrc/mcp-server/*/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testApache-2.0 — see LICENSE for details.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.