MCP server reference
Every Spatibase project has an MCP (Model Context Protocol) server, so AI assistants and your own agents can read the project and, when allowed, change it. This reference is for developers: the URL and transport, what each credential unlocks, how to connect, every tool, dry runs, limits and security notes. For a step-by-step setup in Claude, ChatGPT or Cursor, see Connect an AI assistant.
On this page, $PROJECT_ID is the project’s UUID (the id in the project’s web address), and $EDIT_KEY is an AI assistant key (sb_edit_…) created by a project owner.
URL and transport
Each project has its own MCP endpoint:
https://mcp.spatibase.com/projects/$PROJECT_ID
Copy the exact address from the project’s AI agents page, in Connect your assistant (This project’s assistant address). That value is the source of truth.
The server uses the Streamable HTTP transport and is stateless:
- Every request is a
POSTwith one JSON-RPC message (or a batch) and its ownAuthorizationheader. - Responses are plain JSON. There is no SSE stream and no
Mcp-Session-Id. AGETanswers405. - The server does not remember
initializebetween requests.
Which credential gives which access?
What a client can do follows only from its credential, sent as Authorization: Bearer <credential>. Keys in the query string or in an X-API-Key header are ignored on this endpoint. The tool list a client receives contains only the tools its credential allows; calling any other tool fails with MCP error -32602: Tool … not found.
| Credential | Tiers |
|---|---|
| None, public project | Read: published content only, exactly what the Content Delivery API shows |
| None, private project | Nothing: 401 with WWW-Authenticate: Bearer resource_metadata="…" for OAuth discovery |
Read key sb_live_… |
Read |
Edit key sb_edit_… |
Read + Edit: read and change drafts, never live content |
| Edit key with Allow publishing | + Publish: mark drafts ready, publish the project, stage unpublishes, go back to an earlier version, change group membership and order (groups ship with the next publish) |
| Edit key with Allow deleting | + Delete: delete entries and files into the trash (30 days) |
OAuth access token sb_oat_… |
What the person allowed on the consent screen, capped by their current project role, including Model for owners |
Temporary project token sb_temp_… |
Read, Edit, Publish, Delete and Model on that one temporary project, for 24 hours from when it was issued. It is made by the spatibase command without an account |
What a person’s role allows over OAuth:
| Role | Maximum tiers |
|---|---|
| Owner | Read, Edit, Publish, Delete, Model |
| Editor | Read, Edit, Publish, Delete |
| Developer | Read |
Writing always needs a credential: a public project is only public to read. Only owners create edit keys, in the web app (API, Access & keys, Create key, For an AI assistant). Editors and developers connect their assistants through OAuth instead.
Connect a client
Claude Code
claude mcp add --transport http spatibase "https://mcp.spatibase.com/projects/$PROJECT_ID" \
--header "Authorization: Bearer $EDIT_KEY"
Leave out --header to read a public project, or to sign in with OAuth: run /mcp in Claude Code, choose spatibase and sign in.
Cursor
In .cursor/mcp.json:
{
"mcpServers": {
"spatibase": {
"url": "https://mcp.spatibase.com/projects/PROJECT_ID",
"headers": { "Authorization": "Bearer EDIT_KEY" }
}
}
}
Claude Desktop with a key
Claude Desktop’s configuration file starts local servers, so bridge to the remote server with mcp-remote (needs Node.js):
{
"mcpServers": {
"spatibase": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.spatibase.com/projects/PROJECT_ID",
"--header", "Authorization: Bearer ${SPATIBASE_KEY}"],
"env": { "SPATIBASE_KEY": "EDIT_KEY" }
}
}
}
Claude.ai, ChatGPT and Claude Desktop custom connectors use the URL and OAuth; no key is pasted.
Your own agent
With the MCP TypeScript SDK:
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const url = new URL(`https://mcp.spatibase.com/projects/${process.env.PROJECT_ID}`)
const transport = new StreamableHTTPClientTransport(url, {
requestInit: { headers: { Authorization: `Bearer ${process.env.SPATIBASE_KEY}` } }
})
const client = new Client({ name: 'my-agent', version: '1.0.0' })
await client.connect(transport)
const result = await client.callTool({ name: 'search_entries', arguments: { type: 'store', limit: 5 } })
console.log(result.structuredContent)
curl
One request carries one message. The Accept header may be just application/json.
curl -s -X POST "https://mcp.spatibase.com/projects/$PROJECT_ID" \
-H 'content-type: application/json' \
-H "Authorization: Bearer $EDIT_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"count_entries","arguments":{"type":"store"}}}'
Tools
Results are compact JSON, returned both as text and as structuredContent. Ids and apiKeys are always included so calls can be chained. Geometry is summarised as {type, centroid, bbox, vertices} (a point keeps its coordinates) unless you pass includeGeometry: true.
| Tier | Tool | What it does |
|---|---|---|
| Read | describe_project |
Types (tree, location rules, elements with kind, required flag and options), map feature types, groups, the project’s locales and defaultLocale (with a hint on how to translate), and the caller’s access. A connection that can edit also sees the switched-off Types, marked enabled: false: the assistant can write drafts of them, but the live API leaves them out until someone switches the Type on. Also available as the resource spatibase://projects/{id}/schema. |
| Read | search_entries |
Published entries, filtered by type, subtypes, group, q, filters (for example {"rating": {"gte": 4}} or {"open": true}, with the Content Delivery API operators), bbox, near + radius, nearest (closest first, no radius), within (a GeoJSON polygon), withinEntry (+ withinField) and contains (+ containsField: which zones a point is in), as [lng, lat] pairs; with sort, page, limit (up to 100, default 20), fields and includeGeometry. Text over 300 characters is cut. |
| Read | count_entries |
The number of matches for the same filters. |
| Read | get_entry |
One published entry in full; includeReferences expands reference fields to the full referenced entries. |
| Read | list_groups, get_group |
Groups, and a group’s entries in their order. |
| Read | get_entries_geojson |
A GeoJSON FeatureCollection of up to 200 features; when more match, the Content Delivery API URL for all of them. |
| Read | get_api_spec |
The project’s typed OpenAPI document of the Content Delivery API (an Entry<Type> schema per type, a parameter per field filter) for writing or generating client code, plus its public URL. A private project needs a read key at that URL; the tool never returns a key. A document over 200 KB comes back as the URL only. |
| Edit | search_drafts, get_draft |
Entries as editors see them, with status draft, changed or published, and their translations. changedByAgent: true returns the editors’ review list; missingLocale: "de" returns the entries that still have no German translation, each with missingTranslations counts. |
| Edit | create_entry |
Creates a draft entry (type is an apiKey or id). Takes translations too. |
| Edit | update_entry |
Changes a draft. Only the fields you send change; null clears one. expectedUpdatedAt refuses the write when someone saved in between. translations is merged per language and key, see Translations. |
| Edit | bulk_update_entries |
Up to 50 updates (also with translations). The dry run shows each change from old to new value. |
| Edit | import_features |
Up to 1,000 GeoJSON features or CSV rows (text or row objects) as drafts, with mapping and matchBy (see Import API). With publish: true (this also needs the Publish tier) the imported and updated entries are marked ready and the project is published in the same call, so anything a person had already marked ready ships too. The answer lists ids in row order (null for a skipped row) and, with publish, published and publishFailed. With locale (and matchBy) the rows are translations of existing entries. A longitude outside -180 to 180 is imported wrapped and reported as a warning. |
| Edit | list_datasets |
Spatibase’s free public datasets: slug, name, licence, the fields each can be filtered by (such as country_code), entry count, snapshot date, and what this project already took from each (sources, updateAvailable when a newer snapshot exists). Writes nothing. |
| Edit | import_dataset |
Imports a public dataset into this project (it never creates a project), whole or with filter: {field, value} and/or bbox, up to 10,000 matching entries. Missing types and fields are created; existing ones are never changed (a mismatch is an error naming them). Entries arrive as drafts; ones whose identifier is already in the project are skipped, so running it again adds only what’s missing. The new types’ descriptions carry the dataset’s source and licence. |
| Edit | upload_asset |
Uploads one file: filename, contentBase64, optional mimeType and alt. Up to 3.5 MB after decoding; the same type rules as the media library. Returns {id, filename, url, …}; put the id in an Images or Files value as ["<id>"]. Counts as one write. |
| Edit | list_assets |
Uploaded images and files. |
| Edit | propose_group_changes |
Checks a group change and returns it for an editor to apply. Changes nothing. |
| Edit | list_trash |
The trash. |
| Edit | list_versions, get_version |
The project’s numbered versions (one per publish), newest first, and what one of them changed. Read-only. |
| Publish | mark_ready |
Marks drafts ready to publish, by ids or a filter (default: drafts and unpublished changes), up to 500 entries. Nothing goes live. The dry run lists what would be marked and which entries fail validation and stay drafts. |
| Publish | publish_project |
Publishes the project as one new version. The dry run shows everything that would ship, including entries other people marked ready, what stays out and the changes made by assistants; the real call needs the preview’s token and says if the preview went stale. Returns the version number. |
| Publish | unpublish_entries |
By ids or a filter, up to 500 entries. Stages the unpublish: the entries leave the API at the next publish_project. |
| Publish | restore_version |
Goes back to an earlier version, live at once, as a new version. The dry run lists what would be added, changed and removed and what can’t come back. |
| Publish | set_entry_groups, add_to_group, remove_from_group, reorder_group |
Group membership and order. They change the groups’ drafts and go live with the next publish. |
| Delete | delete_entries, delete_assets |
Up to 100 ids. A live entry is only staged for removal and goes to the trash at the next publish; an entry that was never live and a file go to the trash at once. The dry run lists what links to each item. |
| Delete | restore_from_trash |
Brings an entry back from the trash as a draft, or a file back. |
| Model | create_type, create_element, assign_elements, create_group, update_group, update_project_settings (including locales, the project’s languages) |
Need confirm: true; without it they return a preview of the change. |
Prompts: summarize_project, and find_missing_field (arguments type and field).
No credential can manage API keys, switch a project between public and private, manage members or the webhook, duplicate or delete the project or empty the trash.
Translations
In a project with languages the tools know about them:
- The read tools (
search_entries,get_entry,get_group,get_entries_geojson) takelocale, like the Content Delivery API:locale: "de"returns German with a fallback to the default language andmissingTranslations, andlocale: "*"returns all stored translations. create_entry,update_entryandbulk_update_entriestaketranslations, for example{"de": {"title": "Wald", "values": {"description": "…"}}}. It is merged: only the languages and fields you send change,nullremoves one. Only the title and text and rich text fields can be translated, and only in the project’s non-default languages; other requests are refused with the reason.search_draftstakesmissingLocale.import_featurestakeslocalewithmatchBy, and reads language columns such astitle_de.update_project_settingstakeslocalesand needs the model tier (andconfirm: true).
The workflow the tool descriptions teach: find the entries with missingLocale, read each draft, write translations as drafts, and leave publishing to a person. Translated drafts are marked as changed by an assistant, like every other draft change.
Dry runs and errors
Every tool that takes dryRun defaults to dryRun: true: bulk_update_entries, import_features, import_dataset, mark_ready, publish_project, unpublish_entries, restore_version, delete_entries and delete_assets. The first call shows what would happen; nothing is written until the client calls again with dryRun: false. Model tools work the same way with confirm.
Tool errors come back as tool results with isError: true and the API’s plain message:
{ "error": "This entry changed since you loaded it…", "status": 409, "code": "entry_changed" }
What editors see
Every change appears in the project’s Activity, as “AI assistant (key name)” for an edit key or “person via app” for an OAuth connection. Drafts an assistant changed are flagged in Entries (the Changed by AI switch in the Status filter) until someone edits them or publishes them. The publish_project preview counts them too. Activity has an AI changes checkbox for the change log.
The client name comes from clientInfo.name in an initialize request, otherwise from the first word of the User-Agent header, or from the OAuth client’s registered name.
Limits
| Limit | Value | When exceeded |
|---|---|---|
Calls (tools/call, resources/read, prompts/get) per credential, or per IP without one |
120 a minute | HTTP 429 with Retry-After, JSON-RPC error -32000 |
| Write calls per credential | 30 a minute | Tool error with code: "rate_limited" and retryAfterSeconds |
| Entries changed per credential (created, updated, marked ready, published, unpublished, deleted, restored, group changes) | 20,000 a UTC day | Tool error before anything is written. The count is reserved up front, so parallel calls cannot exceed it. |
| Request body | 5 MB (a file of up to 3.5 MB fits in an upload_asset call) |
413 |
| Tool result | 200 KB | The largest list is shortened and the result says truncated |
| Failed credentials per IP | 60 in 10 minutes | 429, shared with the Content Delivery API |
A wrong, revoked, expired or other project’s key gets 401 with error="invalid_token". An expired temporary project token gets 401 with the code token_expired, one the owner removed in Members gets 401 with token_revoked (the agent should stop and ask, not start a new project), and a temporary project that expired gets 410 with project_expired (for 30 days after it was deleted). Expired or revoked credentials do not count towards the failed-credential limit. See Limits for the limits of the other APIs.
Security notes
- Prompt injection. Tool results are the project’s data. An entry’s text can contain instructions aimed at an assistant; every tool description warns the model about this. Give an assistant the smallest credential that does the job, keep publishing and deleting off unless needed, and review the entries flagged Changed by AI.
- Keys are secrets. An edit key can change drafts. Keep it out of code repositories and web pages, and revoke it when the assistant no longer needs access.
- Browser requests. A request that carries an
Originheader from a website other than the Spatibase web app is refused with403. - Logging. Spatibase logs the tool, the key prefix or connection id, the duration and the size of each call, never a credential or content.
Related
Updated