rf-los-mcp
by aaronjbrown
README.md
# rf-los-mcp
A FastMCP (stdio) server that computes RF line of sight over locally cached
[FABDEM](https://data.bris.ac.uk/data/dataset/s5hqmjcdj8yo2ibzi9b4ew3sn) V1-2
terrain (30 m bare-earth elevation). It is meant to be combined with a
`repeaterbook` MCP server: take repeater lat/lon and output frequency (MHz) from
repeaterbook, then ask this server whether you can reach the repeater from a
point or along a route.
## Setup
```sh
python3 -m venv .venv && .venv/bin/pip install -e .
```
Register with Claude Code (run this yourself; it is not done by the install):
```sh
claude mcp add rf-los -- /absolute/path/to/.venv/bin/rf-los-mcp
```
## Tools
| Tool | Purpose |
| --- | --- |
| `rf_los_check` | LOS between observer A and target B, with Fresnel clearance when a frequency is given |
| `rf_los_route` | Coverage of one or more targets along a route (waypoints, a GPX file or a GeoJSON file) |
| `terrain_elevation` | Ground elevation (m AMSL) for up to 1000 points |
| `terrain_profile` | Elevation profile along a path (max 20,000 samples), optional earth-curvature adjustment |
| `dem_cache_status` | List cached terrain tiles and the cache directory |
| `dem_prefetch` | Download tiles for a bounding box or centre+radius so you can work offline |
Defaults: observer 1.5 m AGL, target 25 m AGL (heights are AGL unless
`*_height_ref="amsl"`), k-factor 4/3, "clear" means 0.6 x first Fresnel zone is
unobstructed, LOS sample spacing 30 m, route spacing 200 m, target snap radius
1000 m, route max range 100 km. Units are metres, kilometres (route distances
only), MHz and decimal degrees.
There is **no default frequency**: without `frequency_mhz` the check is
geometric only and the result says so. Snapping moves the *targets* (repeater
coordinates are often imprecise) to nearby higher ground, never the observer.
For out-and-back routes pass the outbound leg only. Route waypoints and GPX or
GeoJSON points are geometry, resampled every `sample_spacing_m` along the route; only
**named** waypoints plus the start and end get per-waypoint verdicts, so name
the stops you care about (GPX track points are treated as unnamed). A route
call is limited to 50 targets, 10,000 line-of-sight checks (samples x targets)
and about 5 million terrain samples in total, including target snapping.
Other limits: `rf_los_check` paths and `rf_los_route`'s `max_range_km` are at
most 300 km. Any one call (other than `dem_prefetch`) downloads at most 16 new
terrain tiles (about 200 MB), and `dem_prefetch` takes at most 100 tiles
(about 1.2 GB) per call. These limits stop a mistaken or manipulated agent
request from filling the disk or hammering the FABDEM server.
`geojson_path` takes a `.geojson`/`.json` file (for example an OpenStreetMap
export). It must pass the official
[GeoJSON JSON Schema](https://geojson.org/schema/GeoJSON.json) (RFC 7946,
bundled; MIT licence), so any other JSON file is rejected before its contents
are used. Prefer it to copying GeoJSON coordinates into `waypoints`, because
GeoJSON stores positions as `[lon, lat]`. LineString and MultiLineString pieces
are joined end to end (pieces may be reversed or out of order, but branching or
disconnected pieces are rejected). Point features with a `name` property, such
as a summit or hut, are placed on the line as named waypoints and must be within
500 m of it. A file with only Points uses them, in order, as the route.
## Caveats
- Vegetation and buildings are not modelled (bare-earth DEM), so results may be
optimistic through dense forest.
- Route analysis samples the route and the line of sight at discrete spacing
(default 200 m along the route); narrow ridges between samples can be missed.
- Ocean is treated as sea level (0 m). A missing land tile is an error, never 0.
## Terrain cache
Tiles are cached in `$RF_LOS_CACHE_DIR`, else `$XDG_CACHE_HOME/rf-los`, else
`~/.cache/rf-los`. Each 1 degree square tile is about 12 MB. **The first query in
a new area downloads the tile(s) it needs, which can take about a minute per
tile.** To avoid that (or to work offline), prefetch before you go, e.g. ask the
agent to call `dem_prefetch` with a bounding box (`south`, `west`, `north`,
`east`) or `center_lat`, `center_lon`, `radius_km`; areas needing more than
`max_tiles` (default 16) tiles are refused. Offline queries over cached tiles
work normally; uncached land tiles produce an error naming the tile.
Up to 6 decoded tiles are held in memory (about 52 MB each, ~310 MB peak).
## Attribution and licence
Code: copyright (C) 2026 Aaron Brown, licensed under the GNU Affero General
Public License v3.0 only (AGPL-3.0-only); see [LICENSE](LICENSE). If you run a
modified version as a network service (for example a hosted MCP server), you
must offer its users the source of your version under the same licence.
GeoJSON schema: `src/rf_los/data/GeoJSON.schema.json` is from
[geojson/schema](https://github.com/geojson/schema), copyright (c) 2018 Tim
Schaub, MIT licence (see `src/rf_los/data/GeoJSON.schema.LICENSE.md`).
Terrain: FABDEM V1-2 (Neal & Hawker 2023, University of Bristol,
doi:10.5523/bris.s5hqmjcdj8yo2ibzi9b4ew3sn), licensed CC BY-NC-SA 4.0, derived
from Copernicus DEM GLO-30. FABDEM is for **non-commercial use only**; do not
redistribute the downloaded tiles. Results carry this attribution.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues