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.

NameRequiredDescriptionDefault
partNopart number, e.g. "4"
chapterNochapter identifier, e.g. "I"
subpartNosubpart letter, e.g. "B" (requires part)
subject_groupNosubject-group identifier, e.g. "ECFRd158704dcab040e" (requires part)

Output Schema

NameRequiredDescription
nodeYes
pathYes
childrenYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent, non-destructive, closed-world), so the description's job is to add workflow context — which it does by disclosing the no-args default return and the fact that section listing requires an identifier obtained from a prior response. It doesn't discuss result size, pagination, or ordering, so it isn't exhaustive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with the hierarchy shape, then the default behavior, then the workflow hint. No filler and every sentence carries information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return-value explanation is unnecessary, and the description still covers default behavior and the call chain for each level. The only omissions are pagination/volume expectations for large listings, which matters mildly for a browse tool with four optional filters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with examples for each param, so the baseline is 3; the description adds genuine meaning on top by explaining the hierarchy ordering and that subject_group identifiers come from a previous response rather than being constructed by the agent. That prerequisite is not derivable from the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — 'Navigate the Title 38 hierarchy (title → chapter → part → subpart → subject group) and list what exists at a level' — and enumerates the exact levels, so an agent knows precisely what the tool returns. It also implies the boundary versus search_regulations ('before searching') and get_section, which is enough to separate it from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use it ('to discover valid parts/subparts before searching') and describes the no-argument default ('returns the chapters'). The chained-call guidance ('pass the subject_group identifier you saw in a previous response') tells the agent the prerequisite for deeper levels. It stops short of naming specific alternative tools by name, which keeps it at 4.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault
to_dateYeseCFR issue date, YYYY-MM-DD
from_dateYeseCFR issue date, YYYY-MM-DD

Output Schema

NameRequiredDescription
addedYescitations added at the to-date — capped at 100
noticeYesthe limit on what the two dates mean: eCFR serves the revision in force on the requested date and does not report which issue date that is, so the dates are selectors and not a stamp of the revision served
changedYescitations whose content changed — text or heading — capped at 100
removedYescitations removed by the to-date — capped at 100
truncatedYestrue when any citation list was cut at the 100-citation cap
toIssueDateYes
fromIssueDateYes

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/idempotent/non-destructive, but the description adds genuinely new behavioral facts: stateless operation, full-title XML fetched from eCFR and hashed per citation, no cache or local index, and a 100-entry cap per list surfaced via `truncated: true`. The error-before-fetch behavior for invalid dates is also disclosed. Only gap is no indication of latency/cost implications of fetching two full-title XML documents.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the purpose and the changed/added/removed triad, then alternative routing, then behavior, then error cases — a sensible order. The parenthetical about the local CLI is long and dense, but it carries real routing value rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, yet the description still flags the one return-value detail an agent must act on (`truncated: true` meaning a list was cut) plus the 100-entry cap. Combined with stated statelessness, fetch behavior, and error preconditions, nothing an agent needs to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and already documents both params as YYYY-MM-DD, so the baseline is 3; the description adds real value by stressing both are required with no default-to and that identical or impossible dates are rejected pre-fetch. It does not elaborate on the directional semantics beyond the names `from_date`/`to_date`.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Provision-level diff of Title 38 between two eCFR issue dates') and enumerates the three output categories (changed, added, removed). It clearly separates itself from the local CLI `cfr-free diff`, but never mentions the sibling tool `redline`, so an agent must infer how this differs from that overlapping diff tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names the alternative workflow: 'for "what changed since my index was built" run `cfr-free diff <from>` locally, which resolves `to` from the index issue date.' It also states the precondition that both dates are required and that there is no default-to, plus the rejection conditions (identical dates, impossible calendar dates).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault
citationYesthe citation to read, e.g. "38 CFR 4.125"

Output Schema

