Server data from the Official MCP Registry
Deterministic GIS geoprocessing with verifiable provenance on every output
About
Deterministic GIS geoprocessing with verifiable provenance on every output
Security Report
Valid MCP server (1 strong, 2 medium validity signals). No known CVEs in dependencies. Package registry verified. Imported from the Official MCP Registry.
3 files analyzed · 1 issue found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Required
This plugin requests these system permissions. Most are normal for its category.
What You'll Need
Set these up before or after installing:
Environment variable: MAPSMITH_WORKSPACE
How to Install
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-mapsmith-ai-mapsmith": {
"env": {
"MAPSMITH_WORKSPACE": "your-mapsmith-workspace-here"
},
"args": [
"mapsmith"
],
"command": "uvx"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
MapSmith
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.
What you get back
Every dataset comes with the file below, written next to it as
<output>.provenance.json — enough to re-run the analysis without the model that asked
for it:
{
"spec_version": "1.0.0-draft.10",
"producer": {"name": "mapsmith", "version": "0.12.1"},
"operation": "buffer_layer",
"parameters": {"distance_meters": 300.0},
"inputs": [{
"path": "rivers.gpkg",
"sha256": "b24b884f49eee431133d443d842557d3ed21f3978e2c4b1e8e072fd1240effe8",
"crs": "EPSG:4326"
}],
"crs_decisions": {
"analysis_crs": "EPSG:32632",
"reason": "estimated UTM zone for metric buffering on a geographic CRS",
"round_trip": {
"transformation": {
"pipeline": "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=utm +zone=32 +ellps=WGS84",
"accuracy_m": 0.0,
"is_ballpark": false
},
"return_transformation": {
"pipeline": "+proj=pipeline +step +inv +proj=utm +zone=32 +ellps=WGS84 +step +proj=unitconvert +xy_in=rad +xy_out=deg",
"accuracy_m": 0.0,
"is_ballpark": false
}
}
},
"engine": {"name": "geopandas", "version": "1.0.1"},
"environment": {},
"verification": [
{"name": "input_crs_present", "passed": true, "detail": "'input_path': EPSG:4326"},
{"name": "input_not_empty", "passed": true, "detail": "'input_path': 1 features"},
{"name": "result_not_empty", "passed": true, "detail": "1 features"},
{"name": "crs_present", "passed": true, "detail": "EPSG:4326"},
{"name": "crs_matches", "passed": true, "detail": "expected EPSG:4326, got EPSG:4326"},
{"name": "geometry_valid", "passed": true, "detail": "all valid"},
{"name": "x-mapsmith:no_geometry_is_empty", "passed": true, "detail": "none empty"},
{"name": "feature_count_exact", "passed": true, "detail": "expected 1, got 1"},
{"name": "geometry_types", "passed": true, "detail": "expected subset of ['MultiPolygon', 'Polygon'], got ['Polygon']"}
],
"started_at": "2026-08-18T10:15:03Z",
"finished_at": "2026-08-18T10:15:04Z"
}
The checksum and the timestamps are made up; everything else — the CRS decision, the
transformation, every check name and detail — is copied from a record buffer_layer emits
for exactly this call. The real file also carries output, repairs, notes and a
critical flag on each check, left out here for length.
tests/test_showcase.py validates this block against
the specification's own validator and re-runs the operation to compare its crs_decisions
keys with the emitted ones, because a record typed by hand drifts silently while every
generated surface stays correct: it lacked spec_version for two releases, and on
2026-09-06 it was still recording a round trip without the
key that says the coordinates came back — x-mapsmith:round_trip then, and
round_trip
in the specification since 1.0.0-draft.6. This is the record a third-party implementer copies.
Trimmed for the page, not for the file: the real record also carries the output's own path
and hash, any geometry MapSmith had to repair, and the notes it made about how the inputs
were handled — and every check says whether it was critical and, when it failed, what to do
about it. get_provenance returns it for any output — one operation, in full.
For the analysis rather than the step, run_operation with get_lineage takes the final file, hashes it, finds the record claiming those exact bytes and follows every input digest back to the originals. No field points at another record: the chain is recovered from content, so it survives a rename and breaks only when the data changes. It reports a step that failed its checks as one, and says where the walk stopped rather than presenting a history it did not reach.
Why MapSmith
- The unit of work is the analysis, not the tool call. Other GIS servers expose operations and let the caller fire them one at a time; a question that takes five steps is five unrelated calls, and nothing above them knows they belong together. MapSmith takes a typed plan, refuses it if a step reads something a later step produces, runs it in order, writes a manifest for each step and a plan-level record tying them together, and leaves the chain recoverable afterwards from the content digests alone. That is the difference a wrapper around a toolbox cannot have, because there is nothing above the single call to put it in.
- Real geoprocessing, not map CRUD. Built on the proven open geospatial stack: GDAL,
GeoPandas, Shapely, DuckDB Spatial, Whitebox Workflows and exactextract ship today
and QGIS Processing's
native:bufferthrough a GPL-isolated sidecar. - Provenance by design. Every layer MapSmith produces ships with a machine-readable lineage manifest — source datasets with checksums, tools executed, exact parameters, CRS decisions, software versions, timestamps. Everything needed to re-run the analysis without the LLM is in there. No AI slop.
- The engines compute, the model orchestrates. Geometry and numbers only ever come from deterministic tool executions — never from model output.
- 81 operations, reached through 29 goal-level tools. The catalogue is the breadth and the tool list is the choice; it is searchable rather than dumped, because tool-selection accuracy degrades once a few dozen tools are exposed at once, and fastest when two of them apply to the same input. Capability count has no such ceiling, so capability lives in the catalog. Search narrows it on what you declare and then hands over what survives rather than ranking it for you, because measurement said ranking is the wrong verb — see Finding the right operation.
- Model-agnostic infrastructure. Claude, GPT, Qwen, Kimi, GLM — anything that speaks MCP, cloud or local. The leverage is better contracts (typed plans, actionable error codes, a searchable catalog), not weights we would have to maintain. See the manifesto.
Tools
| Tool | What it does |
|---|---|
describe_dataset | CRS, schema/bands, extent, nodata and statistics of any vector or raster dataset |
buffer_layer | Metric buffer with automatic UTM estimation for geographic CRS |
clip_layer | Clip a layer with a mask layer |
overlay_layers | Set-theoretic overlay (intersection/union/difference/…); dropped lower-dimension pieces are declared in the manifest |
dissolve_layer | Merge features per key; the aggregation is recorded in the manifest and the group count verified |
nearest_join | Nearest neighbour with the distance in meters, UTM-measured on geographic CRS (decision recorded) |
explode_layer | Multi-part to single-part, with the part count verified in closed form |
measure_area | Area in m², always: ground on the ellipsoid, or planar converted with the CRS's own declared linear unit (survey feet are not metres). Invalid rings repaired before measuring, and a plane that is not equal-area here comes back with the ratio against the ground area |
merge_layers | Append layers (schema union); null-filled columns are named in the manifest, the count verified against the sum |
simplify_layer | Douglas-Peucker with the drift measured: area/length before and after recorded in the manifest |
centroid_layer | Geometric centroids computed in a metric CRS, never on degrees (decision recorded) |
convert_format | Convert between GeoParquet/GeoPackage/GeoJSON by output extension, re-read and verified (count and CRS). Two conversions are refused with the reason rather than performed: shapefile output, which truncates field names to 10 characters silently, and GeoJSON for a non-WGS84 layer |
reproject_layer | Reproject to any CRS (EPSG code or WKT) |
spatial_join | Join by spatial predicate, auto-routed to the fastest engine (SedonaDB > DuckDB > GeoPandas) |
run_sql | Spatial SQL (DuckDB dialect) over GeoParquet and GDAL formats |
zonal_statistics | Raster statistics per vector zone with exact fractional pixel coverage ([raster] extra) |
hillshade | Shaded relief from a DEM, in-memory Whitebox engine ([whitebox] extra) |
slope | Slope gradient from a DEM in degrees, percent or radians; geographic-CRS DEMs refused ([whitebox] extra) |
aspect | Downslope azimuth from a DEM, 0 = north; flat cells are −1, not nodata ([whitebox] extra) |
flow_accumulation | D8 flow accumulation with automatic depression filling ([whitebox] extra) |
watershed | Watershed delineation from a DEM and pour points ([whitebox] extra) |
preview_map | Interactive in-chat map (MCP Apps) of any datasets, with a provenance card and verification status per layer |
validate_plan | Statically validate a multi-step plan before running anything: operations, arguments, references, input files, simulated CRS flow |
brief_plan | Render a project brief only after plan validation, with a declared source for every statement |
execute_plan | Validate then run a plan step by step, with per-step provenance and a plan-level manifest |
get_provenance | Return the manifest of any MapSmith output: one operation, its inputs and their digests, and what was checked |
list_operations | Catalog search: narrows on what you declare, then returns the surviving set to choose from (status: "choose") or a ranking by engine — BM25, embeddings, or auto; detail=true returns parameters and worked examples |
run_operation | Run any catalog operation by name, including those with no tool of their own; arguments validated against the catalog before anything runs |
server_info | Version, license, available engines |
Finding the right operation
Those are the tools an agent chooses between. Behind them the catalog holds every
operation MapSmith can perform — 81 today, and 56 of them have no tool of their own — and
it is built to hold thousands. (One of the 81 is marked planned and says so when asked:
the roadmap is in the catalog on purpose, so an agent can answer "not yet" instead of
inventing a call.)
The split is the design: tool-selection accuracy degrades past a few dozen exposed tools, while capability count has no such ceiling. That makes reaching scale a retrieval problem, so it is treated as one — and measured like one.
First it narrows, deterministically, on things the caller already knows. Every entry
declares what data it takes (vector, raster, dataset, plan, none), what it hands back
(dataset:vector, dataset:raster, answer, description), whether it demands a projected
CRS, and which family it belongs to.
Measured over 118 answerable requests written by two other model families from job scenarios — a hydrologist with a flood report, a surveyor arguing with a field measurement — neither of which was shown this catalog, because a model handed the entry writes a paraphrase of the entry:
| what the caller declares | candidates left | BM25, found@3 | embeddings, found@3 | right answer in what comes back |
|---|---|---|---|---|
| nothing — words alone | 81 | 31% | 20% | 31% |
| what data I have | 54 | 34% | 24% | 35% |
| + what I want back | 33 | 47% | 42% | 58% |
| + how many datasets I have | 18 | 54% | 51% | 89% |
| + what geometry it holds | 16 | 62% | 62% | 98% |
The last row is new on 2026-09-29, and the row above it shows why. Thirty-one operations
now take one vector layer to another, one more than the threshold below, so a caller who
declared everything it knew was handed a shortlist for the most common request there is, and
delivered fell from 98% to 84%. The fix is one more fact the caller holds: whether its layer
is points, lines or polygons, as a list when it is mixed. An entry declares the families it
accepts only where its code refuses the others, and a test calls each one with every family it
does not declare and requires the refusal, so the declaration cannot hide an operation from a
caller it would have worked for. Writing that test found zonal_statistics accepting line
zones and returning their length as the pixel count; it now refuses them.
Two ranking columns, and that is a correction. This table used to carry one, computed with the default engine — which is the embedding one where its model loads and BM25 where it does not. So the published figures were a measurement of what the machine could download, and a CI run that met a 429 from Hugging Face recomputed the first row as 28% where this page said 18%. Not a flaky test: a number that had never been reproducible on a machine without the model, published under a sentence promising it could be checked.
The two also differ in a way worth seeing. In this run BM25 leads at every declaration level, by eleven points on words alone. The embedding engine earns its place on the phrasings it has never seen, not on the ranking once the set is small.
The last column is not an accuracy figure — it is a property, and the 98% rather than 100% is worth a sentence. The narrowing never drops the right operation: that is asserted per entry and holds for all 81. What the column measures is whether the surviving set was small enough to hand over WHOLE, and for a handful of requests it still is not, so those fall back to a ranked shortlist and the answer can be outside the top three. Ranking decides the order; it does not decide membership; and the 2% is the gap between "cannot lose the answer" and "can show you all of it".
The third row is the scaling wall, and we hit it in one afternoon. On 2026-08-29 the catalogue went from 51 operations to 61. Two rows of that table got worse: the commonest surviving set went from 26 candidates to 34, past the point where the whole set can be handed over, and delivered fell from 100% to 45% while found@3 fell from 48% to 36%. Adding capability had made discovery worse — the failure this page had predicted at eight hundred operations and met at sixty-one.
Raising the threshold would have postponed it by about ten operations. What fixed it is the fourth row: how many datasets you are holding. That is a fact about your situation — one layer or two — not a guess about our vocabulary, it is derivable from each operation's own signature so a test can check the declaration against the code, and it takes the median surviving set from 34 to 9. The catalogue grew by a fifth and discovery got better, but only because a facet arrived with it. That is the trade this design makes, stated rather than discovered later.
It happened again the next day, and this is what watching a curve is for. On 2026-08-30 the
catalogue went from 61 operations to 71. Every ranking figure in that table fell — 28% to 25%
bare, 34% to 27% on the input kind — and the delivered column of the second row fell from 48%
to 27%, because more requests now leave a set too large to hand over whole. The bottom row did
not move: 97%, the same as at 61. Ten more operations, no new facet, and the guarantee held,
which is the first time growth has been absorbed by the facets already there. (Not «and at 51»:
that bottom row is the arity facet, and dataset_inputs did not exist at 51 operations. The 100%
quoted above at that size is the row above it. Two different rows under one sentence is the kind
of comparison this page exists to refuse.)
And again at 74, with two operations that a caller is unusually likely to want. select_features and extract_layer are the remedies MapSmith's own error messages had been recommending, so they sit in the busiest corner of the facet space: the average surviving set went from 16 to 17 and the second row's delivered did not move. The bottom row held at 97% for the third catalogue size running. The margin to the wall is now 13. (Those are the figures as measured on 2026-08-31. They were recomputed on 2026-09-01 against human answers — see the note under the table — which moved them without any catalogue change: the surviving set reads 16 and the bottom row 98%. A paragraph about a transition keeps the figures of the transition.)
And at 76 the cost showed where the new operation lives. summarize_points_in_polygons takes two vector layers and returns one, which is the busiest corner of the facet space — count_in_polygons, spatial_join and clip_layer are its neighbours — so the bottom row's found@3 fell from 58% to 55% under BM25 while the average surviving set stayed at 16 and delivered held at 98%. The operation was added because «average pH by field block» took two calls to answer and so, to somebody searching, did not exist; three points of ranking at the fullest declaration is what that cost, and the guarantee that matters did not move.
And when an existing operation gained an optional input, with no operation added. zonal_statistics took a weights raster on 2026-09-25, so it reads two datasets or three, and an arity that varies is declared as none at all — the rule run_sql and merge_layers follow, because declaring 2 would hide the weighted form from a caller who correctly says they hold three. The price is that it now survives every arity: the average surviving set went from 16 to 17 and the bottom row's found@3 from 55% to 53%, with delivered still at 98%. A facet that stops discriminating for one entry costs every search a candidate.
That is the shape of the trade, and it says when the next facet is due. The figure to watch is not found@3 — a ranker will always get worse as the catalogue grows, and it is a hint. It is the average surviving set at the fullest declaration, the fourth column of that table: 9 at 51 operations, 14 at 61, 16 at 72, 16 at 74, 16 at 75, 17 at 76, 15 at 77, 15 at 78, 16 at 81. (The point at 77 is the first with the geometry declared; without it the set would be 17.) When that crosses the threshold, delivery stops being a property and starts being a ranking again, and the lasting answer is another fact the caller already knows. A bigger threshold is an answer only as far as the caller still chooses well among more, and that is measured below, not assumed. (It said median until 2026-08-29, and published the mean: the median at 81 is 13. The distribution is skewed — most requests leave a small set and a few leave a large one — so the two numbers say different things and the mean is the pessimistic one, which is the right one to watch.)
The requests, both labels and the harness are all in the repository:
tests/data/discovery_queries.json and
benchmarks/discovery_report.py, which recomputes every
number above from those files with no network and no model — so they can be checked rather than
believed, and tests/test_discovery_report.py fails if this page and the harness disagree. The
one exception is the 69%: reproducing that needs the model that did the choosing, and the report
says so where it stops. The measurement of the threshold below is the other way round: its
choices are in the repository, so its figures can be recomputed without the model.
Documentation truncated — see the full README on GitHub.
Reviews
No reviews yet
Be the first to review this server!
More Developer Tools MCP Servers
Rea
Freeby Morluto · Developer Tools
Reverse engineer anything from your terminal or agent with one CLI and MCP server.
Paperclip
Freeby Paperclipai · Developer Tools
Trending hip-hop artist momentum scores across four cultural dimensions.
Fetch
Freeby Modelcontextprotocol · Developer Tools
Web content fetching and conversion for efficient LLM usage
Git
Freeby Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
MCP Marketplace
Freeby mcp-marketplace · Developer Tools
Search and install MCP servers from inside your AI client.
