@cyanheads/census-mcp-server
Query U.S. Census Bureau data, variables, and geography via MCP. STDIO or Streamable HTTP.
8 Tools
Overview
US Census Bureau demographic, economic, and housing data. Find datasets and variables, resolve place names or addresses to FIPS codes, and query or rank estimates across any set of geographies.
Tools
Capability reference
census_list_datasets tool
- Returns dataset codes, names, descriptions, and available vintage years, with an optional keyword filter. Covers ACS5 and ACS1 (data profiles, subject tables, comparison profiles, and
acs/acs1/spp), acs/acsse, Population Estimates, the 2020 Decennial files (Redistricting (P.L. 94-171), dec/dhc, dec/dp, dec/sdhc, dec/ddhca, dec/ddhcb), cbp, ecnbasic, and nonemp
- Each description names the filter predicates the dataset requires and the geography levels it publishes
- Dataset codes (e.g.,
acs/acs5) are what the other tools take. Case is ignored and a two-part code works by its last part alone (acs5, pl); three-part codes such as acs/acs5/profile must be given in full
available_years is exhaustive: any other year fails with year_not_available, naming the years that work
census_list_geographies tool
- Returns one row per geography level and parent hierarchy:
geography_level, whether a parent is required, required_parent_levels, and an example FIPS value. totalLevels counts each level once
geography_level values are the exact inputs to census_query_data and census_compare_geographies; year defaults to the latest vintage
- Fails with
dataset_not_found or year_not_available (naming the years served)
census_search_variables tool
- Whole-word search across label and concept: every query word must match, otherwise the closest matches come back with a notice. A variable or table code in any case returns that variable, or the whole table in code order, first (
B19013 returns B19013_001E then B19013_001M)
- A code the dataset does not publish gets a notice naming the sibling dataset that holds it (
S1701 in acs/acs5 points to acs/acs5/subject) or census_get_variable for an attribute column such as B19013_001EA
- On ACS datasets, estimate (E suffix) and margin-of-error (M suffix) codes come back together; comparison profiles and the other families publish no margins of error. Also finds predicate codes by keyword (
naics) or code (NAICS2017 in cbp)
limit is an integer from 1 to 100 (default 20), rejected rather than clamped when out of range. offset pages the complete match ordering; totalMatches counts matches before paging, truncated means another page exists, and nextOffset names it. variables.json is cached per dataset+year (default TTL 24h)
census_get_variable tool
- Takes one or more variable codes (case-insensitive) and returns label, concept, predicate type, and the table's universe, in the order given. If
groups.json can't be fetched, variables come back without universe and a notice says to retry
- Resolves annotation and flag columns such as
B19013_001EA and EMP_F, with attribute_of naming the column each belongs to and attribute_type its kind (ANNOTATION, FLAG, MARGIN_OF_ERROR). On ACS datasets it also returns estimate_code/moe_code sibling references
- Confirms predicate dimension codes (
NAICS2017, SEX) exist in a dataset; census_list_predicate_values lists their values. dataset defaults to acs/acs5 and year to the latest vintage
variable_not_found when a code isn't defined, naming the sibling dataset that publishes it when one is found (DP03_0062E in acs/acs5 points to acs/acs5/profile)
census_list_predicate_values tool
- Lists the codes a filter dimension accepts, read from the dataset dictionary when it publishes a list and enumerated live otherwise.
NAICS* and POPGROUP have long lists (thousands of codes; narrow with query); EMPSZES, LFO, RCPSZES, TAXSTAT, and TYPOP are live-only
- Dictionary lists are checked against the rows the dataset actually publishes and unpublished codes are dropped;
source says whether the check ran and the notice gives the number withheld. A dictionary list does not validate publication at a supplied geography level
- Live enumeration takes
geography_level (default us) and echoes that scope. Codes vary by level; only levels whose dataset metadata permits an unparented wildcard are accepted
query matches code and label; results are sorted by code before offset/limit paging (limit 1 to 500, default 50). totalCount counts all matches; truncated means another page exists, with nextOffset only then. On ecnbasic, within_naics scopes TAXSTAT and TYPOP to one industry
- The live route and the dictionary check read the data endpoint, so they need
CENSUS_API_KEY: without a working key a live-only dimension fails with missing_api_key, while a dictionary list still answers, unchecked, with a notice naming the key
census_resolve_geography tool
- Named places (e.g., "King County, WA") resolve via TIGERweb; street addresses resolve to tract level via Census Geocoder, with
block_group_fips and place_fips. A 5-digit ZIP (or ZIP+4) resolves to its ZIP Code Tabulation Area (zip code tabulation area)
- Auto-detects
geography_type for state, county, place, tract, and ZIP. Metropolitan/micropolitan statistical areas, combined statistical areas, consolidated cities, and economic places need an explicit geography_type; economic place returns the 8-digit ecnbasic code (Seattle 03363000)
- The place level includes census-designated places, which answer a name only when no incorporated place or county matches exactly; use the full name ("Arlington CDP, VA") or
geography_type: "place" to reach one. Optional county_fips scopes county and tract resolution (county_scope_unsupported for any other level or an address)
- Matching ignores case, accepts a state abbreviation or full name, and tolerates Saint/St. and accent differences. New York City boroughs resolve to their counties ("Brooklyn, NY" → Kings County
047) with a notice
- A name matching several geographies returns
ambiguous_name with every candidate's FIPS code and state. Results carry state_fips (→ parent_fips) and fips_summary (→ geography_fips) for the next tool; statistical areas and ZCTAs omit state_fips
census_query_data tool
- Requires FIPS codes (use
census_resolve_geography for place names). geography_fips: "*" returns every geography at the level within the parent; each row carries geography_fips and the nationally unique geography_geoid. Wildcards return up to limit rows (default 50, max 500) in GEOID order, with offset for paging and totalCount for all matches. truncated is true only when another page exists; nextOffset names that page
- A comma-separated list (
"06,53", or "033,061" with parent_fips: "53") returns one row per listed code at the same level, in GEOID order and paged the same way; codes take the level's width, no "*" member, and the parents a single code needs. The notice names each listed code no row came back for
- Up to 49 variable codes per call, fewer on datasets that add label or record columns (
too_many_variables states the maximum). Unknown codes fail with variable_not_found; level and parent problems fail with parent_required or parent_not_accepted. Optional tract_fips (6 digits, with a concrete county_fips) scopes a block-group or decennial block query to one tract
- Optional
predicates map filters the business, pep, and dec datasets and acs/acs1/spp (e.g., {"NAICS2017": "5112"}). Unset dimensions take a Census default, echoed per row in applied_filters; "*" returns one row per category, labelled in record. pep/charv returns several rows per geography, so pin one (e.g., {"MONTH": "7"})
- ACS sentinel values resolve to their published meanings, and a median in an open-ended interval is flagged
open_ended. On cbp, ecnbasic, and nonemp, withheld values are reported as suppressed with the flag's meaning. A null estimate means suppressed, a text cell (returned under value), or empty
- On
dec/ddhca and dec/ddhcb, a geography's POPGROUP group sits in only one table of a family; a no_data hint or notice names the sibling tables. Requires CENSUS_API_KEY
- Missing-geography guidance names applicable ACS publication thresholds as possible causes alongside code checks, and pairs broader-coverage datasets with variable rediscovery
census_compare_geographies tool
- Ranks all geographies at a level, or a named
geographies list of GEOIDs/bare level codes; within/within_county scope to a state/county, omit for a national comparison
- Ranks on one variable:
sort_by (default the first code; must be a requested code, else sort_by_not_requested), sort_dir (default desc), and limit (1 to 500, default 50). Equal numeric values share competition ranks (1, 2, 2, 4) and are ordered by GEOID; a notice identifies a page boundary inside a tie. A count ranks by size, not rate, so rank a published percentage for a rate (S1701_C03_001E in acs/acs5/subject, DP04_0047PE in acs/acs5/profile)
offset counts rows in the sorted matched set; ranks are assigned before paging. totalCount counts that set, truncated means another page exists, and nextOffset appears only then. A nonempty geographies filter also returns scope_rank against all fetched geographies and scopeCount for that denominator
- Takes the same
predicates, variable limit, validation, and applied_filters as census_query_data; multi-record datasets (pep/charv) or a "*" predicate fail with ambiguous_rows unless predicates pins one. Suppressed values sort last with the same reasons, and text values can't be ranked (the notice says so). Requires CENSUS_API_KEY
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.
Census-specific:
- In-process variable cache with configurable TTL — variables.json fetched once per dataset+year, searched client-side
- Three-API backend: Census Data API for data queries, TIGERweb for named-place resolution, Census Geocoder for address-to-tract
- Automatic retry with backoff on all external API calls
- Keyed tool calls share a 50-second upstream budget across metadata, data, retries, and optional probes; data requests retain a 40-second ceiling within the remaining budget
- FIPS formatting helpers — zero-padded state, county, and tract codes ready to pass between tools
Agent-friendly output:
- Workflow-oriented tool surface —
fips_summary and state_fips return values are ready to pass as geography_fips and parent_fips to the next tool
- Suppression codes decoded — Census negative sentinel values (e.g.,
-666666666) and business-dataset withholding flags (e.g., D) surfaced as their published meanings instead of raw numbers or false zeros
- Recovery hints on errors — ambiguous geography names include candidate lists; missing API key errors include registration URL
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
Register a free key at api.census.gov/data/key_signup.html. Data queries (census_query_data, census_compare_geographies) require it, as does census_list_predicate_values for a dimension it enumerates live; variable search and geography resolution work without one. 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 image defaults to HTTP transport, stateless session mode, and logs to /var/log/census-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "CENSUS_API_KEY=your-key", "ghcr.io/cyanheads/census-mcp-server:latest"]. To build locally, docker build -t census-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 via the barrel in
src/mcp-server/tools/definitions/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.