Performs deterministic GIS geoprocessing with verifiable provenance and automated analysis planning for spatial datasets.
Also known as mapsmith-ai/MapSmith
- Stars
- 2
- Tools
- 29
- Downloads, 30 days
- 885
- Latest version
- 0.12.2
- Last commit
- Nov 13, 2026
- License
- AGPL-3.0
What is MapSmith MCP?
MapSmith performs deterministic GIS geoprocessing with verifiable provenance and automated analysis planning for spatial datasets. It exposes 29 tools, such as describe_dataset, buffer_layer, clip_layer.
Summary generated from the project's README
About MapSmith MCP server
Spatial analysts and developers can perform deterministic GIS geoprocessing by asking natural language questions about spatial datasets. Instead of simply executing a single tool, this server automatically plans a sequence of operations, validates the logic before any files are modified, and executes the workflow using engines like GeoPandas, DuckDB Spatial, exactextract, and Whitebox Workflows. This approach ensures that complex multi-step tasks, such as buffering layers or calculating zonal statistics, are handled with structural integrity.
Users connect any MCP client to the server to run analyses that include verifiable provenance. Every operation generates a lineage manifest containing input checksums, specific parameters, coordinate reference system decisions, and deterministic checks. This allows for complete reproducibility, as the record explains exactly why certain CRS decisions were made. The system is designed to reject malformed plans, such as those with incorrect step ordering or invalid arguments, before the execution phase begins.
What you can do with it
- 1Complex spatial filteringIdentify specific parcels based on proximity to a river and elevation thresholds.
- 2Automated analysis planningGenerate a validated sequence of GIS operations from a single natural language request.
- 3Verifiable geoprocessing provenanceCreate a detailed record of every parameter and coordinate system decision for audit purposes.
- 4Data integrity verificationPrevent errors caused by conflicting georeferencing information in raster files.
Written from the project's README. Every use case is checked against a quote from it.
How to install MapSmith MCP
Formats verified 10 Nov 2026Install MapSmith MCP in Claude Code
$ claude mcp add mapsmith -- uvx mapsmithInstall MapSmith MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"mapsmith": {
"command": "uvx",
"args": [
"mapsmith"
]
}
}
}Install MapSmith MCP in VS Code
.vscode/mcp.json
{
"servers": {
"mapsmith": {
"type": "stdio",
"command": "uvx",
"args": [
"mapsmith"
]
}
}
}Install MapSmith MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"mapsmith": {
"command": "uvx",
"args": [
"mapsmith"
]
}
}
}Install MapSmith MCP in Codex
~/.codex/config.toml
[mcp_servers.mapsmith] command = "uvx" args = ["mapsmith"]
Install MapSmith MCP in Gemini CLI
~/.gemini/settings.json
{
"mcpServers": {
"mapsmith": {
"command": "uvx",
"args": [
"mapsmith"
]
}
}
}MapSmith MCP tools 29
Detected from the READMEdescribe_datasetbuffer_layerclip_layeroverlay_layersdissolve_layernearest_joinexplode_layermeasure_areamerge_layerssimplify_layercentroid_layerconvert_format
| describe_dataset | — | |
| buffer_layer | — | |
| clip_layer | — | |
| overlay_layers | — | |
| dissolve_layer | — | |
| nearest_join | — | |
| explode_layer | — | |
| measure_area | — | |
| merge_layers | — | |
| simplify_layer | — | |
| centroid_layer | — | |
| convert_format | — |
Show all 29 toolsShow fewer tools
reproject_layerspatial_joinrun_sqlzonal_statisticshillshadeslopeaspectflow_accumulationwatershedpreview_mapvalidate_planbrief_planexecute_planget_provenancelist_operationsrun_operationserver_info
| reproject_layer | — | |
| spatial_join | — | |
| run_sql | — | |
| zonal_statistics | — | |
| hillshade | — | |
| slope | — | |
| aspect | — | |
| flow_accumulation | — | |
| watershed | — | |
| preview_map | — | |
| validate_plan | — | |
| brief_plan | — | |
| execute_plan | — | |
| get_provenance | — | |
| list_operations | — | |
| run_operation | — | |
| server_info | — |
Configuration
MAPSMITH_DISCOVERY_LOGMAPSMITH_WORKSPACEConfine all reads/writes to this directory (path jail + sandboxed SQL engine)
MAPSMITH_WORKSPACEConfine all reads/writes to this directory (path jail + sandboxed SQL engine)
| Name | Description | |
|---|---|---|
| MAPSMITH_DISCOVERY_LOG | — | |
| MAPSMITH_WORKSPACE | Confine all reads/writes to this directory (path jail + sandboxed SQL engine) | |
| MAPSMITH_WORKSPACE | Confine all reads/writes to this directory (path jail + sandboxed SQL engine) |
Automated checks
Ran Nov 12, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packagePass
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledNot checked
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesPass
README
View on GitHubMapSmith
Ask a question about your data. Get an analysis — and the record that proves it.
Most GIS servers hand an agent a tool to run. MapSmith takes the question, works out the analysis — usually several operations, in an order that has to be right — validates the plan before a single file is touched, runs it, and leaves a record of every step — which bytes went in, with which parameters and CRS decisions, and which checks ran — that somebody else can read afterwards and rerun to confirm.
Try it
Point any MCP client at MapSmith (one JSON block) and ask, in your own words:
Which parcels lie within 1.5 km of the river and sit at 120 m or lower? Give me the mean elevation and the ground area of each.
That is five operations, two coordinate systems and forty-eight checks. What actually happens is below — the plan, the step that gets rejected for reading something a later step produces, the CRS decision behind every metric step, and an answer you can work out on paper before MapSmith sees the files. That rejection is structural, and so is every other one: a plan that is well formed and answers the wrong question runs instead, which is measured and written down further down this page.
The operations are executed by GeoPandas, DuckDB Spatial, exactextract and Whitebox Workflows — never by the model that asked for them. Every dataset lands on disk next to a lineage manifest: inputs with checksums, the exact parameters, the CRS decisions and why, engine versions, and the deterministic checks that ran on the result.
mapsmith.dev — a real terrain analysis and the manifest that came with it. Both are build products: the figure is rendered from GeoTIFFs MapSmith writes, so the page cannot drift from what the software does.
The manifest is a specified format, not
MapSmith's private output: JSON Schema, a toolchain-free validator, a conformance suite, and a
hundred-line emitter that never imports MapSmith. Records carry spec_version, and CI validates
real MapSmith output against the spec's own validator. The specification is archived and citable
as 10.5281/zenodo.22205213, a concept DOI that
resolves to the latest archived draft. Records written by 0.8.0 to 0.12.1 declare 1.0.0-draft.10, and
0.7.x's declare 1.0.0-draft.8; if the DOI shows an earlier one, the archive has not caught up, and the
repository holds the text they are written against. The names MapSmith puts
in a record beyond the ones the specification defines — every extension check name, and every
extension field in crs_decisions with the reason it is not a synonym of a key the specification
already has — are listed in docs/manifest-vocabulary.md,
generated from the source rather than maintained by hand.
Evidence before promises. A correctness suite in its own organisation, Argleton, grades MapSmith on thirty-three traps whose answers are computed on paper before any system runs: 0.00 silent errors, nothing skipped, against 0.8788 for the obvious way of writing the same code. The runs from 2026-08-30 to 2026-09-15 said 0.00 too, and on one trap they were wrong: its truth was wrong in MapSmith's favour for 26 days, the error was ours twice over, and the erratum says so. Every run since 2026-09-26, the current one included, is scored against the corrected truth. Getting here cost seven defects the suite sent back, and they are listed. Alongside it: an A/B on GABench whose headline is a null result — with the analysis that took our own positive number apart — notebooks on a real USGS DEM of Mount St. Helens, an in-chat map panel that shows the verification status of every layer it draws, and a measurement of our own tool discovery that retracted two numbers this page had already published — including the one in the bullet list below.
A whole analysis, start to finish
This is the thing MapSmith is for, and it is not a single tool call. One question — six parcels, a river, an elevation grid — becomes a plan of five operations chosen out of 81, validated before anything runs, executed step by step, and recorded: the search that narrowed the catalogue, the arguments that mattered, the CRS decision behind each metric step, and the checks that ran on every result.
Nothing in this section is drawn. benchmarks/worked_example.py builds fixtures whose answer can
be worked out on paper, asks the catalogue in the words of the problem, validates and runs the
plan, reads the manifests, and writes what follows; tests/test_worked_example.py fails if this
page and that script disagree. The position column is BM25's rather than the default engine's,
because a published figure should not depend on whether a model download succeeded on the machine
that built the page — the narrowing, which is the point, is identical on both. Two things worth watching: the middle column, where the catalogue
goes from 81 operations to a handful the caller can read; and the CRS column, where every
metric operation says which coordinate system it moved the data into and why.
flowchart TB
ASK["<b>Parcels within 1.5 km of the river whose mean ground elevation is at most 120 m, with the elevation and the ground area of each</b>"]
ASK --> PLAN{{"plan validated<br/>before anything runs"}}
PLAN -. "rejected: FORWARD_REFERENCE" .-> BAD["'mask_path' references '$buffer' which runs later — move step 'buffer' before 'near'"]
BAD:::bad
BUFFER["<b>buffer_layer</b><br/>81 operations → 28 candidates → chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
PLAN --> BUFFER
NEAR["<b>clip_layer</b><br/>81 operations → 13 candidates → chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
BUFFER --> NEAR
HEIGHT["<b>zonal_statistics</b><br/>81 operations → 2 candidates → chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
NEAR --> HEIGHT
AREA["<b>measure_area</b><br/>81 operations → 25 candidates → chosen<br/>CRS WGS 84 (ellipsoidal)<br/>10/10 checks"]
HEIGHT --> AREA
FILTER["<b>select_features</b><br/>81 operations → 25 candidates → chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
AREA --> FILTER
OUT[["3 parcels, each with elevation and ground area"]]
FILTER --> OUT
classDef bad stroke-dasharray: 4 3
| what the agent asks for | it declares | candidates | picked | at position |
|---|---|---|---|---|
| “everything within one and a half kilometres of the river” | vector, dataset:vector, 1 dataset(s), line | 28 of 81 | buffer_layer |
2 |
| “keep only the parcels that fall inside that strip” | vector, dataset:vector, 2 dataset(s), polygon | 13 of 81 | clip_layer |
1 |
| “how high is the ground under each of these parcels” | raster, dataset:vector, 2 dataset(s), polygon | 2 of 81 | zonal_statistics |
2 |
| “how big is each one on the ground” | vector, dataset:vector, 1 dataset(s), polygon | 25 of 81 | measure_area |
1 |
| “drop the ones where the ground is above 120 metres” | vector, dataset:vector, 1 dataset(s), polygon | 25 of 81 | select_features |
1 |
| step | operation | arguments that mattered | CRS decision, recorded | checks |
|---|---|---|---|---|
| buffer | buffer_layer |
distance_meters=1500 |
EPSG:32610 — estimated UTM zone for metric buffering on a geographic CRS |
9/9 |
| near | clip_layer |
mask_path=$buffer |
EPSG:4326 — the mask is already in the input layer's CRS; nothing was reprojected |
12/12 |
| height | zonal_statistics |
zones_path=$near, stats=['mean', 'min'] |
EPSG:4326 — zones and raster share the same CRS |
7/7 |
| area | measure_area |
input_path=$height, method=geodesic |
WGS 84 (ellipsoidal) — ground area computed on the WGS 84 ellipsoid, which is where the coordinates are put first; no map plane is involved, so no projection distortion enters |
10/10 |
| filter | select_features |
input_path=$area, by=field_between, field=mean, maximum=120 |
EPSG:4326 — no CRS change: selecting rows does not touch coordinates |
10/10 |
Every step is inside the plan, the last one included: select_features took 4 rows and returned 3, with a manifest like every other write. This step used to run outside the plan, because the only operation that could answer it was run_sql — which takes its inputs inside a SQL string, declares zero datasets, and therefore cannot join the plan's dataflow. That boundary is deliberate and has not moved: substituting $step into arbitrary strings would be a grammar in which a planner assembles a path out of text. What changed is that it is no longer the only way to ask.
The answer, which can be worked out on paper before MapSmith sees the files: the parcels are squares of 0.0015° at 46.2°N, so each is about 119 m by 167 m, and the elevation ramps west to east across the fixture.
| name | mean | min | area_m2 |
|---|---|---|---|
| North Field | 104.85 | 104.14 | 19303.33 |
| Mill Meadow | 110.51 | 109.8 | 19303.33 |
| Old Orchard | 117.58 | 116.87 | 19303.33 |
The rejected plan is the honest half. Steps in the wrong order are the dominant failure class in
the agent benchmark, so the example includes one and shows what the validator says about it,
before any file is touched. It earned that place while this was being written: the first version
of the plan passed distance_m where the operation declares distance_meters, and the validator
named the argument and listed the three it accepts.
You can step through this on mapsmith.dev, one beat at a time, along with a second case that GitHub cannot show as well: a file MapSmith refuses. Both are executed while that page is built. The residue of the second one is here, because it is the half nobody demonstrates.
What it declines to answer. Asked “How high is the ground at each of these two wells?” against a GeoTIFF that opens in every viewer:
| describing the file first | |
|---|---|
georeferencing_source |
sidecar (.aux.xml) |
georeferencing_sidecar_present |
terrain.tif.aux.xml |
georeferencing_internal_would_give |
cell 10 x 10 at (500000, 5030000) |
terrain.tif is georeferenced twice and nobody chose: the GeoTIFF's own tags say cell 10 x 10 at (500000, 5030000), and terrain.tif.aux.xml beside it says something else. GDAL prefers the sidecar, which is correct — that is how an override works — but sample_raster_at_points would then report numbers from a file you did not name, and this record could not say which. describe_dataset lists both. To choose, either remove the sidecar or set GDAL_GEOREF_SOURCES=INTERNAL for a run that must use the file's own.
A number would have come back. It would have been read off a grid a hundred kilometres away and at twice the cell size, with nothing in the record able to say which of the two readings produced it.
And the analysis comes back out of the file. Given only the last output of the five-step run above — no plan, no filenames, no field pointing at another record — the chain is recovered by hashing it and following every input digest back:
5 operations recovered back to 3 original dataset(s), every one passing its critical checks.
<- select_features 10 checks
<- measure_area 10 checks
<- zonal_statistics 7 checks
<- clip_layer 12 checks
<- buffer_layer 9 checks
(original) elevation.tif
(original) parcels.gpkg
(original) river.gpkg
Quickstart
Add MapSmith to any MCP client over stdio (Claude Desktop, Claude Code, Cursor, VS Code):
{
"mcpServers": {
"mapsmith": {
"command": "uvx",
"args": ["mapsmith"]
}
}
}
Docker is the supported path, and confines the server to the directory you mount:
{
"mcpServers": {
"mapsmith": {
"command": "docker",
"args": ["run", "-i", "--rm",
"-v", "/absolute/path/to/your/data:/data",
"-e", "MAPSMITH_WORKSPACE=/data",
"ghcr.io/mapsmith-ai/mapsmith"]
}
}
}
One-click installs:
or from a terminal: code --add-mcp '{"name":"mapsmith","command":"uvx","args":["mapsmith"]}'
To check it runs before wiring a client, uvx mapsmith starts the server on stdio
(Ctrl-C to quit) — it speaks MCP, not a CLI, so a silent prompt means it is working.
This page describes 0.12.1, which is what that command installs. When main runs ahead of
the published artifact this paragraph says so and names the difference — a reader should never
have to find out by calling a tool that is not there. New in 0.12.1
(release notes): semantic verification of every primary,
secondary and native geometry CRS declaration in calculate_field. New in 0.12.0
(release notes): restricted numeric and boolean field
calculation, executable typed-DAG input contracts and corrected multi-step method-choice
benchmarks. New in 0.11.0
(release notes): the validated project brief, bounded agent
repair, vector/raster conversion and QGIS native:buffer, taking
the catalogue to 80 operations behind 29 tools. If you run 0.10.0 or earlier under
MAPSMITH_WORKSPACE, update: a coordinate reference system -- a tool argument, or the one a
file declares -- could make PROJ read a file outside the workspace or open a network share, and
in run_sql DuckDB's own PROJ could do the same; 0.10.0 closed that for tool arguments and for
GeoPackage, shapefile and GeoParquet inputs, and 0.10.1 for every other format MapSmith accepts
-- under a workspace it now reads each input's coordinate system before GDAL opens the file,
and refuses a format it does not recognise. SECURITY.md has all of it. From
0.9.0, 0.10.0 also: every operation that brings a vector input onto another CRS -- the eight
that align a second input, and the samplers, zones, masks, pour points and stations of the
raster ones -- chooses the datum transformation the way reproject_layer does, which moves
their results where PROJ's own pick was the ballpark and a covering operation or a Greenwich
twin exists (Monte Mario from Rome: up to about 80 m); a datum shift a CRS itself declares
(+towgs84) is no longer recorded as a ballpark; nearest_neighbour_index works in linear
memory and refuses a feature with no position; under a workspace run_sql runs only SELECT,
with coordinate systems and dataset paths as literals; and 103 catalogue phrasings from the QGIS
Training Manual's exercises moved the discovery figures on this page. From 0.8.0, 0.9.0 adds
apportion_by_area, which moves counts and rates from the zones they are reported in to zones
that cross them, and makes a record's transformation describe what PROJ did to the data's own
coordinates rather than to a point chosen from the CRS. That changes what reproject_layer
writes in two cases: coordinates outside every published operation's area are no longer
moved by an operation from elsewhere, and a datum counted from another meridian whose
operations are published only for its Greenwich twin -- Monte Mario from Rome, EPSG:4806 --
goes through that twin. The changelog names both. If you run 0.4.0 to 0.6.1, 0.7.0 and
later change answers you have: every position read from a point-registered raster
(AREA_OR_POINT=Point, the USGS and Copernicus DEMs) was half a cell off, and the
changelog says which operations and why. From 0.7.x, 0.8.0 adds
cluster_points_by_distance and a geometry facet for the catalogue search, digests a
shapefile with its attributes and its CRS, reads any single-layer file geodatabase, and fixes
operations that accepted the wrong geometry and answered anyway (zonal_statistics on line
zones returned their length as a pixel count).
If you read manifests, 0.10.0 renames no key: records still declare 1.0.0-draft.10.
Two keys of MapSmith's own are new inside crs_decisions.transformation:
x-mapsmith:shift_from_crs_definition, when the shift is written into the CRS itself, and
x-mapsmith:default_was_ballpark, when the engine's own choice would have applied no shift.
If you read manifests, 0.9.0 renames no key: records still declare 1.0.0-draft.10.
Three keys of MapSmith's own are new: crs_decisions.x-mapsmith:areas_measured_on, from
apportion_by_area, and x-mapsmith:ballpark_share and x-mapsmith:outside_area_of_use
inside transformation; so are three check names, all from apportion_by_area. A run that
fails where an earlier run's dataset still sits writes its record beside the earlier one, as
<output>.failed.provenance.json, instead of replacing it.
If you read manifests, 0.8.0 renames one key: records declare 1.0.0-draft.10, and a
second input's own georeferencing facts, inputs[].x-mapsmith:environment on 0.7.x, are
inputs[].environment, the name the specification gives them since draft.10. Each input of
an operation that reads more than one dataset also says which argument read it
(inputs[].argument).
If you read manifests, 0.7.x renames no key: records declare spec_version
1.0.0-draft.8, two drafts past 0.6.0's draft.6, and both drafts only narrowed where a
producer may add a key of its own — which MapSmith already did the narrower way. From 0.5.x,
0.6.0 renamed x-mapsmith:round_trip to round_trip; from 0.4.0, 0.5.0 had renamed eight
crs_decisions keys, the top-level mapsmith_version (read producer.version), two fields of
the round trip, two of repairs[] and engine.geometry_library. The changelog lists each with
what to read instead.
Then ask your agent things like:
"Take parcels.gpkg, keep only the parcels within 300 m of the river in rivers.gpkg, and give me the result with the analysis lineage."
The Docker image includes the [raster] and [whitebox] extras. With uvx, pick your
own: uvx --from "mapsmith[raster,whitebox]" mapsmith. Docker — or uvx on a machine
with working wheels — is the only supported installation path: geospatial native
dependencies across three OSes are a support black hole, and issues about broken local
environments will be redirected here.
Two things about the image, because they change what happens on your machine: it sets
MAPSMITH_WORKSPACE=/data itself (the -e above is explicit, not required) and runs as
uid 1000, so pass --user $(id -u):$(id -g) if the directory you mount belongs to another
user; and it is built for amd64 only, so on Apple Silicon it runs under emulation.
MapSmith MCP questions
How do I add MapSmith MCP to Claude Code?
Run claude mcp add mapsmith -- uvx mapsmith in your terminal. Add --scope user to make it available in every project.
How do I add MapSmith MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
What tools does MapSmith MCP provide?
It exposes 29 tools, including describe_dataset, buffer_layer, clip_layer, overlay_layers, dissolve_layer.
How does MapSmith ensure the accuracy of its analysis?
MapSmith validates the analysis plan before execution and generates a lineage manifest containing input checksums, parameters, CRS decisions, and deterministic checks.
What engines are used to execute GIS operations?
The operations are performed by GeoPandas, DuckDB Spatial, exactextract, and Whitebox Workflows.
Can MapSmith handle conflicting georeferencing data?
MapSmith will refuse to answer if a file is georeferenced twice (e.g., via internal tags and a sidecar file) to prevent reporting incorrect numbers without a clear record.