Skip to content

RepeaterBook MCP server

An optional Model Context Protocol server — built on FastMCP 3 — that exposes RepeaterBook lookup to agents.

The server ships as a console script, repeaterbook-mcp, behind the mcp extra. You don't normally install it yourself — point your MCP client at uvx and it will fetch and run the right version on demand, in its own isolated environment. See Register with an MCP client.

You will need a free RepeaterBook account and an API token — see Get an API token. No application registration is required: the token is generated against this project's existing App #114 entry.

To install it anyway — to run it by hand, or to use the subpackage as a library:

uvx --from "repeaterbook[mcp]" repeaterbook-mcp
uv tool install "repeaterbook[mcp]"
uv add "repeaterbook[mcp]"
pip install "repeaterbook[mcp]"

uvx needs --from because the command (repeaterbook-mcp) and the package (repeaterbook) have different names; uv tool install takes the package directly and installs whatever commands it provides.

Get an API token

The server needs a RepeaterBook API token, and refuses to start without one. You do not need to register an application — this project is already registered with RepeaterBook, and the token you generate is issued against that existing registration:

Field Value
Application RepeaterBook Python Client
Application ID App #114
  1. Create a free RepeaterBook account, or log in to an existing one.
  2. Go to API Applications.
  3. Find RepeaterBook Python Client (App #114) in the application list.
  4. Generate a token for it — a string starting with rbuapp_.
  5. Set it as REPEATERBOOK_APP_TOKEN in your MCP client config (below).

The token is tied to your account, not to the application, so treat it as a secret and don't share it. The server's default User-Agent is the one registered for App #114, so the token works with no further configuration.

Configuration (environment)

Variable Purpose Default
REPEATERBOOK_WORKING_DIR Where the SQLite DB + cache live. Created if missing; a leading ~ is expanded. .
REPEATERBOOK_APP_CONTACT Contact address in the API User-Agent. Leave unset. the registered address
REPEATERBOOK_APP_TOKEN Per-user rbuapp_ API token. Required

REPEATERBOOK_APP_TOKEN is required because, as of RepeaterBook's 2026-03-03 API policy, every export needs an approved per-user rbuapp_ token — an unauthenticated request is refused with 401 auth_missing. It has no default, so the server fails at startup rather than on the first tool call.

Leave REPEATERBOOK_APP_CONTACT unset. It defaults to the address registered for App #114, and the API matches an approved application's User-Agent literally — setting it to your own address returns 403 ua_mismatch, even with a valid token. Override it only if you registered your own separate application with RepeaterBook and hold a token for that.

Register with an MCP client

Invoke it through uvx, which needs nothing installed up front:

{
  "mcpServers": {
    "repeaterbook": {
      "command": "uvx",
      "args": ["--from", "repeaterbook[mcp]", "repeaterbook-mcp"],
      "env": {
        "REPEATERBOOK_WORKING_DIR": "~/.repeaterbook",
        "REPEATERBOOK_APP_TOKEN": "rbuapp_..."
      }
    }
  }
}

MCP clients launch servers directly rather than through a shell, so "command" has to be something on the client's PATH. uvx usually is; a console script from a virtual environment usually isn't.

To pin a version, put it in the --from spec — repeaterbook[mcp]==0.10.0.

If you'd rather have the command installed persistently, use uv tool install as above and then point the client straight at it:

{
  "mcpServers": {
    "repeaterbook": {
      "command": "repeaterbook-mcp",
      "env": { "…": "as above" }
    }
  }
}

That only works if the client can find repeaterbook-mcp on its PATH, which is not a given for GUI applications — they often don't inherit the PATH from your shell. Use an absolute path (uv tool dir will tell you where it landed) if the client can't find it.

Token troubleshooting

Symptom Cause Fix
Server exits at startup, app_token validation error REPEATERBOOK_APP_TOKEN unset or empty Set it in the client's env block
401 auth_invalid Token wrong, revoked, or expired Regenerate it on API Applications
403 ua_mismatch User-Agent doesn't match the application the token was issued for Unset REPEATERBOOK_APP_CONTACT

Tools

  • sync_repeaters(country?, state?, region?, modes?) -> SyncResult — download a region into the local store.
  • search_repeaters(lat, lon, radius_km, country?, state?, region?, bands?, modes?, status?, use?, refresh?) -> [RepeaterSpec] — nearby repeaters, distance-sorted.
  • get_repeater(source_id) -> [RepeaterSpec] — one repeater by "state_id:repeater_id".

Every filter is an enum, so its allowed values ride in the tool schema and a client sees them without consulting these docs:

Filter Values
bands M_10 M_6 M_4 M_2 CM_70 CM_33 CM_23 CM_13 CM_3
modes FM DMR DSTAR FUSION P25 NXDN TETRA M17
status ON_AIR OFF_AIR UNKNOWN
use OPEN PRIVATE CLOSED
state RepeaterBook's own NA identifiers — see below

modes is one vocabulary across both tools. For sync_repeaters, DSTAR/FUSION/M17 don't narrow the server-side download (the RepeaterBook API has no filter for them) and are instead filtered locally during search_repeaters.

Scoping a query

RepeaterBook returns at most 3500 rows per response and gives no indication when it truncates. A whole-country query for a large country comes back looking like a success while quietly missing data, so scope narrowly.

Which parameter to use depends on the country:

  • United States, Canada, Mexico — use state. These are served by the export.php endpoint, which understands state_id but not region.
  • Everywhere else — use region, served by exportROW.php.

Mixing them up used to return zero results with no explanation; the tools now reject the combination with an error naming the right parameter.

state values are RepeaterBook's own identifiers, not ISO 3166-2, and cannot be derived from it:

Subdivision RepeaterBook ISO 3166-2
California 06 US-CA
Alberta CA01 CA-AB
Jalisco MX14 MX-JAL

The US uses two-digit FIPS codes; Canada and Mexico use bespoke CA##/MX## numbering that RepeaterBook does not document publicly. They're also unforgiving — 6 is not 06 and returns nothing rather than erroring, and lower-case ca01 is rejected outright. The tools take an enum (repeaterbook.na_states.NAState) so the full list travels in the tool schema and neither mistake is possible.

Because the 3500-row cap hides data, these identifiers can't be discovered by syncing a country and reading them back: any subdivision missing from the truncated slice would be invisible, and a subdivision with no repeaters yet is still a valid query value. That's why the library carries the list.

sync_repeaters returns a SyncResult with count, truncated, skipped and detail. When truncated is true the scope hit the cap and is very likely incomplete.

skipped counts rows RepeaterBook served that could not be modelled and were dropped — community-maintained data occasionally contains these, and one of them should not cost you the rest of the region. Skipped rows still count toward the cap for the purposes of truncated, since the API spent a slot on them.

Syncing

search_repeaters reads the local store. When you pass a country/state/region and the store is empty, it downloads that scope first; otherwise it searches what's already there. Pass refresh=True to force a re-download — syncing re-parses the whole regional payload and re-merges thousands of rows, so it is not something to do on every search.

The repeater-spec contract

Tools return repeater-spec rows — a neutral, source-agnostic shape carrying absolute rx/tx frequencies (the consuming radio derives duplex/offset). The JSON Schema is published with the package at repeaterbook/schemas/repeater_spec.schema.json (src/repeaterbook/… in this repository), and repeaterbook.spec.schema_path() returns its installed location.

Spec wire shape

Each spec is one programmable channel. Mode-specific fields live in params, which is a union discriminated on its own mode field:

{
  "name": "VK4RDM",
  "callsign": "VK4RDM",
  "nearest_city": "Brisbane",
  "rx_frequency_mhz": "439.000000",
  "tx_frequency_mhz": "434.000000",
  "ctcss_tx_hz": "110.9",
  "ctcss_rx_hz": "110.9",
  "dcs_tx_code": null,
  "dcs_rx_code": null,
  "latitude": "-27.470125",
  "longitude": "153.021072",
  "distance_km": "12.345",
  "operational_status": "ON_AIR",
  "use": "OPEN",
  "band": "CM_70",
  "notes": null,
  "allstar_node": null,
  "echolink_node": null,
  "irlp_node": null,
  "wires_node": null,
  "last_update": "2026-01-01T00:00:00Z",
  "source": "repeaterbook",
  "source_id": "48:24371",
  "params": { "mode": "DMR", "dmr_id": "505401", "color_code": "1" },
  "mode": "DMR"
}

Internet-linking nodes

allstar_node, echolink_node, irlp_node and wires_node carry a repeater's node id on each linking network, or null where it has none.

They sit at the top level rather than inside params because they are not RF modes: they do not change how a channel is programmed, and are typically reached by DTMF on an otherwise ordinary FM channel. A repeater that fans out into several specs repeats its node ids on each of them, since the linking backend belongs to the repeater rather than to one mode.

RepeaterBook writes an absent node as the literal "0" as well as as an empty value, and "0" is the most common value in real exports. Both are normalized to null, so a non-null node field always means a real, dialable node.

Uplink and downlink tones are carried separately — ctcss_tx_hz/dcs_tx_code for the uplink, ctcss_rx_hz/dcs_rx_code for the downlink. A repeater can use a different tone in each direction, so collapsing them into one field would silently drop one.

Frequencies, tones, coordinates and distances are Decimal, serialized as decimal strings to avoid binary-float rounding. Each is a named type with a range constraint, emitted once under $defs (FrequencyMHz, CtcssToneHz, LatitudeDeg, LongitudeDeg, DistanceKm) and referenced by every field that uses it, so a consumer can generate one type per quantity.

mode appears twice by design. The copy inside params is the discriminator — it is what makes an FM channel carrying a DMR colour code invalid against the schema. The top-level copy is computed from params.mode, so the two are always consistent, and dict consumers can read spec["mode"] without descending into params.

One consequence: JSON Schema expresses no constraint tying the two mode values together, so the published schema does not cross-check them and a hand-written payload with contradictory mode and params.mode will validate. Payloads this library produces are consistent by construction, and payloads it parses ignore the top-level value and recompute it from params.mode.

The schema is generated in Pydantic's serialization mode. mode is a computed field, and Pydantic emits computed fields only into the serialization schema — the default validation mode would publish a contract missing a key that every response actually carries.

Regenerating the schema

The schema is generated from the RepeaterSpec model. After changing the model, regenerate the committed file from your editable dev environment with:

repeaterbook-write-schema      # or: nox -s schema -- --write

A pre-commit hook regenerates it automatically when you commit a change to spec.py, utils.py, or the schema file. CI runs nox -s schema, which fails if the committed file has drifted from the model.