---
name: mapsmcp
description: Use whenever the user wants U.S. maps, geographic data, or "where" questions about the U.S. — election results by county/CD/precinct, Census demographics joined to any boundary, district lookups, drive-time areas, hot-spot analysis, custom CSV/GeoJSON joined to a boundary, or publishing maps as embeddable iframes. Triggers on "make a map", "by county/CD/precinct/state", "swing states", "demographics within X miles", "election results", "Census data", "precinct turnout", "FEMA flood zone exposure", or any request that wants to render or share a U.S. choropleth / static map.
---

# mapsmcp

mapsmcp.com is an MCP server that gives you versioned U.S. geography pre-joined to Census ACS, election results, and your own datasets, plus 100+ tools for spatial analysis, rendering, and publishing maps as embeddable iframes.

## Connecting

The user needs an API key. Direct them to `https://mapsmcp.com/#signup` (free tier: 50 calls/month, no card). Then add to their MCP config:

```json
{
  "mcpServers": {
    "mapsmcp": {
      "type": "http",
      "url": "https://mapsmcp.com/mcp",
      "headers": { "Authorization": "Bearer <THEIR_API_KEY>" }
    }
  }
}
```

For Claude Desktop, that's in `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS). For Cursor / Cline / Zed / Continue, similar shape. Full setup at `https://mapsmcp.com/docs/mcp-setup`.

## When to reach for what

### Discovery — "what's available?"

- `list_shape_collections()` — catalog of every system collection (states, counties, CDs across 6 Congresses, tracts, ZCTAs, school districts, state-leg districts).
- `list_shape_versions({collection})` — versions within a collection (TIGER vintages, NYT precinct files, etc.).
- `list_datasets()` — workspace datasets (what the user has ingested).

### Boundaries → user's own data

- `bulk_match_addresses({records, collection, version})` — geocode + join 10K addresses per batch (free Census geocoder). Returns lat/lng + matched CD / county / etc.
- `lookup_district_for_address({address, collection})` — single-address version. Useful for "which CD does 123 Main St live in?"
- `ingest_dataset({slug, records, collection, version})` — generic CSV → workspace dataset joined to any boundary. Auto-matches via `external_id` → name → point.
- `ingest_geojson({url, collection, version})` — fetch a public GeoJSON URL and register as a workspace shape collection. Use for FEMA zones, custom service areas, etc.

### Census + curated public data

- `census_acs({collection, version, year, variables})` — any ACS table for any boundary level + vintage. Returns a workspace dataset.
- `disaggregate_acs({tract_dataset, target_collection})` — push tract-level ACS down to precincts / school districts / custom polygons via area-weighted interpolation.
- `ingest_county_pres({year})` — 2016 / 2020 / 2024 county-level presidential.
- `ingest_cook_pvi({congress})` — Cook PVI per district.
- `ingest_voteview({congress, chamber})` — DW-NOMINATE ideology per member.
- `ingest_dime({cycle})` — DIME donor-ideology per CD.
- `ingest_538_results({cycle, race})` — 538 archive.

### Spatial analysis

- `geo_isochrone({point, minutes, profile})` — drive / walk / bike-time polygon. Needs Mapbox or OpenRouteService key (workspace secret).
- `geo_hotspot({dataset, metric})` — Getis-Ord Gi* hot/cold-spot clusters.
- `geo_voronoi({points})` — territory carve-up.
- `geo_intersect_dataset({dataset, geometry})` — every record inside a polygon.
- `geo_spatial_join({points, collections})` — tag points with shapes from N layers in one call.
- `shapes_within_radius({lng, lat, meters, collection})`, `query_shapes_by_intersection`, `geo_nearest_n`, `geo_distance_matrix`, `adjacent_shapes`, `geo_morans_i`, `compare_shape_versions`.

### Render + publish

