MCP server¶
GeoLibre ships an MCP server that authors
GeoLibre projects. Point an MCP client (Claude Desktop, Claude Code, or any
other) at it and you can ask for a map in words: the server writes a real
.geolibre.json project you open in the desktop app, the web app, or the
geolibre Jupyter widget, and can export it as a standalone HTML page.
The project-authoring tools are headless. They need no browser, no running
GeoLibre instance, and no bundled web build. They build project files with the
same project builders the Python package uses, so a project they
write is byte-for-byte the kind the app already loads. The optional show_map
in-chat preview uses a bundled MCP App view.
The live_* tools are the other half. They speak the desktop Notebook relay
(Notebook panel), so a command from an MCP client moves the map
already open in GeoLibre Desktop. Open Processing → Jupyter Notebook once
per app launch; the panel can be closed after that. Web and JupyterLite have
no relay, so those tools have nothing to attach to there.
Install¶
The MCP SDK is an optional extra:
pip install "geolibre[mcp]"
Run it¶
geolibre-mcp --root ~/maps
The server speaks MCP over stdio, which is what desktop clients spawn. The
--root flag is repeatable, and GEOLIBRE_MCP_ROOTS (:-separated, ; on
Windows) does the same job from the environment. With neither set, the workspace
is the current directory. --transport switches from the default stdio to
streamable-http or sse for clients that connect over HTTP.
Client configuration¶
Claude Desktop (claude_desktop_config.json) and most other clients take the
same shape:
{
"mcpServers": {
"geolibre": {
"command": "geolibre-mcp",
"args": ["--root", "/Users/you/maps"]
}
}
}
For Claude Code:
claude mcp add geolibre -- geolibre-mcp --root ~/maps
If geolibre-mcp is not on the client's PATH (common when it was installed
into a virtualenv), give the interpreter instead:
{
"mcpServers": {
"geolibre": {
"command": "/path/to/venv/bin/python",
"args": ["-m", "geolibre.mcp", "--root", "/Users/you/maps"]
}
}
}
In-chat map previews¶
Call show_map(path="city.geolibre.json") after creating or changing a project.
Clients supporting MCP Apps
render an interactive, read-only map inline: saved camera, GeoLibre layer
styling, pan/zoom, and click-to-identify. Other clients receive a text/structured
summary without inlined feature data. Neither preview tool writes the project.
The App uses the workspace @geolibre/core and @geolibre/map/headless
renderer, not a hosted-viewer iframe. It draws MapLibre-native layers (inline
GeoJSON, raster/WMS/WMTS/XYZ tiles, vector tiles, and image overlays). Everything
else in the project is named in a status notice instead of silently missing:
plugin-rendered layers (point clouds, 3D Tiles, COG, ArcGIS, PMTiles, DuckDB,
deck.gl), desktop-local sources (MBTiles, file: paths), video overlays,
Z-elevated GeoJSON, active plugins, legends, map controls, additional map views,
and a non-MapLibre primary renderer. Mapbox basemaps are blank because
credentials are stripped. Configured popup image fields show their URL as text,
because the shared popup renderer would load the image outside the approval
gate below.
The status bar shows loading progress, and failures that need action: a failed project load, a basemap style that could not load (the preview falls back to a blank background and still draws the project's layers), and per-source tile errors, counted rather than repeated. Rendered content stays visible.
The host must support App-to-server tool calls (serverTools): the preview
loads the credential-redacted project through app-only get_map_preview, then
uses app-only tools to approve origins, fetch resource bytes, and close the
preview session. Do not call those tools directly; full feature data is not
part of show_map's model-visible result. Hosts without this capability show
an explanation rather than receiving feature data through the summary.
App-only tool visibility requires a compliant host that honors
ui.visibility: ["app"]. It is not server-side authentication: a client that
ignores this metadata can expose the redacted project and approval/fetch tools
to the model. The consent UI is therefore not an independent server-side
security boundary. Grant, public-address, and resource-limit checks remain
enforced by the server regardless of the caller.
Network access¶
The preview supports public HTTP and HTTPS resources, including WMS endpoints
and hosts discovered inside remotely fetched style documents. One consent panel
lists the origins known from the basemap and visible, renderable layer sources
up front. Allow all or Block all applies to that list, including sources
that begin loading after the basemap style. No resource request is sent before
approval; blocking leaves inline data available over a blank map background.
New origins discovered inside remote styles are batched into the same panel,
not silently approved by an earlier decision. Already-blocked origins stay
blocked. The panel supports light/dark themes, narrow views, and keyboard use.
It uses GeoLibre's shared web/desktop palette from @geolibre/ui/theme.css:
blue primary actions and slate/navy dark surfaces. The host selects light or
dark mode; it does not replace GeoLibre's color scheme.
Approved resources are fetched by the GeoLibre Python MCP server, not the browser. The destination sees the server machine's IP address, which may differ from the browser's IP; the prompt makes this explicit. Resource bytes travel back through app-only MCP tool calls. The bundled view declares no external CSP origins, so browser CORS, mixed-content restrictions, and desktop hosts' wildcard-CSP normalization do not govern these resource requests. The host must still permit the App's server-tool calls.
Consent is scoped to one preview and is not persisted. Server-side previews
expire after 30 minutes of inactivity; successful origin approval and resource
authorization refresh that idle timeout. Origin grants still expire after five
minutes and are renewed only for origins already allowed in that preview.
Closing the preview revokes its grants. Call show_map again to choose
differently or reopen an idle-expired session.
The server allows at most 64 active preview sessions. It refuses new sessions at that limit rather than evicting a live preview and revoking its grants. Closing a preview frees its slot; abandoned sessions retain theirs until the 30-minute idle expiry.
The server checks DNS addresses and connects only to the checked public IPs, with HTTPS certificate/hostname verification using system trust plus certifi's public CA bundle. Loopback, private/LAN, link-local, reserved, and multicast targets are rejected, even after approval; private-network services are not supported by this preview.
Known credentials in project URLs and source configuration are redacted before
the App receives them, and known credential-like resource URL parameters are
rejected. These name-based checks are heuristic, not a guarantee that every
vendor-specific secret is detected. Conservative keys such as key and sr
are rejected even when used for a non-secret value; use a public URL without
these parameters. Do not preview projects containing secrets in custom fields
or URL parameters. No cookies, caller headers, environment proxies, or browser
credentials are forwarded.
Redirects fail closed: use and approve the direct destination URL instead.
Each resource body is limited to 4 MiB (about 5.3 MiB when base64-encoded for
transport) and a 15-second network deadline. An oversized Content-Length is
rejected before reading the body; the streaming limit also applies when the
length is missing or understated. The server requests identity encoding and
rejects compressed responses. Video sources inside a fetched style are removed
because MapLibre loads video outside the consent-controlled request path.
After updating a checkout, run these commands from the monorepo root. Install JavaScript dependencies and rebuild the view before the editable Python install, whose build hook can build any missing frontend assets:
npm install
npm run build:mcp-app
python -m pip install -e "python[mcp]"
Use the Python executable configured in your desktop client's MCP server
command. Fully quit and reopen the client, then make a fresh show_map call:
an already-mounted preview can retain the previous HTML and tool definitions.
Building from a checkout¶
Published wheels include geolibre/static/mcp/show-map.html. From a checkout,
run npm install and npm run build:mcp-app to build, scan, and stage the
single-file MCP App without building the Jupyter app. npm run build:embed
independently builds and stages the Jupyter/web app.
The Python packaging hook runs each frontend build only when its own staged
output is missing. Set GEOLIBRE_FORCE_JS_BUILD=1 to rebuild both from a full
checkout. Prebuilt sdists can produce wheels without Node or the monorepo
sources. Both frontend build outputs and pre-staged package assets are scanned
for credentials before packaging.
The consent panel uses a named vanilla Lucide icon rendered directly into SVG DOM nodes, keeping React's server renderer out of the single-file preview.
The workspace¶
Every path in every tool call is resolved against the allowed roots before the
server touches it, mirroring GEOLIBRE_CONVERSION_ROOTS in the
sidecar. Paths outside them are refused, and so is a symlink
inside a root that points out of it. Relative paths resolve against the first
root, so a client can say city.geolibre.json without knowing the host layout.
Three more guards on writes: the server only writes files ending in .json
(projects) or .html (exports) — a bare .json with no name is refused too —
it refuses to replace an existing file unless the call passes overwrite, and
a tool that edits an existing project first checks the file actually is one, so
an unrelated package.json sitting inside a root cannot be rewritten as a map.
Give it a directory meant for maps, not your home directory.
Tools¶
Project lifecycle¶
| Tool | What it does |
|---|---|
create_project |
Write a new, empty project with a name, center, zoom, and basemap. |
describe_project |
Summarize the camera, basemap, layers, and map controls. Inlined feature data is reported as a count, never echoed back. |
get_point_cloud_annotations |
Per point cloud, how many points the app's annotator relabelled into each class and how many points each instance holds, plus the custom classes, 3D vectors and saved 3D boxes (with status and attributes). |
set_point_cloud_classes |
Define the annotator's custom classes (codes 19-255, name, #rrggbb color); keeps existing labels and boxes. |
prelabel_point_cloud |
Run the app's Whitebox pre-label classifiers on a local copy of a LiDAR layer and save the changed classes as its labels (needs geolibre[pointcloud]). |
write_labeled_point_cloud |
Write a local copy of a LiDAR layer's LAS/LAZ/COPC file with the project's labels and instance ids applied (needs geolibre[pointcloud]). |
list_catalog |
List the named basemaps, color ramps, legend presets, and the plugin ids set_plugin_state accepts, plus the active workspace roots. |
Adding layers¶
| Tool | For |
|---|---|
add_geojson_layer |
Vector data inlined into the project, from a URL, a workspace file, or literal GeoJSON. Self-contained, and the only kind classify_layer can style. |
add_vector_layer |
A large remote FlatGeobuf / GeoParquet / GeoJSON read in place. |
add_raster_layer |
A Cloud Optimized GeoTIFF, with band, colormap, and rescale options. |
add_tile_layer |
A raster XYZ tile template. |
add_tiles_layer |
PMTiles archives and vector tile services. |
add_ogc_layer |
WMS and WMTS endpoints. |
add_lidar_layer |
LAS/LAZ/COPC/EPT point clouds by URL. |
add_spaceborne_lidar_layer |
ICESat-2 (ATL06, ATL08) or GEDI (L2A, L2B, L4A) footprints from a local HDF5 granule (needs geolibre[spaceborne]). |
add_3d_tiles_layer |
OGC 3D Tiles tilesets, by URL or Cesium Ion asset id. |
add_cesium_ion_layer |
Cesium Ion assets (tileset or imagery) by id; rendered by the 3D globe only. |
add_czml_layer |
A CZML (Cesium Language) dynamic 3D scene, by URL or inline packets; rendered by the 3D globe only. |
add_cesium_kml_layer |
Native globe KML/KMZ from a URL, inline XML, or KMZ data URL, preserving document styles and overlays. |
Editing¶
| Tool | What it does |
|---|---|
update_layer |
Rename, show/hide, set opacity, or reorder. |
remove_layer |
Drop a layer. |
style_layer |
Merge style keys (fillColor, strokeWidth, circleRadius, …). |
set_layer_popup |
Choose the fields a click popup shows, their labels and formats, its width and image height, and an optional hover tooltip. |
set_layer_metadata |
Describe a layer for catalogs (title, abstract, keywords, license, attribution, contact, lineage, temporal extent, links): what the app's Metadata dialog edits and exports as a STAC Item. |
classify_layer |
Build a graduated choropleth from a numeric column. |
list_layer_properties |
List a layer's feature properties with sample values. |
set_layer_filter |
Hide the features that do not match a boolean MapLibre expression (the saved filter Select by Expression → Filter layer writes); omit the expression to clear it. |
set_labels |
Label features from a property or a text expression, with size, colors, halo, zoom range, anchor, and the rarer settings under options. Settings left out keep their values. |
Layers are addressed by id or by display name, so a client can work from
what describe_project showed it without tracking UUIDs.
Framing and decoration¶
| Tool | What it does |
|---|---|
set_view |
Set center, zoom, bearing, and pitch, or pass a bbox to frame an area. |
set_basemap |
Switch the background style. |
set_map_legend |
Show the map legend, built from the layers' own symbology. |
add_legend |
Add a legend from a preset, a {label: color} map, or paired lists. |
add_colorbar |
Add a colorbar for continuous data. |
add_swipe |
Configure the split-map comparison slider. |
Plugin state and story maps¶
| Tool | What it does |
|---|---|
set_plugin_state |
Store a plugin's saved project state (the blob it restores on open, such as the Time Slider's timeline). Built-in ids come from list_catalog; an external plugin's id needs allow_unknown. |
set_story_map |
Set a story map's title block, theme, markers, inset, and start/end slides. |
add_story_chapter |
Add a chapter with its camera, text, image, alignment, animation, and layer fades. A camera value left out comes from the project's saved view. |
move_story_chapter / remove_story_chapter |
Reorder or drop a chapter by id, title, or index. |
add_bookmark |
Add a saved view to the Bookmarks panel, optionally in a folder (a new folder name creates it) and with the layers to show when it is opened. A camera value left out comes from the project's saved view. |
remove_bookmark |
Drop a bookmark by id, name, or index. |
The shapes match what the app reads (parseProject in @geolibre/core); a
round-trip test loads a project these tools wrote through it and checks nothing
is dropped or rewritten.
Live desktop map¶
| Tool | What it does |
|---|---|
live_status |
Report whether a GeoLibre Desktop window is listening. |
live_list_layers / live_get_layer |
List the open map's layers or fetch one layer's metadata by id. |
live_get_view / live_set_view / live_fly_to / live_fit_bounds / live_zoom_to_layer |
Read or move the camera. |
live_set_basemap |
Switch the basemap. |
live_add_geojson / live_add_marker / live_add_markers / live_remove_layer |
Add GeoJSON or point markers, or remove a layer. |
live_get_layer_features / live_get_selected_features / live_get_drawn_features / live_identify |
Read layer, selected, or sketched GeoJSON features, or query rendered features at (lng, lat). |
live_set_identify / live_get_identify / live_set_control_visible |
Toggle click-to-identify or show/hide a built-in map control or panel. |
live_set_visibility / live_set_opacity / live_set_style |
Change a layer. |
live_list_algorithms / live_run_algorithm |
List or run client-side processing algorithms on the open map. |
live_list_whitebox_tools / live_run_whitebox_tool |
List or run bundled Whitebox/WASM tools on the open map. |
live_run_model_builder |
Execute a serialized Model Builder graph on the open map. |
live_to_image |
Capture the current viewport as a PNG data URL or save a .png in the workspace. |
Processing is live-only. The algorithm catalog and the engines that run it
(Turf.js, DuckDB-WASM) live in the app, so there is nothing a file tool could
run. The relay waits about five seconds for a result: a longer run keeps going
in the app, and the tool reports that it did not finish in time instead of its
result layer ids, so check live_list_layers before running it again.
Export¶
export_html writes a standalone page that embeds the hosted GeoLibre viewer
and injects the project into it, so the recipient needs no install. Credentials
are stripped from the project on the way out. Layers pointing at local files
will not load for anyone else, so use hosted URLs for a shareable export.
app_url is a trust boundary
export_html's optional app_url names the viewer the exported page
embeds, and the page posts the project to exactly that origin (it must be
an http/https URL). It exists so you can pin a self-hosted deployment.
Treat it as a destination, not a cosmetic setting: whoever opens the exported file hands the project's contents — inlined GeoJSON, layer URLs, the camera — to that origin. Credentials are already stripped, so this is not a key leak, but the rest of the project still travels.
This matters because the caller here is a model, which may be acting on
content it has read. If an exported page points somewhere you did not
choose, that is worth a second look. Only accept an app_url you
intended.
Notes and limits¶
set_viewwith abboxis approximate. A saved project stores a center and zoom, and the app applies those verbatim on load rather than fitting a stored bbox. The server therefore resolves the box to a camera itself, using an assumed map-pane size, and lands within roughly half a zoom level of what the app's own "zoom to layer" would pick. Passcenterandzoomwhen you need exact framing.- Inlined GeoJSON is capped at 50 MB, and a project file the server reads at
256 MB. Past those, use
add_vector_layeror a tiled source. - Remote fetches are checked: a URL whose host resolves to a private, loopback, or link-local address is refused, on every redirect hop as well as the first request, so a crafted URL cannot reach a cloud metadata endpoint.
- File tools write a project.
live_*tools move the open desktop map. A live edit is in the session until you save it from the app. The relay is loopback-only and refuses redirects, so the Jupyter token stays on the machine. The same scripting surface backs the Python widget and the embed API; those remain how a host page or a notebook drives a map without MCP.
Driving the server from an agent¶
GeoLibre ships an agent skill that teaches an external AI
agent how to use these tools well: which add_*_layer tool fits which data,
what order to call things in, and the limits above. Install it from
skills/geolibre/ in the repository.
Under the hood¶
The tools are thin wrappers over geolibre.authoring, a widget-free module of
operations on project dicts (add/remove/restyle a layer, move the camera,
compose the map controls). geolibre.Map delegates to the same module, so the
notebook widget and the MCP server cannot drift apart in how they build a
project.
Renderer and pane authoring¶
Use set_renderer(path, "cesium") to open a project on the globe.
set_map_layout(path, 1, 2, view_kinds=["cesium", "maplibre"]) creates a mixed
grid and returns the secondary panes, with their IDs, as secondaryMapViews. Pass one as pane_id to set_renderer
to change only that pane. Camera tools continue to use longitude/latitude and
the shared zoom, bearing, and pitch convention. The accepted renderer names are
maplibre, mapbox, cesium, and arcgis.