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:

http
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 POST with one JSON-RPC message (or a batch) and its own Authorization header.
  • Responses are plain JSON. There is no SSE stream and no Mcp-Session-Id. A GET answers 405.
  • The server does not remember initialize between 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

bash
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:

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):

json
{
  "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:

js
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.

bash
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) take locale, like the Content Delivery API: locale: "de" returns German with a fallback to the default language and missingTranslations, and locale: "*" returns all stored translations.
  • create_entry, update_entry and bulk_update_entries take translations, for example {"de": {"title": "Wald", "values": {"description": "…"}}}. It is merged: only the languages and fields you send change, null removes 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_drafts takes missingLocale.
  • import_features takes locale with matchBy, and reads language columns such as title_de.
  • update_project_settings takes locales and needs the model tier (and confirm: 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:

json
{ "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 Origin header from a website other than the Spatibase web app is refused with 403.
  • Logging. Spatibase logs the tool, the key prefix or connection id, the duration and the size of each call, never a credential or content.

Updated

Docs support

How can we help?

A person on our team reads every message. We include the page you are on, so you don't have to explain where.

Or write to hello@spatibase.com. For outages, check System status.

Talk to us

Not sure it’s a fit? Let’s find out

Tell us about your use case and what you need, from data model to security and deployment. In 30 minutes we’ll look at it together and tell you honestly if we can help.

Or write to hello@spatibase.com.