Docs · Embedding

Embedding mapsmcp maps in your app

Every public report at /v/<slug> has a matching /embed/<slug> that strips the chrome and CORS-allows everything. Drop it in an iframe, or pull the underlying tiles and config straight into your own MapLibre instance. Same data either way.

1. Drop-in iframe

Any public report is embeddable as-is. No API key. CORS is open.

<iframe
  src="https://mapsmcp.com/embed/your-report-slug"
  width="100%"
  height="640"
  style="border:0; border-radius:8px"
  loading="lazy"
  allow="fullscreen"
></iframe>

That's it — works on any domain, no allowlist required.

2. Customizing with URL params

Append query params to /embed/<slug> to change theme, hide chrome, focus on a single shape, or auto-size the iframe.

<!-- Same map, dark theme, no legend, autosizing iframe, zoomed to a county -->
<iframe
  src="https://mapsmcp.com/embed/your-report-slug?theme=dark&hide_legend=1&autosize=1&scope=us-counties:tiger-2024:48201"
  width="100%"
  style="border:0; border-radius:8px"
></iframe>

<script>
// If you set ?autosize=1 the viewer postMessages its content height. Listen
// for it and size the iframe so the embed has no fixed height.
window.addEventListener('message', (e) => {
  if (e.data && e.data.type === 'mapsmcp:height') {
    const iframe = document.querySelector('iframe[src*="/embed/" + e.data.slug + "/"], iframe[src*="/embed/' + e.data.slug + '?"]');
    if (iframe) iframe.style.height = e.data.px + 'px';
  }
});
</script>

See the full param reference below.

3. mapsmcp.js — drop-in JS SDK

One-line script tag, then call MapsMCP.embed({ slug, container, … }). Under the hood it builds an iframe pointed at /embed/<slug>, but it also wires up the postMessage event protocol so hover and click events surface as plain JS callbacks. No iframe sizing math, no manual event listeners.

<!-- Drop the SDK in any page; works on any domain. -->
<script src="https://mapsmcp.com/mapsmcp.js"></script>
<div id="map" style="height: 480px"></div>
<script>
  const embed = MapsMCP.embed({
    slug: 'your-report-slug',
    container: '#map',
    theme: 'dark',
    // Hover/click events are emitted by the viewer regardless of options;
    // the SDK just forwards them to your callbacks.
    onHover: (e) => {
      // e = { type, slug, feature: {...properties} | null, lngLat: [lng, lat] }
      if (e.feature) statusBar.textContent = e.feature.name;
    },
    onClick: (e) => {
      // e.feature is the same shape as a vector-tile feature properties bag.
      window.location.href = '/feature/' + e.feature.GEOID;
    },
  });

  // Later, switch the active dataset without remounting the iframe:
  embed.update({ dataset: 'your-dataset-slug' });

  // Tear down (e.g. on route change in a SPA):
  embed.destroy();
</script>

Options:

OptionTypeDescription
slugstring (required)The report slug.
containerstring CSS selector or ElementWhere to insert the iframe.
theme"light" · "dark"Color theme for the embedded viewer.
scopestringFocus on a single shape, e.g. "us-cd119:tiger-2024:4812".
datasetstringOverride the default layer (slug or field).
hideLegend · hideTitle · hideTooltipbooleanStrip chrome.
autosizeboolean (default true)Resize iframe to content via postMessage. Set false if you want fixed height.
heightnumber or CSS lengthFixed iframe height (ignored when autosize is on).
onHover(e)functionCalled on hover. e.feature is the properties bag, or null on mouse-out. e.lngLat is [lng, lat].
onClick(e)functionCalled on click. Same payload shape as onHover.

Returned handle: { destroy(), update(patch), iframe, slug }.

4. React / Next.js component

The autosize protocol means you never have to guess at an iframe height — the viewer posts { type: 'mapsmcp:height', px } on init and on any resize.