NameRequiredDescription
kindYes
partYes
pathYes
textYes
noticeYes
chapterYes
citedByYes
headingYes
subpartYes
citationYes
markdownYes
issueDateYes
sourceUrlYes
indexStaleYes
referencesYes
sourceUrlAtIndexDateYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent, openWorld=false, so the safety profile is covered; the description adds the output medium (markdown) and names the returned fields (heading, hierarchy path, issue date, permalink). It doesn't say what happens on an ambiguous or unmatched citation, which is the main remaining behavioral unknown for a lookup tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, each doing distinct work: what it returns, what input it accepts, what comes back. No repetition of the name or title and no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read with full schema coverage, complete annotations, and a declared output schema, the description covers purpose, accepted input forms, and output medium. Nothing an agent needs in order to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, and the description earns above that by enumerating genuinely different citation syntaxes (bare section, §-prefixed, fully qualified, appendix names) that the schema only gestures at with one example. It stops short of stating whether leading/trailing whitespace or part qualifiers are tolerated.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Starts with a specific verb+resource pair ("Fetch the full text of one Title 38 CFR section or appendix") plus the output format (markdown), which is far more informative than the title. It doesn't disambiguate from the similarly named sibling get_section_at, so an agent comparing the two must infer the difference from the sibling's schema rather than this text.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied by "one ... section or appendix" (i.e., a single known citation, not a search), but there is no explicit when-to-use vs. search_regulations, browse_title38, or get_section_at. The accepted-format list helps the agent shape input more than it helps choose this tool over its siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault
at_dateYeseCFR issue date, YYYY-MM-DD
citationYesthe citation to read, e.g. "38 CFR 4.130"

Output Schema

NameRequiredDescription
kindYes
partYes
pathYes
textYes
chapterYes
headingYes
subpartYes
citationYes
markdownYes
sourceUrlYes
requestedDateYes
sourceUrlAtRequestedDateYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the read-only/idempotent annotations, the description discloses key behavior: eCFR serves the revision in force on the date, the response names the requested date rather than the served revision, and calls are stateless with no cache. It also states that dates before 2017-01-01 are rejected as not_found.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with purpose, followed by date-resolution behavior, statelessness, archive boundary, and usage routing. It is dense but every sentence contributes, and there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given two required params, full schema coverage, an output schema, and read-only annotations, the description supplies everything an agent needs: date semantics, rejection boundary, statelessness, and sibling differentiation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds real semantic value about at_date: it resolves to the revision in force, the upstream does not report the served issue date, and pre-2017 dates are rejected. Citation semantics are largely left to the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: reading one Title 38 provision as it stood at a requested date. It explicitly distinguishes itself from the sibling get_section by naming that alternative for current text.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit use cases for date-of-claim and CUE questions, and explicitly routes current-text requests to get_section. The before-2017 exclusion is also stated as a rejection condition.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
staleYes
statusYes
syncedAtYes
issueDateYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive, so the safety profile is covered. The description adds real value beyond that: the semantics of each state, the remediation for 'empty' (run `cfr-free sync`), and the obligation to verify 'stale'/'degraded' results against the current eCFR. It does not mention latency or whether the check is cheap, so a 4 rather than a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single front-loaded sentence that leads with the verdict concept, then defines each state inline. Dense with nested parentheses and em-dashes, which makes it slightly harder to scan, but no sentence is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present the description need not explain return values, yet it still defines the four verdicts and their follow-up actions — more than enough for a zero-param read tool. The one gap is failing to clarify the boundary with the sibling 'index_status'.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is nothing for the description to disambiguate; the baseline of 4 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('one-call readiness verdict for the local index') and enumerates all four possible verdicts with their meanings, so an agent knows exactly what comes back. It does not, however, distinguish itself from the sibling 'index_status', which sounds like an overlapping concept — that ambiguity keeps it from a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Clients should gate queries on this before spending tool calls' gives an explicit when-to-use rule and even a cost rationale. It stops short of naming an alternative or a when-not-to-use case (e.g., after a sync is known to be fresh), and never mentions the similar-sounding 'index_status' sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
staleYes
sectionsYes
syncedAtYes
issueDateYes
appendicesYes
corpusHashYes
maxAgeDaysYes
lastChangesYes
parserVersionYes
structureDriftYes

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive, so the safety profile is carried. The description adds value annotations cannot: the receipt hash is a sha256 over sorted (citation, text+heading) pairs computed at rebuild time and is null on older indexes, and staleness is judged against the CFR_MAX_AGE_DAYS threshold. That is genuine behavioral context about data provenance and edge cases.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: the first front-loads the verb and resource then enumerates the report contents, the second gives the actionable rule. The parenthetical about the receipt hash is long but every clause carries information an agent cannot get from the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be re-explained, and annotations plus the description together cover safety, provenance, and staleness. The one gap is what an agent should do with a stale verdict beyond 'check this before relying' — no remediation or fallback path is described.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Zero parameters, so there is no parameter semantics to explain and the baseline is 4. Nothing in the description misleadingly implies inputs that do not exist.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb ('Report') plus specific resource ('the local Title 38 index') with the exact fields returned enumerated: eCFR issue date, sync time, row counts, receipt hash, staleness. That is far beyond tautology. It does not, however, draw any line against the sibling 'health' tool, which is the most likely confusion for an agent deciding between status-style tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Check this before relying on search results' gives a clear triggering condition tied to search_regulations. It stops short of naming alternatives or saying what to do when the report comes back stale, so it is clear context rather than a full when/when-not/alternatives rule set.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
partsYes

