Skip to main content
Glama

cfr-free

CI License: MIT

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; jsonl and md for 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 redline and cfr-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 by search, serve and mcp (unless CFR_AUTO_SYNC=0); the opt-in periodic re-sync in serve and mcp when CFR_REFRESH_HOURS is set (default 0 = off); serve's /api/v1/diff and /api/v1/redline routes; and mcp'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:latest

The 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:8787

The 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:8787

Every 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:8787

npx 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-free

A 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 the cfr-free command 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. Use npx, 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 provision

npm 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 file

Docker

docker run -d --name cfr-free -p 8787:8787 -v cfr-data:/data \
  ghcr.io/deffenda/cfr-free:latest

The 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 up

Only 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 mcp

That 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

search_regulations

query, page?, per_page?, part?, kind?, chapter?

Ranked hits: citation (+ 38 C.F.R. § display form, ecfr.gov link), heading, snippet, score, matches — with exact-citation pinning, "quoted phrase" search, and claims-acronym expansion (PTSD, TDIU, SMC, …). Each response also carries matchedTerms/unmatchedTerms: the query's content words the returned hits did and did not answer

get_section

citation (e.g. 38 CFR 4.125)

Full section text as markdown + ecfr.gov link + cross-references with availability flags

browse_title38

chapter?, part?, subpart?, subject_group?

Children of the requested hierarchy level (a subject_group holds the sections of one regulatory subject)

index_status

—

Issue date, last sync, row counts, staleness, drift flag, corpus receipt hash, and what the last sync amended/added/removed

list_parts

—

Parts in the index with titles and section/appendix counts

health

—

One-call readiness verdict: ready, stale, degraded (structure drift), or empty

get_section_at

at_date (YYYY-MM-DD), citation

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)

diff_issues

from_date, to_date

Provision-level diff between two eCFR issue dates: changed/added/removed citations (capped, with a truncation flag)

redline

from_date, to_date, citation

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

GET / HEAD

/health

—

GET / HEAD

/metrics

— (Prometheus text format)

GET / HEAD

/api/v1/search

q (required), page, per_page, part, kind, chapter

GET / HEAD

/api/v1/sections/:citation

— (URL-encode citations with spaces)

GET / HEAD

/api/v1/browse

chapter, part, subpart, subjgrp

GET / HEAD

/api/v1/diff

from, to (both required; fetches from eCFR per request)

GET / HEAD

/api/v1/redline

from, to, citation (all required; fetches from eCFR per request)

/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

CFR_INDEX_PATH

~/.cfr-free/index.sqlite

SQLite index location

CFR_MAX_AGE_DAYS

7

Index older than this is reported stale

CFR_AUTO_SYNC

1

Sync at startup when index empty/stale

CFR_REFRESH_HOURS

0

Hours between background refreshes while serving (0 = off)

CFR_HOST

127.0.0.1

HTTP bind address

CFR_PORT

8787

HTTP bind port

CFR_ALLOW_REMOTE

unset

Set 1 to permit non-loopback binds

CFR_LOG_LEVEL

info

debug | info | warn | error

Logs are structured JSON lines on stderr only — stdout is reserved for MCP's JSON-RPC protocol.

Exit codes

Code

Meaning

0

Success

1

Generic error (sync failure, corrupt index, …)

2

Usage or configuration error

3

Index unavailable (empty, and auto-sync disabled)

4

HTTP port could not be bound (in use or permission denied)

5

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 only

Talks 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 facade

  • src/mcp/ — MCP server and tool registrations

  • src/titan/ — HTTP JSON API

  • src/commands/ — one entrypoint per subcommand: mcp, sync, diff, redline, historical, search, serve, export (help and version dispatch from src/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

MIT

Available Tools

9 tools
browse_title38Browse the CFR structureA
Read-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 datesA
Read-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 sectionA
Read-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 dateA
Read-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 readinessA
Read-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 freshnessA
Read-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 indexA
Read-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 datesA
Read-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 regulationsA
Read-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

    A4.1/5.0

    Scored across 9 tools

    Disambiguation3/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    Nine tools is well scoped: discovery, search, current and historical retrieval, change detection, and readiness checks each earn their place without bloat.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Enables 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.
      7
      49 PyPI
      1
      MIT
    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables 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.
      -
    • A
      license
      A
      quality
      B
      maintenance
      Enables 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.
      3
      MIT