tileserver-rs is the first Rust tile server with native Model Context Protocol (MCP) support. Any deployment can expose its tile sources, styles, renderer, and PostGIS/STAC backends as MCP tools — turning the tile server into an LLM-addressable geospatial endpoint.
What you get
| Tier | Tool | Purpose |
|---|---|---|
| A | tileserver_list_sources | List every configured tile source |
| A | tileserver_get_source_tilejson | Full TileJSON 3.0 metadata for one source |
| A | tileserver_list_styles | List every configured map style |
| A | tileserver_get_style | Full MapLibre style JSON |
| A | tileserver_get_tile_metadata | Bounds, zoom range, vector-layer schemas |
| A | tileserver_get_server_info | Version, source/style counts, renderer state |
| B | tileserver_render_static_map | Render a static WebP/PNG/JPEG map (≤ 2048×2048, ≤ 1.5 MB) |
| B | tileserver_get_tile | Fetch raw vector tile bytes at z/x/y |
| B | tileserver_query_features_at_point | Features intersecting a coordinate (PostGIS) |
| B | tileserver_query_features_cql2 | CQL2 filter against a PostGIS table |
| B | tileserver_search_stac_items | Search a configured STAC catalog by bbox/datetime |
| D | tileserver://styles/{id} | Resource template — style JSON |
| D | tileserver://data/{id}.json | Resource template — TileJSON metadata |
Transports
| Transport | Use case | Auth |
|---|---|---|
| stdio | Local — Claude Desktop, Cursor, Copilot CLI, Claude Code | Trusted (inherits user perms) |
| Streamable HTTP | Remote — nested at /mcp inside the main HTTP server | Static Bearer via [mcp].auth_token or OAuth 2.0 DCR via [mcp.oauth] (mutually exclusive) |
claude.ai Custom Connectors require OAuth 2.0 — they do not accept static bearer tokens. Enable [mcp.oauth] and point claude.ai at https://your-server/.well-known/oauth-authorization-server. See the OAuth 2.0 for claude.ai section below.
Quick Start
1. Build with the mcp feature
The MCP server is opt-in to keep default builds slim:
cargo install --git https://github.com/vinayakkulkarni/tileserver-rs --features mcp
If you build from source:
cargo build --release --features mcp
2. Add an [mcp] block to your config
[server]
host = "127.0.0.1"
port = 8080
# Embedded MCP at /mcp (Streamable HTTP). Stdio is always available via
# the `mcp-stdio` subcommand and ignores this block.
[mcp]
enabled = true
# Pick ONE auth mode (mutually exclusive):
# 1. Static bearer — simplest, works with Cursor / Claude Code / Copilot CLI:
# auth_token = "${MCP_AUTH_TOKEN}"
# 2. OAuth 2.0 — required by claude.ai Custom Connectors (see section below):
# [mcp.oauth]
# enabled = true
# issuer_url = "https://tiles.example.com"
# signing_key_path = "/etc/tileserver/mcp-jwt-key.pem"
# Strict CORS for the /mcp endpoint (defaults to ["*"] — lock down in prod):
# cors_origins = ["https://claude.ai", "https://cursor.sh"]
[[sources]]
id = "openmaptiles"
type = "pmtiles"
path = "/data/tiles/openmaptiles.pmtiles"
[[styles]]
id = "osm-bright"
path = "/data/styles/osm-bright/style.json"
A complete example lives at data/configs/mcp.toml.
3a. Wire into Claude Desktop (stdio)
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) — see the Claude Desktop docs for Windows/Linux paths:
{
"mcpServers": {
"tileserver-rs": {
"command": "/usr/local/bin/tileserver-rs",
"args": ["mcp-stdio", "--config", "/path/to/config.toml"]
}
}
}
Restart Claude Desktop. Ask: "What tile sources are loaded on my server?"
3b. Wire into Cursor / Copilot CLI / Claude Code (stdio)
The same mcp-stdio subcommand works for any stdio-speaking client. Add to your client's MCP config:
{
"tileserver-rs": {
"command": "tileserver-rs",
"args": ["mcp-stdio"]
}
}
3c. Use over HTTP (Cursor / Claude Code remote)
Start tileserver-rs normally with [mcp].enabled = true:
tileserver-rs --config config.toml
Point your MCP client at http://localhost:8080/mcp. If auth_token is set in config, add an Authorization: Bearer <token> header.
3d. OAuth 2.0 for claude.ai Custom Connectors
claude.ai's "Custom Connectors" feature requires OAuth 2.0 Dynamic Client Registration (RFC 7591). Static bearer tokens are not accepted.
1. Generate a signing key:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /etc/tileserver/mcp-jwt-key.pem
chmod 600 /etc/tileserver/mcp-jwt-key.pem
2. Enable OAuth in your config (mutually exclusive with auth_token):
[mcp]
enabled = true
[mcp.oauth]
enabled = true
issuer_url = "https://tiles.example.com" # must match your public URL
signing_key_path = "/etc/tileserver/mcp-jwt-key.pem"
token_ttl_secs = 3600 # clamped at 24h
3. Add the connector in claude.ai:
In claude.ai → Settings → Custom Connectors → Add → paste:
https://tiles.example.com
claude.ai discovers the OAuth flow via /.well-known/oauth-authorization-server, dynamically registers itself via /register, walks the consent screen at /authorize, exchanges the code at /token, and finally talks MCP at /mcp with the issued JWT bearer.
Endpoints exposed when [mcp.oauth].enabled = true:
| Endpoint | Method | Purpose |
|---|---|---|
/.well-known/oauth-authorization-server | GET | RFC 8414 discovery |
/.well-known/oauth-protected-resource | GET | RFC 9728 resource metadata |
/register | POST | RFC 7591 Dynamic Client Registration |
/authorize | GET | Consent screen (askama-rendered) |
/approve | POST | Approval → redirects with auth code |
/token | POST | Authorization code & refresh token grants |
Security defaults:
- PKCE
S256is mandatory;plainis rejected per the MCP spec. - JWT access tokens are RS256-signed with the configured key.
- Refresh tokens rotate on every
/tokencall. - Token TTL is clamped at 86400s (24h) regardless of config.
- Tokens are stored in-memory — restarting the server invalidates all active tokens. Plan deployments accordingly.
The in-memory token store is intentional for v1 simplicity. For zero-downtime deployments or long-lived sessions, run tileserver-rs behind a sticky-session reverse proxy and refresh tokens reasonably often (the default 1h TTL plus refresh rotation handles most cases).
Example prompts
Once wired in, try:
- "List all tile sources on my server."
- "Render a static map of Paris at zoom 12, 800x600 pixels."
- "What vector layers are in the openmaptiles source?"
- "Search the
sentinel-2-l2aSTAC collection for items over San Francisco from October 2024." - "Use CQL2 to find all features in the
buildingstable whereheight > 50within bbox[-74, 40.7, -73.9, 40.8]."
Architecture notes
- Same-port nest: the Streamable HTTP service mounts at
/mcpinside the existing axum router, not on a separateTcpListener. One bind, one reverse-proxy config. - Renderer-aware:
tileserver_render_static_mapreturnsisError: truewith a descriptive message when therasterfeature is disabled instead of disappearing from the tool list. - Image size discipline: every static render goes through WebP@Q75 compression, capped at 2048×2048 dimensions and 1.5 MB bytes. The 5 MB hard limit on the Claude side can corrupt sessions irrecoverably (Claude Code #2939) — better to refuse oversized requests than to ship them.
- Error contract: backend failures return
CallToolResult { isError: true }, never JSON-RPC errors. JSON-RPC errors are reserved for protocol-level problems (parse failures, unknown methods). - Feature-gated cascade: CQL2 tools require
postgres; STAC search requiresstac. When those features are off, the tools either don't appear in the tool list or return a clear "not available" error.
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
[mcp].enabled | bool | false | Nest the MCP HTTP service at /mcp. Stdio mode ignores this. |
[mcp].auth_token | string? | unset | Static bearer token for HTTP. Supports ${VAR} env expansion. Mutually exclusive with [mcp.oauth].enabled. |
[mcp].cors_origins | Vec<string> | ["*"] | Allowed CORS origins for /mcp. Lock down in production. |
[mcp.oauth].enabled | bool | false | Enable OAuth 2.0 DCR. Required for claude.ai Custom Connectors. |
[mcp.oauth].issuer_url | string | unset | Public URL the server is reachable at. Required when oauth enabled. |
[mcp.oauth].signing_key_path | path | unset | RSA PKCS#8 private key path for RS256 JWT signing. Required when oauth enabled. |
[mcp.oauth].token_ttl_secs | u64 | 3600 | Access token TTL in seconds. Clamped to 86400 (24h) max. |
The [mcp] block is only honored when tileserver-rs is built with --features mcp. Without that feature, the block is ignored and no MCP code is compiled in.
Prompt templates
When connected, the server advertises four user-role prompt templates that LLM clients can surface as quick actions:
| Prompt | Args | Purpose |
|---|---|---|
describe_style | style_id | Ask the assistant to describe a style's design and use case. |
suggest_cql2_filter | table_id, intent | Get a suggested CQL2 expression for a given intent. |
render_location_preview | location, zoom? | Render a static map preview of a place. |
explain_tile_metadata | source_id | Walk through a source's layers, zoom range, and contents. |
What's deferred
These are planned for follow-up releases:
- CIMD discovery (
oauth_cimd— Claude-Initiated Metadata Discovery, the high-traffic alternative to DCR). - Persistent token store — current OAuth implementation uses an in-memory
Arc<RwLock<HashMap>>token store. Server restart invalidates all tokens. A future release will add ansledor SQLite backend. - Admin tools (
reload_config,invalidate_cache) — gated on a separatemcp-adminsub-feature to keep the security surface small. - Per-tool rate limiting for
render_static_mapandget_tile.