Map Control × Model Context Protocol — a live, shareable web map that AI agents and humans drive from code or conversation.
zermatt-banner.mp4
MapControl is a controllable map for AI agents.
Agents can create maps, add and style geospatial data, move the camera, switch basemaps, render 3D terrain, capture screenshots, and update the map incrementally as they work. Humans open the same map in a browser, watch those changes happen live, interact with the result, and share it with a link.
Control it through:
- MCP — give an AI agent direct map tools (Claude, Cline, any MCP client)
- Python — drive the map from scripts, notebooks, or generated code
- REST — integrate MapControl into any application
All three interfaces operate on the same live map sessions.
Ask an agent: "Show the burn scar near Los Alamos, switch to satellite imagery, and zoom to it." — MapControl gives the agent a map it can actually operate, not just coordinates to describe or a static image to return.
- 🗺️ Live & shareable — every map is a URL. Open it anywhere; it updates in real time over WebSocket.
- 🤖 Agent-native — a first-class MCP server (Streamable HTTP) mounted at
/mcp. Point Claude Desktop, Claude.ai, Cline, or the OpenAI Agents SDK at it and start talking to your map. - 🐍 Typed Python SDK — the
mapcontrolpackage gives you sessions, assets, styles, screenshots, and camera control in a few lines. - 🌍 Real geospatial data — GeoJSON features, styled polygons/lines/points, glyph markers, arcs, and GeoTIFF rasters rendered server-side with colormaps.
- 🏔️ 3D terrain & globe — MapLibre globe with terrain tiles and sky, or classic 2D. Ballistic zoom animations included.
- 📸 Screenshots — capture the current viewport to PNG, straight from a tool call.
- 🎨 Basemaps & themes — OpenStreetMap, Esri Satellite, and Carto Dark work with zero keys; drop in a MapTiler key for vector streets, hybrid, topo, and dataviz styles. Light/dark themes throughout.
- 🔐 Optional auth — self-issued signed scoped tokens for
/mcp, plus a standalone OAuth-style authorization portal for multi-user deployments.
Claude / Cline / any MCP client Python SDK (`mapcontrol`) curl / your app
│ │ │
▼ ▼ ▼
MCP (/mcp) ────────────────────────► REST API ◄───────────────────────┘
│
FastAPI server (sessions,
assets, events, GeoTIFFs,
screenshots, auth)
│
WebSocket fan-out
│
▼
MapLibre GL frontend (live map,
3D terrain, drawing tools)
| Directory | What it is |
|---|---|
server/ |
FastAPI server — REST API, WebSocket hub, MCP server, GeoTIFF & screenshot services, auth portal |
sdk/ |
mapcontrol — typed Python client SDK |
examples/ |
Runnable demo scripts (shapes, terrain, glyphs, GeoTIFFs) + sample data |
docs/ |
Guides — MCP integration, LLM context block, MCP Apps field guide, map-engine comparison, Puppeteer animation skills |
deploy/ |
Deployment helpers (local PyPI index for the SDK) |
All you need is Docker — the published image bundles everything else (Python, GDAL, a headless Chromium for screenshots) and is published multi-arch (linux/amd64 + linux/arm64), so it runs natively on Intel and Apple-Silicon machines alike. No clone required. Save this as docker-compose.yml:
services:
mapcontrol:
image: ghcr.io/esipfed/mc2:latest
ports: ["8000:8000"]docker compose up -dPort 8000 already taken on your machine? (
lsof -i :8000shows what's using it.) Remap only the host side and leave the container port unchanged —ports: ["8080:8000"]— then reach it athttp://localhost:8080. Keep the container on 8000: the image binds 8000 and self-navigates there for screenshots, so changing the container side would break them.
Verify it's up:
curl http://localhost:8000/docs # interactive API docsThat's the whole install. Want maps that survive restarts, share links that work off your machine, or premium basemaps? See docs/deployment.md — it walks from this minimal setup to a full production config and explains what every knob does. (Contributors: git clone this repo and docker compose up --build builds the image locally.)
Install the SDK (pure Python — no GIS stack needed on the client):
pip install "git+https://github.com/ESIPFed/mc2.git#subdirectory=sdk"from mapcontrol import MapControl
mc = MapControl("http://localhost:8000")
session = mc.create_map()
print(session.url) # ← open this in a browser; it updates live
# Fly to San Francisco
session.zoom_to_point(-122.42, 37.77, zoom=12)
# Add a polygon (GeoJSON dict or string)
session.add_polygon(
{
"type": "Feature",
"properties": {"name": "Mission District"},
"geometry": {"type": "Polygon", "coordinates": [[
[-122.43, 37.75], [-122.40, 37.75], [-122.40, 37.77],
[-122.43, 37.77], [-122.43, 37.75]]]},
},
style={"fill_color": "#43b4e440", "stroke_color": "#43b4e4"},
)
session.set_basemap("satellite")
session.set_theme("dark")
shot = session.take_screenshot() # PNG of the current viewWatch the browser tab while the script runs — every call lands on the shared map live.
The MCP server is mounted in-process at /mcp (Streamable HTTP — the current MCP standard). Add it to any MCP client:
Cline / Claude Desktop (mcpServers config):
{
"mcpServers": {
"mapcontrol": {
"url": "http://localhost:8000/mcp"
}
}
}Then just ask:
"Create a map, add a red polygon around Austin, switch to satellite, and zoom to it."
See docs/mcp-integration.md for full setup guides (Claude Desktop, Cline, Claude.ai web, OpenAI Agents SDK) and transport details.
If your system auto-generates analysis code — a planner, an agent loop, a notebook generator — append the context block from docs/llm-context.md to the generator's prompt. One extra block, and the SDK's usage slides right into every generated script: results land on a live, shareable map instead of dying in a stdout log.
It works interactively too — paste it into ChatGPT, Cursor, or Copilot with your script:
"Here's my NDVI script. Put the result raster on the map, outline the study area, and zoom to it."
| Tool | What it does |
|---|---|
create_map |
Create a live map and get its shareable URL |
add_geojson |
Add styled GeoJSON features (polygons, lines, points, glyphs) |
add_arc |
Add a great-circle arc between two points |
add_geotiff |
Upload & render a GeoTIFF raster with a colormap |
zoom |
Fly the camera — to coordinates, bounds, or assets (ballistic animation) |
list_assets |
List everything on a map |
update_style |
Restyle an existing asset |
set_visibility |
Show / hide an asset |
delete_asset |
Remove an asset |
set_basemap |
Switch basemaps (OSM, satellite, dark, MapTiler styles…) |
set_theme |
Light / dark theme |
get_viewport |
Read the current camera position |
take_screenshot |
Capture the current view to PNG |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/maps |
Create a map |
GET |
/api/maps/{id} |
Get map info |
DELETE |
/api/maps/{id} |
Delete a map |
POST |
/api/maps/{id}/sessions |
Create a user session |
POST |
/api/maps/{id}/events |
Send a map command |
GET |
/api/maps/{id}/events |
List events |
GET |
/api/maps/{id}/assets |
List assets |
PATCH |
/api/maps/{id}/assets/{aid} |
Update an asset |
DELETE |
/api/maps/{id}/assets/{aid} |
Delete an asset |
GET |
/map/{id} |
Map frontend (iframe-ready; ?ui=none for a chromeless embed) |
WS |
/ws/{id}/{session} |
WebSocket for real-time events |
Everything lives in server/config.toml — host/port, session TTL, storage paths, default basemap/theme/terrain, and the full basemap catalog. Keyless basemaps (OSM, Esri Satellite, Carto Dark) work out of the box; set MAPTILER_API_KEY to unlock the MapTiler vector styles. Adding a new tile provider is a config-only change.
docs/deployment.md covers the compose setups (minimal → production), what every environment variable does to the map, custom config.toml mounts, and running behind a reverse proxy.
Optional auth for /mcp is controlled via environment (MAPCONTROL_AUTH_MODE) — see server/mapcontrol_server/portal/ for the standalone authorization portal.
Runnable scripts in examples/ show the SDK end to end (start the server first):
python examples/demo.py # tour: shapes, styles, zoom, basemaps
python examples/demo_terrain_showcase.py # 3D terrain + ballistic camera moves
python examples/demo_glyphs.py # glyph markers & labels
# see also: demo_drawing.py, demo_ballistic_zoom.py, demo_multi_session.pySample GeoTIFFs live in examples/data/.
Want to drive the map from a browser instead of Python — for camera animations, recorded flythroughs, or screenshot capture? See the reference Puppeteer animation skills (ballistic flyTo tours, 3D terrain orbits, keyframe stills, frame-sequence recording).
The acceptance suites run inside the same image you deploy — exactly how CI gates every push:
docker compose run --rm mapcontrol python tests/test_mcp.py # MCP conformance
docker compose run --rm mapcontrol python tests/test_mcp_auth.py # MCP authorization(Hacking on the server internals? server/ is a standard installable Python package — pip install -e ".[dev]" && pytest tests/ -v from that directory, with system GDAL and playwright install chromium on your machine. For just running MapControl, Docker is the way.)
| Symptom | Fix |
|---|---|
port is already allocated on start |
Something else is on 8000. Change the compose mapping to e.g. "8010:8000" and point the SDK at http://localhost:8010. |
Shared map links point at localhost |
Set MAPCONTROL_PUBLIC_URL to your server's public URL so map links work off-machine. |
| Maps vanish after container restart | Keep the ./data volume mount from docker-compose.yml — SQLite and uploaded files live there. |
Questions, ideas, demos to show off? Join us on Discord →
MapControl is developed within the ESIP Federation community. Issues and pull requests are welcome.