// React/Next.js component — drop into any page.
export function MapsMCPEmbed({ slug, scope, theme = 'light', hideLegend = false }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onMessage = (e) => {
      if (e.data && e.data.type === 'mapsmcp:height' && e.data.slug === slug) {
        if (ref.current) ref.current.style.height = e.data.px + 'px';
      }
    };
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, [slug]);
  const params = new URLSearchParams({ autosize: '1', theme });
  if (scope) params.set('scope', scope);
  if (hideLegend) params.set('hide_legend', '1');
  return (
    <iframe
      ref={ref}
      src={`https://mapsmcp.com/embed/${slug}?${params.toString()}`}
      style={{ width: '100%', border: 0, borderRadius: 8 }}
      loading="lazy"
    />
  );
}

// Usage
<MapsMCPEmbed slug="your-report-slug" theme="dark" hideLegend />

4. Full control with MapLibre GL JS

When iframes aren't enough — you need custom hover handlers, layered overlays, your own tooltip — bring the tiles + config into your own MapLibre instance. Vector tiles served via PostGIS ST_AsMVT; the resolved config.json includes the report's breaks, palette, and label fields so the styling matches what you'd see in /v/<slug>.

<!-- Full control: drop our vector tiles into your own MapLibre instance. -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" />
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
<div id="map" style="width:100%;height:520px"></div>
<script>
(async () => {
  // Pull the report's resolved layer config (breaks, colors, palette).
  const cfg = await fetch('https://mapsmcp.com/v/your-report-slug/config.json').then(r => r.json());
  const layer = cfg.layers[cfg.default_layer_index];
  const tileUrl = `https://mapsmcp.com/tiles/${cfg.collection}/${cfg.version}/{z}/{x}/{y}.pbf`;

  // Build a MapLibre step expression from the resolved breaks + colors.
  const stops = ['step', ['get', layer.field], layer.colors[0]];
  if (layer.breaks) for (let i = 0; i < layer.breaks.breaks.length; i++) {
    stops.push(layer.breaks.breaks[i], layer.colors[i + 1]);
  }

  new maplibregl.Map({
    container: 'map',
    style: {
      version: 8,
      sources: {
        shapes: { type: 'vector', tiles: [tileUrl], minzoom: 0, maxzoom: 12 },
      },
      layers: [
        { id: 'bg', type: 'background', paint: { 'background-color': '#f3f4f6' } },
        {
          id: 'fill', type: 'fill', source: 'shapes', 'source-layer': 'shapes',
          paint: { 'fill-color': stops, 'fill-opacity': 0.78 },
        },
        {
          id: 'stroke', type: 'line', source: 'shapes', 'source-layer': 'shapes',
          paint: { 'line-color': '#1f2937', 'line-width': 0.3 },
        },
      ],
    },
    bounds: cfg.bbox,
  });
})();
</script>

The tile endpoint is GET /tiles/<collection>/<version>/{z}/{x}/{y}.pbf with a 24-hour immutable cache. Open to all origins.

5. Static PNG / SVG export

For places where interactive maps can't survive: email campaigns, Slack/Teams unfurls, Open Graph share previews, PDF scorecards, slide decks.

<!-- For email digests, OG-tag images, PDFs, slide decks. PNG and SVG. -->
<!-- PNG (raster) -->
<img
  src="https://mapsmcp.com/v/your-report-slug.png?width=1200&height=720&dpi=2"
  alt="Your report title"
  width="1200" height="720"
/>

<!-- SVG (vector — infinite-resolution, smaller for choropleths) -->
<img
  src="https://mapsmcp.com/v/your-report-slug.svg?width=1200&height=720"
  alt="Your report title"
/>

<!-- Open Graph tags — auto-rich-previews when sharing your app URL -->
<meta property="og:image" content="https://mapsmcp.com/v/your-report-slug.png?width=1200&height=630" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

Rendered server-side via d3-geo + @resvg/resvg-js — pure WASM, no headless browser. Cached 5 minutes at the edge so OG-tag scrapers don't hammer the renderer.

6. Embedding private reports — origin allowlist

