Search and trace US federal rules across the Federal Register, eCFR, and Regulations.gov.
Search and trace US federal rules across the Federal Register (proposed/final rules and notices), the eCFR (codified, point-in-time CFR full text, locally mirrored), and Regulations.gov (rulemaking dockets and public comments) via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://federal-regulations.caseyjhand.com/mcp
US federal regulatory law across three official sources: the Federal Register (proposed/final rules and notices), the eCFR (codified, point-in-time CFR text), and Regulations.gov (rulemaking dockets and public comments). Search rules, trace a document from proposal through comments to codified text, and read CFR sections from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
regulations_search_rules | Search Federal Register proposed rules, final rules, notices, and presidential documents by query, type, agency, and date range, ranked by relevance or date |
regulations_get_document | Fetch one Federal Register document by number, with the docket ID, CFR parts, and comment count that chain into other tools |
regulations_browse_cfr | List CFR titles, a title's chapters, or every section and appendix in a part, or full-text-search the codified CFR |
regulations_get_cfr_section | Read codified CFR text for a section, whole part, or appendix, current or as of a past date |
regulations_get_docket | Pull a rulemaking docket and its filed documents from Regulations.gov (key required) |
regulations_find_comments | Fetch public comments on a document or docket, or one comment's full body and attachments (key required) |
regulations_list_open_comments | List proposed rules, comment-requesting final rules, and optionally notices currently open for public comment, soonest closing first |
| Resource | Description |
|---|---|
regulations://document/{documentNumber} | A single Federal Register document — metadata plus cross-source handles (mirrors regulations_get_document) |
regulations://cfr/{title}/{part}/{section} | Codified text of a current CFR section (mirrors regulations_get_cfr_section) |
All resource data is also reachable via the tool surface — tool-only MCP clients lose nothing.
regulations_search_rules toolquery optional, or browse by type (PRORULE/RULE/NOTICE/PRESDOCU), agencies (Federal Register slug, e.g. environmental-protection-agency), and published_after/published_before (real calendar days, YYYY-MM-DD)order is relevance, newest, or oldest; omitted, it is relevance with a query and newest without oneper_page 2–100 (default 20), page 1–50 — the Federal Register caps navigation at 50 pages / 5,000 records and total matches at 10,000; narrow the date window rather than paging deeperdocumentNumber (→ regulations_get_document), agencies as { name, slug } (the slug feeds back into agencies; null when the Federal Register lists an agency by raw name only), docketIds (→ regulations_get_docket / find_comments), regulationIdNumbers, and cfrReferences (→ regulations_get_cfr_section)invalid_filter when the Federal Register rejects a filter value (an agency name or acronym instead of a slug), naming the parameter; upstream_unavailable on a 5xx/timeout, retryable after a brief waitregulations_get_document tooldocument_number (format \d{4}-\d+, e.g. 2025-14555){ name, slug }, abstract, action, effective/comment dates, RINs) plus cross-source handles: docketId, regulationsGovDocumentId, commentCount, and cfrReferencesinclude_full_text inlines the plain-text body as a character window: max_chars (default 64,000, up to 200,000) from offset (default 0). fullTextLength reports the whole body's length and fullTextNextOffset the offset to resume from while text remains; documents of about ten printed pages or fewer come back whole, while a major final rule runs past a million charactersoffset or max_chars implies include_full_text; pairing either with an explicit include_full_text: false fails as full_text_disabled. An offset at or past the end returns empty fullText with the length and a noticefullText is plain text: links reduce to their text, and email addresses the published body obfuscates are decodednot_found when the FR number doesn't exist; upstream_unavailable on a 5xx/timeoutregulations_browse_cfr toolmode: "structure" lists all 50 titles, a title's top-level divisions (chapters or subtitles), or — with title + part — every section and appendix in the part, flattened, each carrying its subpart and subjectGroup; mode: "search" full-text-searches the codified CFRtitle (1–50) and part scope both modes; a part without title is rejected (title_required_for_part) since part numbers repeat across titlesdate is point-in-time in both modes and must be a real calendar day; structure mode rejects a date past the title's up-to-date date (date_out_of_range), and search indexes 2017-01-03 onwardpage (1-based, default 1) and per_page (1–50, default 20) page search results and a part's listing; page and totalCount come back with a notice naming the next page, and a page past the end returns no rows plus the last page. Labels are plain text (eCFR's inline markup stripped)countBasis says what totalCount counts — sections, or eCFR's own section_versions count until the whole hit list has been read. Live search pages through eCFR's first 10,000 hits only; a page past them fails as page_out_of_windowsource (mirror/live) and sourceScope — the mirror only answers a title it holds, never an all-titles query when scoped, so anything it can't answer falls through to the live eCFR APIquery_required when mode="search" has no query; title_not_found / date_out_of_range / page_out_of_window / upstream_unavailable round out the errorsregulations_get_cfr_section tooltitle+part+section), a whole part (title+part, section omitted), or one appendix (title(+part)+appendix) — section and appendix are mutually exclusive (conflicting_target)section accepts the cite the way people write it: "61" in part 141, "§ 141.61", "Sec. 141.61", and "141.61(c)" (paragraph designators dropped; the whole section comes back) all read 40 CFR 141.61. An identifier that resolves as given is never rewritten (14 CFR 241 "25", 26 CFR "48.4061(a)"); a rewrite returns the identifier read in section / cfrCite plus a noticebodyText: offset (default 0) and max_chars (default 64,000, max 200,000) select it, and bodyTextOffset / bodyTextLength / bodyTextNextOffset (present while text remains) page through it — the same contract as regulations_get_document's full text. An offset past the end is an empty window with a noticesections[], a text-free index of the sections in the window (section, heading, cfrCite, and the offset each starts at in the part's text — pass it as offset to jump there)date for point-in-time text, 2017-01-01 through the title's up-to-date date; anything outside that is date_out_of_range, checked before any text request, and a date that is not a real calendar day is rejected at input validationappendix must be passed verbatim as eCFR / regulations_browse_cfr emits it (e.g. Appendix A-1 to Part 50), not a short formappendix to read onesource (mirror/live) reports provenance; current single-section reads are mirror-served when ready, everything else (historical dates, whole-part, appendix reads) falls back to the live eCFR versionernot_found / location_required / upstream_unavailable round out the errorsregulations_get_docket tool · key requiredREGULATIONS_GOV_API_KEY; fetch a docket by docket_id (e.g. EPA-HQ-OAR-2025-0194)document_types filters to Proposed Rule / Rule / Notice / Supporting & Related Material / Other — a docket often holds hundreds of supporting materialsper_page 5–250 (default 25), page 1–20 — Regulations.gov caps a query at 5,000 recordsobjectId chains into regulations_find_comments; frDocNum chains back to regulations_get_documentauth_required (missing/rejected key) names the env var and signup URL; not_found / rate_limited (429, 1,000 req/hr) / upstream_unavailable round out the errorsregulations_find_comments tool · key requiredREGULATIONS_GOV_API_KEY; exactly one of docket_id, document_object_id, fr_document_number, or comment_id — zero or two is rejected (target_required / multiple_targets), never resolved by precedencecomment_id returns one comment's full body and attachments; the other three list a set — the list endpoint carries no body text, so read a comment's substance via comment_idbodyText is a stub and attachmentOnly is true, with the attachment download URLsper_page 5–250 (default 25), page 1–20 — Regulations.gov caps a query at 5,000 records; narrow a high-volume docket with document_object_idauth_required / not_found / rate_limited (429, 1,000 req/hr) / upstream_unavailable round out the errorsregulations_list_open_comments tooltype, query, agencies (Federal Register slug), and closing_before (a real calendar day, YYYY-MM-DD)type takes PRORULE, RULE, and NOTICE; the default (also used for an empty list) is ["PRORULE", "RULE"] — proposed rules plus the direct final and interim final rules that take comment. NOTICE adds several hundred information-collection and other noticesper_page 1–100 (default 20), page from 1; totalCount, totalPages, and nextPage describe the window. The Federal Register can't sort by comment date, so the window is fetched whole (one request up to 2,000 documents, then 2,000 per request) and paged locally; past the Federal Register's 10,000-document limit the response is truncated and holds the 10,000 most recently published matchesdaysRemaining, documentNumber (→ regulations_get_document), agencies as { name, slug }, and docketIds (→ regulations_find_comments)REGULATIONS_GOV_API_KEY is set, commentCount is enriched from the Federal Register document's own embedded Regulations.gov info (no extra call) — keyed reports whichinvalid_filter when the Federal Register rejects a filter value, naming the parameter; upstream_unavailable on a 5xx/timeoutregulations://document/{documentNumber} resourceregulations_get_document without full text — metadata plus cross-source handles, the body never inlineddocumentNumber format \d{4}-\d+ (e.g. 2025-14555)not_found / upstream_unavailable mirror the tool's errorsregulations://cfr/{title}/{part}/{section} resourceregulations_get_cfr_section at the current date and its default 64,000-character window — sections only; read an appendix via the tool's appendix input instead"61", "§ 141.61", "141.61(c)"), with a notice naming the rewrite; a section longer than one window carries bodyTextNextOffset, and the rest is read through the tool's offsetsource (mirror/live) reports provenancenot_found / upstream_unavailable mirror the tool's errorsBuilt 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.
Federal Register / eCFR / Regulations.gov-specific:
search_rules, get_cfr_section, find_comments), not three API clientsREGULATIONS_GOV_API_KEY) layers on topupstream_unavailable well inside a client's request timeoutAgent-friendly output:
source: "mirror" | "live" on every CFR read, and a sourceScope line on search naming what that corpus coverstruncated/notice enrichment, never silently droppedattachmentOnly flags when a comment's substance is a file rather than inline text, with the download URLs, on both the structured and text surfacesauth_required errors — the two keyed tools name the env var and free signup URL rather than passing through a raw 401/403A public instance is available at https://federal-regulations.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"federal-regulations-mcp-server": {
"type": "streamable-http",
"url": "https://federal-regulations.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file. The Federal Register and eCFR tools work with no key; set REGULATIONS_GOV_API_KEY (free at api.data.gov/signup) to enable the Regulations.gov docket and comment tools.
{
"mcpServers": {
"federal-regulations-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/federal-regulations-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"REGULATIONS_GOV_API_KEY": "your-key-here"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"federal-regulations-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/federal-regulations-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"REGULATIONS_GOV_API_KEY": "your-key-here"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"federal-regulations-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "REGULATIONS_GOV_API_KEY=your-key-here",
"ghcr.io/cyanheads/federal-regulations-mcp-server:latest"
]
}
}
}
Omit the
REGULATIONS_GOV_API_KEYline entirely to run the keyless core (Federal Register + eCFR). The two Regulations.gov tools then return an actionableauth_requirederror, andregulations_list_open_commentsruns without comment counts.
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
regulations_get_docket, regulations_find_comments, and comment counts in regulations_list_open_comments). The Federal Register and eCFR tools need no key. The shared key allows 1,000 requests/hour.git clone https://github.com/cyanheads/federal-regulations-mcp-server.git
cd federal-regulations-mcp-server
bun install
cp .env.example .env
# optionally set REGULATIONS_GOV_API_KEY
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
REGULATIONS_GOV_API_KEY | api.data.gov key for the Regulations.gov tools (get_docket, find_comments, and comment-count enrichment in list_open_comments). Optional — the Federal Register and eCFR tools work without it. | — |
FEDERAL_REGISTER_BASE_URL | Federal Register API v1 base URL. | https://www.federalregister.gov/api/v1 |
ECFR_BASE_URL | eCFR API base URL. | https://www.ecfr.gov/api |
REGULATIONS_GOV_BASE_URL | Regulations.gov API v4 base URL. | https://api.regulations.gov/v4 |
ECFR_MIRROR_PATH | Filesystem path for the eCFR SQLite mirror database. | ./data/ecfr-mirror.sqlite |
ECFR_MIRROR_REFRESH_CRON | Cron expression for an in-process mirror refresh (HTTP transport only), e.g. 0 4 * * 0. Unset registers no job. Each run re-harvests every configured title in full, and a tick is skipped until mirror:init has completed once. | — (no job) |
ECFR_MIRROR_TITLES | Comma-separated CFR title numbers to scope the mirror to (e.g. 21,40). Omit to mirror all 50 titles. Cites and searches outside the set fall through to the live eCFR API, as does any all-titles search while this is set. | — (all titles) |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Build and run:
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
Populate/refresh the eCFR mirror (out-of-band, idempotent — the codified-text tools work against the live eCFR API until this completes, so it's a latency optimization, not a hard dependency):
bun run mirror:init # full build across all 50 titles, resumable (scope with ECFR_MIRROR_TITLES)
bun run mirror:refresh # re-harvest every configured title against the latest eCFR issues
bun run mirror:verify # report row counts and the last-synced issue date
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security, changelog sync
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against the linter rules
docker build -t federal-regulations-mcp-server .
docker run --rm -e REGULATIONS_GOV_API_KEY=your-key -p 3010:3010 federal-regulations-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/federal-regulations-mcp-server. The build stage installs dependencies with --ignore-scripts — better-sqlite3 is a build/ingest-only dependency whose native compile is skipped, and the runtime reads the mirror through Bun's built-in bun:sqlite. Populate the mirror in a running container with docker exec <container> bun run mirror:init; mount a volume over /usr/src/app/data so the synced index survives container recreation. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources, inits the three services, and schedules the mirror refresh on HTTP when ECFR_MIRROR_REFRESH_CRON is set. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts) — the seven regulations_* tools. |
src/mcp-server/resources | Resource definitions (*.resource.ts) — the document and CFR-section resources. |
src/services/federal-register | Federal Register API v1 client (keyless). |
src/services/ecfr | eCFR API client (keyless) — versioner, structure, search, and section XML parsing. |
src/services/ecfr-mirror | eCFR codified-text mirror (MirrorService — SQLite + FTS5), its read path, and the opt-in refresh job. |
src/services/regulations-gov | Regulations.gov v4 client (X-Api-Key) — dockets and comments. |
scripts/ecfr-mirror-*.ts | Out-of-band mirror lifecycle: init, refresh, verify. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tseCFR content is "authoritative but unofficial" per the Office of the Federal Register — not the official legal edition of the Code of Federal Regulations; verify against govinfo.gov for legal research. This server is not affiliated with or endorsed by the Office of the Federal Register, the Government Publishing Office, or the General Services Administration.
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/federal-regulations-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-federal-regulations-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/federal-regulations-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/federal-regulations-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.