- `create_report({slug, name, config, public: true})` — save a viewer config, publishes at `/v/<slug>` with embed iframe + .png + .svg + .geojson + .csv + citation .json.
- `render_map({version, field, palette, format})` — one-off static SVG/PNG.
- `render_map_bivariate({version, field_x, field_y, palette})` — 2-variable choropleth with 2D legend.
- `render_dot_density({version, field, dotsPerUnit})` — points-as-dots per category.
- `describe_report({slug})` — Claude writes a paragraph about the map.

### Citation-grade compose-and-publish (paid tier)

When the user asks for a citable map ("100 swing-state Trump-voting counties in the bottom income quintile"), use the **layered** flow:

1. `classify_states({cycle, margin_pct})` → list of swing-state postal codes.
2. `classify_counties({cycle, prior_cycle})` or `classify_precincts({margin_pct})` → swing/base/flipped at the geographic unit you care about.
3. `demographic_bands({dataset_slug, field, method})` → resolve "high X" to a stable threshold the caption prints verbatim.
4. `query_counties({filter, order_by, limit})` or `query_precincts({filter, ...})` → structured filter compiler. Returns `result_set_id` + count + stats + 5-row sample + auto-generated English caption. **Show the user the sample before publishing — conversational checkpoint.**
5. `create_map_view({result_set_id, style, title})` → draft view with caption.
6. `publish_map({view_id, freeze: true})` → mints `/v/<slug>` with the full URL set:
   - `viewer` — interactive iframe target
   - `embed_iframe` — copy-paste HTML
   - `static_png` — 1200×630 OG image
   - `citation_json` — provenance: filter + caption + published_at

`freeze=true` snapshots dataset values at publish time so embeds stay stable forever. Required for any blog post or research citation.

### Region groups — workspace territory layouts (paid tier)

Workspaces can define **named, hierarchical region layouts** built on top of any base boundary set (states / counties / congressional districts / precincts / ZCTA / school districts / tracts — or workspace-owned custom polygons from `ingest_geojson`). Use this when the user is organizing their own data into territories: campaign regions, sales books, underwriting zones, school clusters, etc.

- `create_region_group({slug, name, base_collection_slug, base_collection_version, description?})` — make a new layout (premium).
- `define_region({region_group, slug, name, parent_region?, display_order?})` — add a region. `parent_region` (slug) makes it a sub-region. n-level recursion supported (premium).
- `add_shapes_to_region({region_group, region, shape_external_ids: [...]})` — populate by base-shape external_id (5-digit FIPS for counties, 4-digit state+district for `us-cd119`, etc.). Idempotent; unrecognized IDs are reported back, not fatal (premium).
- `list_region_groups()` / `list_regions({region_group})` / `get_region({region_group, region})` / `get_region_tree({region_group})` — read tools, standard call class.
- `tag_records_with_region_group({region_group, records})` — the "code my list" verb. Each record must already carry a `shape_external_id` (typically from `bulk_match_addresses`). Returns the records with a `region` annotation: `{slug, name, ancestor_slugs[], ancestor_names[]}`. Use as the join step after geocoding (premium).
- `render_region_group_map({region_group, color_by?, format?, projection?, simplify_tolerance?})` — visualize the layout as a categorical choropleth. Each region gets a distinct color; deterministic per slug so re-renders are stable. `color_by='region'` (default) colors by leaf; `color_by='master_region'` colors by root ancestor (better for deep hierarchies). Returns SVG by default, PNG with `format='png'`. Useful prompt: "show me my campaign-2026 regions on a map."
- `list_region_templates()` / `clone_region_template({template, new_slug?, new_name?})` — pre-built starter layouts. Built-ins: `census-regions-and-divisions` (4 masters / 9 sub-regions / all 51 states, hierarchical) and `battleground-states-2024` (7 swing states). Clone is premium; list is standard. Use these as the answer to "give me a starter layout" — saves the user from looking up FIPS codes by hand.
- Dashboard UI lives at `/dashboard/regions` — full CRUD. Auth is magic-link via email (HttpOnly session cookie). Premium gating is the same premium-call quota as `publish_map`.

