# Sms

> simple mbtiles server

- **Type:** MCP server
- **Install:** `agentstack add mcp-markuman-sms`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [markuman](https://agentstack.voostack.com/s/markuman)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [markuman](https://github.com/markuman)
- **Source:** https://github.com/markuman/sms

## Install

```sh
agentstack add mcp-markuman-sms
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 🗺️ OSM - self host the entire planet 🌎 in ~30 minutes 🚀

**s**imple **m**btiles **s**erver

#### SETUP - TL;DR

```
mkdir osm
wget --continue -O osm/planet.mbtiles https://nx93856.your-storageshare.de/public.php/dav/files/apP4zEacCwqPfWd/planet.mbtiles
wget --continue -O osm/contours.mbtiles https://nx93856.your-storageshare.de/public.php/dav/files/apP4zEacCwqPfWd/contours_90m-Z11-Z13_30m-Z14.mbtiles
podman run -ti --rm -p 9000:9000 --name sms -v "$(pwd)/osm/:/data/" registry.gitlab.com/markuman/sms:latest
firefox http://localhost:9000
```

requirements: 
* `podman` (_or `docker`_)
* 100 GB storage is required (_1 core and 512MB memory are sufficient_)
* optional 315 GB storage is required for contour lines

notes:
* "_~30 minutes_" depends on your bandwidth ...and the hidrive performance of ionos.

# credits

* https://github.com/onthegomap/planetiler is used to generate the planet.mbtiles file
* https://github.com/uktrade/mbtiles-s3-server is the origin code-base of my `sms` project

# Usage

## nextcloud GpxPod

1. Deploy the container/service behind a webproxy (_caddy, nginx, traefik,...you name it._) to get a valid SSL certificate.
2. Goto GpxPod Settings -> Tile Servers
    * Type: Vector
    * Server address: `https:///v1/styles/osm-bright-gl-style@1.0.0/style.json?fonts=fonts-gl@1.0.0&tiles=mytiles@1.0.0`

## URL Parameter

You can add coordinates and Zoomlevel.

`https://maps.osuv.de/?lat=48.1374&lng=11.5752&zoom=9`

## Reverse Geolocation / Photon Integration

* sms supports [photon](https://github.com/rtuszik/photon-docker) reverse geocoding server.

Just set `-e PHOTONSERVER="https://photon.osuv.de"` for your sms Container.

This also enables the `geocode` and `reverse_geocode` MCP tools; without it
they are not registered at all.

### Two consumers, possibly two URLs

`PHOTONSERVER` is substituted into `index.html` at startup, so **the browser**
talks to Photon directly with that URL — it has to be publicly reachable.

The MCP tools, on the other hand, call Photon **from inside the container**.
If both run in podman/docker, the public hostname often resolves to a LAN
address the container network cannot route to (split-horizon DNS), and you get
`Connection refused`. Changing `PHOTONSERVER` is not the fix: that would break
the frontend.

Use `PHOTONSERVER_INTERNAL` for the server-side path instead:

```bash
podman network create osm

podman run -d --rm --network osm --name photon \
  -e UPDATE_STRATEGY=DISABLED \
  -p 8888:2322 \
  -v /home/m/osm/photon/:/photon/data \
  docker.io/rtuszik/photon-docker:2.1.1

podman run -ti --rm --network osm --name sms \
  -p 9000:9000 \
  -e PHOTONSERVER="https://photon.osuv.de" \
  -e PHOTONSERVER_INTERNAL="http://photon:2322" \
  -v /home/m/osm/sms/:/data/ \
  localhost/sms:dev
```

If `PHOTONSERVER_INTERNAL` is unset, server-side calls fall back to
`PHOTONSERVER`, which is the right thing when both are reachable from
everywhere (single host, no container network in between).

## Configuration

Tilesets are configured through numbered environment variable groups
(`MBTILES__1__*`, `MBTILES__2__*`, …):

| Variable | Required | Description |
|---|---|---|
| `PORT` | yes | Listen port |
| `MBTILES__n__URL` | yes | Path to the `.mbtiles` file |
| `MBTILES__n__IDENTIFIER` | yes | Tileset name used in URLs |
| `MBTILES__n__VERSION` | yes | Tileset version used in URLs |
| `MBTILES__n__MIN_ZOOM` | yes | Minimum zoom level |
| `MBTILES__n__MAX_ZOOM` | yes | Maximum zoom level |
| `HTTP_ACCESS_CONTROL_ALLOW_ORIGIN` | no | CORS header value |
| `PHOTONSERVER` | no | Public Photon base URL; enables geocoding and is embedded into the map UI |
| `PHOTONSERVER_INTERNAL` | no | Photon URL used for server-side calls (MCP tools) when the public one is not reachable from inside the container |
| `TILE_CACHE_SIZE` | no | Decoded road tiles kept in memory (default 2000; contour and POI caches get a quarter of that each) |
| `ROUTE_MAX_TILES` | no | Corridor tile limit per segment (default 1200) |
| `ROUTE_MAX_CROW_KM` | no | Straight-line limit per segment in km (default 50) |
| `GPX_DIR` | no | Where generated GPX files are stored (default `$TMPDIR/sms-gpx`) |
| `GPX_MAX_FILES` | no | Keep at most this many GPX files (default 200) |
| `GPX_TTL_SECONDS` | no | Delete GPX files older than this (default 86400) |

## HELP WANTED

* Improve Style
  * special for Nextcloud GxpPod
  * provide more style? remove some?

## planet.mbtiles

My provided planet.mbtiles is generated by using https://github.com/onthegomap/planetiler  
I just followed this tutorial: https://github.com/onthegomap/planetiler/blob/main/PLANET.md

## contours.mbtiles

Contours will be displayed if a file called `contours.mbtiles` is places next to `planet.mbtiles`.  
There are two different contours files provided. 90m resolution in Zoom Level 11 to 14 and 90m resolution in Zoom Level 11 to 13 with 30m resolution in Zoom Level 14.

> GLO-30: "produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved"  
> GLO-90: "produced using Copernicus WorldDEM-90 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved"

## Core API

### `GET /`

Serves the map UI frontend (index.html) with optional URL parameters for coordinates and zoom level.

**URL Parameters:**
- `lat` — Latitude coordinate
- `lng` — Longitude coordinate
- `zoom` — Zoom level

**Example:** `https://maps.osuv.de/?lat=48.1374&lng=11.5752&zoom=9`

### `GET /v1/capabilities`

Returns JSON capabilities indicating available optional features.

**Response:**
```json
{
  "contours":  true,
  "routing":   true,
  "mcp":       true,
  "gpx":       true,
  "geocoding": false,
  "mcp_tools": ["search_poi", "plan_route", "export_gpx"],
  "difficulty_data": {
    "sac_scale": true, "via_ferrata_scale": true,
    "trail_visibility": true, "smoothness": true
  },
  "routing_limits": { "max_tiles": 1200, "max_crow_km": 50.0, "zoom": 14 },
  "tile_cache": {
    "roads":    { "entries": 812, "max_entries": 2000, "hits": 4210, "misses": 812 },
    "contours": { "entries": 240, "max_entries": 500,  "hits": 190,  "misses": 240 },
    "poi":      { "entries": 0,   "max_entries": 500,  "hits": 0,    "misses": 0 }
  }
}
```

- `contours` — whether `contours.mbtiles` was detected and loaded
- `routing` — always `true`; indicates the `/v1/route/` endpoint is available
- `mcp` — the MCP endpoint at `POST /mcp` is available
- `gpx` — GPX export is available
- `geocoding` — whether `PHOTONSERVER` is configured (enables the `geocode`
  and `reverse_geocode` MCP tools)
- `mcp_tools` — names of the registered MCP tools
- `difficulty_data` — which hiking attributes the loaded tiles actually carry.
  All `false` on stock OpenMapTiles tiles (see *Hiking attributes* below), and
  in that case a missing warning means missing data, not easy terrain. The
  flags flip to `true` as soon as a tile containing them has been decoded.
- `sac_scale_values` — accepted values for `max_sac_scale`, easiest first
- `tile_cache` — live hit/miss counters of the decoded-tile caches

### `GET /v1/poi/{identifier}@{version}?lat={lat}&lon={lon}&category={category}&radius={radius}`

Search for Points of Interest (POI) within a radius and return GeoJSON features.

**Path Parameters:**
- `identifier` — Tileset identifier
- `version` — Tileset version

**Query Parameters:**
- `lat` — Latitude coordinate (required)
- `lon` — Longitude coordinate (required)
- `category` — POI category (required): `supermarket`, `pharmacy`, `hospital`,
  `fuel`, `charging_station`, `alpine_hut`, `camp_site`, `shelter`,
  `drinking_water`, `cave`, `viewpoint`, or `emergency`

  `drinking_water`, `cave`, `viewpoint` and `emergency` need tiles built with
  the extended profile linked above — stock OpenMapTiles does not put springs,
  wells or cave entrances into the `poi` layer at all. Note that
  `drinking_water` includes springs and cattle troughs: that is a location, not
  a potability guarantee.

  Note that `alpine_hut` covers only real mountain huts
  (`alpine_hut`, `wilderness_hut`, `basic_hut`). OpenMapTiles files bus stop
  shelters and public air-raid shelters under `subclass=shelter`, which around
  Garmisch is 113 of 122 hits and mostly unnamed — those live in the separate
  `shelter` category so they cannot bury the actual huts.
- `radius` — Search radius in km, default 15, max 50 (optional)

**Response:** GeoJSON FeatureCollection of POIs, each with a
`properties.distance_km`. Sorted by distance, but named POIs come first
within the same ~500 m band — an unnamed hut slightly closer is less useful
than a named one you can look up.

### `GET /v1/tiles/{identifier}@{version}/{z}/{x}/{y}.mvt`

Fetch a tile in Mapbox Vector Tile (MVT) format.

**Path Parameters:**
- `identifier` — Tileset identifier (e.g., from `MBTILES__1__IDENTIFIER`)
- `version` — Tileset version (e.g., from `MBTILES__1__VERSION`)
- `z` — Zoom level
- `x` — Tile column coordinate
- `y` — Tile row coordinate (converts from Web Mercator to TMS)

**Special:** `contours@1.0.0` is auto-loaded if `contours.mbtiles` exists.

**Gzip Negotiation:** Returns gzip-compressed tiles if client sends `Accept-Encoding: gzip`.

**Response:** `application/vnd.mapbox-vector-tile` (MVT format)

### `GET /v1/styles/{identifier}@{version}/style.json`

Returns a MapLibre GL style JSON with injected tile, font, and sprite URLs.

**Path Parameters:**
- `identifier` — Style identifier (e.g., `osm-bright-gl-style`)
- `version` — Style version (e.g., `1.0.0`)

**Query Parameters:**
- `tiles` — Tile source in format `{tile_id}@{tile_version}` (required)
- `fonts` — Font source in format `{font_id}@{font_version}` (required)

**Available Styles:**
- `dark-matter-gl-style@1.0.0`
- `fiord-color-gl-style@1.0.0`
- `maptiler-3d-gl-style@1.0.0`
- `maptiler-terrain-gl-style@1.0.0`
- `maptiler-basic-gl-style@1.0.0`
- `maptiler-toner-gl-style@1.0.0`
- `osm-bright-gl-style@1.0.0`
- `positron-gl-style@1.0.0`
- `osuv-style@1.0.0`

**Response:** `application/json` with complete style JSON including:
- Tile source URL: `/v1/tiles/{tiles}@{version}/{z}/{x}/{y}.mvt`
- Glyph URL: `/v1/fonts/{fonts}@{version}/{fontstack}/{range}.pbf`
- Sprite URL: `/v1/styles/{identifier}@{version}/sprite`
- Contour layers (if contours.mbtiles detected): `contour-line` and `contour-label` (visible when toggled in frontend)

### `GET /v1/styles/{identifier}@{version}/sprite.json`

Returns the sprite JSON index file.

**Path Parameters:**
- `identifier` — Style identifier
- `version` — Style version

**Response:** `application/json` with sprite image references and bounds

### `GET /v1/styles/{identifier}@{version}/sprite@2x.json`

Returns the high-resolution (2x) sprite JSON index file.

**Path Parameters:**
- `identifier` — Style identifier
- `version` — Style version

**Response:** `application/json` with high-res sprite references

### `GET /v1/styles/{identifier}@{version}/sprite.png`

Returns the sprite image (1x resolution).

**Path Parameters:**
- `identifier` — Style identifier
- `version` — Style version

**Response:** `image/png`

### `GET /v1/styles/{identifier}@{version}/sprite@2x.png`

Returns the sprite image (2x resolution for retina displays).

**Path Parameters:**
- `identifier` — Style identifier
- `version` — Style version

**Response:** `image/png`

### `GET /v1/fonts/{identifier}@{version}/{stack}/{range}.pbf`

Returns merged glyph data in Protocol Buffer format for font rendering.

**Path Parameters:**
- `identifier` — Font identifier (`fonts-gl`)
- `version` — Font version (`1.0.0`)
- `stack` — Comma-separated font stack (max 4 fonts, e.g., `Noto Sans Regular,Noto Sans Bold`)
- `range` — Glyph range (e.g., `0-255`)

**Response:** `application/vnd.google.protobuf`
- Gzip-compressed if client sends `Accept-Encoding: gzip`
- Merges glyphs from all fonts in stack, deduplicating by glyph ID

### `GET /v1/route/{identifier}@{version}?from={lat},{lon}&to={lat},{lon}&profile={profile}`

Compute a walking or cycling route between two coordinates using the vector tile road network.
No external routing engine required — routing is performed entirely server-side from the MBTiles data.

**Path Parameters:**
- `identifier` — Tileset identifier (same as used for `/v1/tiles/`)
- `version` — Tileset version

**Query Parameters:**
- `from` — Start point as `lat,lon` (required)
- `to` — End point as `lat,lon` (required)
- `profile` — Routing profile: `foot` (default) or `bike`
- `buffer_km` — Corridor half-width around the straight line (optional).
  Defaults to 10 % of the segment length, at least ~3.3 km. Raise it when a
  detour around a lake or a closed area is needed.
- `elevation` — `true` to add `ascent_m`/`descent_m` from `contours.mbtiles`
  and switch the duration estimate to DIN 33466 (optional)
- `max_sac_scale` — hardest SAC grade allowed: `hiking` (T1),
  `mountain_hiking` (T2), `demanding_mountain_hiking` (T3), `alpine_hiking`
  (T4), `demanding_alpine_hiking` (T5), `difficult_alpine_hiking` (T6).
  Ways tagged *above* the limit are excluded from routing. Requires tiles with
  hiking attributes (optional)
- `allow_via_ferrata` — `false` excludes ways tagged as via ferrata or with
  fixed ladders (optional, default `true`)
- `prefer_routes` — `true` prefers ways carrying a marked hiking route
  (`iwn`/`nwn`/`rwn`/`lwn`), or a cycle route for `profile=bike` (optional)
- `follow_route` — follow one named route, e.g. `Malerweg` or the ref `E3`
  (optional)

  Note these are **independent axes**: a way can be T6 scrambling without any
  ferrata tag, and a cabled route can be tagged T2. On a real Zugspitze test,
  excluding ferratas alone still produced a T6 route — for a safe tour set
  both. The response says so via `terrain_warnings` when only one is used.

**Example:**
```
GET /v1/route/mytiles@1.0.0?from=48.137,11.575&to=48.155,11.602&profile=foot
```

**Response:** GeoJSON Feature with LineString geometry
```json
{
  "type": "Feature",
  "geometry": {
    "type": "LineString",
    "coordinates": [[11.575, 48.137], ...]
  },
  "properties": {
    "distance_km":  3.241,
    "duration_min": 43.2,
    "profile":      "foot",
    "tiles_loaded": 36,
    "buffer_tiles": 2.0,
    "nodes":        12847,
    "cache_hits":   30,
    "cache_misses": 6,
    "snap_start_m": 12.4,
    "snap_end_m":   31.9
  }
}
```

**Profile weights** (OpenMapTiles `transportation` layer `class` attribute):

| Road class | `foot` | `bike` |
|---|---|---|
| `footway`, `path`, `pedestrian` | preferred (1.0) | not passable |
| `cycleway` | allowed (1.3) | preferred (1.0) |
| `track` | 1.1 | 1.2 |
| `residential`, `living_street` | 1.2–1.3 | 1.1 |
| `tertiary` | 1.8 | 1.4 |
| `secondary` | 2.5 | 1.6 |
| `primary` | not passable | 2.5 |
| `motorway`, `trunk` | not passable | not passable |
| `steps` | 1.2 | not passable |

**Duration estimate:** 4.5 km/h for `foot`, 15 km/h for `bike`. With
`elevation=true` and `contours.mbtiles` present, `foot` switches to a
DIN 33466 / SAC estimate instead (300 m ascent or 500 m descent per hour,
combined as `max(horizontal, vertical) + min(horizontal, vertical) / 2`).

### Hiking attributes: sac_scale, via ferratas, surface

OpenMapTiles is a **rendering** schema. Its transportation layer whitelists the
attributes it keeps, and `sac_scale`, `trail_visibility` and
`via_ferrata_scale` are not on that list — they exist in the OSM source data
but are dropped during tile generation. The practical consequence is severe: a
router cannot tell a T1 stroll from a T5 scramble, and **via ferratas appear as
ordinary paths**. A route from the Zugspitze to the Alpspitze happily runs over
"Stopselzieher" and "Höllentalsteig", both cabled climbing routes with ladders
and a glacier crossing.

sms therefore evaluates these attributes when they are present:

| Attribute | Used for |
|---|---|
| `sa

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [markuman](https://github.com/markuman)
- **Source:** [markuman/sms](https://github.com/markuman/sms)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-markuman-sms
- Seller: https://agentstack.voostack.com/s/markuman
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
