MCP Server
Connect Claude, Cursor, or your own MCP client to Mapiry and manage maps, locations, categories, tags, and layers with tools instead of HTTP calls
Updated October 11, 2026
Mapiry also speaks the Model Context Protocol, so MCP clients — Claude Desktop, Cursor, and others — can discover and call the API without hand-written glue.
The endpoint is:
https://mapiry.com/mcpEverything below about authentication, scopes, rate limits, and ownership applies unchanged. There is no separate MCP credential: use the same API key as the REST API.
Configuring a client
Most clients take a JSON block. Use the same key you created earlier:
{
"mcpServers": {
"mapiry": {
"type": "http",
"url": "https://mapiry.com/mcp",
"headers": {
"Authorization": "Bearer mk_live_your_key_here"
}
}
}
}Where that goes:
| Client | Location |
|---|---|
| Claude Desktop | claude_desktop_config.json → mcpServers |
| Cursor | Settings → MCP → Add server |
| Cline / Continue / most IDE agents | Their MCP server list |
The Authorization header must be sent on every request, including the first initialize. Requests without a key get 401 MISSING_API_KEY.
Verify it works
Do this before debugging your client. If the handshake succeeds, the problem is on the client side.
With MCP Inspector — the official test client:
npx @modelcontextprotocol/inspector@latestOpen the URL it prints, choose Streamable HTTP, enter https://mapiry.com/mcp, and add your key under Authorization as Bearer mk_live_.... You should see 24 tools listed. Selecting one shows its schema and lets you call it.
With curl — a raw handshake:
curl -X POST https://mapiry.com/mcp \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'A working response contains serverInfo: { name: "mapiry", version: "1.0.0" }.
Then ask what it can do:
curl -X POST https://mapiry.com/mcp \
-H "Authorization: Bearer mk_live_your_key_here" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'A 401 here means auth, not MCP — see the troubleshooting table below. A 200 with tools means the server is fine and your client is misconfigured.
ChatGPT does not work with this server yet
This is worth knowing before you try it.
ChatGPT cannot send a custom API key. Its connector framework authenticates users through an OAuth 2.1 flow or runs anonymously — there is no field for a static bearer token. OpenAI's documentation is explicit about this: ChatGPT "does not support machine-to-machine OAuth grants such as client credentials, service accounts, or JWT bearer assertions, nor can it present custom API keys."
So pointing ChatGPT at https://mapiry.com/mcp from Settings → Plugins → Add custom MCP server will fail at the authentication step, no matter which key you hold. The API key in the block above is a static credential, and ChatGPT will not send it.
What works with ChatGPT today:
| Path | How | Works now? |
|---|---|---|
| ChatGPT app / connectors | Plugins → Add custom MCP server | No — needs OAuth |
Responses API (tools: [{ type: "mcp" }]) | Pass the key in the tool's authorization field | Yes, if you build the integration yourself |
The Responses API path is programmatic, not a ChatGPT setting — it is for code you run, not the chat product:
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-6-astra",
"input": "List my maps and add a bakery to the Tokyo one",
"tools": [
{
"type": "mcp",
"server_label": "mapiry",
"server_url": "https://mapiry.com/mcp",
"authorization": "mk_live_your_key_here"
}
]
}'Two caveats. The API does not store the authorization value, so you must resend it on every request. And the docs describe that field as an OAuth access token — Mapiry sends it as Authorization: Bearer <key>, which is what the MCP spec requires, but this combination has not been tested against a live key.
What ChatGPT support would require: an OAuth 2.1 authorization server in front of the MCP endpoint, conforming to the MCP authorization specification — protected-resource metadata, an authorization server with discovery documents, PKCE, and the resource parameter echoed through the flow. OpenAI strongly recommends using an established identity provider rather than building one. That is a substantial piece of work and a product decision, not a configuration change.
If ChatGPT support matters, that is the next change to scope.
Common workflows
Every workflow starts with list_maps. Tools take map IDs, not names or slugs, and there is no tool that accepts a slug — so if you do not already know the ID, look it up first.
Add a place to an existing map
list_maps→ find the map'sidcreate_locationwith{ mapId, name, lat, lng }
list_maps
→ { data: { maps: [{ id: "clxyz...", title: "Tokyo Coffee", ... }] } }
create_location { mapId: "clxyz...", name: "Fuglen Tokyo", lat: 35.6654, lng: 139.7006 }
→ { data: { id: "loc...", name: "Fuglen Tokyo", ... } }Build a map from a list of places
create_mapwith{ title, country, city }bulk_create_locationswith{ mapId, locations: [...] }— up to 100 per call
create_map { title: "Best Coffee in Tokyo", country: "Japan", city: "Tokyo" }
→ { data: { id: "clxyz...", slug: "best-coffee-in-tokyo" } }
bulk_create_locations { mapId: "clxyz...", locations: [
{ name: "Fuglen Tokyo", lat: 35.6654, lng: 139.7006 },
{ name: "Bear Pond Espresso", lat: 35.6585, lng: 139.6968 }
]}
→ { data: { created: [ ... ], count: 2 } }For more than 100 places, call bulk_create_locations repeatedly — one batch per call. Each batch is atomic on its own.
Tag and categorize as you go
create_location accepts tag names, created on demand. Categories work the same way via category:
create_location {
mapId: "clxyz...",
name: "Fuglen Tokyo",
lat: 35.6654, lng: 139.7006,
category: "Coffee",
tags: ["wifi", "outdoor-seating"]
}Leave tags out to keep a location's existing tags. Send [] to remove them all. Matching is case-insensitive, so "Wifi" and "wifi" are the same tag.
Read-only review
With a READ_ONLY key, list_maps, get_map, list_locations, list_categories, list_tags, and list_layers all work. Write tools return INSUFFICIENT_SCOPE.
list_maps → list_locations { mapId } → list_categories { mapId }Before you start
Four things that trip people up.
There is no geocoding tool. You must supply lat and lng yourself. If you have addresses rather than coordinates, geocode them first — the API will not guess, and a location with wrong coordinates fails silently rather than erroring.
bulk_create_locations is all-or-nothing. One invalid row and nothing is created. If a large import keeps failing, bisect the list to find the bad row rather than resubmitting it whole.
list_maps is not optional. There is no lookup by title or slug. A client that needs a map ID must call list_maps and match on the title itself.
Deletes are permanent. delete_map removes the map with all of its locations, categories, and tags. There is no undo.
Pagination: pageSize is capped at 200, and out-of-range values are clamped rather than rejected. page starts at 1.
What you can do
Each tool performs the same operation as its REST endpoint, with the same rules and the same { "data": ... } response — so a client needs one parser for both.
Maps
| Tool | What it does |
|---|---|
list_maps | List your maps with location counts — start here to discover map IDs |
create_map | Create a map |
get_map | Get a map with its categories and tags |
update_map | Update a map; only supplied fields change |
delete_map | Permanently delete a map |
Locations
| Tool | What it does |
|---|---|
list_locations | List locations, filterable by name, category, or tag |
create_location | Add a location; coordinates are required |
bulk_create_locations | Create up to 100 locations atomically |
get_location | Get one location with its tags and layers |
update_location | Update a location |
delete_location | Delete a location |
set_location_layers | Replace the layers a location belongs to |
Categories, tags, and layers each have the same five tools: list_*, create_*, update_*, delete_* (layers additionally appear on locations via set_location_layers).
Tools return errors as a result marked isError, carrying the same error codes the REST API uses (NOT_FOUND, VALIDATION_ERROR, CONFLICT, INSUFFICIENT_SCOPE, …).
Writing your own client
If you are not using an existing MCP client, the protocol dance is four steps. Send the Authorization header on every one of them.
initialize— negotiate the protocol version. The response gives you the server's name and version.notifications/initialized— a notification, no response expected. Skip it and some clients misbehave.tools/list— returns each tool's name, description,inputSchema, and annotations. Cache this; it does not change per request.tools/call—{ name, arguments }. The result is acontentarray plus anisErrorflag.
Handling results:
isError: false— success. Thecontent[0].textis JSON containing{ "data": ... }, identical to the REST response body, so one parser serves both surfaces.isError: true— the operation failed.content[0].textis JSON containing{ "error": { code, message } }with the same codes the REST API uses. Branch oncode, not onmessage— the message is human-readable text and its wording can change.
A tool that does not exist, or arguments that fail the tool's schema, also come back as isError: true rather than a protocol error. Treat any isError result as a failed operation, and a JSON-RPC error response as a transport or protocol failure.
If you only need a few tools, pass allowed_tools where your client supports it — importing all 24 constrains model selection more than importing the five you want.
What you cannot do
API key management is deliberately not exposed. There is no tool to create, list, or revoke keys. A client that could mint keys could escalate its own access, and one that could revoke them could lock you out. Manage keys in your account settings.
Scopes and limits
Your key's scope decides which tools you can call:
| Scope | Tools |
|---|---|
READ_ONLY | The list_* and get_* tools |
READ_WRITE | All tools |
Each tool also carries hints describing its effect — read-only, destructive, idempotent — so a client can tell what a call will do before making it.
Tool calls count against the same per-account rate limits as REST requests. If you use both surfaces, the quota is shared.
Troubleshooting
| Symptom | Cause |
|---|---|
401 MISSING_API_KEY | No Authorization header, or it is not a Bearer token |
401 INVALID_API_KEY | Key unknown or revoked |
401 API_KEY_EXPIRED | Key past its expiry date |
403 PRO_SUBSCRIPTION_REQUIRED | Account has no active Pro subscription |
403 INSUFFICIENT_SCOPE | A READ_ONLY key called a write tool |
429 RATE_LIMIT_EXCEEDED | Account quota exhausted — check Retry-After |
If a tool reports isError with NOT_FOUND for a map you can see in the dashboard, the map is owned by a different account — the API only ever exposes maps owned by the authenticated key's owner.