For reports backed by sensitive data — internal whip counts, donor concentration, pre-publication scorecards — you don't want the report world-readable, but you do want to embed it in your own app. Set config.embed_origins on a public: false report to allowlist specific origins:

// MCP call — make a private report embeddable from your app's origin(s) only.
// The report is NOT visible at /v/<slug> on a direct browser visit; it only
// renders when an iframe (or fetch) on an allowed origin loads it.

create_report({
  slug: "internal-territory-map",
  name: "Internal territory map — confidential",
  public: false,            // not world-public
  config: {
    version: "tiger-2024",
    collection: "us-counties",
    datasets: [{
      slug: "territory-assignments-q3",
      field: "status",
      palette: "categorical",
      categories: { "active": "#16a34a", "at_risk": "#dc2626", "growing": "#86efac", "stable": "#fca5a5", "unassigned": "#9ca3af" }
    }],
    embed_origins: [
      "https://app.yourbrand.com",
      "https://staging.yourbrand.com"
    ]
  }
});

// Then on app.yourbrand.com:
//   <iframe src="https://mapsmcp.com/embed/internal-territory-map">
// 200 + the map.
//
// From anywhere else (different host, direct browser visit, curl with no
// Referer): HTTP 403. The slug existence isn't leaked from non-allowed
// origins because we serve the 403 with the same path regardless.

Behavior:

This is origin-based gating, not authentication — it leans on the same Origin/Referer headers browsers already use for CORS. Don't rely on it for high-security material; for that, use signed URLs (queued) or keep the report fully private and proxy through your own backend.

7. Workspace-stored API keys

Mapbox basemap tokens and isochrone provider keys can be stored once per workspace and referenced by name — no more pasting pk.eyJ… into every report config or tool call. Three MCP tools manage them:

// 1. Store the Mapbox token once.
set_workspace_secret({ name: "mapbox", value: "pk.eyJ1Ijoi..." });

// 2. Reference it from any report (instead of pasting the raw token).
create_report({
  slug: "median-income-by-county",
  name: "Median household income — U.S. counties (ACS 2023)",
  public: true,
  config: {
    tiles: true,
    version: "tiger-2024",
    collection: "us-counties",
    shape_overlays: [{ field: "B19013_001E", palette: "sequential" }],
    mapbox_token_secret: "mapbox",   // <-- resolved server-side
    mapbox_style: "mapbox/light-v11"
  }
});

// 3. Same for geo_isochrone — store the provider key once, use everywhere.
set_workspace_secret({ name: "mapbox-isochrone", value: "pk.eyJ1Ijoi..." });
geo_isochrone({
  point: [-77.005, 38.890],
  minutes: [10, 20, 30],
  provider: "mapbox",
  api_key_secret: "mapbox-isochrone"   // <-- instead of api_key: "..."
});

// 4. Rotate without touching reports: just set_workspace_secret again
// with the same name and a new value. Every report that references it
// picks up the new token on next viewer load.

How it resolves:


Query-parameter reference

ParamValuesApplies toEffect
themelight (default) · dark/v · /embed Dark background + panel/legend colors. Useful when embedding inside dark UIs.
hide_title1/v · /embed Removes the title panel (top-left).
hide_legend1/v · /embed Removes the legend (bottom-left).
hide_tooltip1/v · /embed Suppresses the hover tooltip. Useful for non-interactive screenshots.
bare1/v Shortcut for hide_title + hide_footer. Implied by /embed/.
datasetslug or field name/v · /embed · .png · .svg Overrides the report's default layer when it has multiple datasets/overlays.
scopecollection:version:external_id/v · /embed Focuses on a single shape (e.g. us-cd119:tiger-2024:4812 for TX-12). Useful for per-district detail pages.
autosize1/embed Viewer posts { type: 'mapsmcp:height', slug, px } to window.parent on init + resize so the iframe can grow with content.
width / height200–4000 px.png · .svg Image render dimensions. Default 1024 × 640.
dpi1–4.png Raster density multiplier. 2 = retina-quality.

Questions? Open a ticket via the submit_ticket MCP tool, or email [email protected].