TDQS

A3.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent, non-destructive, and closed-world behavior, so the safety profile is fully covered. The description adds that results come specifically from the 'local index' and what fields are returned, but the return fields are already in the output schema, so the incremental behavioral disclosure is modest.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences: the first states scope and return content, the second states the use case. Front-loaded and free of filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With a rich output schema and complete annotations, the description covers purpose, scope, and use case, leaving little for an agent to guess. Only minor details (ordering, whether the index can be stale/incomplete) are unstated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, so the baseline is 4; nothing about parameter meaning needs documenting and the schema fully covers the (empty) input.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Enumerate') and resource ('Title 38 CFR part') scoped to the local index, and states the returned fields (structure-tree title, section/appendix counts). It is clear on its own, but it never names or contrasts with siblings like browse_title38 or search_regulations, which could plausibly overlap.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Use this to discover which parts are searchable' gives a concrete when-to-use condition tied to a downstream action. It stops short of stating when-not-to-use or pointing to an alternative sibling for related tasks, so it is clear context without exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault
to_dateYeseCFR issue date, YYYY-MM-DD
citationYesthe citation to redline, e.g. "38 CFR 4.125" (≤ 120 chars)
from_dateYeseCFR issue date, YYYY-MM-DD

Output Schema

NameRequiredDescription
hunksYes
coarseYes
noticeYesthe limit on what the two dates mean: eCFR serves the revision in force on the requested date and does not report which issue date that is, so the dates are selectors and not a stamp of the revision served
statusYes
toTextNo
citationYes
fromTextNo
toIssueDateYes
fromIssueDateYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the readOnly/idempotent annotations: discloses that dates are selectors rather than verified issue stamps, that full-title XML is refetched per call with no cache, and the exact error ordering (date validation before fetch, not_found after fetch). Also explains the coarse-block degradation when the core line cap is exceeded.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose and organized around output, error behavior, and statelessness, with no filler sentences. It is dense to the point of being a single long paragraph, which slightly hurts scanability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a stateless remote-fetch diff tool, it covers return shape, failure modes, cost implications (full-title fetch per call), and degraded-mode output, and an output schema already documents the payload. Nothing an agent needs in order to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning the schema cannot: the dates act as selectors for the revision in force, not as a guarantee of which issue date was served. The citation parameter itself gets no added semantics beyond the schema example.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: a line-level redline of one Title 38 provision between two eCFR issue dates, enumerating the outcomes (changed/added/removed/unchanged). It is clearly narrower than whole-title siblings like diff_issues or single-date get_section_at.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Clear usage context: both dates are required and there is no default-to, so the agent knows it must supply a comparison window. However, it never names diff_issues or get_section_at as alternatives, so routing between the comparison tools is left implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

NameRequiredDescriptionDefault
kindNorestrict to one provision kind: section or appendix
pageNo1-based result page (default 1)
partNorestrict to one CFR part number
queryYessearch terms, e.g. "traumatic brain injury compensation"
chapterNorestrict to one chapter, exact case-sensitive Roman numeral (e.g. "I")
per_pageNoresults per page (default 10, max 20)

Output Schema

