@cyanheads/openstates-mcp-server
Search bills, legislators, committees, and events across all 50 US states, DC, and 5 US territories via MCP. STDIO or Streamable HTTP.
Overview
US state legislatures from Open States: all 50 states, DC, and 5 US territories. Search bills, legislators, committees, and events, and find who represents any location.
Tools
| Tool | Description |
|---|---|
openstates_search_bills | Search state legislative bills across all covered US jurisdictions with full-text search, jurisdiction/session filtering, subject tags, and sponsor lookups |
openstates_get_bill | Fetch full detail for a specific bill by OCD ID or three-part path (jurisdiction + session + bill_id) |
openstates_search_people | Search state legislators and officials within a jurisdiction by name, chamber, or district, or fetch specific people by OCD person ID |
openstates_get_legislators_by_location | Find every legislator representing a geographic coordinate (latitude/longitude) — state legislators and the federal delegation |
openstates_search_committees | List committees for a jurisdiction (experimental — not all states have coverage) |
openstates_get_committee | Fetch committee detail by OCD organization ID, with optional membership roster |
openstates_search_events | Search hearings, floor sessions, and committee meetings (experimental) |
openstates_get_event | Fetch full event detail including agenda, participants, and media links |
openstates_list_jurisdictions | List all 56 jurisdictions (50 states, DC, and 5 US territories) covered by Open States with session identifiers and coverage metadata |
openstates_get_jurisdiction | Fetch full metadata for a specific jurisdiction including all legislative sessions and their identifiers |
Resources
| Resource | Description |
|---|---|
openstates://jurisdiction/{+jurisdiction_id} | Jurisdiction metadata including current sessions, coverage dates, and bill/people update timestamps |
All resource data is also reachable via tools. Use openstates_get_jurisdiction for programmatic jurisdiction lookups; the resource is useful for injecting jurisdiction context as stable reference material.
Prompts
| Prompt | Description |
|---|---|
openstates_bill_research | Structured framework for analyzing a state bill: summary, sponsors, committee referrals, action timeline, vote record, and related legislation |
openstates_legislator_profile | Research framework for profiling a legislator: sponsored bills, committee assignments, voting record, and contact details |
Skills
| Skill | Description |
|---|---|
openstates-guide | Identifier chains and gates for agents calling the tools: session pinning, reading vote events, committee roster scans, hearing searches, and the request budget |
Plugin installs and the .mcpb bundle pick it up from skills/; MCP clients that support the Skills extension receive it from the server.
Capability reference
openstates_search_bills tool
- Either
jurisdictionorqis required by the schema — aq-only search spans all 56 jurisdictions and exceeds the upstream timeout for a common term, so pairing them is the reliable form - Filters:
session,chamber(upper/lower),classification,subjecttags,sponsor,sponsor_classification, andaction_since/updated_since/created_sinceISO 8601 date filters includeinlines sponsorships, actions, votes, abstracts, versions, documents, and related bills — avoids follow-upopenstates_get_billcallssortdefaults toupdated_desc;sort=latest_action_descsurfaces bills currently moving; pagination up to 20 per page (default 10)- Empty-result notice echoes every applied filter and names the jurisdiction by name when it isn't recognized
openstates_get_bill tool
- Lookup by
openstates_id(preferred) or the three-part pathjurisdiction+session+bill_id; a call missing both fails withmissing_lookup_params openstates_idisocd-bill/<uuid>copied fromopenstates_search_bills(case and surrounding whitespace are normalized); a<jurisdiction>/<session>/<bill_id>path is also accepted. Any other value fails locally withinvalid_openstates_id, with no upstream call- Accepts legislature-format bill identifiers (e.g.,
HB 1000,SB 42) includeinlines sponsorships, actions, votes, versions, documents, abstracts, other titles/identifiers, and related billsnot_foundwhen a well-formed ID or a three-part path resolves to nothing
openstates_search_people tool
- Either
jurisdictionorid(OCD person IDs) is required — an unscoped search exceeds the upstream timeout even for a name-only query idresolves any number of specific OCD person IDs in one call — the IDs that bill sponsorships and committee memberships hand back — and needs no jurisdiction alongside itorg_classification:upper/lower/executive/legislature(every legislator — both chambers, or the single body in Nebraska and DC; executive officials excluded); omitting it returns every officeholder including executives- Case-insensitive substring
namematch, plusdistrict;includeadds offices, links, other_names, other_identifiers, sources - Party is reported on every result but cannot be filtered on
- Pagination up to 20 per page
openstates_get_legislators_by_location tool
- Pass decimal-degree
latitude/longitude; does not geocode addresses - Returns both government tiers in one call: state legislators plus the coordinate's two US Senators and one US Representative
jurisdiction.classification(statevscountry) is the tier discriminator —current_role.org_classificationis not, since it'supper/lowerfor a US Senator exactly as for a state senatorstateCount/federalCountenrichment fields report the tier split- Out-of-range coordinates fail as
invalid_coordinate; a location with no coverage returns an empty-result notice
openstates_search_committees tool
jurisdictionis required — the schema rejects an all-states request- Filter by
classification(committee/subcommittee) andchamber(a chamber filter omits joint committees);parentscopes to one committee's subcommittees include=membershipsreturns the full roster with member roles- Experimental: Open States is working to restore committee support and not all states have coverage — the output's
coverageNotefield always documents this - Pagination up to 20 per page
openstates_get_committee tool
- Fetch by OCD
committee_id(fromopenstates_search_committees) include=membershipsreturns the roster;include=links/sourcesadd reference URLs- Experimental — not all states have committee data;
not_foundwhen the ID doesn't exist
openstates_search_events tool
jurisdictionis required — the events endpoint has no all-states searchafter/beforescope to an ISO 8601 date range;require_bills=truefilters to events with a bill on the agendainclude=agenda,participantsreturns full meeting context- Experimental: most states don't publish event data — an empty unfiltered result may mean no coverage, not no events; an empty filtered result (
require_bills,after,before) names those filters in its notice - Pagination up to 20 per page
openstates_get_event tool
- Fetch by OCD
event_id(fromopenstates_search_events) includeadds agenda, participants, links, media, and documents- Experimental — event coverage is limited;
not_foundwhen the ID doesn't exist
openstates_list_jurisdictions tool
- Returns all 56 jurisdictions (50 states, DC, and 5 US territories) in one default call — pages are merged server-side, since the upstream
per_pageceiling of 52 no longer covers the full set classificationfilter defaults tostateinclude=legislative_sessionsreturns every historical and current session identifier — required before filtering bill searches by session, since formats vary by state (e.g.,2025,2025-2026,2025rs,2025s1)include=organizations/latest_runsadd chamber/executive-body and scraper-run metadata
openstates_get_jurisdiction tool
- Fetch one jurisdiction by OCD-ID, state name, or two-letter abbreviation
include=legislative_sessionsreturns all session identifiers with date rangesnot_foundwhen the identifier doesn't resolve
openstates://jurisdiction/{+jurisdiction_id} resource
jurisdiction_idaccepts the raw OCD ID (slashes intact) asopenstates_list_jurisdictionsreturns it, a state name (multi-word allowed), or a two-letter abbreviation; percent-encoded forms are decoded once, and a malformed escape is a validation error- Returns jurisdiction metadata as
application/json: current legislative sessions, coverage dates, bill/people update timestamps - Always includes
legislative_sessions— use to prime session identifiers without a tool call not_foundwhen the identifier doesn't resolve;rate_limitedwhen the shared key's request cap is reached
openstates_bill_research prompt
- Arguments:
jurisdiction,session,bill_id— all required - Returns one user message directing a structured research brief: overview, sponsors, legislative history, vote record, related legislation, bill text, and a passage assessment
openstates_legislator_profile prompt
- Arguments:
name,jurisdiction— both required - Returns one user message directing a structured profile: identity/role, sponsored legislation, committee assignments, voting record, and a summary
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.
Open States-specific:
- Full Open States v3 API coverage: bills, people, committees, events, and jurisdictions
- Dual lookup modes on the bill fetcher (OCD ID or jurisdiction + session + bill identifier)
- Geo-based legislator lookup via the Open States people-by-geo endpoint, spanning both state and federal tiers
- Server-level instructions prime the agent with session-discovery workflow and
includeparameter strategy before any tool call - Per-key request budgeting (
OPENSTATES_DAILY_REQUEST_BUDGET) and a two-tier timeout ladder guard the shared upstream key - Every tool and the jurisdiction resource can fail with
rate_limitedwhen Open States' per-minute or daily cap, or the server's own daily budget, is reached: the error is retryable, carriesretryAfter(seconds), and costs one attempt rather than a retry loop
Agent-friendly output:
- Empty-result recovery: search tools echo the applied filters and suggest how to broaden when no results are returned
- Experimental coverage notes (
coverageNote) on committee and event tools — surfaces the limitation instead of a silent empty result includeparameter pattern across all search and get tools — avoids N+1 follow-up calls for common research workflows
Getting started
Public Hosted Instance
A public instance is available at https://openstates.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"openstates-mcp-server": {
"type": "streamable-http",
"url": "https://openstates.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Requires an Open States API key — register free at open.pluralpolicy.com.
Add the following to your MCP client configuration file:
{
"mcpServers": {
"openstates-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/openstates-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENSTATES_API_KEY": "your-api-key"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"openstates-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/openstates-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"OPENSTATES_API_KEY": "your-api-key"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"openstates-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "OPENSTATES_API_KEY=your-api-key",
"ghcr.io/cyanheads/openstates-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENSTATES_API_KEY=your-key bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- An Open States API key — register free at open.pluralpolicy.com.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/openstates-mcp-server.git
- Navigate into the directory:
cd openstates-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env and set OPENSTATES_API_KEY
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
OPENSTATES_API_KEY | Required. Open States API key from open.pluralpolicy.com. | — |
OPENSTATES_API_BASE_URL | Open States API base URL. | https://v3.openstates.org |
OPENSTATES_DAILY_REQUEST_BUDGET | Maximum upstream requests per rolling 24 hours. Once spent, calls are rejected before the request is issued as rate_limited with retryAfter, so an over-budget call costs nothing upstream. The default matches the v3 free-tier daily cap — raise it if your key's tier allows more. | 250 |
OPENSTATES_REQUEST_TIMEOUT_MS | Per-attempt upstream deadline in milliseconds (minimum 1000). Expiry is non-retryable, so a request that cannot complete costs one wait rather than four. Open States answers a scoped query anywhere from under a second to ~57s and its own gateway gives up near 60s — lower this to fail sooner, raise it to wait out a slow query. | 45000 |
OPENSTATES_TOTAL_REQUEST_BUDGET_MS | Wall-clock ceiling in milliseconds for one call across every retry attempt and the backoff between them. Must be at least OPENSTATES_REQUEST_TIMEOUT_MS; startup rejects a lower value. | 90000 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. src/index.ts declares stateless via createApp({ sessionMode }) — no tool suspends for caller input, so no session state is needed. Setting this overrides the declaration; the framework schema default is auto, which resolves to stateful. | stateless |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path. | /mcp |
MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments. | none |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, notice, warning, error). | info |
MCP_GC_PRESSURE_INTERVAL_MS | Opt-in Bun-only forced-GC interval (ms). Try 60000 if heap grows under sustained HTTP load. | 0 (disabled) |
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 instrumentation. | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL; traces go to /v1/traces and metrics to /v1/metrics. | none |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Opt-in OTLP log export (e.g. http://localhost:4318/v1/logs); the base endpoint never enables it. | none |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name only, capped by LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
-
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t openstates-mcp-server .
docker run --rm -e OPENSTATES_API_KEY=your-key -e MCP_TRANSPORT_TYPE=http -p 3010:3010 openstates-mcp-server
The Dockerfile defaults to HTTP transport, explicitly sets MCP_SESSION_MODE=stateless, and logs to /var/log/openstates-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools, resources, prompts, and inits the Open States service. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Ten tools across bills, people, committees, events, and jurisdictions. |
src/mcp-server/resources | Resource definitions. Jurisdiction metadata resource. |
src/mcp-server/prompts | Prompt definitions. Bill research and legislator profile prompts. |
src/services/openstates | Open States API v3 service layer — HTTP client, request handling, domain types. |
skills/ | Agent skills the server publishes to the agents calling its tools. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the arrays in
src/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.