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/*.
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
| Page | URL | Purpose |
|---|---|---|
| Connected apps | /admin/mcp/connected-apps | List 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/devices | List 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:
| Method | Path | Response |
|---|---|---|
GET | /__admin/oauth/clients | JSON 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/sessions | JSON 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
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).
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.
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.