Skip to content

Use mapcv with AI agents

mapcv ships an MCP server and an agent skill. Together they let a coding agent go through the same journey you would, inspect the labels → write a config → validate → plan → generate → check, without guessing at config keys: the agent reads the schema from the real models and gets mapcv’s own error messages.

The server is an optional extra, so the base install stays small:

Terminal window
pip install "mapcv[mcp]"
mapcv mcp --help

Without the extra, mapcv mcp prints the install command and exits. The examples below use uvx, which runs the server without installing anything into your project; replace it with mapcv mcp if you installed mapcv yourself.

Every client starts the server the same way:

Terminal window
uvx --from "mapcv[mcp]" mapcv mcp # read-only, in the current folder
uvx --from "mapcv[mcp]" mapcv mcp --root ~/datasets # the only folder it may touch
uvx --from "mapcv[mcp]" mapcv mcp --root ~/datasets --allow-write

The environment variable MAPCV_MCP_ALLOW_WRITE=true does the same as --allow-write, for clients that configure servers through the environment (the Claude Code plugin uses it).

The plugin installs the skill and registers the server in one step:

/plugin marketplace add tahamukhtar20/mapcv
/plugin install mapcv@mapcv

The plugin asks once whether the agent may write datasets (off by default). The server works in the folder Claude Code was started in.

To register only the server, without the skill:

Terminal window
claude mcp add mapcv -- uvx --from "mapcv[mcp]" mapcv mcp
claude mcp add --scope project mapcv-write -- uvx --from "mapcv[mcp]" mapcv mcp --allow-write

Any other MCP client works too: the server speaks MCP over stdio.

An agent can run long jobs, download imagery and write files, so the server is built to be hard to misuse. These rules are enforced by the server, not left to the agent’s judgement.

Rule What it means
Read-only by default Without --allow-write the tools write_config, generate and split do not exist: the server does not list them, so there is nothing to call or refuse. The server’s instructions tell the agent which mode it is in, and that it must ask you to restart with --allow-write to write. The agent can still inspect labels, validate and plan, read a finished dataset, and compute stats and verify (which write stats.json or SHA256SUMS only with --allow-write).
One folder --root (default: the current folder) is the only place the server reads or writes. Every path an agent passes, and every path a config resolves to (labels.path, a local imagery.path, writer.staging_dir), must be inside it after symbolic links are resolved. .., absolute paths elsewhere and links that point out are refused with a message that names the root. A Shapefile’s sidecar files (.prj, .dbf, …) count as paths too, and so does a local file a STAC catalog names. In an existing dataset folder, a symbolic link that points outside and a hard link (a file with a second name, which may be outside) are refused before anything is written.
Plan first, confirm big jobs generate plans before it starts and refuses a job that plan marks large (over 20,000 tiles, about 5 GB of download plus output, or 1,000,000 patches) unless the call passes confirm_large=true. The refusal carries the plan, so the agent can show it to you and ask.
Credentials stay hidden Whatever url_template carries (keys in the query, tokens in the path, user information) is removed from every tool result and error, including the SDK’s own errors for arguments of the wrong type, the way the CLI shows only scheme://host/.... With --allow-local-urls, a url_template on a link-local or cloud metadata address (such as 169.254.169.254) gets a warning from validate_config and plan. Do not paste a key into the chat: write the config with a placeholder and put the real key into the file yourself.
Public addresses only The tools connect to public addresses only, so an agent (or a label file that talks it into a URL) cannot use the server to reach this machine or your private network. A URL that names localhost, a loopback, private, link-local or carrier-grade NAT address is an error in validate_config and plan; a host name that resolves to one, and a redirect to one, are refused when connecting. The refusal reads the same whether something listens there or not. To use a tile or GeoTIFF server on your own machine or network, start the server with --allow-local-urls.
Config files only write_config writes only .yaml or .yml files, and only after the content validates as a mapcv config.
Interruptible generate reports progress per chunk and stops when the call is cancelled. The chunk being written is finished first (minutes for a big region on one connection), then the run stops; finished chunks are kept and calling it again resumes. plan, info and the other read tools keep answering meanwhile.

What the server does not do: it does not limit which public hosts a config’s url_template or remote GeoTIFF may contact, and a config can name any public https:// imagery. Review the config the agent wrote, as you would any file it produces. Each imagery source’s license and credit line are listed under Providers & licensing.

Two things work differently from the CLI, because both would write outside --root: generate does not use the on-disk tile cache, and plan and generate refuse labels.osm, whose answer is cached there (run mapcv plan / mapcv generate in a terminal, or point labels.path at an OSM extract). validate_config and write_config accept labels.osm but return a warning saying so, so the agent learns it before it plans.

