census-mcp-server

v0.3.2 pre-1.0

Query U.S. Census Bureau data, variables, and geography via MCP. STDIO or Streamable HTTP.

census.caseyjhand.com/mcp
claude mcp add --transport http census-mcp-server https://census.caseyjhand.com/mcp
codex mcp add census-mcp-server --url https://census.caseyjhand.com/mcp
{
  "mcpServers": {
    "census-mcp-server": {
      "url": "https://census.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http census-mcp-server https://census.caseyjhand.com/mcp
{
  "mcpServers": {
    "census-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://census.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "census-mcp-server": {
      "type": "http",
      "url": "https://census.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://census.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

8

census_list_datasets

Browse available Census Bureau datasets with their supported vintage years. Use as the starting point when the right dataset is unknown — ACS5, ACS1, population estimates, decennial census, and the business datasets (County Business Patterns, Economic Census, Nonemployer Statistics) serve different use cases. Pass the dataset_id value to the dataset parameter in other census tools. Each description names the predicates a dataset requires and the geography levels it publishes, both of which vary by dataset.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_list_datasets",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "filter": {
      "description": "Keyword to filter datasets by name or description. Omit to list all datasets.",
      "type": "string"
    }
  },
  "additionalProperties": false
}
view source ↗

census_list_geographies

List the geography levels available for a given Census dataset and year, along with the parent geographies each level requires. Use before querying to confirm that the target geography level exists — ACS1 omits many sub-state levels, and not all datasets support tracts or block groups. The geography_level values returned here are the valid inputs to the geography_level parameter in census_query_data and census_compare_geographies.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_list_geographies",
    "arguments": {
      "dataset": "<dataset>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset": {
      "type": "string",
      "description": "Dataset code (e.g., \"acs/acs5\", \"acs/acs1\"). Use census_list_datasets to discover valid values."
    },
    "year": {
      "description": "Vintage year. Defaults to the latest available year for the dataset.",
      "type": "number"
    }
  },
  "required": [
    "dataset"
  ],
  "additionalProperties": false
}
view source ↗

census_search_variables

Search Census variables by keyword across variable labels and concept groups. Returns variable codes with human-readable labels — use this to go from a concept like "median household income" to the variable code B19013_001E needed for data queries. On ACS datasets it returns both estimate (E suffix) and margin-of-error (M suffix) codes so you can request both; other dataset families publish no margins of error. Also use it to find the predicate codes a dataset filters on, such as NAICS2017 in cbp. When total_matches exceeds the limit, narrow the query to see more specific results.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_search_variables",
    "arguments": {
      "query": "<query>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "Keyword to search (e.g., \"median household income\", \"poverty\", \"bachelor's degree\"). Multi-word queries search for all terms."
    },
    "dataset": {
      "description": "Dataset to search within (default: \"acs/acs5\"). Use census_list_datasets to discover options.",
      "type": "string"
    },
    "year": {
      "description": "Vintage year to search (default: latest available for the dataset).",
      "type": "number"
    },
    "limit": {
      "description": "Maximum results to return (default: 20, max: 100). Increase if total_matches greatly exceeds the limit.",
      "type": "number"
    }
  },
  "required": [
    "query"
  ],
  "additionalProperties": false
}
view source ↗

census_get_variable

Fetch full metadata for one or more Census variable codes — label, concept group, predicate type, universe, and margin-of-error sibling references. Use to confirm a variable code before building a query, or to look up what a known code means. On ACS datasets it returns estimate_code and moe_code sibling references so you can request both without a separate search; other dataset families publish no margins of error and carry neither field. It also resolves predicate codes such as NAICS2017 or SEX, confirming a filter dimension exists in a dataset before a query uses it — for the values a dimension accepts rather than the dimension itself, call census_list_predicate_values.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_get_variable",
    "arguments": {
      "variables": "<variables>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "variables": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "One or more variable codes to look up (e.g., [\"B19013_001E\", \"B19013_001M\"]). Variable codes are case-sensitive."
    },
    "dataset": {
      "description": "Dataset the variables belong to (default: \"acs/acs5\"). Use census_list_datasets to discover valid values.",
      "type": "string"
    },
    "year": {
      "description": "Vintage year (default: latest available for the dataset).",
      "type": "number"
    }
  },
  "required": [
    "variables"
  ],
  "additionalProperties": false
}
view source ↗

