@cyanheads/fcc-broadband-mcp-server
Access FCC broadband availability, coverage analysis, and digital divide data for US geographies and census blocks via MCP. STDIO or Streamable HTTP.
10 Tools • 2 Resources • 1 Prompt • 1 Skill
Overview
FCC broadband data from Form 477 (June 2015 – June 2021) and the Broadband Data Collection (June 2022 onward). Geocode coordinates to census blocks, look up the ISPs and advertised speeds at a block, and summarize, compare, or rank geographies by unserved population for BEAD and equity research.
Tools
Resources
Resource data is also reachable through tools: fcc_get_coverage_summary for geographies, fcc_search_providers for holding companies.
Prompts
Skill
fcc-broadband-guide covers geography and holding-company identifier chains, comparable population scopes, and sampling limits. It ships with plugin installs, npm, Docker, and the desktop bundle, and is available over MCP at skill://fcc-broadband-guide/SKILL.md.
Capability reference
fcc_geocode_block tool
latitude and longitude in decimal degrees, resolved against 2010 census boundaries through the FCC Geo API
- Returns
blockFips (15 digits, ready for fcc_search_availability), censusVintage, countyFips, countyName, stateFips, stateCode, and stateName; coordinates over water or outside US coverage fail as block_not_found. Structured FCC backend faults fail as geo_backend_unavailable with guidance to retry the same coordinates later
fcc_search_availability tool
block_fips (15 digits, 2010 vintage) is required; tech_filter (Form 477 technology codes), min_speed_down (Mbps), and consumer (true consumer, false business) narrow the rows
- One row per offering with
hoconum, techCode / techLabel, maxDownloadMbps, maxUploadMbps, consumer, and business, plus totalProviders counting distinct holding companies; an unfiltered block with no providers fails as block_not_found, while a filtered query that matches nothing returns an empty list with a notice
fcc_search_geographies tool
- Required nonblank
name matches a case-insensitive literal substring; optional geography_type is state, county, cd, place, cbsa, or tribal, limit defaults to 20 (max 100), and offset starts at 0
- Returns distinct
{ geography_type, geography_id, name } matches in stable name/type/GEOID order, plus count, offset, and nextOffset only when more matches remain. Empty and past-end pages carry guidance; no total count is computed
- Uses the live FCC lookup with
year='2021' even when the mirror is enabled. Include a location qualifier in name to narrow matches; CBSA codes do not establish state membership. Pass the selected type and GEOID to fcc_get_coverage_summary
fcc_get_coverage_summary tool
geography_type is one of nation, state, county, cd, place, cbsa, tribal; geography_id is required for every type but nation; urban_rural_filter (all / R / U) and tribal_filter (all / T / N) narrow the population
- Returns
population by provider count (noCoverage, oneProvider, twoProviders, threeOrMore, total), coveragePct, unservedPct, competitivePct, and a per-segment breakdown. An empty demographic selection returns zero totals and a notice after confirming geography rows exist; a missing geography still fails as geography_not_found
- Required reads share a 48-second deadline (
coverage_timeout); invalid geography inputs fail as invalid_geography_combo or invalid_geography_id_shape
- National coverage is named
United States locally and retains geography.id: "0"; it needs no geography-name lookup
fcc_compare_areas tool
geography_ids of one geography_type (every type but nation), or compare_all_states: true for the 50 states plus DC; the first 300 distinct IDs are compared, with duplicates and excluded IDs disclosed. sort_by is unserved_pct (default), unserved_pop, coverage_pct, or competitive_pct
- Every sort ranks worst first, so
rank 1 is the area most in need; rows carry id, name when resolved, the provider-count populations, and all three percentages. Fails as missing_geography_ids, invalid_all_states_combo, invalid_geography_id_shape, or no_data_found
unmatchedIds identifies queried IDs without coverage rows, separately from IDs excluded by the comparison cap. Reads run in batches of at most 50; ranking happens once across all returned statistics
fcc_find_underserved tool
state (USPS state or territory code) scopes the search, otherwise it runs nationwide; geography_type is county (default), cd, place, or cbsa; urban_rural_filter defaults to R, min_unserved_pop to 1, and limit to 20 (max 100). Start offset at 0 and follow nextOffset
- Areas are ranked globally by
noCoverage, with GEOID tie-breaking, before paging; rows carry rank, id, name, oneProvider, total, unservedPct, and coveragePct. totalFound counts matches before paging. An unrecognized state fails as unknown_state; notices distinguish no matching areas from an exhausted page
fcc_search_providers tool
name_search (case-insensitive partial name), state, and tech_filter are all optional and must hold on the same filing; limit defaults to 50 (max 200). Start offset at 0 and follow nextOffset; unknown USPS state or territory codes fail as unknown_state before querying
- Deduplicated holding companies with
hoconum, holdingCompanyName, and national statesServed / techCodes that the filters never narrow. No match returns an empty list with a notice; a live search over its 30-second budget fails as live_search_timeout
- A full-corpus mirror (
FCC_MIRROR_ENABLED=true) pages the complete matching company set in hoconum order and reports totalCount. Against the live API, scanTruncated: true marks a sample and withholds totalCount; continuation covers only the sampled set, which can change between calls
fcc_get_provider tool
hoconum (digits only) from fcc_search_providers or the providers directory resource
- Returns
techCodes / techLabels and speedTierPopulation, which counts each person once and omits empty tiers; business-only carriers come back with both lists empty. Fails as provider_not_found, or live_provider_timeout past the 30-second budget
fcc_list_filing_periods tool
- Always returns the Form 477 periods;
include_bdc: true adds BDC as-of dates when credentials are configured
periods newest first, each with asOfDate and source (form477 or bdc), plus form477Count, bdcCount, and hasBdcCredentials; missing credentials leave bdcCount at 0 rather than raising an error
fcc_list_downloads tool
as_of_date (YYYY-MM-DD) is required; filter by data_type (availability default, or challenge), category (Summary, State, Provider), technology_type (Fixed Broadband, Mobile Broadband, Mobile Voice), state, and provider_name; limit defaults to 50 (max 200), paged with offset
- Returns file metadata and a
downloadUrl per file, not file contents; totalFiles counts every match and nextOffset is omitted on the last page. Fails as invalid_as_of_date for an unsupported date
- Available to MCP clients only when both BDC credentials are configured at startup. Otherwise the HTTP landing page retains its disabled card and setup guidance
fcc-broadband://geography/{type}/{id}/summary resource
type takes the seven fcc_get_coverage_summary geography types; id is a FIPS GEOID checked for digit count, 0 for nation
- Fixed at 25/3 Mbps and satellite-inclusive
acfosw; returns population, the three percentages, and segments as application/json. Use fcc_get_coverage_summary for the terrestrial default or other thresholds and filters
fcc-broadband://providers/list/{offset} resource
offset pages the directory 25 entries at a time in hoconum order; start at 0 and follow nextOffset
- Entries carry
hoconum and, when resolved, holdingCompanyName; each page reports total and count. To find one company by name, use fcc_search_providers
broadband_equity_analysis prompt
- Arguments:
region (free text) and focus (underserved, rural, tribal, or all), both required
- Returns one user message laying out a five-step analysis: baseline coverage, focus area, demographic cross-reference, provider landscape, and historical screening context
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-specific:
- Three FCC sources, all US government public-domain data: Form 477 on FCC Open Data (Socrata), the BDC Public Data API, and the FCC Geo API
- Every Form 477 tool and resource works without credentials and reads the June 2021 filing, the last one; BDC data is exposed only as bulk-file manifests.
fcc_list_downloads is advertised to MCP clients only when both FCC_BDC_USERNAME and FCC_BDC_HASH_VALUE are configured
- The area tools accept every nonempty alphabetical combination of
a DSL, c cable, f fiber, o other, s satellite, and w fixed wireless. The default acfow excludes satellite; explicit acfosw includes it. Deployment tools use numeric technology codes instead
- Area speed keys select download/upload pairs in Mbps:
0.2 → 0.2/0.2, 4 → 4/1, 10 → 10/1, 25 → 25/3 (default), 100 → 100/10, 250 → 250/25, and 1000 → 1000/100. Historical population aggregates cannot establish current BEAD eligibility or address service; the 100/10 tier does not establish 100/20 availability
- Coverage analysis reads the pre-aggregated Area Table (
xvwq-qtaj), not the block-level deployment table, so state- and county-scale queries stay fast
- Optional local Form 477 mirror (
FCC_MIRROR_ENABLED=true) serves queries from SQLite, falling back to the live API for anything not ingested
Agent-friendly output:
- Filter echo: search, summary, and ranking tools return
appliedFilters, and every Form 477 result carries dataVintage
- Partial results say so:
scanTruncated and scanRowCap flag a result drawn from a capped upstream scan (fcc_search_providers, fcc_find_underserved), and an empty match returns a notice instead of an error
- Typed failures: reasons such as
block_not_found, invalid_geography_id_shape, and credentials_required, each with a recovery hint
Getting started
Public Hosted Instance
A public instance is available at https://fcc-broadband.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
Self-Hosted / Local
Add the following to your MCP client configuration file.
Or with npx (no Bun required):
For Streamable HTTP, set the transport and start the server:
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- Optional: FCC BDC credentials for
fcc_list_downloads and BDC filing periods. Generate a token at broadbandmap.fcc.gov under "Manage API Access".
- Optional: a Socrata app token (
FCC_OPENDATA_APP_TOKEN) for higher Form 477 rate limits.
Installation
- Clone the repository:
- Navigate into the directory:
- Install dependencies:
- Configure environment:
Configuration
See .env.example for the full list of optional overrides.
BDC credentials can come from the process environment or .env; process values take precedence. Empty, whitespace-only, and unsubstituted placeholder values count as absent. Restart after configuring both credentials to enable fcc_list_downloads; Form 477 filing periods remain available without them.
Running the server
Local development
-
Build and run:
-
Run checks and tests:
Docker
The Dockerfile defaults to HTTP transport and stateless session mode, logging to /var/log/fcc-broadband-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them. The image also ships the mirror scripts and a writable data/ directory.
Local Form 477 mirror
Form 477 is frozen at the June 2021 filing, so it can be ingested once into a local SQLite index. The mirror is off by default, and the full corpus is about 9 GB of source data and takes hours to ingest, so the bootstrap can be scoped to the states you query:
Then set FCC_MIRROR_ENABLED=true. In Docker, run the same scripts with docker exec <container> bun run mirror:init -- --states 11,53. Run them from a source checkout or the image: the scripts import through the @/ path alias, which an npm install does not map.
- Block and geography lookups serve from the mirror once the queried state is fully ingested; anything else falls back to the live API.
- Geography discovery and name resolution always use the live 2021 lookup: the mirror does not retain its year column. Missing names do not discard coverage statistics.
fcc_search_providers, fcc_get_provider, and geographies with no state in their GEOID (cbsa, tribal, nation) serve from the mirror only after a --full ingest.
- Mirror name search matches word prefixes (
comc finds "Comcast") where the live API matches any substring (mcast), so a mid-word fragment can return fewer results.
- The ingest is idempotent and resumes an interrupted run from its saved cursor. Set
FCC_OPENDATA_APP_TOKEN first for a full ingest. Bun uses the built-in bun:sqlite; Node.js needs the optional better-sqlite3 peer dependency.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
- Socrata returns every field as a string, so parse counts and speeds (
has_0–has_3more, maxaddown, maxadup) explicitly
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.