cfr-free
Read-only MCP server for searching, reading, browsing, and auditing Title 38 CFR data, with local index tools plus live eCFR historical, diff, and redline lookups.
Search the local Title 38 CFR index with ranked results and optional filters for part, kind, and chapter.
Read full CFR sections or appendices as markdown with citations and source links.
Browse the Title 38 hierarchy by chapter, part, subpart, or subject group.
Check local index freshness, structure drift, row counts, corpus hash, and readiness status.
List all indexed CFR parts with section and appendix counts.
Fetch a provision as it stood at a specific date from eCFR.
Diff two eCFR issue dates to see changed, added, or removed provisions.
Generate a line-level redline of one provision between two eCFR issue dates.
Exposes Prometheus-compatible metrics via the /metrics HTTP endpoint, enabling monitoring and alerting on server health and usage.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cfr-free What does 38 CFR 4.125 say about diagnosing mental disorders?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cfr-free
Self-hosted Title 38 CFR data service: it builds a local, full-text searchable index of Title 38 of the Code of Federal Regulations (the VA benefits rulebook) straight from the public eCFR API, and serves it through an MCP server (for AI assistants like Claude) and a small HTTP API ("Titan-style" query surface). No cloud dependencies, no API keys, no telemetry — one Node process and one SQLite file.
What you get
Local full-text search over every section and appendix of Title 38, powered by SQLite FTS5 (BM25-ranked with citation/heading weighting, relevance snippets, and the matched terms shown per hit).
Honest about images: a few provisions publish their rating tables as eCFR images rather than text. Those provisions carry an explicit
[image content not included: <path>]marker where the graphic belongs, so a missing table is never mistaken for complete text. Nothing is fetched or transcribed — the service stays fully offline.Read-only web UI (search, browse, section reads, line-level redlines, copy-a-citation) over the same JSON API.
MCP server with nine read-only tools any MCP client can use: six offline index-backed ones (
search_regulations,get_section,browse_title38,index_status,list_parts,health) plus three that fetch the live eCFR versioner per call (get_section_at(one provision as it stood at a date),diff_issues,redline).HTTP JSON API for scripts and apps: search, read sections, browse the CFR hierarchy, health check, Prometheus metrics.
Measured retrieval: a frozen query set and runner score the search over citations, abbreviations and plain-language questions — see
docs/research/retrieval-baseline-2026-10-20.md.Freshness tracking: the index records the eCFR issue date it was built from and warns when it goes stale or drifts from the structure tree.
Backup-friendly:
cfr-free export <path> [jsonl|md]writes a consistent snapshot of the index you can copy anywhere (sqlite default;jsonlandmdfor pipeline ingestion).
Related MCP server: cfr-mcp
Requirements
Node.js ≥ 22.5 (uses the built-in
node:sqlite; SQLite ≥ 3.50 with FTS5, bundled with Node 22.5+)Network access only for
cfr-free sync,cfr-free diff,cfr-free redlineandcfr-free historical(~2–3 MB from ecfr.gov per full-title fetch; the two-date views fetch twice); the startup refresh of a stale or empty index bysearch,serveandmcp(unlessCFR_AUTO_SYNC=0); the opt-in periodic re-sync inserveandmcpwhenCFR_REFRESH_HOURSis set (default 0 = off);serve's/api/v1/diffand/api/v1/redlineroutes; andmcp's three eCFR-backed tools (get_section_at,diff_issues,redline), which fetch from eCFR on every call. Reads from a fresh index never touch the network.
Quickstart
Five ways in, depending on what you have. The first needs nothing but Docker; the second needs only npm; the third needs only Node and runs the package straight from a release tarball (no compiler); the fourth needs npm and a compiler; the fifth needs a checkout.
New here? First 10 minutes picks one of these routes and walks it end to end: build the local index, search it, read a provision and cite it.
1. Docker (nothing to install)
docker run -d --name cfr-free -p 8787:8787 -v cfr-data:/data \
ghcr.io/deffenda/cfr-free:latestThe image is public and pulls anonymously. It syncs the index on first
start (~5 s, ~2–3 MB from eCFR) and serves the API on
http://127.0.0.1:8787; the index persists in the cfr-data volume. Any
CLI command works the same way, e.g. docker run --rm -v cfr-data:/data ghcr.io/deffenda/cfr-free:latest sync to refresh, or … export /data/backup.sqlite to snapshot into the volume. Details under
Docker below.
2. From npm (no compiler, no Docker)
npm install -g cfr-free
# or, with no install at all:
npx -y cfr-free version # -> 0.2.1
npx -y cfr-free sync # build the local index (~2-3 MB, ~5 s)
npx -y cfr-free serve # HTTP API on http://127.0.0.1:8787The published tarball ships the compiled dist/, so this route needs
neither a compiler nor a checkout — it is the fastest way in.
3. From a release tarball (no compiler)
npx -y https://github.com/deffenda/cfr-free/releases/download/v0.2.1/cfr-free-0.2.1.tgz version # -> 0.2.1
npx -y https://github.com/deffenda/cfr-free/releases/download/v0.2.1/cfr-free-0.2.1.tgz serve # HTTP API on http://127.0.0.1:8787Every GitHub release carries the package tarball at
releases/download/<tag>/cfr-free-<version>.tgz; for v0.2.1 that
tarball holds the same contents the npm registry serves (verified by
unpacking both: identical file trees). npx installs the tarball
directly from the URL — the cfr-free package itself never comes from
the registry, no compiler runs on first use, and the version in the URL
pins the release exactly. npx does still fetch the package's runtime
dependencies from npm (they are not bundled). Verified on the v0.2.1
release, 2026-10-29: both commands above ran end to end (version →
0.2.1; serve answered /health with HTTP 200). Swap the tag and
filename together to pick another release.
4. From GitHub (no publish step)
npx -y github:deffenda/cfr-free version # -> 0.2.2
npx -y github:deffenda/cfr-free serve # HTTP API on http://127.0.0.1:8787npx fetches and compiles the project on first use, so the first run takes
a minute and needs a compiler (npm runs the project's prepare script when
it installs from a git URL, and that script runs tsc). The Docker route
above needs neither.
To use it as a dependency of your own project:
npm install github:deffenda/cfr-freeA global install from a git URL (
npm install -g github:…) does not work, and the failure is npm's rather than the project's. Measured on npm 11.20.0, five runs in a row left the SAME broken end state: the package is not installed, and thecfr-freecommand npm puts in your PATH points at a file that does not exist, so running it fails with "no such file or directory".Whether npm reports the failure varies, and the quiet case is the more misleading one: most runs print "added 104 packages" and exit 0, so nothing looks wrong until you run the command; some abort part-way with
ENOENT … chmod … node_modules/which/bin/node-which(exit 254). On npm 10.9.9 the same class of failure presents as a dropped dependency tree (ENOENT … readlink … node_modules/.bin/fxparser). All of this was measured, and it reproduces on the unmodified repository too. Usenpx, a project-local install, the npm route above, or the Docker image instead — all four were re-verified on npm 11.20.0.
5. From a source checkout
# requires Node >= 22.5
git clone https://github.com/deffenda/cfr-free.git
cd cfr-free
npm ci # install the pinned dependencies
npm run build # compile to dist/ (the CLI entry point is dist/cli.js)
node dist/cli.js version # -> 0.2.2 (verifies the build)
node dist/cli.js sync # build the local index (~2-3 MB download, ~5 s)
node dist/cli.js serve # HTTP API on http://127.0.0.1:8787
# or
node dist/cli.js mcp # MCP server on stdio (default command)
node dist/cli.js export ./backup.sqlite # consistent snapshot of the local index
node dist/cli.js search PTSD # ranked provisions from the local index (top 10)
node dist/cli.js diff 2026-01-01 2026-06-01 # provision changes between two issue dates
node dist/cli.js redline 2026-01-01 2026-06-01 4.125 # line-level redline of one provisionnpm link in the checkout puts cfr-free on your PATH, after which the
examples above work verbatim as cfr-free ….
cfr-free sync writes the index at ~/.cfr-free/index.sqlite. To try it
against a throwaway index instead of your own, point CFR_INDEX_PATH
somewhere else:
CFR_INDEX_PATH=/tmp/cfr-smoke.sqlite node dist/cli.js sync # ~5 s, then delete the fileDocker
docker run -d --name cfr-free -p 8787:8787 -v cfr-data:/data \
ghcr.io/deffenda/cfr-free:latestThe container syncs on first start (~5 s, ~2–3 MB from eCFR), then serves
the HTTP API; the index persists in the cfr-data volume mounted at /data.
Any CLI command works the same way, e.g.
docker run --rm -v cfr-data:/data ghcr.io/deffenda/cfr-free:latest sync to
refresh, or … export /data/backup.sqlite to snapshot into the volume.
The image runs as a non-root user and binds 0.0.0.0 with the remote opt-in
already set — inside a container the published port (-p) is the exposure
gate, and loopback would make it unreachable. Outside Docker the CLI still
defaults to 127.0.0.1 with CFR_ALLOW_REMOTE=1 required. When you do
expose it, a per-peer token-bucket rate limit (60-burst, 10 req/s) turns on
automatically. Images are published to GHCR on v* tags with
build-provenance attestations.
Prefer Compose? docker compose up -d --build serves the API on
127.0.0.1:8787 with a named volume, healthcheck, and sync left explicit
(docker compose run --rm cfr-free sync) so starts never trigger a surprise
download.
The package is on npm (npm view cfr-free version prints the latest; see
Quickstart). Releases publish from the v*-tag workflow
with OIDC provenance (no publish token), gated behind the
NPM_TRUSTED_PUBLISHING repository variable plus one-time
trusted-publisher registration on npmjs.com. The publish job verifies
before it publishes that the packed tarball carries the executable and
every file it imports, and that the tag matches the package.json
version — so a release cannot ship a broken package.
Verify the install (smoke checklist)
Every command below was run against an isolated throwaway index
(CFR_INDEX_PATH=/tmp/smoke.sqlite) on the 2026-10-16 corpus; the
outputs are what you should see. Nothing outside /tmp is touched.
The install routes were measured, not assumed. A clean global install of
the packed tarball into a throwaway prefix was run and the installed binary
was exercised (--version, and a search against a copy of the index); the
GitHub Release asset route — the same tarball, fetched from a release URL —
was created, downloaded and installed end to end in a scratch
repository; it now also runs straight from this repository's own
releases (v0.2.1, verified 2026-10-29, see Quickstart). A
project-local npm install github:deffenda/cfr-free was run and its
binary exercised, and npx -y github:deffenda/cfr-free was run, MCP over
stdio included.
The route that does not work — npm install -g github:… — was measured
too, and the failure is npm's (see Quickstart). The sequence
below was run against a provisioned checkout; what it does not cover is an
install on a machine with no npm cache and no network beyond the first
fetch.
export CFR_INDEX_PATH=/tmp/smoke.sqlite # never your real index
# 1. the build is what you think it is
node dist/cli.js version # -> 0.2.2
# 2. network: build the index (~2-3 MB, ~5 s)
node dist/cli.js sync
# -> synced 2953 sections + 17 appendices (issue date 2026-10-16; ...
# 9 provisions contain image content not included; 2134 ms; corpus hash ...)
# 3. HTTP read (no network from here on)
CFR_PORT=8791 node dist/cli.js serve & SERVER_PID=$!
curl -s http://127.0.0.1:8791/health | head -c 80
# -> {"status":"ok","issueDate":"2026-10-16","syncedAt":...
curl -s 'http://127.0.0.1:8791/api/v1/search?q=4.130&per_page=1' | head -c 80
# -> {"query":"4.130","page":1,"perPage":1,"total":42,...
curl -s http://127.0.0.1:8791/api/v1/sections/4.130 | head -c 80
# -> {"citation":"4.130","kind":"section","heading":"Schedule of ratings...
# 4. offline restart: reads come from the local index, no network
kill "$SERVER_PID"; CFR_PORT=8791 node dist/cli.js serve & SERVER_PID=$!
# /health answers identically — that is the point
# 5. MCP over stdio: initialize, then call a tool
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"1.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_section","arguments":{"citation":"38 C.F.R. § 4.130"}}}' \
| node dist/cli.js mcp | tail -1 | head -c 120
# -> {"result":{"content":[{"type":"text","text":"{\n \"citation\": \"4.130\"...
# (note: the DOTTED display citation resolves — P1-03)
# 6. export: the whole corpus, offline
node dist/cli.js export /tmp/smoke.jsonl jsonl
# -> exported 2970 provisions as jsonl (17062107 bytes) to /tmp/smoke.jsonl
kill "$SERVER_PID"; rm -f /tmp/smoke.sqlite /tmp/smoke.jsonl # clean upOnly steps 1–2 need the network; 3–6 are fully offline once the index exists. If step 3 answers and step 4 answers identically after the restart, your install reads from the local index exactly as intended.
Use with Claude (MCP)
Add to your MCP client config (e.g. claude_desktop_config.json):
{
"mcpServers": {
"cfr-free": {
"command": "npx",
"args": ["-y", "cfr-free", "mcp"]
}
}
}Using Claude Code? The same server registers in one command (verified 2026-10-29):
claude mcp add cfr-free -- npx -y cfr-free mcpThat writes the local scope — this project only, the default;
claude mcp add -s user cfr-free -- npx -y cfr-free mcp registers it for
every project instead.
All of these forms run npx -y cfr-free mcp under the hood: nothing
installed, no path to edit, and the published tarball ships compiled —
no compiler runs on first use. The JSON config also works from the git URL
("args": ["-y", "github:deffenda/cfr-free", "mcp"]), which npx fetches
and compiles on first use (verified end to end: the list_parts tool
answers over stdio). If you built it from a checkout, use the absolute
path to the built CLI, because MCP clients do not inherit your shell's
PATH:
{
"mcpServers": {
"cfr-free": {
"command": "/absolute/path/to/cfr-free/dist/cli.js",
"args": ["mcp"]
}
}
}Then ask things like "What does 38 CFR 4.125 say about diagnosing mental disorders?" or "Search Title 38 for traumatic brain injury compensation."
MCP tools
Tool | Arguments | Returns |
|
| Ranked hits: citation (+ |
|
| Full section text as markdown + ecfr.gov link + cross-references with availability flags |
|
| Children of the requested hierarchy level (a |
| — | Issue date, last sync, row counts, staleness, drift flag, corpus receipt hash, and what the last sync amended/added/removed |
| — | Parts in the index with titles and section/appendix counts |
| — | One-call readiness verdict: |
|
| One provision as it stood at a date: the eCFR revision in force then, fetched statelessly on every call (no local index; the point-in-time archive starts 2017-01-01) |
|
| Provision-level diff between two eCFR issue dates: changed/added/removed citations (capped, with a truncation flag) |
|
| Line-level redline of one provision between two eCFR issue dates: status, both sides' text, and per-line hunks |
All tools are read-only; the six index tools query only the local index and
never hit the network, while get_section_at, diff_issues and redline
fetch from eCFR on each call (a point-in-time revision, or two issue dates'
full-title XML). Search and section responses carry a freshness envelope
(indexStale + a notice when the local index has aged out) so a client can
never unknowingly quote a lapsed version.
Beyond tools, the server exposes three MCP resources that clients can read
directly — cfr://index/status (freshness JSON: issue date, last sync, row
counts, staleness, drift, corpus receipt hash), cfr://title38/parts (parts with titles and
counts), and cfr://title38/definitions (§ 3.1 master definitions section
detail) — plus two prompts: cfr_research (topic, part?), which walks
a model through the search → read → cite workflow, and
cfr_claim_evaluation (issue, part?, citation?), which encodes the
adjudicator workflow: readiness gate → locate criteria → quote verbatim →
follow cross-references → paste-ready citations with source URLs.
HTTP API
Method | Path | Query params |
|
| — |
|
| — (Prometheus text format) |
|
|
|
|
| — (URL-encode citations with spaces) |
|
|
|
|
|
|
|
|
|
/health and /metrics additionally expose diskFree/diskTotal and the
cfr_free_disk_free_bytes/cfr_free_disk_total_bytes gauges when the platform
provides fs.statfs (as of boot or last refresh round); omitted otherwise.
The three index-backed reads (/api/v1/search, /api/v1/sections/:citation,
/api/v1/browse) send a strong ETag over the response payload. Resend it as
If-None-Match and the server answers 304 Not Modified with no body when the
text has not changed, so a client can confirm the provision it quotes without
downloading it again:
etag=$(curl -sD - -o /dev/null 'http://127.0.0.1:8787/api/v1/sections/4.125' \
| awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $etag" \
'http://127.0.0.1:8787/api/v1/sections/4.125' # 304/health, /metrics and the two eCFR-fetching routes (/api/v1/diff,
/api/v1/redline) carry no validator, and an error response is never a 304.
curl 'http://127.0.0.1:8787/api/v1/search?q=traumatic%20brain%20injury&part=4'
curl 'http://127.0.0.1:8787/api/v1/sections/4.125'
curl 'http://127.0.0.1:8787/api/v1/browse?chapter=I&part=17'
curl 'http://127.0.0.1:8787/metrics'The full request/response contract (schemas, error envelopes) is documented in
docs/openapi.yaml (OpenAPI 3.1).
The API is intentionally minimal: GET-only, no request bodies, no
templating, bound to 127.0.0.1 by default. The deliberate non-JSON
responses are /metrics (Prometheus exposition text for scrapers) and the
static web UI assets. There is no authentication — if you bind it to a
non-loopback address you must put it behind your own reverse proxy/auth
(CFR_ALLOW_REMOTE=1 is required to allow that, as an explicit opt-in).
The serve root / also serves a read-only web UI (search + section reader
with chapter/part browse + a redline view over the same JSON API, hash-routed) —
no auth, loopback bind by default like the rest of the surface.
Configuration
All configuration is environment variables (flags are deliberately avoided so MCP client configs stay uniform):
Variable | Default | Meaning |
|
| SQLite index location |
|
| Index older than this is reported stale |
|
| Sync at startup when index empty/stale |
|
| Hours between background refreshes while serving (0 = off) |
|
| HTTP bind address |
|
| HTTP bind port |
| unset | Set |
|
|
|
Logs are structured JSON lines on stderr only — stdout is reserved for MCP's JSON-RPC protocol.
Exit codes
Code | Meaning |
| Success |
| Generic error (sync failure, corrupt index, …) |
| Usage or configuration error |
| Index unavailable (empty, and auto-sync disabled) |
| HTTP port could not be bound (in use or permission denied) |
| Refused to overwrite an existing export file (remove it or pick another path) |
Scripts can branch on these — e.g. retry later on 3, pick another port
on 4.
Development
npm ci
npm run ci # format:check + lint + workflow lint + typecheck + tests + build + audit
npm run test:coverage
npm run scan # gitleaks + osv-scanner + trivy + semgrep (skips missing tools)Local LLM review (Gemma via LM Studio)
npm run review # reviews docs/SPEC.md + src/ with the local model
npm run review:spec # spec onlyTalks to LM Studio's OpenAI-compatible endpoint (http://localhost:1234 by
default, model google/gemma-4-12b). Configure with LMSTUDIO_BASE_URL,
LMSTUDIO_MODEL, LLM_REVIEW_TIMEOUT_MS, or --model / --base-url /
--max-tokens / --instruction flags. Reasoning models are handled
automatically (retries with a larger token budget when the visible answer
has not been emitted yet).
Architecture
eCFR API ──sync──▶ SQLite (sections + FTS5 + structure meta)
▲ ▲
│ │
MCP tools (stdio) HTTP API (node:http)src/core/— eCFR client, XML parser, FTS5 store, sync, service facadesrc/mcp/— MCP server and tool registrationssrc/titan/— HTTP JSON APIsrc/commands/— one entrypoint per subcommand:mcp,sync,diff,redline,historical,search,serve,export(helpandversiondispatch fromsrc/cli.ts)docs/SPEC.md— design decisions and review dispositions
Sync is atomic: the whole index is replaced inside one BEGIN IMMEDIATE
transaction under WAL, so concurrent readers never see a partial build, and
an interrupted sync simply rolls back.
Data source & attribution
Regulatory content comes from the eCFR API operated by the Office of the Federal Register and the Government Publishing Office. eCFR content is in the public domain (17 U.S.C. § 105). cfr-free is an independent project and is not endorsed by or affiliated with the VA, the OFR, or GPO. This tool is not legal advice; verify citations against the official eCFR.
Security
See SECURITY.md for reporting vulnerabilities and the security posture summary. Reported by default: lint/strict-TS/typecheck/coverage/Audit gates (plus local trivy/semgrep/osv-scanner/gitleaks scans), zero runtime dependencies beyond the MCP SDK, XML parser hardening (no DOCTYPE, bounded depth/size), FTS query sanitization (no SQL injection surface), and loopback-only networking by default.
License
Available Tools
9 toolsbrowse_title38Browse the CFR structureARead-onlyIdempotent
Navigate the Title 38 hierarchy (title → chapter → part → subpart → subject group) and list what exists at a level. With no arguments, returns the chapters. Use this to discover valid parts/subparts before searching. Most sections live inside a subject group: pass the subject_group identifier you saw in a previous response to list the sections it holds.
Inputs: part, chapter, subpart, subject_group. Returns: node, path, children
diff_issuesDiff two eCFR issue datesARead-onlyIdempotent
Provision-level diff of Title 38 between two eCFR issue dates: which citations changed, which were added, and which were removed between from_date and to_date (both YYYY-MM-DD, both required — this tool has no default-to; for "what changed since my index was built" run cfr-free diff <from> locally, which resolves to from the index issue date). Stateless: both dates' full-title XML is fetched from eCFR and hashed per citation on the fly — no local index needed, no cache. Each citation list is capped at 100 entries; truncated: true means at least one list was cut. Identical dates or an impossible calendar date (e.g. month 13) are rejected as errors before any fetch.
Inputs: to_date (required), from_date (required). Returns: added, notice, changed, removed, +3 more
get_sectionRead a CFR sectionARead-onlyIdempotent
Fetch the full text of one Title 38 CFR section or appendix as markdown. Accepts flexible citations: "4.125", "§ 4.125", "38 CFR 4.125", or appendix names like "Appendix A to Part 4". Returns heading, hierarchy path, issue date and an ecfr.gov permalink.
Inputs: citation (required). Returns: kind, part, path, text, +12 more
get_section_atRead one provision as it stood at a dateARead-onlyIdempotent
One Title 38 provision as it stood at a requested date (YYYY-MM-DD, required). eCFR serves the revision IN FORCE on that date, so a date that is not itself an issue date resolves to the revision that was in force then; the upstream does not report which issue date it served, so the response names the REQUESTED date and never a served-revision date. Stateless: the revision is fetched from eCFR on every call — no local index, no cache. eCFR's point-in-time archive starts at 2017-01-01; a date before that is rejected as not_found. Use this for date-of-claim and CUE questions ("what did this section say when the claim was filed?"); for the CURRENT text use get_section.
Inputs: at_date (required), citation (required). Returns: kind, part, path, text, +8 more
healthIndex readinessARead-onlyIdempotent
One-call readiness verdict for the local index: "ready" (query away), "stale" (older than CFR_MAX_AGE_DAYS — verify results against the current eCFR), "degraded" (structure drift — provisions may be missing; verify against the current eCFR), or "empty" (no index yet — run cfr-free sync). Clients should gate queries on this before spending tool calls.
Inputs: no named parameters. Returns: stale, status, syncedAt, issueDate
index_statusLocal index freshnessARead-onlyIdempotent
Report how current the local Title 38 index is: eCFR issue date, last sync time, row counts, the corpus receipt hash (a sha256 over the sorted per-provision (citation, text+heading) pairs at rebuild time, null on older indexes) and whether the index is stale relative to CFR_MAX_AGE_DAYS. Check this before relying on search results.
Inputs: no named parameters. Returns: stale, sections, syncedAt, issueDate, +6 more
list_partsList CFR parts in the local indexARead-onlyIdempotent
Enumerate every Title 38 CFR part present in the local index, with its structure-tree title and section/appendix counts. Use this to discover which parts are searchable.
Inputs: no named parameters. Returns: parts
redlineLine redline of one provision between two eCFR issue datesARead-onlyIdempotent
Line-level redline of one Title 38 provision between two eCFR issue dates: which lines of citation changed, were added, or were removed between from_date and to_date (both YYYY-MM-DD, both required — this tool has no default-to). The dates are SELECTORS — eCFR serves the revision in force on the requested date; they are not a verified stamp of which issue date was served. Returns the status (changed/added/removed/unchanged), both sides' raw text, and one hunk per line (same/add/del with 1-based anchors into each side's text). Identical or impossible dates (e.g. month 13) are rejected as errors before any fetch; a citation missing from BOTH dates is a not_found error after the fetches. Stateless: both dates' full-title XML is fetched from eCFR per call — no local index needed, no cache. When either side exceeds the core's line cap the per-line walk is replaced by two whole-text coarse blocks (coarse: true) — passed through uncapped here, the core's own bound.
Inputs: to_date (required), citation (required), from_date (required). Returns: hunks, coarse, notice, status, +5 more
search_regulationsSearch Title 38 CFR regulationsARead-onlyIdempotent
Full-text search across the local Title 38 (VA) CFR index using ranked keyword matching. Returns section citations, headings, relevance snippets, bm25 scores (lower is better) and the query terms visible in each hit. The CANDIDATE SET is every provision matching AT LEAST ONE query term, so a provision that answers part of the question stays in the results instead of being excluded by the words it does not use; total counts those candidates (plus the pinned citation when the candidate set did not already hold it). The hits are ordered by: (1) a hit whose HEADING carries the query's content terms as a phrase (or the corpus phrase a synonym stands for), (2) a hit matching EVERY query term ahead of one matching only some, (3) bm25, lower is better. Use part to scope results to a single CFR part (e.g. "4" for ratings, "17" for prosthetics), kind to restrict results to one provision kind ("section" or "appendix"), and chapter to scope to one chapter by its Roman-numeral identifier (e.g. "I"; exact, case-sensitive). Follow up with get_section to read a hit in full. If the query itself is a citation (e.g. "4.130" or "38 CFR 4.16"), that exact section is pinned to the top of page 1 (totals include the pin on every page) and marked direct: true — the reliable client signal; the pinned hit's inResults flag reports whether it also matched the ranked results. Quoted spans (e.g. "exact regulatory language") are emitted as FTS5 phrase queries — the last token gets a prefix wildcard (so "language" also matches "languages"); earlier tokens are matched exactly. Hits are ranked by LEXICAL OVERLAP with the query, not by legal authority: the first hit is the provision sharing the most words with the query, which is often not the provision that governs the question. Read each hit's heading and fetch its full text with get_section before you cite it. Each response also carries matchedTerms and unmatchedTerms: the query's content words (stopwords, single characters and bare numerals removed), split into those the returned hits answered and those they did not. A non-empty unmatchedTerms means the returned text does not speak to that part of your question — the governing provision may be a later hit, may be reachable only under different wording, or may not be in this index. Never cite a provision you have not read in full, and never state that Title 38 does not address a question on the basis of this response alone.
Inputs: query (required), kind, page, part, +2 more. Returns: hits, page, query, total, +7 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 9 tools
Two pairs overlap meaningfully: `health` and `index_status` both report index readiness/staleness (health is a verdict, index_status is a report), and `list_parts` and `browse_title38` both serve hierarchy discovery. The temporal tools (get_section vs get_section_at, diff_issues vs redline) are clearly delineated, but the agents still have to guess between the readiness and discovery pairs.
Most tools follow a verb_noun pattern (list_parts, search_regulations, get_section, browse_title38, diff_issues), with `get_section_at` a sensible variant. `health` and `redline` are bare noun/verb outliers that break the pattern, though they remain readable.
Nine tools is well scoped: discovery, search, current and historical retrieval, change detection, and readiness checks each earn their place without bloat.
Coverage is strong across the CFR lifecycle: discovery, full-text search, current and point-in-time retrieval, title-level diff, and line-level redline. Minor gaps remain — no tool to trigger or repair a sync/index rebuild (CLI-only), and historical text is only retrievable per-provision, not searchable.
Maintenance
Related MCP Connectors
Read a CFR or eCFR section, or search US federal regulations and the Federal Register.
Veterans benefits and VA claims reference: VA disability ratings, compensation rates, 38 CFR search
Search and read US federal regulations with legal and inline citations linking to ecfr.io.
Search, quote and link U.S. federal procurement regulations from official sources.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to access US FDA regulations (21 CFR) through natural language queries, providing tools for retrieving regulatory information.327 npmMIT
- AlicenseAqualityBmaintenanceEnables AI assistants to retrieve, search, and track changes to US Code of Federal Regulations sections via the eCFR API, returning actual regulation text with citations and point-in-time date support.749 PyPI1MIT
- FlicenseNot gradedqualityCmaintenanceEnables agents to search a local, fully offline personal document library (Markdown/txt/PDF/docx) via SQLite FTS5 trigram search with subject-terminology expansion, returning every hit with its domain, document title, section path, and page or offset provenance. It also exposes domain statistics, incremental ingestion, and document deprecation as tools, alongside a read-only, runtime-persona-scoped ask/coverage mode.-
- AlicenseAqualityBmaintenanceEnables searching and reading the US Code of Federal Regulations through tools that return excerpts, full text, legal citations, and linked source URLs, with read-only, stateless access.3MIT