census_list_predicate_values

List the codes a Census filter dimension accepts, so a predicates map can be written without guessing. Answers the question left open when census_query_data or census_compare_geographies reports that a dimension was left unset. Which route a dimension takes depends on the vintage: NAICS and POPGROUP always publish a value list in the dataset dictionary, and on the current vintages EMPSZES, LFO, RCPSZES, TAXSTAT, and TYPOP publish none and are enumerated here against the live data endpoint instead. A dictionary value list is a classification shared across Census products rather than a list of what one dataset serves, and roughly half of its codes typically return no rows anywhere — those are checked against the dataset's own published rows and dropped, and the response source field says whether that check ran. The dictionary lists run to thousands of codes and are best narrowed with query. Pass the returned code as the dimension's value in predicates.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_list_predicate_values",
    "arguments": {
      "predicate": "<predicate>",
      "dataset": "<dataset>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "predicate": {
      "type": "string",
      "description": "Filter dimension code to enumerate (e.g., \"EMPSZES\", \"LFO\", \"POPGROUP\", \"NAICS2017\"). Case-sensitive. The response notice of census_query_data names the dimensions a dataset declares, and census_search_variables finds them by keyword."
    },
    "dataset": {
      "type": "string",
      "description": "Dataset the dimension belongs to (e.g., \"cbp\", \"nonemp\", \"ecnbasic\", \"dec/ddhca\", \"pep/charv\"). Use census_list_datasets to discover valid values. Dimension codes are vintage-specific, so the dataset and year must match the query the values are for."
    },
    "year": {
      "description": "Vintage year (default: latest available for the dataset).",
      "type": "number"
    },
    "query": {
      "description": "Keyword to narrow the list, matched case-insensitively against each code and label (e.g., \"software\" against NAICS2017, \"exempt\" against TAXSTAT). Omit to list from the start. NAICS and POPGROUP run to thousands of codes, so a keyword is the practical way to use them.",
      "type": "string"
    },
    "within_naics": {
      "description": "Industry code to scope the enumeration by, for dimensions the Census publishes per industry. On ecnbasic, TAXSTAT and TYPOP return only the all-establishments row until a NAICS sector is named — pass a sector code such as \"62\" (Health Care) or \"42\" (Wholesale Trade) and the result is complete for that industry alone. Ignored for dimensions with a published value list. Get sector codes by calling this tool on the dataset's own NAICS dimension. Blank is treated as omitted.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^\\d{2,8}(-\\d{2})?$",
          "description": "A NAICS code: 2 to 8 digits, or a hyphenated sector range such as \"31-33\"."
        }
      ]
    },
    "limit": {
      "description": "Maximum codes to return (default: 50, max: 500).",
      "type": "number"
    }
  },
  "required": [
    "predicate",
    "dataset"
  ],
  "additionalProperties": false
}
view source ↗

census_resolve_geography

