A five-step tutorial. Real data, real maps, no Python notebook.
You're sitting on a list of 2,000 donor addresses. Your boss wants to know: which congressional districts do they live in, what's the median income of those districts, and what's the closest field office to each donor's home? You're an analyst, not a GIS engineer.
The old answer: download a TIGER shapefile, install GDAL, write a Python notebook with geopandas, manually pull a Census table, geocode the addresses with a separate service, do the spatial join, render the map with matplotlib, hand it off as a PNG. Half a day of glue code.
The new answer: ask Claude.
This walkthrough uses Maps MCP — a single platform that gives you versioned U.S. geography, Census data joined to it, and 80+ tools for spatial analysis under one API key. (It's also a Model Context Protocol server, so Claude Desktop and Cursor users can drive it from chat — but the workflow below is the same whether you call it from code or from an agent.)
# One-time setup
brew install jq
export MAPSMCP_KEY=mp_live_... # from mapsmcp.com signup
You can use Claude Desktop, Cursor, Claude Code, or the Anthropic SDK — anywhere MCP works. The examples below show the raw JSON-RPC for clarity; in practice you just ask the agent in plain English and it picks the right tool.
The first task is the unsexy one: turning street addresses into congressional districts. bulk_match_addresses does it in one call — batched geocoding via the U.S. Census geocoder (free, no key), then point-in-polygon against the 119th Congress shapes.
{
"method": "tools/call",
"params": {
"name": "bulk_match_addresses",
"arguments": {
"records": [
{ "id": "donor-1", "street": "1600 Pennsylvania Ave", "city": "Washington", "state": "DC" },
{ "id": "donor-2", "street": "350 Fifth Ave", "city": "New York", "state": "NY" }
// ... 1,998 more
],
"collection": "us-cd119",
"version": "tiger-2024"
}
}
}
Output: one row per donor with the matched congressional district, the geocoder's normalized address, and the lat/lng. Your original record id is echoed back so you can join it to your CRM.
Store the geocoded donors as a workspace dataset so we can run analytics against the joined shape data:
{
"name": "ingest_dataset",
"arguments": {
"slug": "campaign-donors-2024",
"records": [/* the bulk_match_addresses output */],
"shape_collection": "us-cd119",
"shape_version": "tiger-2024"
}
}
Now donors-2024 lives in your workspace, joined to the 441 districts. Every donor knows which CD they're in; every CD knows how many donors it has.
Want to know median household income by CD? census_acs wraps the Census ACS API and joins to the same shapes:
{
"name": "census_acs",
"arguments": {
"collection": "us-cd119",
"version": "tiger-2024",
"year": 2023,
"variables": ["B19013_001E", "B01001_001E", "B03002_003E"],
"slug": "cd119-acs-2023"
}
}
That's median household income (B19013_001E), total population (B01001_001E), and non-Hispanic white count (B03002_003E) for every CD in the 119th Congress, ingested as a workspace dataset. Free upstream, no API key required.
Two operations. First, a choropleth showing donor density by CD:
{
"name": "create_report",
"arguments": {
"slug": "donor-density-2024",
"name": "Campaign donor density by congressional district",
"public": true,
"config": {
"version": "tiger-2024",
"collection": "us-cd119",
"datasets": [{
"slug": "campaign-donors-2024",
"field": "donor_count",
"agg": "sum",
"palette": "sequential",
"classify": { "method": "quantile", "bins": 7 }
}]
}
}
}
You get a public URL of the form /v/<your-slug>. Pan, zoom, hover — vector tiles via PostGIS ST_AsMVT. Embeddable as an iframe; fetch as PNG with /v/<your-slug>.png?width=1200 for slide decks.
Want a 20-minute-drive catchment around your San Francisco field office? That's geo_isochrone:
{
"name": "geo_isochrone",
"arguments": {
"point": [-122.42, 37.77],
"minutes": [20],
"profile": "driving",
"provider": "mapbox",
"api_key_secret": "mapbox"
}
}
Then intersect that polygon with your donor dataset (geo_intersect_dataset) to get the list of donors actually inside the catchment.
The interesting one. describe_report runs the report through Claude Opus 4.7 and returns a plain-English narrative — for captions, briefs, or "send me a paragraph summary" requests:
{
"name": "describe_report",
"arguments": { "report": "" }
}
The response is a short prose summary that names the highest- and lowest-value regions on the rendered map, with the underlying numbers cited inline. Useful for press-release first drafts, brief memos, or "give me a paragraph" requests inside a chat agent. The output is grounded in the actual published report — the same data the iframe shows.
That's it. Five operations, no geopandas, no matplotlib, no notebook. Each one is the kind of thing analysts have been doing for decades — Maps MCP pays the integration tax once so you can chain them as a single workflow.