Documentation

When the mcp feature is enabled with OAuth (see the MCP Server guide), tileserver-rs ships a small web admin UI for managing the OAuth state directly. No CLI, no manual SQL — just two pages at /admin/mcp/*.

Warning

Admin-bind only. The admin UI and its underlying REST endpoints are mounted on the admin listener (server.admin_bind), not the public listener. They are never exposed alongside your public tile routes. Bind the admin listener to a private interface (loopback, VPN, or internal network) — never to 0.0.0.0 on a public host.

What you get

PageURLPurpose
Connected apps/admin/mcp/connected-appsList every OAuth client registered via Dynamic Client Registration (DCR). Inspect scopes, count active sessions, revoke a client (cascade-deletes all its refresh tokens).
Devices/admin/mcp/devicesList every outstanding refresh token. Each row is one device or browser tab where a connected app is logged in. Revoke a single device session without removing the client.

When to use this

  • A user reports "Claude desktop is hanging" — check the Devices page to see if their token is expired or stuck.
  • You're rotating credentials — bulk-revoke all clients before re-registering.
  • You want to audit which third-party apps have access — the Connected apps page shows every registered client with its scopes, redirect URIs, and last-seen timestamp.
  • A user lost a laptop — revoke just that device session (and let the user re-authenticate from their other devices).

Backend: SQLite persistence

The admin UI surfaces state from the OAuth store. By default the store is in-memory (clients and tokens evaporate on restart). For a real deployment, build with the mcp-persistence feature and point store_path at a writable SQLite file:

# Build with both MCP + persistence enabled
cargo build --release --features mcp,mcp-persistence
# config.toml
[mcp.oauth]
enabled = true
issuer_url = "https://your-server.example.com"
signing_key_path = "/var/lib/tileserver-rs/oauth.pem"
# Path to a writable SQLite file. Schema is created on first launch.
store_path = "/var/lib/tileserver-rs/oauth-store.sqlite"

Setting store_path in a binary built without mcp-persistence fails fast at startup — persistence you configured is never silently ignored. Without store_path, the admin UI still works against the in-memory store — but every restart resets the state, which limits its usefulness.

REST API

The admin UI is a thin layer over four REST endpoints. They're admin-bind-only and have no authentication of their own — they trust the network boundary. You can hit them directly from curl or any HTTP client on the admin interface:

MethodPathResponse
GET/__admin/oauth/clientsJSON array of all registered clients with derived session counts
DELETE/__admin/oauth/clients/{client_id}{ok, deleted, revoked_sessions} — cascade-deletes refresh tokens
GET/__admin/oauth/sessionsJSON array of all outstanding refresh tokens
DELETE/__admin/oauth/sessions/{token_id}{ok, deleted, revoked_sessions: null} — single-session revoke

Idempotent deletes

Both DELETE endpoints are idempotent. {deleted: false} is a normal success response (not an error) — it just means the resource was already gone.

Auth-code lifecycle

The sessions endpoint reports refresh tokens only. Single-use authorization codes issued during the OAuth dance are stored in the same backend, but they're transient (5-minute TTL) and never surface in the UI or the REST API.

Design

The admin UI shares the client-wide "Direction I" design system (OKLch palette with a violet accent, table-density layout, sharp corners). The public map viewer at /, /styles/*, and /data/* uses the same token set, honoring the light/dark toggle across both surfaces.

If you embed tileserver-rs inside a larger Nuxt app, you can disable the admin pages by removing the admin directory under apps/client/app/pages/ or by reverse-proxying /admin/* to 404 on the public host.

Troubleshooting

Info

Pages 404 on the public listener. That's intentional. /admin/* is served only from the admin listener. Either bind the admin listener to the same port as the public one (development), or reverse-proxy /admin/* and /__admin/* to the admin port (production).

Info

Tokens disappear after restart. You're running without persistence. Build with --features mcp,mcp-persistence and point [mcp.oauth].store_path at a SQLite file on a persistent volume.

Info

Empty state on the Devices page. No client has completed an authorization-code-to-refresh-token exchange yet. The first time a user signs into Claude.ai or a desktop client through the OAuth flow, a row will appear.