Healthcare · Published 2026-05-24

Where to open your next clinic

A regional health system runs 12 clinics across North Carolina and is planning the 13th. Which tract has the most uninsured residents and the longest drive to the nearest existing location? find_underserved_areas answers in one call; render_map with pin_overlays paints the result.

Step 1 — The existing clinics, as service points

Twelve clinic addresses, one line each in your input:

const clinics = [
  { name: "Raleigh — Main",         street: "...", city: "Raleigh",     state: "NC", zip: "27607" },
  { name: "Durham — South",         street: "...", city: "Durham",      state: "NC", zip: "27713" },
  { name: "Greensboro — Battleground", ...},
  …9 more
];

mcp.call("bulk_match_addresses", {
  records: clinics, collection: "us-counties", version: "tiger-2024",
})
  // → one row per clinic, each with lat/lng + containing county

Step 2 — Run the gap analysis

For NC tracts only, find the ones where population is above 8,000 AND the nearest existing clinic is more than 15 km away.

mcp.call("find_underserved_areas", {
  service_points: clinicsGeocoded.map((c) => ({ lng: c.lng, lat: c.lat, name: c.name })),
  demand_variable: "B27001_001E",   // population for whom insurance status is known
  demand_threshold: 8000,
  distance_threshold_m: 15000,       // 15 km ≈ 9 miles
  year: 2023,
  state_fips: "37",                  // scope to NC
  limit: 20,
})
  // → ranked list of underserved tracts (top `limit` returned):
  //   [
  //     {
  //       external_id, name,
  //       demand: ,
  //       nearest_distance_m: ,
  //       nearest_service_name: 
  //     },
  //     ...
  //   ]
  //   Plus a `methodology` string describing the filter + sort.

The methodology string returned alongside the ranked list documents exactly how the result was produced: which collection + version was queried, which Census variable was the demand metric, the threshold values used for both demand and distance, and the sort order. That string is what you copy into the report's citation block so a reviewer can re-run the same filter.

Step 3 — Render the map

Stitch the 20 candidate tracts into a dataset, render with the existing clinics as labeled pins on top.

mcp.call("ingest_dataset", {
  slug: "nc-clinic-gaps-2026",
  records: underserved.map((t) => ({ external_id: t.external_id, demand: t.demand })),
  collection: "us-tracts", version: "tiger-2024",
})

mcp.call("render_map", {
  version: "tiger-2024", collection: "us-tracts",
  dataset: "nc-clinic-gaps-2026", field: "demand",
  palette: "sequential",
  scope: { state: "NC" },
  title: "NC clinic-network gaps · 2023 ACS",
  subtitle: "Tracts with insurance-status pop > 8K and no existing clinic within 15 km",
  pin_overlays: [{
    name: "Existing clinics",
    color: "#dc2626",
    points: clinicsGeocoded.map((c) => ({ lng: c.lng, lat: c.lat, label: c.name })),
  }],
  boundary_overlays: [{ collection: "us-counties", version: "tiger-2024", stroke: "#94a3b8" }],
})

Step 4 — Publish the citation-grade version

mcp.call("publish_map", { view_id: …, freeze: true })
  → /v/nc-clinic-gaps-2026   (iframe + PNG + citation block listing the
                                threshold values verbatim)

Why this beats the consulting deliverable

Most gap-analysis deliverables ship as a one-time PDF: pull the data, render in QGIS, hand it over, the strategy team makes a decision, the file gets buried. Here the map is a live /v/<slug> URL with a citation block — anyone can re-render with different thresholds (8K → 5K, 15 km → 20 km) without re-hiring the consultant. The pin layer is part of the report config, so re-publishing with the same pins is idempotent.

Try it yourself

Free tier — 50 calls / month, no card.

Get an API key

Tools used: bulk_match_addresses, find_underserved_areas, ingest_dataset, render_map (with pin_overlays + boundary_overlays), publish_map. find_underserved_areas is premium-gated.