Turning Census Boundaries into a 3D Interactive Map with Zero Manual Tooling
May 2026 · 7 min read
What if you could point an AI agent at a ZIP code and get back a fully interactive 3D map of any Census variable — complete with animated fly-to tours, color-ramped extrude columns, and satellite basemaps — in under two minutes, with no GIS software installed?
That's exactly what we built this week, and it took four natural-language prompts.
▲ Live Demo: Interactive 3D map of ZIP 80921 — drag to rotate, scroll to zoom, click blocks for details.
↗ Open Interactive Map Full ScreenThree MCP servers working together through a Hermes agent:
| Component | Role | MCP Server |
|---|---|---|
| TigerWeb | Geography — census block boundaries as GeoJSON | Custom FastMCP wrapper around the Census Bureau's GeoServices REST API |
| Census Data API | Demographics — population counts by age, sex, race | Custom FastMCP wrapper with variable search, group expansion, and query execution |
| MapLibre GL JS | Visualization — 3D extruded choropleth in the browser | Generated inline by the agent (no server — pure client-side) |
The agent orchestrates all three: it discovers variables, queries geometry, runs the census query, joins data to shapes, and writes a self-contained HTML file. No middleware. No manual shapefile downloads.
"Census 2020 SF1 children under 16 by census block in ZIP code 80921 displayed map with color range"
That's it. Here's what happened under the hood:
The agent searched the 2020 Decennial DHC dataset (the 2020 successor to SF1) for age-related variables. It found table P14 — "Sex by Age for the Population Under 20 Years" — containing single-year age bins from 0 to 19. Children under 16? That's P14_001N (total under 20) minus the eight variables for ages 16–19.
No manual lookups. No variable code sheets.
ZIP codes aren't Census geographies. The agent used TigerWeb's PUMA_TAD_TAZ_UGA_ZCTA service to fetch the ZCTA 80921 polygon, then queried all 2020 census blocks in El Paso County, Colorado, and ran a point-in-polygon spatial filter on block centroids to identify exactly which 292 blocks fall inside the ZIP code.
That's the kind of spatial logic you'd normally write in PostGIS or geopandas. The agent did it with Python stdlib in 15 lines.
With the GEOID list in hand, the agent ran the Census API query, pulling 11,930 rows for El Paso County, filtering to 292, and computing under_16 on the fly.
Result: 4,658 children under 16 across 292 blocks.
The agent merged census data into the block GeoJSON properties, then wrote a MapLibre GL JS HTML file with:
All self-contained in a single 1.1 MB HTML file — uploadable to any blob storage.
Normally this workflow requires downloading TIGER/Line shapefiles from the Census FTP, importing into QGIS or PostGIS, writing SQL joins or geopandas scripts, and running a local web server with GeoJSON endpoints.
Here, TigerWeb serves as a live GeoJSON API for every census geographic layer — states, counties, tracts, block groups, blocks, ZCTAs, PUMA, CBSA, tribal areas, and more. The Census Data API serves the demographic tables. The agent is the glue.
The point-in-polygon filter (centroid lat/lon against ZCTA polygon vertices) ran in pure Python with a classic ray-casting algorithm. No PostGIS, no geopandas, no Docker container. Just math.
This is the kind of thing MCP enables: domain-specific servers that expose exactly the primitives an LLM needs, in the shape it expects.
Extruding census blocks by population turns an abstract choropleth into something you can read intuitively. The blocks with 447 children rise dramatically above their neighbors. The 78 blocks with zero children are dark, flat specks. You can fly around, tilt, and zoom — it feels like looking at a city skyline where every building is a neighborhood's child population.
| Tool | Description |
|---|---|
list_services | 36 map services (States, Tracts, ZCTAs, CBSA, etc.) |
get_service_info | Layer metadata |
query_boundaries | Feature query with WHERE clause + geometry |
identify_location | Point-in-layer lookup |
find_boundary | Text search by name |
export_map | Rendered PNG export with bbox |
Key design choice: TigerWeb's REST API supports WHERE clauses, outFields, resultRecordCount, and returnGeometry=true/false. The MCP server exposes these directly rather than abstracting them — giving the agent full control over query shape and payload size.
| Tool | Description |
|---|---|
search_variables | Keyword search across ~30K variables |
get_group_variables | Expand a table (e.g., P14) into all columns |
get_variable_details | Full metadata for a single variable |
run_query | Full Census API query with geography clauses |
list_datasets | Dataset discovery (ACS, Decennial, etc.) |
The variable search is the killer feature — 9,068 matches for "age under 16" across the DHC dataset, scored by relevance. Without it, you'd be scrolling through PDF data dictionaries.
| Census blocks | 292 |
| Tracts spanned | 10 |
| Children under 16 | 4,658 |
| Mean per block | 16.0 |
| Median per block | 10 |
| 90th percentile | 37 |
| Highest block | 447 |
| Time to build | ~2 minutes (4 prompts) |
The final product is a single HTML file you can open locally, upload to S3/R2/Azure Blob, drop on GitHub Pages, or share as an attachment. No build step. No API keys in the client. No server dependency.
The Census Bureau publishes some of the richest demographic data in the world, but accessing it has historically required specialized tools and domain knowledge — understanding table schemas, geographic hierarchies, FIPS codes, and the difference between a ZIP code and a ZCTA.
MCP servers compress that learning curve to zero. You don't need to know that ZIP 80921 is in El Paso County (08041), or that children-under-16 is P14 minus ages 16–19, or that TigerWeb's blocks layer is layer_id=2 in the Tracts_Blocks service. The agent discovers all of that on its own.
What took half a day with QGIS + ACS downloads + Python joins now takes two minutes with natural language.
All three MCP servers are available as open-source FastMCP projects. Wire them into your Hermes agent config and you can go from "show me" to "interactive 3D map" with a single prompt.
# ~/.hermes/config.yaml
mcp_servers:
census_data:
command: python
args: ["/path/to/census-mcp/server.py"]
env:
CENSUS_API_KEY: ${CENSUS_API_KEY}
tigerweb:
command: python
args: ["/path/to/tigerweb-mcp/server.py"]
Then just ask:
"Show me median household income by census tract in Chicago as a 3D map"
"Map population density by block group in Phoenix with a color ramp"
"Export all block-level housing units in ZIP 90210 as a standalone HTML file"
The agent handles the rest.
Built with Hermes Agent, FastMCP, TigerWeb GeoServices REST API, Census Data API, and MapLibre GL JS.