Map Control × Model Context Protocol — a live, shareable web map that AI agents (and humans) drive from code.
Ask your AI assistant to "draw the burn scar over Los Alamos and fly the camera to it" — and watch it happen live in a browser tab. MapControl is a headless map server with a real-time MapLibre frontend: create maps, add GeoJSON and GeoTIFF overlays, animate the camera, switch basemaps and themes, take screenshots — over a Python SDK, a REST API, or the Model Context Protocol for Claude, Cline, and any other MCP client.
Think of it as the Star Trek computer's map console. You say the words; the map moves.
- 🗺️ 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 |
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). No clone required. Save this as docker-compose.yml:
services:
mapcontrol:
image: ghcr.io/esipfed/mc2:latest
ports: ["8000:8000"]docker compose up -dVerify 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/.
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.