Quickstart

Python 3.11 or newer. Everything below runs on your own machine.

Install and start
git clone https://github.com/uaskpro/rtfm
cd rtfm
python -m venv .venv && . .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e .

rtfm seed        # optional: a small demo canon to look at
rtfm reindex     # build the derived index from the files
rtfm search "one-page checkout"

Connect an agent over MCP

RTFM is an MCP server on stdio. Clients differ in where they keep the config, but they all launch the same command.

Claude Desktop · claude_desktop_config.json
{
  "mcpServers": {
    "rtfm": {
      "command": "rtfm",
      "args": ["mcp", "--as-owner"],
      "env": { "RTFM_ROOT": "/path/to/your/canon" }
    }
  }
}

--as-owner grants that client read access to private knowledge too. Leave it off for a client that should see only the public part.

Any MCP client · the command it needs to run
# read-only, public knowledge only
RTFM_ROOT=/path/to/your/canon rtfm mcp

# read-only, public + private (a local agent you trust)
RTFM_ROOT=/path/to/your/canon rtfm mcp --as-owner

# additionally register the write tools (off by default)
RTFM_ROOT=/path/to/your/canon rtfm mcp --as-owner --allow-write

OpenAI’s and Google’s agent SDKs both speak MCP over stdio and take this command; consult their own docs for the key the config uses, since it differs between them and changes faster than this page.

If the console script is not on the client’s PATH
{
  "command": "/path/to/.venv/bin/python",
  "args": ["-m", "rtfm.cli.main", "mcp", "--as-owner"]
}

Tools

The tools reached for most often. Read tools are always registered; write tools exist only under --allow-write, and are refused for an anonymous context even then. The full set — observations, evidence extraction, visibility and the ingest variants — is listed in the repository README.

rtfm_facts(question, budget?)

Answer a free-text question with facts, their sources, the open gaps and any conflicts. The main entry point for an agent.

rtfm_get(id)

Fetch one entity card.

rtfm_search(query, limit?)

Full-text search over titles, aliases and bodies.

rtfm_neighbors(id, relation?)

Outgoing relations and incoming backlinks for one entity.

rtfm_resolve(id, question?, budget?)

The effective context pack for a known entity: cards, claims, sources, open gaps.

rtfm_explain(id)

The provenance trail for one entity.

rtfm_list_gaps(status?)

What the base knows it does not know.

rtfm_validate(—)

Validate the whole canon: schema and cross-entity invariants. Reads everything, so it is refused for an anonymous context — start the server with --as-owner to use it.

rtfm_add_source(source_id, title, text, …) write

Register material as a checksummed source. No extraction.

rtfm_ingest(source_id, observed_in?, extract?) write

The full pipeline over a registered source: observe, discover entities, stub, open gaps, extract claims. Idempotent.

rtfm_open_gap(subject_id, questions, …) write

Record explicitly that something is not known.

rtfm_set_maturity(entity_id, level) write

Move a fact along its lifecycle. Verifying is an owner’s act, never the pipeline’s.

The fact pack

What rtfm_facts returns. The same structure comes back over HTTP. Role and date are stated once per source; each fact names its source by id.

The fact pack
Field Type Description
question string The question as it was asked.
found boolean False means nothing cleared the relevance threshold. With an empty facts list this is the honest refusal — a guarantee, not a judgement.
subject / subject_title string | null What the pack is about, if it settled on one thing.
facts[] array id, statement, subject, applies_to, polarity, source, locator, confidence, conflicting, conflicts_with.
sources[] array id, title, authority (normative | historical | reference), date, dated, version, uri.
open_gaps[] array id, title, subject, gap_status, priority, questions.
conflicts[] array The facts that disagree with one another, in the same shape as facts[].
note string Set when there is nothing to return, explaining that in words.
Example response
{
  "question": "does Simple support guest checkout?",
  "found": true,
  "note": "",
  "subject": "module:opencart/simple",
  "subject_title": "Simple",
  "facts": [
    {
      "id": "claim:opencart/simple--opencart-simple-docs-2cdea105",
      "statement": "Simple supports guest checkout without account registration",
      "subject": "module:opencart/simple",
      "polarity": "assert",
      "applies_to": null,
      "source": "source:opencart/simple-docs",
      "locator": null,
      "confidence": "medium",
      "conflicting": true,
      "conflicts_with": ["claim:opencart/simple--vendor-simple-review-2cdea105"]
    },
    {
      "id": "claim:opencart/simple--opencart-simple-changelog-3b215e37",
      "statement": "Simple 2.4 adds a one-click express checkout option",
      "subject": "module:opencart/simple",
      "polarity": "assert",
      "applies_to": "2.4",
      "source": "source:opencart/simple-changelog",
      "locator": null,
      "confidence": "medium",
      "conflicting": false,
      "conflicts_with": []
    }
  ],
  "sources": [
    {
      "id": "source:opencart/simple-docs",
      "title": "OpenCart Simple documentation",
      "authority": "normative",
      "date": "2026-03-11",
      "dated": true,
      "version": 3,
      "uri": null
    },
    {
      "id": "source:opencart/simple-changelog",
      "title": "OpenCart Simple changelog",
      "authority": "historical",
      "date": "2026-05-02",
      "dated": true,
      "version": 1,
      "uri": null
    }
  ],
  "open_gaps": [
    {
      "id": "knowledge_gap:opencart/simple-knowledge",
      "title": "Needs knowledge: module:opencart/simple",
      "subject": "module:opencart/simple",
      "gap_status": "open",
      "priority": "high",
      "questions": ["What does the licence cost?"]
    }
  ],
  "conflicts": []
}

Illustrative values; the fields are the ones the payload carries.

Loading documents

Ingestion is the one place a model is used. It runs when you load material, never when an agent queries — and it can run against a local endpoint, so documents need not leave the machine.

From the command line
# register a file as a source, then run the pipeline over it
rtfm add-source source:docs/handbook --file handbook.md --title "Handbook"
rtfm ingest source:docs/handbook

# a whole directory, tracked as a corpus: unchanged files never reach the model
rtfm ingest-dir ./docs --namespace handbook

Choosing the extraction model
# Anthropic
export ANTHROPIC_API_KEY=...

# or any OpenAI-compatible endpoint, including a local one
export OPENAI_API_KEY=...
export OPENAI_BASE_URL=http://localhost:11434/v1

# with neither set, extraction is deterministic and no network call is made

Without a key RTFM still runs end to end — the deterministic adapter extracts less, and nothing else changes.

Owner mode and access

Two levels, decided at startup, enforced in SQL rather than in the UI.

Anonymous

Public knowledge only. Private entities are not returned, not counted, and their existence is not disclosed.

Owner

Public and private. In the web UI this is a password login; for MCP it is the --as-owner flag on a server you started yourself.

Writing

Always a separate right. Write tools are not registered without --allow-write, and the core refuses a write from an anonymous context even when they are.

Self-hosting

There is no hosted service. RTFM is a Python package and a directory of files; run it wherever those can live.

Run the web UI
uvicorn asgi:application --port 8000
# the canon root comes from RTFM_ROOT, defaulting to the working directory

The canon is plain files, so backup is whatever you already do with a repository. The SQLite index is derived: if it is lost or corrupted, `rtfm reindex` rebuilds it from the files.