To get an OSM extract over MCP, have the agent fetch it with curl and convert it with jq; the skill has the commands. Overpass answers HTTP 406 to a request without a User-Agent, and can answer HTTP 200 with an HTML error page when it is busy (retry later, use a smaller box or a mirror such as overpass.kumi.systems).

Tool Needs --allow-write What it does
describe_config_schema no Every config model’s fields with type and default, generated from the pydantic models, plus the rules between fields (which tasks need labels, which image formats each imagery type accepts, the exact message of each invalid combination). Found by validating real configs, so it cannot go stale. About 3,000 tokens; section (for example labels) narrows it to one key, full_schema: true adds the raw JSON schema.
validate_config no The checks and messages of mapcv validate, for a file or for text, and more. Misspelt keys are reported together with the other independent errors, each with the closest valid key (“Did you mean ‘source’?”). The label and imagery files must exist, a labels.label_field must be an attribute of the label file (with a suggestion), and writer.stack_sources needs sources with matching bands: each is an error (valid: false), because plan or generate would fail. warnings carry what this server cannot do (labels.osm). Imagery is not read.
inspect_labels no Fields of a vector label file (GeoJSON, KML, GeoPackage, Shapefile or GeoParquet) with distinct values and counts, feature and geometry counts, the extent in lon/lat (usable as region) and which fields can be labels.label_field.
plan no Tiles, patches, sizes, memory and warnings, like mapcv plan, plus large and the reason. Downloads nothing. It does not estimate what it does not download: download_bytes is null for files and catalogs, and the Sentinel-2 scene is chosen at generate; notes say so. blocking lists problems that make generate stop (such as sources that cannot be stacked, or a grid of more than 3,000,000 patches, which plan counts without building). It runs while a generate runs.
info no A dataset’s task, every imagery source (sources; source is the first), class balance, split sizes and complete: false for a dataset whose generate was interrupted (call generate again to resume), null for one written by an older mapcv.
stats no (save needs it) Per-band mean and std, class balance and median-frequency class weights, like mapcv stats, for train (default), val, test or all. Writes stats.json only with save: true. warnings say when generate did not finish the dataset, since the numbers then cover only the patches written so far.
verify no (write_sums needs it) Whether every file of a dataset is present and intact (and matches SHA256SUMS, when there is one), like mapcv verify; deep decodes every image. Writes SHA256SUMS only with write_sums: true.
write_config yes Validates, then saves a config inside the root. Refuses to overwrite unless asked. A config whose files do not exist yet is saved as a draft, and the result lists the problems.
generate yes Plans, then builds or resumes the dataset, like mapcv generate. Output is byte-identical to the CLI’s.
split yes Re-splits a dataset from its manifest, like mapcv split.

Each result is structured JSON (also sent as text, for clients that only show text) with a one-line summary first. Errors are results marked as errors, with the message mapcv would print and no traceback.

A few things worth knowing when you read the results:

  • Naming the class. Without labels.label_field, every polygon is class 1, shown as labeled (segmentation) or object (detection, instance, classification). Name it by listing the file under labels.files with a class: labels: {files: [{path: buildings.geojson, class: building}]}.
  • Object counts. For detection and instance datasets info counts an object once in every patch that shows it, so a feature cut by a patch edge counts twice; plan.objects counts the features in the region. The two differ.
  • Instance id masks. With instance.id_mask: true, a mask value is the patch’s first, second, … instance, a per-patch number; it is not the COCO annotation id.
  • 0/255 change masks with change.before and change.after: set ignore_index: null on both label sets and change.change_value: 255.
  • Labels are local files. file:// and https:// work for imagery.path; labels.path must be a file in the project folder.

agent/skills/mapcv/SKILL.md in the repository teaches an agent the journey, config recipes per task (segmentation, detection, instance, classification, change, regression), imagery (XYZ, Earth Engine, Sentinel-2 as EOPF Zarr or STAC COGs, GeoTIFF/COG and mosaics) and labels (several files, OpenStreetMap, label rasters), where each source’s license and credit line are listed, and what to do about the real error messages. The Claude Code plugin installs it. For other agents that read SKILL.md skill folders, copy the agent/skills/mapcv/ folder into their skills folder.

Start the server with --allow-write in a folder that holds buildings.geojson, then ask:

Make a segmentation dataset of the buildings in buildings.geojson from Esri imagery at about 0.4 m per pixel, 256 px patches. Show me the plan before you download anything.

The agent calls inspect_labels (the file has a class field with two values and an extent), writes mapcv.yaml with write_config, runs validate_config and plan, and shows you the tile count, patch count and output size. After you agree it calls generate, then info. If a plan is marked large, it has to ask you first.

This site publishes plain-text versions for models: https://tahamukhtar20.github.io/mapcv/llms.txt (an index), llms-full.txt (every page) and llms-small.txt (a shorter version), all generated from these pages at build time.