rfc-mcp

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT

README

rfc-mcp

Go Reference CI codecov GitHub Release

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 rejects HEAD requests, so every fetch is a GET. RFC bodies never change once published, so the local cache under --raw-dir is valid forever and is never re-fetched or expired.
  • update keeps 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 the update command above for the atomic-swap mechanics).
  • Every build/update stamps 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, no get_section/get_toc content): 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 .txt for them. The pipeline detects this from the <format> list in each entry's rfc-index.xml record and skips fetching their bodies rather than retrying a guaranteed 404 on every build/update run (reported as TEXT_UNAVAILABLE in the completion summary).
  • ~684 RFCs (~7% of the corpus, overwhelmingly pre-1000) degrade to a single whole-body section numbered body rather 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 via search, get_section(rfc, "body"), or get_document; it's just not addressable by a fine-grained section number.
  • Plain-text parsing only - RFCs are parsed from the .txt rendition 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 synthetic header slug covers the RFC's title-block content (document series header, title, author list, date) that precedes the first real heading, and body covers 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL