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 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 |
- Create a free RepeaterBook account, or log in to an existing one.
- Go to API Applications.
- Find RepeaterBook Python Client (App #114) in the application list.
- Generate a token for it — a string starting with
rbuapp_. - Set it as
REPEATERBOOK_APP_TOKENin 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:
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 theexport.phpendpoint, which understandsstate_idbut notregion. - Everywhere else — use
region, served byexportROW.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:
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.