Resolve a place name or street address to Census FIPS identifiers. Converts names like "King County, WA", "Seattle, WA", or "Seattle-Tacoma-Bellevue, WA" to the codes required by census_query_data and census_compare_geographies. Use before querying when you have a place name rather than raw FIPS codes — state_fips maps to parent_fips and fips_summary maps to geography_fips in downstream tools, and geography_type is itself the geography_level to query at.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_resolve_geography",
    "arguments": {
      "name": "<name>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Place name (e.g., \"King County, WA\", \"Seattle, WA\", \"California\") or street address (e.g., \"1600 Pennsylvania Ave NW, Washington, DC 20500\"). Include the state abbreviation to disambiguate places with common names — it narrows a statistical area as well, matching any state the area spans, so \"Kansas City, MO\" and \"Kansas City, KS\" both reach the MO-KS metro area. For a statistical area, the name is the full hyphenated one the Census publishes (\"Seattle-Tacoma-Bellevue, WA\" for the metro area, \"Seattle-Tacoma, WA\" for the combined one) — a single city name matches it too when only one area contains that city."
    },
    "geography_type": {
      "description": "Geography level to resolve to, named exactly as census_query_data's geography_level and census_list_geographies name it. Auto-detection covers only state, county, place, and tract: state for a two-letter abbreviation or a spelled-out state name, county when the name contains \"County\"/\"Borough\"/\"Parish\", tract when it contains \"Tract\", otherwise place with a fallback to county. The other three are never auto-detected and must be set explicitly, because their names overlap city names — \"metropolitan statistical area/micropolitan statistical area\" covers both metro and micro areas and yields a 5-digit code, \"combined statistical area\" yields a 3-digit code, and \"consolidated city\" covers the eight merged city-county governments (Nashville-Davidson, Louisville/Jefferson County, Indianapolis, Athens-Clarke County, Augusta-Richmond County, Butte-Silver Bow, Milford CT, Greeley County KS). Setting it explicitly also overrides auto-detection — \"New York\" auto-detects as the state, so New York City needs \"place\".",
      "type": "string",
      "enum": [
        "state",
        "county",
        "place",
        "tract",
        "metropolitan statistical area/micropolitan statistical area",
        "combined statistical area",
        "consolidated city"
      ]
    },
    "county_fips": {
      "description": "County FIPS code to resolve within — 1 to 3 digits, zero-padded here to the 3 the Census stores. A tract name is unique only inside its county, so a bare tract name matching two counties comes back as ambiguous_name until this is set: take the countyFips of the candidate you want from that error and re-call. Only county and tract sit within a county, so this restricts resolution to those two levels — pairing it with any other geography_type, or with a street address, is a county_scope_unsupported error rather than a scope quietly dropped. census_query_data takes the same code as its own county_fips but pads nothing, so hand it the 3-digit county_fips returned here, not the shorter value.",
      "type": "string",
      "pattern": "^\\d{1,3}$"
    }
  },
  "required": [
    "name"
  ],
  "additionalProperties": false
}
view source ↗

census_query_data