NameRequiredDescription
hitsYes
pageYes
queryYes
totalYes
noticeYes
perPageYes
matchModeYes
indexStaleYes
matchedTermsYes
indexIssueDateYes
unmatchedTermsYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial behavior beyond the read-only annotations: candidate-set semantics, three-tier ranking order, bm25 interpretation, citation pinning with `direct` and `inResults`, quoted-phrase/prefix behavior, and explicit caveats that ranking is lexical not authoritative. These details materially affect how an agent should interpret and cite results.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the purpose and core search semantics, and nearly every sentence carries operational meaning. It is dense and long, but the length is largely justified by the complexity of ranking, pinning, and matchedTerms behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given six parameters, full schema descriptions, an output schema, and read-only annotations, the description covers what an agent needs: result ordering, candidate-set scope, citation pinning, matched/unmatched terms, and citation-safety guidance.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description still adds useful semantics: example part numbers ('4' for ratings, '17' for prosthetics), clarification that chapter is exact and case-sensitive, and the effect of `page` on pinned citation totals.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: full-text search across the local Title 38 CFR index. It clearly distinguishes itself from siblings by telling the agent to follow up with get_section to read a hit in full.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides concrete scoping guidance for `part`, `kind`, and `chapter`, and explains that citation queries are pinned. It does not explicitly say when to use browse_title38 or get_section_at instead, but the follow-up workflow with get_section is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 7 tool updatesv0.2.1
    • Changedbrowse_title381 field changed
      • addedInput schema / properties / subject_group
        Added value: +{
        +  "description": "subject-group identifier, e.g. \"ECFRd158704dcab040e\" (requires part)",
        +  "maxLength": 40,
        +  "type": "string"
        +}
    • Addeddiff_issues
    • Changedget_section3 fields changed
      • addedOutput schema / properties / citedBy
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / sourceUrlAtIndexDate
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "citation",
        -  "kind",
        -  "heading",
        -  "chapter",
        -  "part",
        -  "subpart",
        -  "path",
        -  "issueDate",
        -  "markdown",
        -  "text",
        -  "sourceUrl",
        -  "references",
        -  "indexStale",
        -  "notice"
        -]New value: +[
        +  "citation",
        +  "kind",
        +  "heading",
        +  "chapter",
        +  "part",
        +  "subpart",
        +  "path",
        +  "issueDate",
        +  "markdown",
        +  "text",
        +  "sourceUrl",
        +  "sourceUrlAtIndexDate",
        +  "references",
        +  "citedBy",
        +  "indexStale",
        +  "notice"
        +]
    • Addedget_section_at
    • Changedindex_status2 fields changed
      • addedOutput schema / properties / corpusHash
        Added value: +{
        +  "type": [
        +    "string",
        +    "null"
        +  ]
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "issueDate",
        -  "syncedAt",
        -  "sections",
        -  "appendices",
        -  "stale",
        -  "maxAgeDays",
        -  "parserVersion",
        -  "structureDrift",
        -  "lastChanges"
        -]New value: +[
        +  "issueDate",
        +  "syncedAt",
        +  "sections",
        +  "appendices",
        +  "stale",
        +  "maxAgeDays",
        +  "parserVersion",
        +  "structureDrift",
        +  "corpusHash",
        +  "lastChanges"
        +]
    • Addedredline
    • Changedsearch_regulations8 fields changed
      • addedInput schema / properties / chapter
        Added value: +{
        +  "description": "restrict to one chapter, exact case-sensitive Roman numeral (e.g. \"I\")",
        +  "maxLength": 8,
        +  "minLength": 1,
        +  "pattern": "^[IVXLCDM]+$",
        +  "type": "string"
        +}
      • addedInput schema / properties / kind
        Added value: +{
        +  "description": "restrict to one provision kind: section or appendix",
        +  "enum": [
        +    "section",
        +    "appendix"
        +  ],
        +  "type": "string"
        +}
      • addedOutput schema / properties / hits / items / properties / partTitle
        Added value: +{
        +  "type": [
        +    "string",
        +    "null"
        +  ]
        +}
      • addedOutput schema / properties / hits / items / properties / sourceUrlAtIndexDate
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / properties / hits / items / required
        Previous value: -[
        -  "citation",
        -  "citationDisplay",
        -  "sourceUrl",
        -  "direct",
        -  "heading",
        -  "part",
        -  "path",
        -  "snippet",
        -  "score",
        -  "matches",
        -  "inResults"
        -]New value: +[
        +  "citation",
        +  "citationDisplay",
        +  "sourceUrl",
        +  "sourceUrlAtIndexDate",
        +  "direct",
        +  "heading",
        +  "part",
        +  "partTitle",
        +  "path",
        +  "snippet",
        +  "score",
        +  "matches",
        +  "inResults"
        +]
      • addedOutput schema / properties / matchedTerms
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / unmatchedTerms
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "query",
        -  "page",
        -  "perPage",
        -  "total",
        -  "matchMode",
        -  "indexIssueDate",
        -  "indexStale",
        -  "notice",
        -  "hits"
        -]New value: +[
        +  "query",
        +  "page",
        +  "perPage",
        +  "total",
        +  "matchMode",
        +  "indexIssueDate",
        +  "indexStale",
        +  "notice",
        +  "matchedTerms",
        +  "unmatchedTerms",
        +  "hits"
        +]
  2. 6 tool updatesv0.1.0
    • First observedbrowse_title38
    • First observedget_section
    • First observedhealth
    • First observedindex_status
    • First observedlist_parts
    • First observedsearch_regulations

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
ResponsivenessSyncing

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    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
    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