@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.
10 Tools • 1 Resource • 2 Prompts
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
Resources
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
Skills
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
jurisdiction or q is required by the schema — a q-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, subject tags, sponsor, sponsor_classification, and action_since/updated_since/created_since ISO 8601 date filters
include inlines sponsorships, actions, votes, abstracts, versions, documents, and related bills — avoids follow-up openstates_get_bill calls
sort defaults to updated_desc; sort=latest_action_desc surfaces 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 path jurisdiction + session + bill_id; a call missing both fails with missing_lookup_params
openstates_id is ocd-bill/<uuid> copied from openstates_search_bills (case and surrounding whitespace are normalized); a <jurisdiction>/<session>/<bill_id> path is also accepted. Any other value fails locally with invalid_openstates_id, with no upstream call
- Accepts legislature-format bill identifiers (e.g.,
HB 1000, SB 42)
include inlines sponsorships, actions, votes, versions, documents, abstracts, other titles/identifiers, and related bills
not_found when a well-formed ID or a three-part path resolves to nothing
openstates_search_people tool
- Either
jurisdiction or id (OCD person IDs) is required — an unscoped search exceeds the upstream timeout even for a name-only query
id resolves 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 it
org_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
name match, plus district; include adds 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 (state vs country) is the tier discriminator — current_role.org_classification is not, since it's upper/lower for a US Senator exactly as for a state senator
stateCount/federalCount enrichment 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
jurisdiction is required — the schema rejects an all-states request
- Filter by
classification (committee/subcommittee) and chamber (a chamber filter omits joint committees); parent scopes to one committee's subcommittees
include=memberships returns the full roster with member roles
- Experimental: Open States is working to restore committee support and not all states have coverage — the output's
coverageNote field always documents this
- Pagination up to 20 per page
openstates_get_committee tool
- Fetch by OCD
committee_id (from openstates_search_committees)
include=memberships returns the roster; include=links/sources add reference URLs
- Experimental — not all states have committee data;
not_found when the ID doesn't exist
openstates_search_events tool
jurisdiction is required — the events endpoint has no all-states search
after/before scope to an ISO 8601 date range; require_bills=true filters to events with a bill on the agenda
include=agenda,participants returns 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 (from openstates_search_events)
include adds agenda, participants, links, media, and documents
- Experimental — event coverage is limited;
not_found when 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_page ceiling of 52 no longer covers the full set
classification filter defaults to state
include=legislative_sessions returns 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_runs add 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_sessions returns all session identifiers with date ranges
not_found when the identifier doesn't resolve
openstates://jurisdiction/{+jurisdiction_id} resource
jurisdiction_id accepts the raw OCD ID (slashes intact) as openstates_list_jurisdictions returns 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_found when the identifier doesn't resolve; rate_limited when 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
include parameter 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_limited when Open States' per-minute or daily cap, or the server's own daily budget, is reached: the error is retryable, carries retryAfter (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
include parameter 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:
Self-Hosted / Local
Requires an Open States API key — register free at open.pluralpolicy.com.
Add the following to your MCP client configuration file:
Or with npx (no Bun required):
Or with Docker:
For Streamable HTTP, set the transport and start the server:
Prerequisites
Installation
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
- Configure environment:
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
See .env.example for the full list of optional overrides.
Running the server
Local development
Docker
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
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 request-scoped logging, ctx.state for 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:
License
Apache-2.0 — see LICENSE for details.