Query a Census dataset for one or more variables at a specific geography. Accepts FIPS codes for the target geography — use census_resolve_geography to convert place names to FIPS when needed. On ACS datasets, labeled estimates and margin-of-error values are returned together. Suppression codes (geography too small, data not collected) are decoded into human-readable reasons rather than passed through as raw negative numbers. Pass geography_fips as "*" to return all geographies at the level within the parent. On the business datasets (cbp, ecnbasic, nonemp), pep/charv, and dec/ddhca, use predicates to filter by industry, size class, or population group — a query that omits one is answered with a default the Census API picks, which is an all-categories total on some dimensions and a single category on others. Each row names the defaults that were applied in applied_filters, and census_list_predicate_values enumerates the codes a dimension accepts. One geography can also come back on more than one row: pep/charv publishes an April estimates base alongside its July estimate, and each row carries a record field saying which it is.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_query_data",
    "arguments": {
      "variables": "<variables>",
      "geography_level": "<geography_level>",
      "geography_fips": "<geography_fips>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "variables": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Variable codes to retrieve (e.g., [\"B19013_001E\", \"B19013_001M\"]). Max 50 per request. Use census_search_variables to find codes. On ACS datasets only, each estimate has a margin-of-error counterpart at the same code with the E suffix swapped for M — request both to get the margin alongside the estimate. Other dataset families (pep, dec, cbp, ecnbasic, nonemp) publish no margins of error, and an E-final code there is an ordinary code with no M sibling. A code can also name a text column rather than a measure — GEO_ID, on every dataset, is the nationally unique geography identifier and comes back under value with estimate null, which is the code to request when a stable join key is what is wanted."
    },
    "geography_level": {
      "type": "string",
      "description": "Level of the target geography (e.g., \"county\", \"tract\", \"state\", \"zip code tabulation area\"). Use census_list_geographies to see valid values for the dataset."
    },
    "geography_fips": {
      "type": "string",
      "description": "FIPS code for the target geography (e.g., \"033\" for a county, \"*\" for all geographies at the level within the parent). Use census_resolve_geography to obtain this value — it is returned as fips_summary. The Census API matches this literally and its width follows geography_level, so it is passed through unpadded: a county is 3 digits (\"051\", not \"51\") and a tract is 6. parent_fips and county_fips are zero-padded for you; this one is not."
    },
    "parent_fips": {
      "description": "State FIPS code when querying sub-state levels (e.g., \"53\" for Washington). Required for county, tract, and block-group queries. census_resolve_geography returns this as state_fips. Pass \"*\" to span every state. Blank is treated as omitted.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^(\\*|\\d{1,2})$",
          "description": "1 to 2 digits, zero-padded here to the 2 the Census stores, or \"*\"."
        }
      ]
    },
    "county_fips": {
      "description": "County FIPS code when querying tracts or block groups within a specific county (e.g., \"033\" for King County within WA). Required for tract and block-group queries scoped to a county — use alongside parent_fips (state). census_resolve_geography returns this as county_fips. Pass \"*\" to span every county in the state, which is the only way a block-group query reaches a whole state. Blank is treated as omitted.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^(\\*|\\d{1,3})$",
          "description": "1 to 3 digits, zero-padded here to the 3 the Census stores, or \"*\"."
        }
      ]
    },
    "predicates": {
      "description": "Filter values keyed by variable code, sent as extra query parameters — e.g. {\"NAICS2017\": \"5112\"} to count only software publishers in cbp. The business datasets (cbp, ecnbasic, nonemp), pep/charv, and dec/ddhca declare filter dimensions such as industry (NAICS2017/NAICS2022), legal form (LFO), size class (EMPSZES/RCPSZES), tax status (TAXSTAT), operation type (TYPOP), sex (SEX), age (AGE), and population group (POPGROUP). Leaving one unset is not an error: the Census API substitutes its own default, which is the all-categories total on cbp NAICS2017 but a single population group on dec/ddhca POPGROUP and a single sector on ecnbasic NAICS2022 — so an unfiltered value can read like a total without being one. Every unset dimension is named in the response notice and its applied default is echoed per row in applied_filters. Code names vary by dataset and vintage — cbp 2023 uses NAICS2017 while nonemp 2023 uses NAICS2022 — so read them from the notice or from census_search_variables. Call census_list_predicate_values for the codes a dimension accepts; NAICS values are standard North American Industry Classification System codes at any depth (51 information, 5112 software publishers).",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "string"
      }
    },
    "dataset": {
      "description": "Dataset to query (default: \"acs/acs5\"). Use census_list_datasets to discover valid values.",
      "type": "string"
    },
    "year": {
      "description": "Vintage year (default: latest available for the dataset).",
      "type": "number"
    }
  },
  "required": [
    "variables",
    "geography_level",
    "geography_fips"
  ],
  "additionalProperties": false
}
view source ↗

census_compare_geographies