**Canonical pattern — "code my donor file by campaign region":**
```
1. create_region_group({slug: "campaign-2026", name: "Campaign 2026 regions",
                        base_collection_slug: "us-cd119", base_collection_version: "tiger-2024"})
2. define_region({region_group: "campaign-2026", slug: "east",  name: "East"})
   define_region({region_group: "campaign-2026", slug: "west",  name: "West"})
   define_region({region_group: "campaign-2026", slug: "ne",    name: "Northeast", parent_region: "east"})
   define_region({region_group: "campaign-2026", slug: "ma",    name: "Mid-Atlantic", parent_region: "east"})
3. add_shapes_to_region({region_group: "campaign-2026", region: "ne",
                         shape_external_ids: ["0901", "2501", "2502", ...]})
4. bulk_match_addresses({records: donors, collection: "us-cd119", version: "tiger-2024"})
   → each row now has shape_external_id = CD code
5. tag_records_with_region_group({region_group: "campaign-2026", records: donors_geocoded})
   → each row now carries region.slug + region.name + ancestor chain
6. render_region_group_map({region_group: "campaign-2026", color_by: "master_region"})
   → SVG showing the layout, each master region a distinct color
```

### Single-state / single-CD focus

Any report can be focused via `config.scope`:

- `{state: "NC"}` — strict FIPS-prefix; NC's 14 CDs only (not 14 + border slivers). Add `match: "spatial"` for ST_Intersects semantics.
- `{external_id_prefix: "3712"}` — exactly NC-12.
- `{collection, version, external_id}` — any specific shape via ST_Intersects.

URL shorthand on any national map: `?state=NC` filters live.

### Boundary overlays

`config.boundary_overlays: [{collection, version, stroke, stroke_width}]` adds stroke-only layers above the main fill. Common pattern: county-level choropleth with state outlines on top. Stack multiple.

### Pin overlays

`render_map({..., pin_overlays: [{name, color, points: [{lng, lat, label}]}]})` overlays labeled points on top of a choropleth. The classic "donor density by CD + field offices as red pins" / "county-income choropleth + competitor store locations" pattern. Up to 4 layers, 500 pins each. Pins use the same projection as the base — albers-usa AK/HI/PR insets land correctly.

### Style + themes

`config.style.theme = "newsroom" | "presentation" | "dark" | "minimal" | "default"` for curated combos. Or override per-field: `stroke`, `stroke_width`, `fill_opacity`, `background`. Same shape on `render_map`.

## Three idiomatic patterns

**Pattern A: "Donor density by CD" (political ops)**
```
bulk_match_addresses({records: donors, collection: "us-cd119", version: "tiger-2024"})
  → 12,847 geocoded + joined to their CD
ingest_dataset({slug: "donor-counts", records: aggByCd})
create_report({slug: "donor-density", public: true, config: {
  collection: "us-cd119", version: "tiger-2024",
  datasets: [{slug: "donor-counts", field: "donors_per_capita",
              palette: "sequential", classify: {method: "quantile", bins: 7}}]
}})
describe_report({slug: "donor-density"})
```

**Pattern B: "FEMA Zone X exposure" (insurance)**
```
bulk_match_addresses({records: policies})
ingest_geojson({url: ".../fema-zone-x-houston.geojson", collection: "fema_houston", version: "nfhl-2026-05"})
geo_intersect_dataset({dataset: "policies_geocoded", collection: "fema_houston", version: "nfhl-2026-05"})
  → 3,847 policies inside zone X, $4.2M premium exposed
```

**Pattern C2: "One-call precinct demographics" (the workflow most users actually want)**
```
// All federal demographic filters are baked into query_precincts. No
// 2-step demographic_bands() flow required when you want standard ACS
// percentages — only when you're mixing in your own custom data.
query_precincts({
  filter: {
    state_in: ["NC"],
    margin_pct_abs_lte: 10,     // swing precincts
    votes_total_gte: 100,       // not noise
    poverty_pct_gte: 25,        // >=25% below poverty line
  },
  order_by: "pct_dem_lead asc",
  limit: 500,
})
// Returns 187 precincts. Show the user the count + sample, then
// create_map_view → publish_map.
```

