README
¶
rfc-mcp
An MCP (Model Context Protocol) server that makes IETF RFCs accessible to LLMs.
Background
RFCs are the primary reference for Internet protocols, but they are difficult for LLMs to work with effectively:
- Too many documents - Nearly 9,800 RFCs have been published since 1969, spanning every era of Internet protocol design.
- Individual documents can be huge - Foundational specs like TCP (RFC 9293) or BGP-4 (RFC 4271) run to a hundred pages or more.
- Inconsistent formatting across five decades - Plain-text RFCs range from modern unpaginated documents to 1980s page-and-form-feed layouts to free-form 1970s documents with no section numbering at all.
- Heavy cross-referencing - RFCs constantly reference, obsolete, and update each other (e.g. RFC 9293 obsoletes RFC 793 and six others, and updates three more); reading one document in isolation gives an incomplete picture.
- Status and errata complexity - The same protocol can be described across an original RFC, several updates, and a list of errata reports, and knowing which parts are still current matters.
This tool addresses these challenges by parsing the plain-text RFC bodies published at rfc-editor.org, structuring the content by section, and storing everything in a SQLite database with full-text search (FTS5). An MCP server then exposes tools for browsing, searching, and following cross-references — letting an LLM navigate RFCs the way a protocol engineer would.
Why not RAG?
A RAG (Retrieval-Augmented Generation) approach — chunking documents, generating embeddings, and performing vector similarity search — is a common solution for document Q&A. However, RFCs are structured technical documents where that approach has significant drawbacks:
- Loss of structure - RAG splits documents into flat chunks, discarding the section hierarchy needed to navigate a spec (e.g. jumping straight to "Section 3.10.7.4" of RFC 9293).
- No relationship traversal - Vector search cannot follow "obsoletes", "updates", or cross-reference relationships between RFCs.
- Noisy retrieval - Similarity search may return loosely related chunks instead of the exact section needed.
- Additional cost - Embedding generation and vector database hosting add infrastructure and API costs.
This tool takes a structure-aware approach instead: it preserves each RFC's section hierarchy (including legacy pagination quirks), enables precise section-level retrieval by number or by slug, supports full-text search with FTS5 syntax, and resolves cross-references between RFCs. All data is stored in a single SQLite file with no external dependencies.
Getting Started
Build a self-contained Docker image
The Dockerfile is multi-stage and builds the database directly, producing a
self-contained image with the SQLite database baked in. No LibreOffice or
other heavy dependency is needed — RFCs are parsed straight from plain text.
# Build an image with the full RFC corpus baked in (default, ~8 min, ~865 MB)
docker build -t rfc-mcp:latest .
# ...or restrict the database to a numeric RFC range (fast smoke-test image)
docker build --build-arg FROM_RFC=9290 --build-arg TO_RFC=9295 -t rfc-mcp:smoke .
# stdio transport (Claude Code / IDE integration)
docker run --rm -i rfc-mcp:latest
# HTTP transport
docker run --rm -p 8080:8080 rfc-mcp:latest serve --db /rfc.db --transport http --addr :8080
FROM_RFC/TO_RFC default to empty, which bakes in the full corpus. Set
either (or both) to build a database restricted to a numeric RFC range.
Deploy to Cloud Run
To run on Cloud Run, see cloudbuild.yaml (build + push + deploy) and
service.yaml (Cloud Run service spec).
1. Install
go install github.com/higebu/rfc-mcp/cmd/rfc-mcp@latest
Requires Go 1.26+. No CGO, no external runtime dependencies — RFC bodies are parsed from plain text only.
2. Build the database
Download and import RFCs into the database. Downloaded .txt bodies are
cached (see Data sources & update cadence
below), so re-running build after an interruption resumes cheaply.
# Download and import the full RFC corpus (~8 min with 16 workers, ~865 MB)
rfc-mcp build --db data/rfc.db
# ...or restrict to a numeric range, e.g. for a quick local test
rfc-mcp build --db data/rfc.db --from 9290 --to 9295
This fetches rfc-index.xml and errata.json, then each RFC's plain-text
body individually (rfc-editor.org has no bulk tarball), parses it into
sections, and inserts everything into the SQLite database.
3. Register with your MCP client
Claude Code
claude mcp add --scope user rfc -- rfc-mcp serve --db /path/to/data/rfc.db
VS Code / GitHub Copilot
code --add-mcp '{"name":"rfc","command":"rfc-mcp","args":["serve","--db","/path/to/data/rfc.db"]}'
Claude Desktop
Add to your configuration file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"rfc": {
"command": "rfc-mcp",
"args": ["serve", "--db", "/path/to/data/rfc.db"]
}
}
}
Streamable HTTP (remote deployment)
Start the server with HTTP transport:
rfc-mcp serve --db data/rfc.db --transport http --addr :8080
Optionally enable Bearer token authentication:
export RFC_MCP_BEARER_TOKEN=$(openssl rand -hex 32)
rfc-mcp serve --db data/rfc.db --transport http --addr :8080
Then configure your client to connect via HTTP:
{
"mcpServers": {
"rfc": {
"url": "http://your-server:8080",
"headers": {
"Authorization": "Bearer YOUR_SECRET_TOKEN"
}
}
}
}
GET /health returns 200 OK without authentication, for platform health
checks (Cloud Run, Kubernetes liveness/readiness probes, etc.).
For container platforms like Cloud Run or Heroku that inject a PORT
environment variable, the server automatically switches to HTTP transport
and binds to :$PORT. Explicit flags or RFC_MCP_TRANSPORT /
RFC_MCP_ADDR always take precedence.
See examples/systemd/ for production deployment with systemd.
MCP Tools
Browsing RFCs
| Tool | Description | Key Parameters |
|---|---|---|
list_rfcs |
List RFCs, optionally filtered | query (title substring), stream, status, wg, limit, offset |
get_metadata |
Title, status, dates, obsoletes/updates, errata | rfc (required) |
get_errata |
Full errata detail (original/corrected text, notes, submitter, dates) | rfc (required), status, type, section |
get_toc |
Table of contents of an RFC | rfc (required) |
get_section |
Section content, each section prefixed with its heading line (paginated). A title-only section fetched without include_subsections returns a summary of its subsections instead of empty text |
rfc, section_number (required), include_subsections, offset, max_lines, max_chars |
get_document |
Full text of an RFC as one document (paginated) | rfc (required), offset, max_lines, max_chars |
get_metadata(rfc: 4271) returns:
{
"rfc": 4271,
"title": "A Border Gateway Protocol 4 (BGP-4)",
"status": "Draft Standard",
"stream": "IETF",
"date": "2006-01",
"page_count": 104,
"wg": "idr",
"area": "rtg",
"authors": ["Y. Rekhter", "T. Li", "S. Hares"],
"keywords": ["BGP-4", "routing"],
"abstract": "This document discusses the Border Gateway Protocol (BGP), ...",
"draft": "draft-ietf-idr-bgp4-26",
"doi": "10.17487/RFC4271",
"errata_url": "https://www.rfc-editor.org/errata/rfc4271",
"obsoletes": [1771],
"updated_by": [4724, 6286, 6608, 6793, 7606, 7607, 7705, 8212, 8654, 9072, 9687, 9774],
"errata": [
{ "id": 150, "status": "Verified", "type": "Editorial", "section": "9.1.1" },
{ "id": 1332, "status": "Rejected", "type": "Technical", "section": "4.5" }
]
}
status is title-cased for readability even though rfc-index.xml stores it
upper-case (DRAFT STANDARD); list_rfcs returns the raw upper-case form.
Errata are a compact summary (id/status/type/section only) — follow
errata_url for the full original/corrected text of a specific erratum.
get_errata(rfc: 9293, type: "Editorial") returns the full detail behind
that summary, filterable by status, type, and/or section
(case-insensitive; section ignores a trailing . on either side, since
the source data is inconsistent about it):
[
{
"id": 8126,
"rfc": 9293,
"status": "Verified",
"type": "Editorial",
"section": "3.3.1",
"orig_text": "the sequence space labeled 3 in Figure 3",
"correct_text": "the sequence space labeled 2 and 3 in Figure 3",
"notes": "In Figure 3, the send window shoud be 2(sequence numbers of unacknowledged data) and 3(sequence numbers allowed for new data transmission).",
"submitted_date": "2024-10-01",
"submitter_name": "zhihua.li",
"verifier_name": "Zaheduzzaman Sarker",
"updated_date": "2025-03-18 08:36:20"
}
]
An RFC with no matching errata returns [], not an error.
Searching
| Tool | Description | Key Parameters |
|---|---|---|
search |
Full-text search across all RFCs | query (required), rfc, rfcs, limit (default 10) |
The search tool supports SQLite FTS5 query syntax:
- Phrase search:
"three way handshake" - Boolean operators:
AMF AND UE,retransmission OR retransmit,NOT deprecated - Prefix matching:
retransmi* - Column filter:
title:security,content:handshake - Proximity:
NEAR(SYN ACK, 5) - Hyphenated terms (e.g.
three-way-handshake) are auto-quoted to avoid FTS5 syntax errors
Example — search(query: "three way handshake", rfc: 9293, limit: 3):
[
{
"rfc": 9293,
"number": "3.5",
"title": "Establishing a Connection",
"snippet": "The \"<mark>three</mark>-<mark>way</mark> <mark>handshake</mark>\" is the procedure used to establish a connection. ..."
}
]
Cross-references
| Tool | Description | Key Parameters |
|---|---|---|
get_references |
Get cross-references between RFCs | rfc (required), section_number, direction (outgoing default, or incoming), include_subsections |
outgoing requires section_number and returns the RFCs referenced from
that section; incoming only requires rfc and returns every section (in
any RFC) that references it. Example —
get_references(rfc: 9293, section_number: "3.10.7.4", direction: "outgoing"):
[
{
"source_rfc": 9293,
"source_section": "3.10.7.4",
"target_rfc": 793,
"context": "...the original behavior described in RFC 793 follows in this paragraph. ..."
},
{
"source_rfc": 9293,
"source_section": "3.10.7.4",
"target_rfc": 5961,
"target_section": "3",
"target_title": "Blind Reset Attack Using the RST Bit",
"context": "...RFC 5961 [9], Section 3 describes a potential blind reset..."
}
]
target_section/target_title are only present when the reference could be
resolved to a specific section (e.g. via a numeric bracket citation like
[9] resolved through the References section); a bare RFC 793 mention
without a section number omits them.
Internet-Drafts
Unlike the RFC tools above (SQLite only, fully offline), these tools fetch
directly from the IETF Datatracker and the
IETF document archive over the network
on every call — there's no local Internet-Draft database. Set
RFC_MCP_DISABLE_DRAFTS=1 to skip registering them entirely for
offline/no-egress deployments.
| Tool | Description | Key Parameters |
|---|---|---|
search_drafts |
Search Internet-Drafts by title/name substring and/or working group | query, name_contains, group, include_expired, limit, offset |
get_draft_metadata |
Title, abstract, page count, submission/expiry dates, and (if published) the resulting RFC number | name (required) |
get_draft_toc |
Table of contents of a draft | name (required), revision |
get_draft_section |
Section content, addressed and paginated the same way as get_section |
name (required), revision, section_number (required), include_subsections, offset, max_lines, max_chars |
get_ipr |
IETF IPR (patent) disclosures against an RFC or draft | rfc or name (exactly one required) |
name accepts both a bare draft name (draft-ietf-quic-transport) and one
with an explicit revision suffix (draft-ietf-quic-transport-34); the first
four tools resolve to the latest revision unless one is given explicitly
(via revision, or embedded directly in name).
search_drafts(query: "quic", group: "quic") returns:
{
"drafts": [
{
"name": "draft-ietf-quic-multipath",
"rev": "21",
"title": "Managing multiple paths for a QUIC connection",
"expires": "2026-09-18T09:40:37Z",
"pages": 42
}
],
"total_count": 1,
"limit": 20,
"offset": 0
}
Only Active drafts are returned by default; set include_expired: true
to widen the search to every lifecycle state (expired, replaced, and
drafts already published as an RFC).
get_draft_metadata(name: "draft-ietf-quic-transport") returns:
{
"name": "draft-ietf-quic-transport",
"rev": "34",
"title": "QUIC: A UDP-Based Multiplexed and Secure Transport",
"abstract": "This document defines the core of the QUIC transport protocol. ...",
"pages": 151,
"time": "2022-02-19T08:46:51Z",
"expires": "2021-07-19T02:14:40Z",
"rfc": 9000,
"hint": "This draft was published as RFC 9000; use get_metadata/get_toc/get_section/get_document with rfc=9000 instead."
}
rfc/hint are only present once a draft has been published as an RFC;
expires is surfaced as-is even when it's in the past, since a draft's own
submission metadata is never edited after the fact. get_draft_toc and
get_draft_section follow the exact same table-of-contents/pagination
shape as get_toc/get_section (see above), just addressed by draft
name/revision instead of an RFC number.
get_ipr(rfc: 3261) returns:
{
"searched_docs": ["rfc3261", "draft-ietf-sip-rfc2543bis"],
"disclosures": [
{
"id": 62,
"url": "https://datatracker.ietf.org/ipr/62/",
"title": "AT&T's Patent Statement pertaining to draft-ietf-sip-rfc2543bis",
"state": "posted",
"holder": "AT&T",
"licensing": "reasonable",
"has_patent_pending": true,
"patent_info": "...",
"time": "2002-01-08T00:00:00Z",
"docs": ["draft-ietf-sip-rfc2543bis"]
},
{
"id": 579,
"url": "https://datatracker.ietf.org/ipr/579/",
"title": "AT&T's statement about IPR claimed in RFC 3261",
"state": "posted",
"holder": "AT&T",
"licensing": "see-below",
"has_patent_pending": true,
"patent_info": "...",
"time": "2005-05-23T07:00:00Z",
"docs": ["rfc3261"]
}
],
"total_count": 2
}
An RFC's disclosures aren't automatically carried over from its originating
draft, and some are filed directly against the RFC name instead — so
get_ipr searches the RFC name, its originating Internet-Draft (resolved
via the Datatracker's became_rfc relationship), and any draft(s) that
draft itself replaced (one hop); get_ipr(name: "draft-...") follows the
same one-hop replaces fan-out without the RFC step. searched_docs
reports exactly which document names were queried. Only Posted
disclosures are returned; pending/parked/rejected/removed
disclosures are excluded. licensing and has_patent_pending/
patent_info are absent for a "generic" disclosure (one with a free-form
statement instead of structured patent info).
Draft plain-text bodies are cached on disk forever per revision (a specific
revision never changes once submitted); Datatracker metadata (latest
revision, title, abstract, expiry) and IPR disclosures are cached for 1
hour — shorter than the RFC tools' 24-hour rfc-index.xml cache, since
drafts (and their IPR filings) move faster.
Command Reference
serve
Start the MCP server.
| Flag | Description | Default |
|---|---|---|
--db |
Path to SQLite database | rfc.db |
--transport (-t) |
Transport type: stdio or http (env: RFC_MCP_TRANSPORT; defaults to http when PORT is set) |
stdio |
--addr |
HTTP listen address (env: RFC_MCP_ADDR, or PORT interpreted as :$PORT) |
:8080 |
--bearer-token |
Bearer token for HTTP auth (env: RFC_MCP_BEARER_TOKEN) |
Set RFC_MCP_DISABLE_DRAFTS=1 to skip registering the
Internet-Draft tools (search_drafts,
get_draft_metadata, get_draft_toc, get_draft_section, get_ipr) —
useful for offline or no-egress deployments, since unlike the RFC tools
they perform live network requests to the IETF Datatracker/archive on every
call. No corresponding flag; env var only.
build
Download and import RFCs into the database (recommended for initial setup).
| Flag | Description | Default |
|---|---|---|
--db |
Output SQLite database path | data/rfc.db |
--workers |
Number of parallel workers | NumCPU |
--timeout |
HTTP timeout | 30s |
--from |
Only process RFCs numbered >= this (0 = no lower bound) | 0 |
--to |
Only process RFCs numbered <= this (0 = no upper bound) | 0 |
--raw-dir |
Directory to cache downloaded RFC .txt files |
$XDG_CACHE_HOME/rfc-mcp/raw |
--base-url |
Override the RFC Editor root URL | https://www.rfc-editor.org |
download
Download RFC plain-text bodies without importing them.
| Flag | Description | Default |
|---|---|---|
--raw-dir |
Directory to save downloaded RFC .txt files |
$XDG_CACHE_HOME/rfc-mcp/raw |
--workers |
Number of parallel downloads | NumCPU |
--timeout |
HTTP timeout | 30s |
--from |
Only download RFCs numbered >= this | 0 |
--to |
Only download RFCs numbered <= this | 0 |
--base-url |
Override the RFC Editor root URL | https://www.rfc-editor.org |
import
Import a single RFC .txt file into the database.
| Flag | Description | Default |
|---|---|---|
--db |
Output SQLite database path | data/rfc.db |
Usage: rfc-mcp import --db data/rfc.db path/to/rfcNNNN.txt
import-dir
Import all RFC .txt files in a directory into the database.
| Flag | Description | Default |
|---|---|---|
--db |
Output SQLite database path | data/rfc.db |
--workers |
Number of parallel parse workers | NumCPU |
Usage: rfc-mcp import-dir --db data/rfc.db ./raw
update
Refresh an existing database in place: fetch RFCs newly issued since the
last build, and refresh metadata and errata for the entire corpus (both are
small, live-fetched documents, so a wholesale refresh is cheap). Already
cached RFC bodies are never re-fetched, since bodies are immutable once
published. The refresh happens on a VACUUM INTO'd copy of the database and
is only swapped into place atomically once it succeeds, so a database
serve is reading concurrently is never left partially written.
| Flag | Description | Default |
|---|---|---|
--db |
SQLite database path | data/rfc.db |
--workers |
Number of parallel workers | NumCPU |
--timeout |
HTTP timeout | 30s |
--raw-dir |
Directory to cache downloaded RFC .txt files |
$XDG_CACHE_HOME/rfc-mcp/raw |
--base-url |
Override the RFC Editor root URL | https://www.rfc-editor.org |
completion
Generate shell completion scripts: rfc-mcp completion <bash|zsh|fish>.
Data sources & update cadence
rfc-index.xml(~13.6 MB) - metadata for every RFC: 9,794 issued entries plus 188 allocated-but-never-issued entries. Cached for 24 hours by default (RFC_MCP_CACHE_TTL_HOURS).errata.json(~11.5 MB) - all published errata reports (7,961 as of this writing). Same 24-hour cache.- Per-RFC plain text (
https://www.rfc-editor.org/rfc/rfcN.txt) - fetched one file at a time; rfc-editor.org publishes no bulk tarball and rejectsHEADrequests, so every fetch is aGET. RFC bodies never change once published, so the local cache under--raw-diris valid forever and is never re-fetched or expired. updatekeeps a database current without a full rebuild: it fetches only RFC numbers not already in the database, and refreshes metadata and errata for the whole corpus unconditionally (see theupdatecommand above for the atomic-swap mechanics).- Every
build/updatestamps a build timestamp in the database, which the MCP server reports to clients (in its Instructions and in "not found" hints) so an LLM can tell how current the baked snapshot is.
A full build of the entire corpus (measured 2026-07-11, 16 workers) takes
about 8 minutes and produces an ~865 MB SQLite database: 9,982 RFC rows
(9,794 issued + 188 not-issued), 321,372 sections, 284,869 extracted
cross-references, and 7,961 errata records.
Internet-Drafts (see Internet-Drafts above) are never
part of this database — the search_drafts/get_draft_*/get_ipr tools
fetch from the IETF Datatracker and
archive on demand instead, with their
own cache: draft bodies forever per revision, Datatracker metadata and IPR
disclosures for 1 hour. All three live under the same
$XDG_CACHE_HOME/rfc-mcp root as rfc-index.xml/errata.json/
--raw-dir, under a drafts/ subdirectory.
Limitations
- 7 RFCs have no plain-text body at all and are metadata-only (title,
status, dates, etc. from
rfc-index.xml, but no sections, no full-text search, noget_section/get_toccontent): RFC 8, 9, 51, 418, 500, 530, and 598. These are 1969–1973 documents that were only ever distributed as scanned images or on paper; rfc-editor.org has no.txtfor them. The pipeline detects this from the<format>list in each entry'srfc-index.xmlrecord and skips fetching their bodies rather than retrying a guaranteed 404 on everybuild/updaterun (reported asTEXT_UNAVAILABLEin the completion summary). - ~684 RFCs (~7% of the corpus, overwhelmingly pre-1000) degrade to a
single whole-body section numbered
bodyrather than a proper section breakdown. This is the parser's documented Tier-3 fallback for documents whose layout doesn't match either the column-0 heading pattern or a matchable in-document table of contents (free-form 1970s memos, unusual typesetting, etc.). Full text is still searchable and retrievable viasearch,get_section(rfc, "body"), orget_document; it's just not addressable by a fine-grained section number. - Plain-text parsing only - RFCs are parsed from the
.txtrendition published by rfc-editor.org, not the XML source. Section content is stored verbatim (no dedent or reflow), preserving ABNF and packet-diagram alignment, but any leftover pagination artifact the cleanup heuristics miss will appear as-is. - Section numbering conventions: dotted numeric (
4.1), lettered appendices (A.2), or a slug for unnumbered headings — lowercase the heading and replace spaces/apostrophes with hyphens (abstract,security-considerations,iana-considerations,acknowledgments,authors-address, etc.). A syntheticheaderslug covers the RFC's title-block content (document series header, title, author list, date) that precedes the first real heading, andbodycovers the Tier-3 whole-body fallback described above.
License
MIT
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
rfc-mcp
command
|
|
|
Package db implements the SQLite storage layer for rfc-mcp: RFC metadata, section text, full-text search, cross-references, and errata.
|
Package db implements the SQLite storage layer for rfc-mcp: RFC metadata, section text, full-text search, cross-references, and errata. |
|
ingest
|
|
|
drafts
Package drafts fetches Internet-Draft metadata and plain-text bodies on demand from the IETF Datatracker and archive.
|
Package drafts fetches Internet-Draft metadata and plain-text bodies on demand from the IETF Datatracker and archive. |
|
errata
Package errata parses the RFC Editor's errata.json into db.Errata records.
|
Package errata parses the RFC Editor's errata.json into db.Errata records. |
|
pipeline
Package pipeline orchestrates fetching rfc-index.xml, errata.json, and per-RFC plain-text bodies from the RFC Editor, and storing them via db.DB.
|
Package pipeline orchestrates fetching rfc-index.xml, errata.json, and per-RFC plain-text bodies from the RFC Editor, and storing them via db.DB. |
|
rfcindex
Package rfcindex parses the RFC Editor's rfc-index.xml into db.RFC records.
|
Package rfcindex parses the RFC Editor's rfc-index.xml into db.RFC records. |
|
rfctxt
Package rfctxt parses the plain-text RFC bodies served from https://www.rfc-editor.org/rfc/rfcN.txt into a flat list of sections.
|
Package rfctxt parses the plain-text RFC bodies served from https://www.rfc-editor.org/rfc/rfcN.txt into a flat list of sections. |
|
internal
|
|
|
testutil
Package testutil provides shared test helpers for the rfc-mcp project.
|
Package testutil provides shared test helpers for the rfc-mcp project. |