Compare one or more variables across multiple geographies at the same level — all counties in a state, all states nationally, or a named set of specific geographies. Results are sorted and ranked. Covers queries like "rank states by poverty rate", "compare median income across WA counties", or "which census tracts in King County have the highest renter rate." Omit within to compare all geographies nationally at the level. Suppressed values are decoded to human-readable labels rather than passed through as raw negative sentinels. On the business datasets (cbp, ecnbasic, nonemp), pep/charv, and dec/ddhca, use predicates to rank within one industry, size class, or population group — a comparison that omits one ranks on a default the Census API picks, which is an all-categories total on some dimensions and a single category on others. Each row names the defaults that were applied in applied_filters, and census_list_predicate_values enumerates the codes a dimension accepts. A dataset that publishes several records per geography cannot be ranked until one is pinned: pep/charv publishes an April estimates base and a July estimate, so a comparison that pins neither fails with ambiguous_rows rather than giving every geography two ranks — pass predicates {"MONTH": "7"} for the July estimate.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "census_compare_geographies",
    "arguments": {
      "variables": "<variables>",
      "geography_level": "<geography_level>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "variables": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Variable codes to compare (e.g., [\"B17001_002E\", \"B17001_001E\"]). On ACS datasets, add the margin-of-error counterpart of a code (same code, E suffix swapped for M) for reliability context. Other dataset families (pep, dec, cbp, ecnbasic, nonemp) publish no margins of error."
    },
    "geography_level": {
      "type": "string",
      "description": "The level to compare across (e.g., \"state\", \"county\", \"tract\"). Use census_list_geographies to see valid values for the dataset."
    },
    "within": {
      "description": "State FIPS to constrain results (e.g., \"53\" to compare counties or tracts within WA only). Omit to compare all geographies at the level nationally. Use census_resolve_geography to get state_fips. Pass \"*\" to span every state. Blank is treated as omitted.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^(\\*|\\d{1,2})$",
          "description": "1 to 2 digits, zero-padded here to the 2 the Census stores, or \"*\"."
        }
      ]
    },
    "within_county": {
      "description": "County FIPS to constrain tract or block-group comparisons to a single county within the state specified by within (e.g., \"033\" for King County). Required when geography_level is \"tract\" or \"block group\" and you want county-scoped results. census_resolve_geography returns this as county_fips. Pass \"*\" to span every county in the state, which is the only way a block-group comparison reaches a whole state. Blank is treated as omitted.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^(\\*|\\d{1,3})$",
          "description": "1 to 3 digits, zero-padded here to the 3 the Census stores, or \"*\"."
        }
      ]
    },
    "geographies": {
      "description": "Optional list of specific geographies to include; only these are returned. Prefer full GEOIDs — the level concatenated with its parents, e.g. \"53033\" for King County WA and \"06037\" for Los Angeles County CA — which are nationally unique and so work across states. Bare level codes (\"033\") are also accepted but match that code in every state unless within scopes them to one. A GEOID is easiest taken from the geography_geoid field of a census_query_data or census_compare_geographies row; from census_resolve_geography, concatenate state_fips, then county_fips when it is present, then fips_summary. Entries that match nothing, and bare codes that match more than one state, are named in the response notice.",
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "predicates": {
      "description": "Filter values keyed by variable code, applied to every geography in the comparison — e.g. {\"NAICS2017\": \"5112\"} to rank counties by their software-publisher establishment count in cbp. The business datasets (cbp, ecnbasic, nonemp), pep/charv, and dec/ddhca declare filter dimensions such as industry (NAICS2017/NAICS2022), legal form (LFO), size class (EMPSZES/RCPSZES), tax status (TAXSTAT), operation type (TYPOP), sex (SEX), age (AGE), and population group (POPGROUP). Leaving one unset is not an error: the Census API substitutes its own default, which is the all-categories total on cbp NAICS2017 but a single population group on dec/ddhca POPGROUP and a single sector on ecnbasic NAICS2022 — so a ranking can read like an overall one without being it. Every unset dimension is named in the response notice and its applied default is echoed per row in applied_filters. Code names vary by dataset and vintage — cbp 2023 uses NAICS2017 while nonemp 2023 uses NAICS2022 — so read them from the notice or from census_search_variables. Call census_list_predicate_values for the codes a dimension accepts; NAICS values are standard North American Industry Classification System codes at any depth (51 information, 5112 software publishers).",
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "string"
      }
    },
    "dataset": {
      "description": "Dataset to query (default: \"acs/acs5\"). Use census_list_datasets for valid values.",
      "type": "string"
    },
    "year": {
      "description": "Vintage year (default: latest available for the dataset).",
      "type": "number"
    },
    "sort_by": {
      "description": "Variable code to sort by (default: first variable in the list). Must be one of the requested variable codes.",
      "type": "string"
    },
    "sort_dir": {
      "description": "Sort direction (default: \"desc\" — highest value first).",
      "type": "string",
      "enum": [
        "asc",
        "desc"
      ]
    },
    "limit": {
      "description": "Maximum geographies to return (default: 50, max: 500). When results are truncated, total_count indicates how many matched.",
      "type": "number"
    }
  },
  "required": [
    "variables",
    "geography_level"
  ],
  "additionalProperties": false
}
view source ↗