Skip to main content
Glama
README.md
# spatial-mcp

> **Experimental proof of concept.** This is not production software and it is not an ALA
> Try it on a test portal first (for example the LA demo, spatial.l-a.site), never directly on production.

spatial-mcp lets you administer a Living Atlases **Spatial Portal** by talking to an AI assistant (Claude, or any
[MCP](https://modelcontextprotocol.io) client) instead of clicking through the spatial-service admin pages.
It is aimed at the people who run a portal: adding layers, checking them, fixing their metadata, watching the
background tasks.

You ask in plain language, for example *"add this shapefile as a layer of regions"*. The assistant checks
the file, shows you what it is going to do, and only does it after you say yes.

## What you can do with it

**Add a layer** (it follows the Living Atlases wiki page
[Adding Layers](https://github.com/AtlasOfLivingAustralia/documentation/wiki/Adding-Layers), step by step):
- check a zip before uploading it: that it has SHP, SHX, DBF and PRJ; that it is in WGS84; the DBF encoding;
  which column holds the name of each area;
- upload it, create the layer, create the field, wait for the background tasks and reload the intersect configuration;
- check the result the way the wiki's step 8 does: the layer and field are listed, the areas exist, KML and
  intersect work, GeoServer draws it, the admin page opens.

**Look after existing layers**
- list and search layers and fields; see their areas (objects);
- change a layer's display name, description, classification (the tree in the portal), licence, or enable/disable it;
- change a field's name, description, or whether it is searchable in the gazetteer;
- delete a layer, a field or an unused upload;
- compare your layers with another portal (e.g. spatial.ala.org.au) and copy a layer definition from it.

**Background tasks**
- see what is queued, running or failed, and the log of a task;
- cancel or re-run a task;
- run maintenance processes (thumbnails, tabulations…) and the portal's analyses (Area report, AOO/EOO,
  Points to grid…, see ALA's [Spatial Portal tools](https://support.ala.org.au/support/solutions/articles/6000208466-tools)).

**Areas**: create an area from WKT or GeoJSON, like the portal's *Import > Areas*, and delete it.

## Before you start

You need:
1. **Node.js 20 or later** on the computer where the assistant runs.
2. **An admin account** on the portal: the same username (e-mail) and password you use to log in to the
   spatial admin pages, with the admin role (e.g. `ROLE_ADMIN`). The assistant logs in with it the way your
   browser does. Without an account you can still use the read-only tools (list layers, fields, intersect…).

## Install

```bash
git clone https://github.com/living-atlases/spatial-mcp
cd spatial-mcp
npm ci
npm run build
```

This leaves the server in `dist/stdio.js`. Below, replace `/path/to/spatial-mcp` with the full path of the folder
you cloned (`pwd` shows it). Use the full path: the assistant starts the server from another folder.

**1. Store your password** once, in a terminal. It is typed without echo and kept in your system's keyring (GNOME
Keyring / KWallet, macOS Keychain, Windows Credential Manager), or, where there is none, in a file only you can read
under `~/.config/spatial-mcp/`. It never goes into the assistant's configuration:

```bash
SPATIAL_USERNAME=you@example.org node /path/to/spatial-mcp/dist/stdio.js set-password --spatial https://spatial.l-a.site/ws
```

**2. Add it to Claude Code** (one line; adapt the address and your account). There is no password in it:

```bash
claude mcp add spatial -e SPATIAL_USERNAME=you@example.org -e SPATIAL_ALLOWED_DIRS=/home/you/layers -- node /path/to/spatial-mcp/dist/stdio.js --spatial https://spatial.l-a.site/ws
```

For Claude Desktop, add the same to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "spatial": {
      "command": "node",
      "args": ["/path/to/spatial-mcp/dist/stdio.js", "--spatial", "https://spatial.l-a.site/ws"],
      "env": {
        "SPATIAL_USERNAME": "you@example.org",
        "SPATIAL_ALLOWED_DIRS": "/home/you/layers"
      }
    }
  }
}
```

| Setting | What it is |
|---|---|
| `--spatial` (or `SPATIAL_URL`) | Your spatial-service address, ending in `/ws` |
| `SPATIAL_USERNAME` | Your admin account (its password is the one stored with `set-password`) |
| `SPATIAL_ALLOWED_DIRS` | Folders the assistant may upload zips from (recommended) |
| `SPATIAL_READONLY=1` | Look, don't touch: every change is refused (previews still work) |
| `SPATIAL_TOKEN_STORE=file` | Keep the password in the 0600 file instead of the keyring |

To change the stored password, run `set-password` again; `forget-password` removes it. `SPATIAL_PASSWORD` (the
password in plain text in the configuration) still works but is deprecated.

Restart Claude Desktop after editing the file (in Claude Code, `claude mcp list` shows whether it connected).
Then ask *"Is the spatial MCP working?"*: it answers with the portal version, the number of layers, how it logs in
and whether your admin access works.

## Adding a layer, step by step

1. **Prepare the zip.** One layer per zip: a shapefile (SHP, SHX, DBF, PRJ) or a grid (HDR, BIL, PRJ), in WGS84,
   with the DBF in ISO-8859-1. A GeoTIFF has to be converted first (`gdal_translate -of EHdr …`).
2. **Ask the assistant to check it:** *"Check ~/layers/ibra7_regions.zip"*. It tells you what is wrong, if anything,
   and which column looks like the name of each area.
3. **Ask for the layer:** *"Add ~/layers/ibra7_regions.zip as a contextual layer called `ibra7_regions`, shown as
   'IBRA 7 Regions', under Biodiversity > Region, with the REG_NAME_7 column as the name of each area."*
   It shows you a **preview** of exactly what it will fill in on the admin form. Nothing has been changed yet.
4. **Say yes.** It uploads, creates the layer and the field, and waits for the tasks. Small layers finish in a
   few minutes; for big ones it tells you it is still working and checks back when you ask.
5. **Check it:** *"Check the new layer with a point in Canberra."* It runs the wiki's checks and tells you which
   ones pass.

You can also do it one step at a time (*"just upload it"*, *"now create the field"*), exactly like in the admin pages.

On spatial-service 3.1.0 a new layer answers point queries (*intersect*) only after spatial-service is restarted;
the check in step 5 tells you so. Everything else (areas, search, map) works straight away.

### What the result looks like

A layer added this way on the LA demo portal: Australia's states and territories, from a Natural Earth shapefile
([`test/fixtures/mcp_demo_aus_states.zip`](test/fixtures/mcp_demo_aus_states.zip)). It shows up in the portal like
any layer added by hand, and after the restart mentioned above a point query answers with the state
(`/ws/intersect/cl10004/-23.70/133.88` → *Northern Territory*):

![The layer in the portal's list of layers](docs/img/layers-list.png)

![The layer's page](docs/img/layer-detail.png)

![The layer drawn by the portal's GeoServer](docs/img/map-geoserver.jpg)

## More things to ask

- *"Which layers do we have under Area Management?"*
- *"Move the layer `ibra7_regions` to Biodiversity > Region and set the licence to CC BY."*
- *"Hide the layer `test_old`"* (disable it) or *"delete the layer `mcp_poc_…` and its upload."*
- *"What tasks failed this week? Show me the log of the last FieldCreation."*
- *"Re-run the thumbnails."*
- *"Which layers does spatial.ala.org.au have that we don't?"*
- *"Create an area from this GeoJSON and run an Area report on it."*

## Is it safe?

- **Nothing changes without your OK.** Every change is first shown as a preview; the assistant has to ask you
  and repeat the call with an explicit confirmation. Deleting and cancelling always ask.
- **It cannot do what the admin pages cannot do.** It fills in the same forms you would, with the same limits
  (lengths, allowed values, fields that cannot change once a layer exists). If a new spatial-service version
  changes a form in a way it does not understand, it stops instead of guessing.
- **It only uploads `.zip` files**, never from hidden folders, and only from `SPATIAL_ALLOWED_DIRS` if you set it.
- **Your password is not in the assistant's configuration**: it is kept in your system's keyring (or a file only you
  can read). It and any tokens are never shown to the assistant, not even in error messages.
- **`SPATIAL_READONLY=1`** turns it into a look-only tool.

It is still a proof of concept: use a test portal, and keep an eye on what it does.

## When something goes wrong

| Message | What to do |
|---|---|
| *no password stored for …* | Run `set-password` (see *Install*). |
| *login … was refused* | Wrong username or stored password: try them in the browser, then run `set-password` again. |
| *needs a logged-in user with the admin role* (401/403) | Check `SPATIAL_USERNAME` and the stored password, and that your account has the admin role in the portal. |
| *only accepts a web-session login on its admin pages* | You use the experimental OIDC login on spatial-service 3.1.0: add `SPATIAL_USERNAME` and `set-password`. |
| *Not logged in to the portal yet* (OIDC) | Finish the login in the browser window it opened (or open the link it gives), then ask again. |
| *OIDC discovery failed* | Check `SPATIAL_OIDC_ISSUER`: `<issuer>/.well-known/openid-configuration` must open in the browser. |
| *login did not reach spatial-service* | Your portal's login page is not the usual CAS one (e.g. a different identity provider). Please open an issue. |
| *not in WGS84* | Reproject the layer to EPSG:4326 (QGIS *Export > Save as*, or `ogr2ogr -t_srs EPSG:4326`). |
| *"sname" is needed* | Tell it which DBF column holds the name of each area (the check in step 2 suggests one). |
| *Refused by the form contract* | What you asked for doesn't fit the admin form (too long, not an allowed value, or it can't be changed). Change it, or do it by hand. |
| *LayerCreation/FieldCreation failed* | Ask for the task log. The usual causes are the projection, the encoding, or GeoServer. |
| *is not a field of the form* | The spatial-service version changed its admin form. Please open an issue. |

## Experimental: OIDC login (work in progress)

spatial-mcp can also log you in with OIDC, in your browser, on the portal's usual login page, so it never sees a
password. **It is not the default yet** because spatial-service 3.1.0 does not accept it everywhere: it does for its
API, tasks, areas and the intersect reload, but its admin pages (adding and editing layers, uploads, the task list)
only accept a browser session (see [DEVELOPMENT.md](DEVELOPMENT.md#findings-about-spatial-service-310), finding 1).
So on 3.1.0 keep `SPATIAL_USERNAME` and the stored password as above: with both, the admin pages use that session and
the rest uses your OIDC login. *"Is the spatial MCP working?"* tells you whether your portal's admin pages take the
OIDC login (`adminPagesAcceptToken`).

To try it, add the portal's login server and a client id registered for spatial-mcp (see below):

```bash
claude mcp add spatial -e SPATIAL_OIDC_ISSUER=https://auth.l-a.site/cas/oidc -e SPATIAL_OIDC_CLIENT_ID=spatial-mcp -e SPATIAL_USERNAME=you@example.org -e SPATIAL_ALLOWED_DIRS=/home/you/layers -- node /path/to/spatial-mcp/dist/stdio.js --spatial https://spatial.l-a.site/ws
```

The first time something needs it, your browser opens on the portal's login page; log in and go back to the
assistant (if the browser did not open, the assistant gives you the link). You can also say *"log in to the
portal"*, or log in from a terminal beforehand:

```bash
SPATIAL_OIDC_ISSUER=https://auth.l-a.site/cas/oidc SPATIAL_OIDC_CLIENT_ID=spatial-mcp node /path/to/spatial-mcp/dist/stdio.js login
```

You stay logged in: spatial-mcp keeps a *refresh token* in the same keyring (or 0600 file). When it expires or is
revoked, the browser login opens again. `… dist/stdio.js logout` (or *"log out of the portal"*) forgets it.

Less common settings: `SPATIAL_OIDC_SCOPE` (default `openid profile email roles ala offline_access`),
`SPATIAL_OIDC_FLOW=device` (log in from another device with a code, for a machine without a browser, if your login
server supports it), `SPATIAL_OIDC_REDIRECT_PORT` / `SPATIAL_OIDC_REDIRECT_HOST` (login servers that need an exact
redirect address, e.g. Cognito).

### Registering spatial-mcp in your login server

spatial-mcp is a *public* OIDC client: it has no secret, uses the Authorization Code flow with PKCE (S256), and
receives the login on a local address, `http://127.0.0.1:<random port>/callback` (RFC 8252). Register it once per
login server, and give its client id to your admins.

**CAS 6/7 (ALA CAS)**: add a service like this one (JSON service registry; ala-install's `oidc-keys-add` role writes
the same fields to MongoDB):

```json
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "id": 1001,
  "name": "spatial-mcp",
  "clientId": "spatial-mcp",
  "clientSecret": "",
  "serviceId": "^http://127\\.0\\.0\\.1:\\d+/callback$",
  "tokenEndpointAuthenticationMethod": "none",
  "supportedGrantTypes": ["java.util.HashSet", ["authorization_code", "refresh_token"]],
  "supportedResponseTypes": ["java.util.HashSet", ["code"]],
  "scopes": ["java.util.HashSet", ["openid", "profile", "email", "roles", "ala", "offline_access"]],
  "bypassApprovalPrompt": true,
  "generateRefreshToken": true,
  "jwtAccessToken": true
}
```

`jwtAccessToken` matters: spatial-service only accepts access tokens that are JWTs.

**Keycloak**: create a client `spatial-mcp` with *Client authentication* off, *Standard flow* on, *PKCE method*
`S256` (Advanced), and valid redirect URI `http://127.0.0.1/*` (Keycloak accepts any port on a loopback address;
if your version does not, set `SPATIAL_OIDC_REDIRECT_PORT` and register that exact port). Optional: *OAuth 2.0
Device Authorization Grant* on, for `SPATIAL_OIDC_FLOW=device`. Add a *User Realm Role* mapper with token claim name
`role` (multivalued, in the access token), so spatial-service sees the admin role. spatial-service also requires a
`client_id` claim by default: if your access tokens lack it (they carry `azp`), add a *Hardcoded claim* mapper
`client_id` = `spatial-mcp`, or drop it from `security.jwt.requiredClaims`. Keycloak refuses scopes it does not
know (`ala` is CAS's): set `SPATIAL_OIDC_SCOPE=openid profile email offline_access`.

**Amazon Cognito**: an app client *without* a client secret, *Authorization code grant*, scopes `openid profile email`,
callback URL `http://localhost:8765/callback` (Cognito only allows `http` for `localhost`, with an exact port), then
set `SPATIAL_OIDC_REDIRECT_HOST=localhost`, `SPATIAL_OIDC_REDIRECT_PORT=8765` and `SPATIAL_OIDC_SCOPE=openid profile email`.
The issuer is `https://cognito-idp.<region>.amazonaws.com/<user pool id>`.

**spatial-service** must accept JWTs (`security.jwt.enabled: true`, with `security.jwt.discoveryUri` pointing to the
same login server) and read the admin role from the claim your login server uses (`security.jwt.roleClaims`:
`role` by default, `cognito:groups` for Cognito). By default it also requires the claims `sub`, `iat`, `exp`,
`client_id`, `jti` and `iss` in the access token (`security.jwt.requiredClaims`). It does not check the audience
unless `security.jwt.acceptedAudiences` is set: if it is, add spatial-mcp's client id to it.

To update it later: `git pull && npm ci && npm run build` in the same folder, then restart the assistant.

## Using it without installing anything (for portal operators)

The same server can run next to spatial-service, so admins connect to it by URL instead of installing it. For
now that mode only covers reads, tasks and areas on spatial-service 3.1.0: its admin pages need a browser session,
which the remote mode does not have. See [DEVELOPMENT.md](DEVELOPMENT.md#remote-http-transport).

## More

- [DEVELOPMENT.md](DEVELOPMENT.md): how it works, design decisions, tests and CI, what spatial-service would need.
- License: [MPL-2.0](LICENSE) (compatible with spatial-service's MPL-1.1; see DEVELOPMENT.md).
- Sibling POC for the GBIF IPT: [ipt-mcp](https://github.com/vjrj/ipt-mcp).

TDQS

B3.2/5.0

Scored across 31 tools

Disambiguation4/5

Most tools pair a distinct spatial resource with a distinct action, so list/get/update/delete and task/admin operations are generally easy to tell apart. The main confusion risk is spatial_add_layer versus spatial_create_layer/spatial_create_field, plus spatial_list_objects versus spatial_search_gazetteer, but the descriptions largely clarify these boundaries.

Naming Consistency4/5

The dominant spatial_verb_noun pattern is consistent and lower_snake_case is used throughout. The un-prefixed inspect_layer_zip and a few noun-only names such as spatial_health, spatial_layer_admin, and spatial_form_contract deviate from the otherwise verb-first convention.

Tool Count2/5

With 31 tools, this is a large surface that exceeds the 25+ threshold for a heavy toolset. Many tools are individually justified by the spatial-service admin lifecycle, but composite operations like spatial_add_layer and overlapping workflow tools make the set feel larger than necessary.

Completeness5/5

The toolset covers the full layer and field lifecycle: inspect, upload, create, update, verify, delete, task management, remote comparison/import, and user areas. There are no obvious dead ends, and both admin and user-facing workflows appear well supported.

Maintenance

ActivityMaintained
ResponsivenessNo issues