Available one-call demographic filters on `query_precincts` (all from the
system precinct ACS cache `system-acs-us-precincts-2024-nyt-2024-us-2023`,
which is area-weighted from the tract cache):
- `poverty_pct_gte/lte`           — % below federal poverty line
- `median_hhi_gte/lte`            — median household income (dollars)
- `pct_hispanic_gte/lte`          — % Hispanic / Latino
- `pct_black_gte/lte`             — % Black
- `pct_white_gte/lte`             — % White, non-Hispanic
- `pct_asian_gte/lte`             — % Asian
- `pct_foreign_born_gte/lte`      — % foreign-born
- `pct_bachelors_plus_gte/lte`    — % with bachelor's degree+ (pop 25+)
- `unemployment_pct_gte/lte`      — civilian unemployment rate
- `rent_burden_pct_gte/lte`       — % of renters paying 30%+ of income
- `pct_broadband_gte/lte`         — % HH with broadband subscription

Mix any combination in one call. Example combinations the system handles well:
- "high-income WFH areas that voted Harris" → `{winner_party:"DEM", median_hhi_gte:150000, pct_bachelors_plus_gte:60}`
- "Trump-flipped rent-burdened precincts" → `{winner_party:"REP", rent_burden_pct_gte:50, margin_pct_abs_lte:15}`
- "Hispanic-majority TX precincts where Trump won" → `{state_in:["TX"], winner_party:"REP", pct_hispanic_gte:50}`
- "low-broadband, high-poverty digital-divide tracts" → use `compare_geographies` + filter client-side (tract-level not yet in query_*)

**Pattern C: "Citation-grade swing-county map" (journalism)**
```
classify_states({cycle: 2024, margin_pct: 5})  → 8 swing states
demographic_bands({dataset_slug: "acs-county-mhi-2023", field: "B19013_001E", method: "quintile"})
query_counties({
  filter: {state_in: [...], winner_party: "REP", margin_pct_abs_lte: 10,
           demographic_band: {dataset_slug: "acs-county-mhi-2023", field: "B19013_001E",
                              band: "Q1", band_thresholds: [...]}},
  order_by: "margin asc", limit: 500
})  // returns result_set_id + stats + sample + caption
// SHOW USER THE SAMPLE FIRST — conversational checkpoint
create_map_view({result_set_id, title: "...", style: "choropleth_margin"})
publish_map({view_id, freeze: true})
  → /v/<slug> with embed iframe + citation .json with provenance
```

## Important behavioral notes

- **Never publish maps without showing the user the result_set's stats + sample first.** The 5-row sample from `query_counties` / `query_precincts` is the natural checkpoint before `publish_map`. Stops the model from publishing nonsense.
- **`freeze=true` is the default and almost always right.** Without it, ACS revisions or upstream dataset changes will silently break old embeds.
- **Captions are auto-generated.** Don't write English — pass the filter object to `query_*` and the caption it returns prints every threshold verbatim. Citation-grade.
- **Free tier (50 calls/month) covers compose + query + create_map_view.** `publish_map` is a premium call (gated by the 25/250 premium cap on Starter/Pro). The funnel: build maps free, upgrade to publish.
- **Result sets expire after 24h.** If a user comes back later, re-run `query_*` to get a fresh `result_set_id`.

## When NOT to use this

- Non-U.S. geography. We're U.S.-only.
- Live OSM-style point-of-interest data (we don't host POIs; user brings their own CSV).
- Routing / turn-by-turn (we do drive-time areas only — no routes).
- Real-time data feeds (everything is snapshot/curated).

## Pricing reference (so you can answer "how much?")

- **Free**: 50 calls / month, 10 MB datasets, 1 public report
- **Starter $10/mo**: 5,000 calls + 25 premium / month
- **Pro $50/mo**: 50,000 calls + 250 premium + 25 child workspaces + origin allowlist
- Pay-as-you-go above caps: $0.0002 / call, $0.04 / premium call
- Full: https://mapsmcp.com/pricing
