Skip to content
15Reference

MCP server

Connect Claude Desktop, Cursor, Claude Code or any MCP client to your workspace: transports, scopes, the twenty-one tools, resources and prompts, the agent budget, and worked sessions.

What it is

Spatly ships an official Model Context Protocol server. An agent connected to it sees your workspace the way you do, with projects, layers, features, beats and metrics, and it can write chapters, insight cards and share links through the same REST API and the same permission model. It is on every plan, including Free.

Spatly is also an MCP consumer: a tool on your own server (inventory, CRM, IoT, tickets) can feed a Spatly dataset. That direction is documented under Data & live sources.

Connect

TransportEndpointAuth
Streamable HTTP (remote)https://spatly.io/mcpAuthorization: Bearer spk_…
stdio bridge (local)npx -y @spatly/mcpSPATLY_TOKEN=spk_… (plus SPATLY_API_URL when self-hosted)

The bridge replays every tool over /api/v1, so both transports expose exactly the same tools. Create the token under Settings → API tokens; that page also prints the snippets below with your own origin filled in.

Claude Desktop · Cursor, local bridge
{ "mcpServers": { "spatly": { "command": "npx", "args": ["-y", "@spatly/mcp"], "env": { "SPATLY_TOKEN": "spk_…" } } } }
Claude Desktop · Cursor, remote
{ "mcpServers": { "spatly": { "url": "https://spatly.io/mcp", "headers": { "Authorization": "Bearer spk_…" } } } }
Claude Code
claude mcp add --transport http spatly https://spatly.io/mcp --header "Authorization: Bearer spk_…"

The Settings → API tokens page prints both, ready to paste:

Workspace settings, API tokens: the REST and MCP snippets and the token table.
  1. API tokens: the tab where machine access lives.
  2. Snippets: the curl line and the MCP client entry, with this deployment's origin already filled in.
  3. Create token: pick read alone for an agent that should only read.
  4. Tokens: prefix, scopes and last use, so you can tell which agent is actually calling.

A handshake looks like this:

initialize → serverInfo
{ "name": "spatly", "version": "1.0.0" }

Scopes and the budget

  • Scopes: read, write, publish, carried by the token. A tool whose scope the token lacks returns a friendly forbidden naming the scope, not a protocol error.
  • Agent budget: maxCallsPerMin (60) → rate_limited with retryAfterSec; maxRowsPerCall (500) clamps every feature query and list; requireBbox can force a bounding box. A live connection adds its own queryPolicy and can be hidden from agents entirely (agentAccessible: false).
  • Kill switch: MCP can be turned off for a whole workspace (mcpEnabled).

Rows are never dumped wholesale: every list carries nextCursor, and feature results carry total and the budget that was applied. See Plans, limits & alerts.

Tools

Twenty-one, in five groups.

ToolScopeWhat it does
list_projectsreadprojects { items, nextCursor }, templates flagged
get_projectreadone project with layers, beats, surfaces (share urls), datasets, metrics
create_projectwritea new empty project
list_layersreadthe layers of a project
describe_layerreadfields with statistics, the style, the classification, dataset urls
list_datasetsreadworkspace datasets
describe_datasetreadschema, bbox, time field, connection health
query_featuresreadbbox / where / q / select / orderBy / limit → FeatureCollection, budget-capped
search_featuresreadfree text over the label fields
spatial_filterreadpolygon · near { lng, lat, radiusM } · bbox, plus where
geocode · reverse_geocodereadcached OpenStreetMap lookups (attribution required)
list_beatsreadthe project's saved map states
set_map_viewwritecreate a beat from a camera or a bbox, with layer states and highlights
list_surfacesreadthe story / slides / dashboard / insight surfaces of a project
create_story_chapterwritea chapter with its beat, inserted at index
list_kpisreadmetrics with their current values
get_kpi_valuereadone value, scoped by bbox / aoi / where / selection / from-to
query_timeseriesreada bucketed series
create_insight_containerwritean embeddable insight, collapsed card plus expanded workspace
publish_share_linkpublishpublish a surface → { publicId, url, embedUrl }

Every result comes back as JSON text and as structuredContent. Errors arrive as isError with code: message (not_found, forbidden, rate_limited, bad_request…), never as protocol failures, so an agent can read the message and recover.

notewhere on the feature tools is the object grammar, { "mag": { "gte": 7 } }. MapLibre expressions belong in layerStates[].filter and highlight.filter. See REST API.

Resources and prompts

Two resource templates (they appear under resources/templates/list, not resources/list, because both take an id):

URI templateContents
spatly://projects/{id}the same payload as get_project
spatly://datasets/{id}/schemafields, statistics, bbox, time field

One prompt:

PromptArgumentsWhat it does
story_from_datasetprojectId, datasetId, chapters?, audience?walks an agent from a dataset to a published story: describe → read the data → outline → create chapters → publish

Worked sessions

"Create a six-chapter story from this dataset"

  1. list_datasets → pick the dataset, or POST /api/v1/datasets/from-url to add one.
  2. get_project → note layers[0].id and the story surface id (create one in Studio if the project has none).
  3. describe_layer → field names and statistics. list_kpis → the headline numbers. query_features { geometry: false, orderBy, limit: 20 } → the facts you will write about.
  4. Per chapter: create_story_chapter { surfaceId, title, text, bbox | camera, layerStates: { "<layerId>": { "filter": [">=", ["get","mag"], 5] } }, highlight }.
  5. publish_share_link { surfaceId, visibility: "unlisted" } → hand back the url.

Or invoke story_from_dataset and let the prompt drive.

"Wire a live KPI"

  1. Connect the source (Data → From URL / REST API / MCP source / catalog). The scheduler refreshes it.
  2. Create a metric in Studio, for example count with filter [">=", ["get","mag"], 5] and spatialScope: "viewport".
  3. get_kpi_value { metricId, bbox } returns the number; a dashboard polls /api/live/metrics/:id with an ETag; query_timeseries gives the sparkline.
  4. Optionally add an alert so a threshold crossing e-mails someone.

"Embed an insight"

  1. list_kpis { projectId } → pick up to four metric ids.
  2. geocode the place → set_map_view { bbox }, or pass bbox straight to the next call.
  3. create_insight_container { projectId, name, text, metricIds, bbox, cta: "Open insights" }.
  4. publish_share_link { surfaceId } → drop the embedUrl on a page. See Embedding.

Guardrails worth setting before you hand an agent a token

  • Give read-only agents a read-only token. write and publish are separate for a reason.
  • Turn on requireBbox if the workspace holds large datasets. It stops "select everything" by construction.
  • Set agentAccessible: false on any connection an agent has no business reading.
  • Watch usage per day, per tool and per token under Settings → API tokens.

Self-hosting notes

  • SPATLY_API_URL points the stdio bridge at your own deployment.
  • pnpm --filter @spatly/web mcp:smoke exercises the HTTP transport; scripts/mcp-stdio-smoke.ts does the same for the bridge.

Verified on 2026-09-02 against the local development build: initialize, tools/list (21 tools), tools/call list_projects, resources/templates/list, resources/read spatly://projects/{id} and prompts/list were each called over streamable HTTP with a real workspace token.

Was this page helpful?

Something wrong or missing? Write to hello@spatly.io.