@ultimaps/mcp
This server lets MCP clients (Claude, Codex, etc.) discover and render Ultimaps maps, returning images and reports directly in the conversation.
Render maps as PNG or SVG: choropleths from numbers, category maps, explicit region colors, and pinned locations for countries, states, provinces, counties, continents, the world, and US ZIP areas.
Customize output: titles, legends, color palettes, themes, region borders, labels, extra layers (lakes, roads, cities, rivers, borders), and image size/scale.
Fuzzy region matching: accepts names, codes, and aliases; auto-corrects typos and reports unmatched keys with suggestions.
Preview without rendering via
dryRunto validate region matching and the color plan.Discover maps and regions with
list_mapsandget_map_regions, including search and paging.Works keyless (with rate limits and attribution) or with an optional
ULTIMAPS_API_KEYfor higher quotas, larger output, SVG, and no attribution.Saves every render to a file on your machine and provides inline images, edit links, embeddable image URLs, and structured reports.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@ultimaps/mcpShow a choropleth of population by US state."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@ultimaps/mcp
An MCP server for Ultimaps. Ask Claude, Codex or any MCP client for a map and get the image back in the conversation. It covers choropleths from pasted numbers, category maps and pinned locations, for countries, states, provinces, counties, continents and the world.
"Map US states by population." · "Color the EU members by currency." · "Show France's regions with these unemployment rates: …" · "Pin our five offices on a map of Europe."
No account needed. Without an API key the server uses the keyless tier. Set ULTIMAPS_API_KEY for plan quotas and larger or unbranded output.
Can't run a local command? In ChatGPT (developer mode), claude.ai or any client that connects to remote MCP servers, add the hosted server instead: https://api.ultimaps.com/mcp. In ChatGPT, turn it on in each conversation where you want maps. It has the same three tools and nothing to install. See Hosted server. This package is the local version: requests come from your own IP address with your own key, and every image is saved to a file on your computer.
Tools
Tool | What it does |
| Renders a map as an image (PNG, or SVG with a Pro key), shown inline when it is small enough. Choropleth (numbers), categories (labels) or explicit colors, plus pins, title, a legend with its own heading, theme, region labels (names, values or both) and extra layers. Region names are matched fuzzily, and every correction or miss is reported with suggestions. |
| The map catalog: |
| One map's region keys and titles, 200 per page, searchable by key, title or alias. |
All three are read-only. Agents rarely need the discovery tools: render_map accepts region names, codes and aliases directly.
Related MCP server: mcp-maptiler
Install
Requires Node.js 20 or newer.
Claude Code
claude mcp add ultimaps -- npx -y @ultimaps/mcp
# with a key:
claude mcp add ultimaps --env ULTIMAPS_API_KEY=um_live_… -- npx -y @ultimaps/mcpCodex
codex mcp add ultimaps -- npx -y @ultimaps/mcp
# with a key:
codex mcp add ultimaps --env ULTIMAPS_API_KEY=um_live_… -- npx -y @ultimaps/mcpCodex writes TOML, not the JSON below. The equivalent entry in ~/.codex/config.toml (or .codex/config.toml in a trusted project):
[mcp_servers.ultimaps]
command = "npx"
args = ["-y", "@ultimaps/mcp"]Claude Desktop
Download ultimaps-<version>.mcpb from the latest release and open it: Claude Desktop installs it in one click and asks for the optional API key. Or add this to claude_desktop_config.json:
{
"mcpServers": {
"ultimaps": {
"command": "npx",
"args": ["-y", "@ultimaps/mcp"],
"env": { "ULTIMAPS_API_KEY": "" }
}
}
}Cursor
~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"ultimaps": { "command": "npx", "args": ["-y", "@ultimaps/mcp"] }
}
}VS Code
.vscode/mcp.json:
{
"servers": {
"ultimaps": { "type": "stdio", "command": "npx", "args": ["-y", "@ultimaps/mcp"] }
}
}Hosted server
You don't need this package to use the hosted server. Add the URL to your client:
claude mcp add --transport http ultimaps https://api.ultimaps.com/mcp
# with a key:
claude mcp add --transport http ultimaps https://api.ultimaps.com/mcp \
--header "Authorization: Bearer um_live_…"Clients with a config file take the URL alone, for example ~/.cursor/mcp.json:
{
"mcpServers": {
"ultimaps": { "url": "https://api.ultimaps.com/mcp" }
}
}The hosted server sends a key as an Authorization header, not as ULTIMAPS_API_KEY. In ChatGPT and claude.ai you can't set headers, so renders there use the anonymous tier: PNG up to 1600 px with attribution, up to 30 per hour per conversation. Every render also comes back as an image link that expires after 30 days. Images too large for the conversation, and all SVGs, come back only as the link. ChatGPT doesn't show images returned by tools, so the link is how you see the map there.
Configuration
Variable | Default | Purpose |
| none (keyless) | API key from Studio → Workspace → API. An invalid key is an error and never falls back to keyless. |
|
| Point at a local API during development ( |
Limits
Keyless | Free key | Pro key | |
Renders | 30/hour, 5/minute | 500/month, 50/day, 10/minute | 5,000/month, 1,000/day, 30/minute |
Output | PNG up to 1600 px, attribution | PNG up to 1600 px at scale 2 (a 3200 px raster), attribution | PNG up to 4000 px at scale 4, SVG, no attribution |
All keys in a workspace share the monthly quota. The daily and per-minute caps are per key, so a free key stops at 50 renders a day long before the month runs out.
Dry runs skip the monthly quota but still count against rate limits. When five or fewer keyless renders remain in the hour, the result says so.
What a render returns
The image, inline, when it is small enough (781 KB for PNG, 98 KB for SVG). The default 1200 px width usually fits. Every render is also written to
<tmpdir>/ultimaps-mcp/and the path is in the result, so in a terminal client the file is the thing you can actually open. Nothing is re-rendered or resampled on your behalf.A text report: auto-corrected and unmatched region keys with suggestions, the resolved choropleth plan, warnings (such as
layer_unavailableorlabels_overlap), a link to open the map in Ultimaps Studio for editing, and the render id for support.An embeddable image URL for keyless PNG renders whose whole spec fits a ~2,000-character URL, so small datasets only. The URL carries the spec, not the image, and renders keyless, so it is not offered when a key is set.
structuredContentwith the same report as data, described by the tool'soutputSchema.
Privacy Policy
This server runs on your machine, has no telemetry of its own and talks only to the Ultimaps API (https://api.ultimaps.com, or the ULTIMAPS_API_URL you set). The full Ultimaps privacy policy is at ultimaps.com/privacy-policy.
What is sent to Ultimaps. Tool calls are ordinary HTTPS requests to the Ultimaps API, so they arrive from your IP address with an ultimaps-mcp/<version> user agent. render_map sends the whole map request: region keys and values, pins, title and styling. If you set ULTIMAPS_API_KEY, it is sent with render requests only.
What Ultimaps keeps. Each render request is logged: the request content, the map and outcome, timing, your IP address and user agent, and for keyed requests which key and workspace made it. This lets support look up a render id, lets the "Open in Studio" link rebuild your map, and counts usage. The request content is kept for 30 days and the IP address and user agent for 90 days. What remains is a usage record, tied to your workspace when a key was used. Ultimaps uses a third-party error-monitoring service that may receive details of a failed request. Ultimaps does not sell this data or use it for advertising.
What stays on your machine. Every rendered image is written to <tmpdir>/ultimaps-mcp/ and this server never deletes it. What your MCP client keeps, including tool results in your conversation, is governed by its own privacy policy.
Shareable links. The embeddable image URL offered for small keyless renders contains the full map request. Anyone with the link can read it.
The hosted server. The server at https://api.ultimaps.com/mcp runs on Ultimaps' servers, not yours, so the section above about your machine doesn't apply to it. The full privacy policy covers it.
Questions or deletion requests: support@ultimaps.com, quoting the render id.
Development
npm install
npm run typecheck
npm test # unit + in-process protocol tests
npm run build # → dist/End-to-end over stdio against a real API (spends render units from your tier):
npm run build
node scripts/smoke.mjssrc/generated/render-request.schema.json is generated from the Ultimaps API contract; do not edit it by hand. Tool and property descriptions live in src/descriptions.ts. Arguments are never validated locally: the API is additive-only, and its 400 responses carry suggestions.
Issues and pull requests are welcome. Releases are cut by the Ultimaps team, and accepted changes ship with the next release.
The package publishes a bin and nothing else. import '@ultimaps/mcp' is not
supported and fails by name, because dist/index.js starts a stdio server when
it loads.
License
MIT © Ultimaps. See LICENSE.
Available Tools
3 toolsget_map_regionsGet map regionsARead-onlyIdempotentInspect
Look up one map: title, regionType, regionCount, labels, layers, and its regions as {key, title}, 200 per page.
You rarely need this before rendering. render_map accepts region names, codes and common aliases and auto-corrects typos, reporting what it matched.
Use it to build a dataset covering every region of a map, to see how a map names its regions, or to resolve a key render_map could not match.
query searches keys, titles and aliases, so no hits means the map really has no such region. Use offset for further pages.
| Name | Required | Description | Default |
|---|---|---|---|
| mapId | Yes | Map id from list_maps, e.g. "united-states", "europe", "united-states-california". | |
| query | No | Search region keys, titles and aliases, e.g. "saint" or "US-TX". Omit to page through every region. | |
| offset | No | Skip this many regions. Pages hold 200 regions; use the offset the previous result names. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only, idempotent, and open-world behavior, lowering the bar. The description adds meaningful behavioral depth: pagination limit of 200 per page, the region result shape, and the 'no hits means no such region' semantics for query. This goes beyond what annotations alone convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact yet information-dense, with no filler. It front-loads the operation and result shape, then gives usage guidance and parameter behavior in just a few sentences.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only lookup with no output schema, the description fully covers what is returned, pagination, query semantics, and when to prefer an alternative. An agent has enough context to call it correctly without further investigation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Input schema coverage is 100%, so the baseline is 3. The description adds slight interpretive value for query ('no hits means the map really has no such region') but mostly restates what the schema already documents about mapId, query, offset, and 200-region pages.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb and resource ('Look up one map'), enumerates the returned data fields, and clearly distinguishes this tool from render_map and list_maps. An agent can tell exactly what it will get and which sibling it should use instead.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says this tool is rarely needed before renderingainer and points to render_map as the alternative that accepts aliases and corrects typos. It then lists concrete use cases: building a full dataset, inspecting region naming, or resolving unmatched keys.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_mapsList mapsARead-onlyIdempotentInspect
List the maps Ultimaps can render: the world, continents, countries subdivided into states, provinces, counties or departments, and US ZIP code areas.
Each row is {id, title, regionType, layers, regionCount, labels}. id is the mapId for render_map and get_map_regions; it is the map's slug on ultimaps.com (the last path segment, not the whole URL) and always means the current edition.
regionCount is how many region keys a complete dataset needs. labels: false means the map has no region labels, so style.labels does nothing there. layers are the ids render_map's layers can turn on.
Pass query to search ids, titles, region types and categories (e.g. "counties", "germany", "africa").
Only call this when you don't know the id: common ids such as world, united-states and europe just work, and an unknown id usually comes back with the closest ids.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Search map ids, titles, region types and categories, e.g. "counties", "germany", "africa". Omit to list every map. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (readOnlyHint, openWorldHint, idempotentHint), the description discloses detailed output behavior: exact row shape, meaning of id as a slug that always refers to the current edition, the implication of labels:false on style.labels, and how layers relate to render_map. This gives agents actionable behavioral context not present in annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but every sentence adds value: it starts with the core purpose, then explains row semantics, then query, then when to call. Advice like using common ids without listing is actionable. There is no filler and the structure front-loads the most important information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given there is no output schema, the description fully compensates by specifying the output shape and the meaning of each field, including relationships to sibling tools and the practical consequence of labels:false. It also covers query behavior and fallback behavior for unknown ids, so an agent has everything needed to call and interpret the result.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema already covers the single parameter (query) with a description identical to the one in the tool description, including the 'omit to list every map' behavior. With 100% schema coverage, the description adds no new meaning beyond repeating examples, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'List the maps Ultimaps can render' and enumerates the kinds of maps (world, continents, countries subdivided into states, provinces, counties, departments, US ZIP areas). It also links the output to sibling tools ('id is the mapId for render_map and get_map_regions'), differentiating it as the catalog tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to call it: 'Only call this when you don't know the id' and gives concrete exceptions ('common ids such as world, united-states and europe just work'). It also explains how to narrow results with query, giving examples. This directly guides tool selection against siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
render_mapRender mapARead-onlyInspect
Use this whenever the user asks for a map of real places: coloring countries, states, counties or ZIP codes by data, a choropleth or heat map, a map with a legend, or pins on a map. Prefer it to generating an image or plotting with code. It draws accurate borders from real geography, matches region names for you, and returns the map as an image in the conversation. Maps cover the world, continents, countries, their states, provinces and counties, and US ZIP code areas. Color regions with ONE of these (choropleth and categories are mutually exclusive):
choropleth: a number per region, e.g. {"values": {"California": 39.5, "Texas": 30.5}}. Classes, breaks and palette are picked from the data unless you set them. Say what the numbers are withkind(andmidpointwhen they have a meaningful center,unitfor a short label unit): that is what gets percents, currencies, years and diverging scales right. When the data does not cover every region of the map, addnoData: trueso the legend explains the uncolored ones.categories: a label per region, e.g. {"values": {"France": "Euro", "Poland": "Złoty"}}. Colors are assigned automatically unless you setcolors.regions: explicit hex colors, e.g. {"Texas": "#1D4ED8"}. Also works on top of either mode as an override. Region keys can be names, ISO/FIPS-style codes or common aliases. Typos are auto-corrected, and every correction or unmatched key is reported with suggestions, so there is no need to call get_map_regions first. GetmapIdfrom list_maps if you don't know it. Also available: pins (locationswith lat/lon),title,legend(position,title,subtitle),style(theme, borders, region labels;style.labels.content: "value"prints each region's value instead of its name and"name_value"stacks the name above the value, both needingchoroplethorcategories;style.labels.fit(default "auto") shortens labels that don't fit or overlap and never hides one; any still overlapping are reported in alabels_overlapwarning with their keys), extralayers, andoutputsize. Always give a data legend alegend.title: what is measured, its unit and the year ("Median household income (US$, 2023)"). An untitled legend leaves the reader guessing. Short units ("%", "°C", "t") go inchoropleth.unit, long ones ("litres per person") in the legend title.dryRun: truevalidates and previews region matching and the color plan without rendering. Without an API key: 30 renders per hour and 5 per minute (dry runs count), PNG up to 1600 px wide, with Ultimaps attribution on the image. The default 1200 px width is right for viewing in chat. Larger widths and scales are for files and are saved to disk instead of shown. Formats: png, or svg with a Pro key. PDF is not available. Every render is also written to a file and the path reported. Relay that path: in a terminal client the inline image is visible to you but NOT to the user, and the file is the only thing they can open. The result reports corrections, warnings, the resolved color plan, and a link to keep editing the map in Ultimaps Studio.
| Name | Required | Description | Default |
|---|---|---|---|
| mapId | Yes | Map id from list_maps, e.g. `world`, `united-states`, `europe`, `united-states-california`. It is the map's slug on ultimaps.com (the last path segment, not the whole URL) and always resolves to the current edition. | |
| style | No | ||
| title | No | ||
| dryRun | No | Validate and preview without rendering: returns the region matching report, the resolved choropleth plan and a legend preview. No image and no monthly quota for keyed calls, but it still counts against the rate limit (one of the 30 keyless renders per hour). | |
| layers | No | Extra geographic layers to draw, e.g. {"cities": true, "roads": true}. Each map ships a subset (`layers` in list_maps and get_map_regions). Turning on a layer the map lacks is not an error: it is skipped with a `layer_unavailable` warning. Omitted layers are off, except layers a map turns on by default (currently `admin0` on ZIP-code maps); pass false to hide those. | |
| legend | No | ||
| output | No | Image size and format. Keep the default width (1200 px) for images viewed in the conversation: much larger renders, and any scale of 2 or more, are too big to show inline and are saved to a file instead. Without an API key: width up to 1600 px, an explicit height up to 1600 px, scale 1. Hard caps: width×scale and height×scale ≤ 8192, and at most 4096×4096 pixels in total. | |
| regions | No | Override layer. Region key to hex color, applied AFTER choropleth/categories coloring and winning over it. Precedence: choropleth/categories paint first, `regions` overrides win, and everything else gets `choropleth.noDataColor` (choropleth mode), else `style.defaultRegionColor`, else the theme default (in choropleth mode, a contrasting gray when the theme's region color is too close to the palette's lightest class). | |
| locations | No | Pin markers. They compose with every mode. | |
| categories | No | Categorical coloring mode. Mutually exclusive with `choropleth`. Unmatched keys simply leave their regions uncolored. The no-data fill is `style.defaultRegionColor`, and `noDataColor` does not apply here. | |
| choropleth | No | Numeric choropleth mode. Mutually exclusive with `categories` (one scale per map). `regions` composes with it as an override layer. If none of the keys in `values` match, the request fails even under `onUnmatched: warn`, since there is nothing to build a scale from. `type`, `classes`, `method` and `palette` are all optional: whatever you leave out is suggested from the data, and the resolved plan is reported in the result. Do send `kind` whenever you know what the numbers are, `midpoint` when they have a meaningful center, and a short `unit`: the values alone cannot tell 42% from 0.42 or a change from a level. | |
| onUnmatched | No | warn = render anyway (unmatched regions get the no-data fill) and report. error = 400 with per-key suggestions. Recommended for production pipelines: error. Exception: in choropleth mode, ZERO matched keys is a 400 even under warn. Region keys in `style.labels.hide` count too: a typo there is a 400 under error. | warn |
Output Schema
| Name | Required | Description |
|---|---|---|
| file | No | Set when the image was saved to disk (too large to inline, or SVG). |
| dryRun | Yes | |
| format | No | Image format; absent on dry runs. |
| legend | No | Dry runs: legend preview, [{label, color}] or {points, stops} for a gradient. |
| editUrl | No | Opens this map in Ultimaps Studio for editing or export. |
| embedUrl | No | Keyless GET image URL, when the render is shareable that way. |
| renderId | Yes | The API render id; quote it to Ultimaps support. |
| warnings | Yes | |
| choropleth | No | The resolved choropleth plan. Dry runs add `reason` and `breaks`. |
| regionMatching | Yes | |
| keylessRemaining | No | Keyless renders left in the current hour. |
| matchingTruncated | Yes | True when `corrected`/`unmatched` list only the first 5 of their counts. Repeat the call with dryRun: true for the full report. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With readOnlyHint/openWorldHint already declaring safety, the description still adds substantial operational context beyond annotations: rate limits (30/hour, 5/minute, dry runs count), keyless PNG width cap and Ultimaps attribution, the fact every render is written to a file and the path must be relayed, and that inline images are invisible to terminal users. It also explains the dryRun preview and the labels_overlap warning behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the use case and alternative before diving into mode details, which is correct ordering. It is long, but the length is justified by a 12-parameter nested tool; a few sentences (e.g. the label-fit mechanics restated from schema) could be trimmed without loss.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists and the description still flags the key result behaviors (corrections/warnings reported, resolved color plan, Studio link, file path) so the agent knows what to relay. Given the complexity and nested schema, the description is complete enough to call the tool correctly on first attempt.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 75%, and the prose adds genuinely new semantics beyond it: choropleth and categories are mutually exclusive, region keys accept names/codes/aliases with auto-correction, kind/midpoint/unit drive diverging vs sequential decisions, and an untitled legend is called out. This is description carrying meaning the schema cannot express.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb (render/draw) and concrete resource (a map of real places) with a scoping trigger ('whenever the user asks for a map'). It enumerates the actual outputs (choropleth, heat map, legend, pins) so an agent can match intent to tool without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use it ('whenever the user asks for a map of real places'), when to prefer it over alternatives ('prefer it to generating an image or plotting with code'), and routes to siblings: mapId comes from list_maps, and 'there is no need to call get_map_regions first.' Alternatives and prerequisites are both covered.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.2.0- Changed
render_map28 fields changed- changed
Input schema / properties / choropleth / descriptionPrevious value: -"Numeric choropleth mode. Mutually exclusive with `categories` (one scale per map). `regions` composes with it as an override layer. If none of the keys in `values` match, the request fails even under `onUnmatched: warn`, since there is nothing to build a scale from. `type`, `classes`, `method` and `palette` are all optional: whatever you leave out is suggested from the data, and the resolved plan is reported in the result."New value: +"Numeric choropleth mode. Mutually exclusive with `categories` (one scale per map). `regions` composes with it as an override layer. If none of the keys in `values` match, the request fails even under `onUnmatched: warn`, since there is nothing to build a scale from. `type`, `classes`, `method` and `palette` are all optional: whatever you leave out is suggested from the data, and the resolved plan is reported in the result. Do send `kind` whenever you know what the numbers are, `midpoint` when they have a meaningful center, and a short `unit`: the values alone cannot tell 42% from 0.42 or a change from a level." - changed
Input schema / properties / choropleth / properties / format / descriptionPrevious value: -"Number format for every choropleth surface: class labels, gradient ticks and value labels. Always pass an object, e.g. {\"preset\": \"compact\"}, never a bare string. Presets: `auto` (default, inferred from magnitude), `plain` (39,538,223), `compact` (39.5M), `percent` (the input is a FRACTION, so 0.42 prints as 42% and 0.423 as 42.3%, because the preset trims trailing zeros. An explicit {\"style\": \"percent\", \"decimals\": 1} keeps them and prints 42.0%. For data already in percent points use {\"suffix\": \"%\"}), `currency` ($39,538,223). Or combine options, e.g. {\"decimals\": 1, \"suffix\": \"M\"} or {\"style\": \"currency\", \"currency\": \"EUR\"}. {\"d3\": \"~s\"} is a raw d3-format escape hatch. The result echoes the resolved format."New value: +"Number format for every choropleth surface: class labels, gradient ticks and value labels. Always pass an object, e.g. {\"preset\": \"compact\"}, never a bare string. Presets: `auto` (default, inferred from magnitude), `plain` (39,538,223), `compact` (39.5M), `percent` (the input is a FRACTION, so 0.42 prints as 42% and 0.423 as 42.3%, because the preset trims trailing zeros. An explicit {\"style\": \"percent\", \"decimals\": 1} keeps them and prints 42.0%. For data already in percent points use {\"suffix\": \"%\"}), `currency` ($39,538,223). Or combine options, e.g. {\"decimals\": 1, \"suffix\": \"M\"} or {\"style\": \"currency\", \"currency\": \"EUR\"}. {\"d3\": \"~s\"} is a raw d3-format escape hatch. Add \"locale\" for a non-US number style: \"de-DE\" (1.234,5 / 1,2 Mio.), \"fr-FR\" (1 234,5), \"es-ES\", \"it-IT\", \"pt-BR\", \"de-CH\" (1’234.5), \"en-IN\" (12,34,567 / 12L), \"nl-NL\", \"pl-PL\", \"sv-SE\"; it combines with presets and d3, and {\"locale\": \"de-DE\"} alone is the automatic format in that style. The result echoes the resolved format." - added
Input schema / properties / choropleth / properties / format / properties / localeAdded value: +{ + "default": "en-US", + "description": "Number style: separators, currency placement and compact suffixes. `{\"decimals\": 1, \"locale\": \"de-DE\"}` turns 1234567.5 into \"1.234.567,5\"; with `compact: true` it gives \"1,2 Mio.\". Combines with `preset` and `d3`; on its own (`{\"locale\": \"de-DE\"}`) it is the automatic format in that number style.\n", + "enum": [ + "en-US", + "de-DE", + "fr-FR", + "es-ES", + "it-IT", + "pt-BR", + "de-CH", + "en-IN", + "nl-NL", + "pl-PL", + "sv-SE" + ], + "type": "string" +} - added
Input schema / properties / choropleth / properties / kindAdded value: +{ + "description": "What the numbers are. Send it whenever you know. `percent` = already ×100 (42 prints \"42%\"), `fraction` = a 0–1 share (0.42 prints \"42%\"), `currency` (set `unit` to the code, e.g. \"EUR\"), `year` (1788, no separator), `change` (signed labels, diverging around 0), or `count`, `rate`, `index`. Once a kind is given, the palette is diverging only with `midpoint` or `kind: \"change\"`, so data that merely dips below zero stays sequential.", + "enum": [ + "count", + "rate", + "percent", + "fraction", + "currency", + "change", + "year", + "index" + ], + "type": "string" +} - added
Input schema / properties / choropleth / properties / legendEndLabelsAdded value: +{ + "description": "Steps legends: also print the two outer ends of the bar (ignored for groups and gradient legends). An end that the automatic breaks left open, or that the data reaches past, reads \"< 10\" / \"≥ 90\" rather than a round bound the data never reaches. Groups legends already print every range and gradients their ends, so only steps change. Default false.\n", + "type": "boolean" +} - changed
Input schema / properties / choropleth / properties / method / descriptionPrevious value: -"Break method. Absent = suggested from the data. quantile = equal number of regions per class, equalInterval = equal-width value intervals, jenks = natural breaks (minimal within-class variance), pretty = rounded human-readable edges. Additive-only enum: more methods may be added, none removed.\n"New value: +"Break method. Absent = round, publishable edges picked from the data (a constant step such as 0 · 10 · 20 · 30, or a 1-2-5 ladder such as 0 · 10K · 25K · 50K · 100K for skewed data), reported as `pretty`. quantile = equal number of regions per class, equalInterval = equal-width value intervals, jenks = natural breaks (minimal within-class variance), pretty = rounded human-readable edges. Additive-only enum: more methods may be added, none removed.\n" - added
Input schema / properties / choropleth / properties / midpointAdded value: +{ + "description": "The meaningful center of the data: 0 for a change or balance, 100 for an index where 100 is the reference (EU27 = 100), 1 for a ratio. Gives a diverging palette with its neutral color on this value. Leave it out for one-directional data.", + "type": "number" +} - added
Input schema / properties / choropleth / properties / noDataAdded value: +{ + "additionalProperties": false, + "description": "Adds a \"No data\" row to the legend for regions without a value: true, or {\"label\": \"Not reported\"}. Off unless you send it. Send it whenever your data does not cover the whole map (the result reports how many regions have no value); without it those regions are drawn in a gray the legend does not explain. With full coverage the row is left out. A `nodata_low_contrast` warning means the default fill for those regions is hard to tell from the palette (gray palettes): send `noDataColor`.", + "properties": { + "label": { + "maxLength": 40, + "minLength": 1, + "type": "string" + } + }, + "type": [ + "boolean", + "object" + ] +} - changed
Input schema / properties / choropleth / properties / noDataColor / descriptionPrevious value: -"Fill for regions absent from `values`. Choropleth mode only. Overrides `style.defaultRegionColor` when both are set.\n"New value: +"Fill for regions absent from `values`. Choropleth mode only. Overrides `style.defaultRegionColor` when both are set. When neither is set, the theme's region color is used if it stays apart from the palette (CIEDE2000 ≥ 12 from the lightest class, ≥ 8 from every class) or is transparent by design; otherwise a light, slightly theme-tinted gray that does and keeps the region borders visible, so missing data never reads as the lowest class or as the neutral of a diverging palette. Gray palettes can leave no such color: the theme color is then darkened and a `nodata_low_contrast` warning asks for an explicit `noDataColor`.\n" - changed
Input schema / properties / choropleth / properties / palette / descriptionPrevious value: -"ColorBrewer palette id (the server matches case-insensitively). Absent = suggested from the data: diverging when the values cross zero (rdbu when they sit symmetrically around zero, rdylgn when they do not), sequential blues otherwise. Note that `rdylgn` only comes back out. It is not in the enum above, and requesting it is a `400`. Sequential palettes suit one-directional data, and diverging palettes (rdbu, brbg, spectral, piyg, prgn, puor, rdgy, rdylbu) suit data around a midpoint.\n"New value: +"ColorBrewer palette id (the server matches case-insensitively). Absent = suggested: `rdbu` centered on `midpoint` when one is given (or `kind` is `change`); with no hints, `rdbu` centered on 0 when the values spread on both sides of zero; sequential blues otherwise. Sequential palettes suit one-directional data, and diverging palettes (rdbu, brbg, spectral, piyg, prgn, puor, rdgy, rdylbu) suit data around a midpoint.\n" - added
Input schema / properties / choropleth / properties / unitAdded value: +{ + "description": "Short unit printed on every label, up to 16 characters: \"%\", \"°C\", \"t\", \"km²\". Put long units (\"litres of pure alcohol per person\") in `legend.title` instead. With `kind: \"currency\"` it is the currency code or symbol. Ignored for `kind: \"fraction\"` and when `format` is set, except {\"locale\": \"de-DE\"} (or {\"preset\": \"auto\", \"locale\": \"de-DE\"}), which keeps kind and unit in that number style. An explicit `format` decides only the labels: `kind` and `midpoint` still decide sequential vs diverging.", + "maxLength": 16, + "minLength": 1, + "type": "string" +} - changed
Input schema / properties / layers / properties / admin0 / descriptionPrevious value: -"Surrounding land, as a filled area of neighbouring countries or states."New value: +"Surrounding land, as a filled area of neighboring countries or states." - changed
Input schema / properties / layers / properties / cities / descriptionPrevious value: -"Major cities as labelled points."New value: +"Major cities as labeled points." - changed
Input schema / properties / legend / properties / position / descriptionPrevious value: -"left/right take a column carved from the map area (180 px, at most 30% of the width); top/bottom take a horizontal band between the title and the map. Absent = left for item/group legends, bottom for gradient/steps. Gradient and steps legends are horizontal-only, so left/right becomes bottom with a warning. Legends do not wrap: a wide legend on a narrow image can overflow, reported as a `legend_overflow` warning."New value: +"left/right take a column carved from the map area (180 px, widened up to 260 px for long labels or a legend title, at most 30% of the width); top/bottom take a horizontal band between the title and the map. Absent = picked per render: inside the map over empty space (sea, the margin beside a narrow map) when the legend fits there clear of land, else gradient/steps go to the bottom band and item/group legends to the left column or the bottom band, whichever leaves the map larger. Leave it out unless you need the legend outside the map. Gradient and steps legends are horizontal-only, so left/right becomes bottom with a warning. Band items wrap into rows; a bar too wide for a narrow image is reported as a `legend_overflow` warning." - added
Input schema / properties / legend / properties / subtitleAdded value: +{ + "description": "Line under the legend title, at the label size in the regular weight, e.g. the source or the base year. Same wrapping as `title`.\n", + "maxLength": 160, + "minLength": 1, + "type": "string" +} - added
Input schema / properties / legend / properties / titleAdded value: +{ + "description": "Heading above the legend rows. Send one for every data legend: what is measured, its unit and the year, e.g. \"Median household income (US$, 2023)\". It is never derived from `choropleth.unit`: short units (\"%\", \"°C\") go in `unit` and print on every label, long ones belong here. Long titles wrap to fit the legend; `\\n` forces a break.", + "maxLength": 120, + "minLength": 1, + "type": "string" +} - changed
Input schema / properties / onUnmatched / descriptionPrevious value: -"warn = render anyway (unmatched regions get the no-data fill) and report. error = 400 with per-key suggestions. Recommended for production pipelines: error. Exception: in choropleth mode, ZERO matched keys is a 400 even under warn.\n"New value: +"warn = render anyway (unmatched regions get the no-data fill) and report. error = 400 with per-key suggestions. Recommended for production pipelines: error. Exception: in choropleth mode, ZERO matched keys is a 400 even under warn. Region keys in `style.labels.hide` count too: a typo there is a 400 under error.\n" - changed
Input schema / properties / output / properties / height / descriptionPrevious value: -"Optional. When omitted, derives as a snug fit: the map's aspect ratio applied to the map area that remains after the title band (50 px) and any left/right legend column take their cut. A top/bottom legend adds its band (60 px for item legends, 70 px for gradient/steps bars) on top of that. An explicit height is delivered exactly, and the title and legend then shrink the map rather than the canvas.\n"New value: +"Optional. When omitted, derives as a snug fit: the map's aspect ratio applied to the map area that remains after the title band (50 px) and any left/right legend column take their cut. A top/bottom legend adds its band (60 px for item legends, 70 px for gradient/steps bars, plus the height of a `legend.title`/`subtitle`) on top of that. A left/right legend too tall for that canvas (many items, a wrapped title) grows the derived height until it fits. An explicit height is delivered exactly, and the title and legend then shrink the map rather than the canvas.\n" - changed
Input schema / properties / regions / descriptionPrevious value: -"Override layer. Region key to hex color, applied AFTER choropleth/categories coloring and winning over it. Precedence: choropleth/categories paint first, `regions` overrides win, and everything else gets `choropleth.noDataColor` (choropleth mode), else `style.defaultRegionColor`, else the theme default.\n"New value: +"Override layer. Region key to hex color, applied AFTER choropleth/categories coloring and winning over it. Precedence: choropleth/categories paint first, `regions` overrides win, and everything else gets `choropleth.noDataColor` (choropleth mode), else `style.defaultRegionColor`, else the theme default (in choropleth mode, a contrasting gray when the theme's region color is too close to the palette's lightest class).\n" - changed
Input schema / properties / style / properties / labels / descriptionPrevious value: -"Region labels, off by default. Each map ships its own curated label set; on a map with `labels: false` in list_maps, `show: true` renders nothing and a `labels_unavailable` warning is reported."New value: +"Region labels, off by default. Each map ships its own curated label set; on a map with `labels: false` in list_maps, `show: true` renders nothing and a `labels_unavailable` warning is reported. `content`: \"name\" (default), \"value\" (the choropleth number in the legend's format, or the category label; regions without data get no label) or \"name_value\" (name above value; regions without data show the name only). `fit`: \"auto\" (default) shortens labels that don't fit or overlap (to the value on a choropleth, the name on a category map); labels never disappear, and any still overlapping are listed in a `labels_overlap` warning. \"off\" draws every label in full. `hide`: region keys whose label never shows (matched like data keys). A typo is auto-corrected or reported as unmatched; under `onUnmatched: \"error\"` an unmatched key fails the request like a data key." - changed
Input schema / properties / style / properties / labels / properties / content / descriptionPrevious value: -"What each label prints. `name` is the region's name. `value` prints the region's choropleth number instead, formatted by the resolved `choropleth.format` (the same string the legend and tooltips show). Regions without a value get no label. Requires `choropleth`. With `categories`, or with neither, it is a `validation_error`. Labels are not collision-checked: a long number in a small region overflows it, exactly as in Studio."New value: +"What each label prints. `name` is the region's name. `value` prints the region's value instead: the choropleth number formatted by the resolved `choropleth.format` (the same string the legend and tooltips show), or the category label. Regions without a value get no label. `name_value` prints the name with the value on a line below it; regions without a value show the name only. Both value modes require `choropleth` or `categories`; with neither it is a `validation_error`." - changed
Input schema / properties / style / properties / labels / properties / content / enumPrevious value: -[ - "name", - "value" -]New value: +[ + "name", + "value", + "name_value" +] - added
Input schema / properties / style / properties / labels / properties / fitAdded value: +{ + "default": "auto", + "description": "`auto` shortens labels that don't fit or overlap and never hides one; `off` draws every label in full. Under `auto` a `name_value` label too big for its region, or overlapping a higher-priority one, drops to its value (choropleth; the legend only gives a range) or its name (categories; the fill already says the category). Priority: curated outside labels (small regions labeled beside the map), then larger regions. Labels still overlapping are drawn and reported in a `labels_overlap` warning. Studio's own default is off; a map opened in Studio from a render keeps the value sent.", + "enum": [ + "auto", + "off" + ], + "type": "string" +} - added
Input schema / properties / style / properties / labels / properties / hideAdded value: +{ + "description": "Region keys whose label never shows. Matched like data keys; corrections and misses are reported in `regionMatching`. Ignored with a `label_lists_ignored` warning when `show` is false.", + "items": { + "maxLength": 200, + "minLength": 1, + "type": "string" + }, + "maxItems": 5000, + "type": "array" +} - added
Output schema / properties / choropleth / properties / regionsWithoutDataAdded value: +{ + "description": "Regions of the map with no value, drawn in the no-data color.", + "minimum": 0, + "type": "integer" +} - added
Output schema / properties / warnings / items / properties / countAdded value: +{ + "description": "`labels_overlap` only: region labels still overlapping.", + "minimum": 0, + "type": "integer" +} - added
Output schema / properties / warnings / items / properties / keysAdded value: +{ + "description": "`labels_overlap` only: first overlapping region keys (up to 10).", + "items": { + "type": "string" + }, + "type": "array" +} - added
Output schema / properties / warnings / items / properties / shortenedAdded value: +{ + "description": "`labels_overlap` only: labels shortened.", + "minimum": 0, + "type": "integer" +}
3 tool updates
v0.1.0- First observed
get_map_regions - First observed
list_maps - First observed
render_map
TDQS
Scored across 3 tools
The three tools split cleanly along action lines: list_maps enumerates available maps, get_map_regions inspects one map's regions, and render_map produces an image. The descriptions even pre-empt the most likely confusion by telling the agent it rarely needs get_map_regions before rendering and should only call list_maps when the id is unknown.
All three follow a strict snake_case verb_noun pattern (render_map, get_map_regions, list_maps), with the verb accurately signalling the operation type. No mixing of conventions or vague verbs.
Three tools is lean but each earns its place in a clear discover→inspect→render pipeline, and the server has a narrowly scoped domain. It sits just under the typical 3-15 comfort range, but nothing feels redundant or padded.
The core workflow is fully covered: enumerate maps, resolve/verify region keys, and render with validation via dryRun plus file output. Minor gaps exist around post-render management (no tool to fetch, re-edit, or delete previously rendered maps beyond the Studio link), but agents can work around these.
Maintenance
Related MCP Connectors
Hosted MCP for live place search, geocoding & turn-by-turn routes. No Maps API key to manage.
Turn rows into bar, line, pie and scatter chart images plus an optional PDF or HTML report.
Routing, geocoding, places, matrix and map tools for location-aware AI workflows.
Maps for agents: truck/ADR routing, geocoding, places, matrices, isochrones, EV and fleet planning.
Related MCP Servers
AlicenseBqualityCmaintenanceEnables AI agents to become geospatially intelligent assistants with tools for location search, smart routing, round trip planning, reverse geocoding, isochrone analysis, route visualization, geofence management, and interactive map display.813 npm7Apache 2.0- AlicenseNot gradedqualityBmaintenanceEnables geocoding, reverse geocoding, elevation profiles, static map PNGs, and coordinate reprojection using MapTiler's OpenStreetMap data.6 npmMIT
- FlicenseNot gradedqualityBmaintenanceEnables interactive map display with up to 50 marked places and optional route lines, using a bundled Leaflet UI and OpenStreetMap tiles.-
- AlicenseNot gradedqualityDmaintenanceEnables geocoding, reverse geocoding, place search, nearby discovery, routing, and distance calculations using Mappls/MapMyIndia location data for India.17 npmMIT