
fcc-spectrum-mcp-server
io.github.cyanheadsv0.1.1Updated Oct 1, 2026
Search FCC radio licenses, find nearby transmitter sites, and see who is licensed on a frequency.
Installation
In SourceWeft
- Open fcc-spectrum-mcp-server in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
@cyanheads/fcc-spectrum-mcp-server
Search FCC radio licenses, find nearby transmitter sites, and see who is licensed on a frequency via MCP. STDIO or Streamable HTTP.
Overview
US radio spectrum licensing from the FCC Universal Licensing System (ULS), served from a local SQLite index of the FCC's weekly and daily bulk files. Look up a callsign, licensee, or FCC Registration Number (FRN); read a license's sites, antennas, frequencies, power, and emission designators; find licensed transmitter sites within a radius of a point; and see who is authorized on a frequency or band, market-area spectrum blocks included. No API key, and no call to the FCC at request time. Runs as a stdio process or a local Streamable HTTP server.
The default index covers land mobile (private, commercial, and broadcast auxiliary), microwave, cellular, market-area wireless, paging, coast stations, broadband radio (BRS/EBS), and amateur licenses, plus spectrum leases. GMRS, ship, and aircraft licenses are opt-in. Broadcast stations (AM/FM/TV) and satellite earth stations are separate FCC systems and are not included.
Tools
Resources
Also reachable via fcc_spectrum_get_license.
Capability reference
fcc_spectrum_search_licenses tool
- Filters:
callsign,licensee(every word must match the start of a word in the name),frn,radio_service, andstate(the licensee's mailing state) — at least one required;statusnarrows toA(default),C,E,L,P,T,X, orany limit1–100 (default 25) with cursor paging; each row carries theusito pass tofcc_spectrum_get_license, plusisLease,licenseeRedacted, and location and frequency counts- Typed errors:
no_criteria,unknown_radio_service,service_not_indexed,invalid_cursor,index_not_ready
fcc_spectrum_get_license tool
- Exactly one of
callsignorusi(identifier_requiredotherwise); a callsign shared by several records returns the active one, else the most recent, and lists the others inotherCallsignRecords - Pages large records: whole locations up to 50 sites and 100 antennas per call, up to
max_frequenciesfrequency rows (1–1000, default 100), and 100 leases;nextLocationOffsetandnextLeaseOffsetfeedlocation_offsetandlease_offseton the next call - A miss returns
found: falsewithguidance(plus up to 5 callsign-prefixcandidates), not an error;technicalRetained: falsemarks a record whose status keeps no sites or frequencies
fcc_spectrum_find_transmitters tool
latitude/longitudeas decimal degrees or DMS strings,radius_km0.1–100 (default 5); optionalfrequency_low/frequency_highinunit(kHz,MHzdefault,GHz),radio_service, andstatus(Adefault,L,X, oranyfor all three)- Sites nearest first,
limit1–100 (default 25) with cursor paging; each site lists up tomax_frequencies_per_sitefrequencies (1–50, default 10), withfrequencyCountcarrying the full count - Typed errors:
invalid_frequency_range,unknown_radio_service,service_not_indexed,invalid_cursor,index_not_ready
fcc_spectrum_search_frequencies tool
frequency_lowrequired,frequency_highoptional, inunit; a site assignment matches when its occupied band (widened by its emission bandwidth) overlaps the query, a market block when its filed edges do;kindissite,market, orboth(default)- Narrow by
state,radio_service,licensee, andstatus(Adefault,L,X,any);limit1–200 (default 50) with cursor paging, frequency ascending; each row'skindsays whether it is a site assignment or a market block - Typed errors:
invalid_frequency_range,unknown_radio_service,service_not_indexed,invalid_cursor,index_not_ready
fcc_spectrum_list_reference tool
topic:radio_services,license_statuses,location_types,antenna_types,applicant_types,operator_classes, orcoverage;filternarrowsradio_servicesto entries containing every word givencoveragereportsindex.status(none,building,ready), per-group record, site, and frequency counts with snapshot times, and whether redaction is on; it works before the index is built
fcc-spectrum://license/{callsign} resource
- The first page of
fcc_spectrum_get_licensefor a callsign (up to 100 frequency rows) asapplication/json, withdataAsOf, location, site, and frequency totals, and anoticenaming the tool call that reads the rest; cached 1 hour - Typed errors:
index_not_ready,license_not_found
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
FCC ULS-specific:
- Keyless and local at request time — every query reads the SQLite index; network access is needed only to build and refresh it
- Index generations behind an atomic pointer — a rebuild writes a new file and the server switches to it within a minute, so the index being served is never modified in place
- Occupied-band frequency matching — each assignment's band is widened by the bandwidth parsed from its emission designators, so a query near a wide channel's edge still finds it
- Spectrum leases are first-class records —
isLeasemarks them, the licensee shown is the lessee, and lease links are listed in both directions - Site state is derived from coordinates when the filing leaves it blank (common for microwave and BRS/EBS), and flagged
stateFromCoordinates - Individual licensees are redacted by default, and contact details are never ingested — see Individual-licensee redaction
Agent-friendly output:
- Freshness and filter echo — every data response carries
dataAsOf, and search tools echoappliedFilterswith normalized values and defaults - Truncation disclosure —
truncated,shown,cap, andtotalCountplus anoticethat names the next call, so a partial page is never read as the whole - Absent stays absent — fields ULS leaves blank are omitted rather than filled, and filed coordinates are kept as DMS text (
coordinatesDms) when they fail validation - Discriminated output —
found,kind,isLease,licenseeRedacted, andtechnicalRetainedlet callers branch on data, not string parsing
Getting started
The index must be built once before any search works — see First-run setup. The package does not ship FCC data; until
mirror:inithas run, data tools fail withindex_not_readyandfcc_spectrum_list_referencewith topiccoveragereports the build state.
Add the following to your MCP client configuration file:
Or with npx (no Bun required):
Or with Docker (mount a volume at /usr/src/app/.mirror so the index persists across containers):
For Streamable HTTP, set the transport and start the server:
FCC_SPECTRUM_MIRROR_DIR must name the directory mirror:init built. A relative path resolves against the process's working directory, which an MCP client chooses, so use an absolute path.
First-run setup
The index is built from the FCC's public ULS bulk files at data.fcc.gov — weekly full snapshots per service group, plus daily incrementals. On the default groups a build downloads about 1.1 GB of zips, one at a time; a measured build produced 3,270,502 license records and an index of about 1.8 GB in about 3 minutes.
Build it once from a source checkout (see Installation) or the Docker image:
With Docker, run the same commands against the volume: docker run --rm -v fcc-uls:/usr/src/app/.mirror ghcr.io/cyanheads/fcc-spectrum-mcp-server:latest bun run mirror:init.
Keeping it current:
- HTTP transport. The server schedules the weekly rebuild on Sundays at 16:00 and the daily refresh at 17:00, in the process's local time (the Docker image runs in UTC; the FCC publishes snapshots Sunday morning US Eastern). A daily refresh that has fallen more than six days behind runs the weekly rebuild instead. Neither job runs until
mirror:inithas published a first index. - stdio. Nothing is scheduled. Run
mirror:refreshdaily andmirror:initweekly from cron or another scheduler; daily files never remove licenses, so removals arrive with the weekly rebuild. - Disk. A rebuild writes a new generation beside the one being served and keeps the downloaded zip until its group is loaded, so leave room for a second index plus the largest zip (about 420 MB). Older generations are deleted at the start of the next rebuild.
- Concurrency. An ingest lock in the index directory lets one
mirror:initormirror:refreshrun (or scheduled job) write at a time; an interruptedmirror:initresumes from its last completed step when rerun.
Prerequisites
- Bun v1.4 or higher (or Node.js v24+).
bun:sqliteis built into Bun; under Node, installbetter-sqlite3beside the server (declared as an optional peer dependency). - Disk for the index at
FCC_SPECTRUM_MIRROR_DIR: about 1.8 GB on the default groups, plus rebuild room as above. - Network access to
data.fcc.govwhen building or refreshing the index. Queries need none.
Installation
For local development, or to build the index from source:
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
- Build the index (see First-run setup):
Configuration
See .env.example for the full list of optional overrides.
Service groups
The FCC splits its weekly snapshots into service groups, named as its files are. Adding a group takes effect at the next mirror:init; a removed group drops out at the next weekly rebuild.
Individual-licensee redaction
ULS records name the person behind many licenses, and not only amateur ones: individuals also hold land mobile, paging, and microwave licenses. FCC_SPECTRUM_REDACT_INDIVIDUALS is on by default and fails safe:
- A record is an individual's when its applicant type is
I, or blank in the amateur and GMRS services. A blank type or typeH(Other) in any service also counts when the filing carries a person's name parts, or when the licensee name contains no organization word (Inc,County,Church,Wireless, …), so an organization named without one is redacted too. While redaction is on, its licensee name and city arenullwithlicenseeRedacted: true, and its sites' street addresses are omitted. - Trustee names are withheld on every license, since a trustee is always a person.
- Licensee name search excludes individuals, and the response
noticesays so. A callsign, USI, or FRN lookup still returns the record, redacted. - Coordinates, county, state, and technical data stay: they are the spectrum record.
- Redaction applies when a response is built, so changing the setting needs a restart, not a rebuild.
Licensee mailing street addresses, ZIP codes, PO boxes, attention lines, phone numbers, fax numbers, and email addresses are never ingested, for anyone.
Known limitations
- HTTP mode rebuilds in-process. The scheduled weekly rebuild runs inside the server process and can slow responses while it runs.
- Dense-band frequency searches return partial pages.
fcc_spectrum_search_frequenciesover a crowded band (all of 150–174 MHz, or the whole spectrum) reads the band in frequency order and stops each call short. A page can hold fewer rows thanlimitand still carrynextCursor, andtotalCountis then a lower bound (totalIsLowerBound: true). A sparse filter across a wide band can return several empty pages before its first rows; astate,radio_service, orlicenseefilter narrow enough to search directly keeps the exact count. - Sites, frequencies, and market blocks are kept only for live licenses (
Aactive,Lpending legal,Xterm pending). Expired, cancelled, and terminated records keep their licensee, status, dates, and lease links. - No county-to-market mapping. Market-area licenses carry a market code and name but not the counties inside the market, so state filtering reads the state codes in the market name. ULS cuts market names at 30 characters, which can drop the state.
- Radius search sees only located sites. Mobile, control-station, and temporary locations often file no coordinates.
- Derived states are approximate near borders, within about a kilometer of a state line; territories other than Puerto Rico get no derived state.
- Removals lag up to a week. Daily files never delete licenses; the weekly rebuild does. A refresh gap longer than the daily window forces a full rebuild.
- Priority Access Licenses are not found by frequency. ULS files each 3.5 GHz PAL as a 10 MHz channel width with no frequency; list them with
fcc_spectrum_search_licensesandradio_servicePL. - Frequencies at a shared location number cannot be tied to a site. ULS files antennas and frequencies against the location number, so when a license files several sites under one number, no frequency can be placed at a single site.
- Station class codes pass through undecoded (
FB2,FXO,MO). - Coordinates are NAD83 as filed, with no datum shift; a few fail validation and appear only as DMS text.
- A few implausible emission bandwidths pass. A designator 20% of its frequency or wider is treated as a filing error and ignored; narrower implausible filings are taken as filed.
Running the server
Local development
-
Build and run:
-
Run checks and tests:
Docker
The Dockerfile defaults to HTTP transport with stateless sessions, ships the mirror:init / mirror:refresh / mirror:verify CLI, pre-creates a writable .mirror directory owned by the runtime user (mount a volume there), and logs to /var/log/fcc-spectrum-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging; data access goes through the ULS index service - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts - The index is the source of truth at runtime — build and refresh it out-of-band, keep blank ULS fields absent, and never fabricate a value
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.
License data comes from the FCC Universal Licensing System (ULS), a US government work in the public domain (17 U.S.C. §105). Credit it as "FCC Universal Licensing System (ULS)" with the dataAsOf time each response carries. This project redistributes none of that data; operators download it from the FCC when building the index. This server is independent of the FCC and not endorsed by it.
Source: README.md at commit 3d06f12
Tools
0Version history
1- v0.1.1LatestOct 1, 2026

