MCP tools
knomit is MCP-native. A peer connects over the Model Context Protocol and gets a small, sharp tool set whose descriptions carry the guidance a model needs to call them correctly — the descriptions are written as prompt engineering for the calling agent, not as human reference, so no prompt scaffolding is needed to use the tools well.
Knowing when to reach for them is a separate problem, and one the tool
descriptions cannot solve: a tool description is only read once the model has
already decided to look at that tool. That is what
Claude Code’s skills and CLAUDE.md block add.
The tools
Section titled “The tools”| Tool | What it does |
|---|---|
knomit_learn | Write one or more facts to the knowledge base in a single commit. Deduplicates within a category automatically. |
knomit_query | Search by free text, entity, domain, path, or confidence threshold. sort=recent browses by recency; results are paginated. |
knomit_explain | Traverse a fact’s versioned provenance graph via its refs (paginated BFS), anchored at a commit or at HEAD. |
knomit_update | Revise an existing fact’s fields. |
knomit_retract | Remove a fact. Git history retains the provenance. |
knomit_review | Interactive session to review and maintain the knowledge base (prune, distill, reflect). |
knomit_hypothesize | Generate falsifiable hypotheses from synthesis facts. |
knomit_repos | List the repos (mounts) behind this endpoint: name, stable repo id, branch, and role. |
Read-only mode
Section titled “Read-only mode”Each tool is registered with a write flag, and a read-only server drops every
tool carrying it. Three tools survive: knomit_query, knomit_explain, and
knomit_repos. The other five — knomit_learn, knomit_update,
knomit_retract, knomit_review, knomit_hypothesize — are never added to
the server, so they do not appear in tools/list at all rather than failing at
call time. knomit_review and knomit_hypothesize count as writes because
both commit facts.
Read-only is set by KNOMIT_READ_ONLY (or read_only in knomit.toml) and is
startup-only — it is read once when the server is constructed and cannot be
toggled at runtime. The MCP dispatch routes are exempt from the HTTP method
gate, because MCP reads arrive as POSTs; tool filtering is what enforces
read-only-ness here. See Deployment for the operator view.
The endpoint
Section titled “The endpoint”The MCP endpoint is branch-scoped — each agent reads and writes its own branch:
POST /api/v1/repos/{repo}/branches/{branch}/mcpThe agent branch is logged on startup (branch=agent/hostname-abc123) and is
also available via GET /api/v1/repos/{repo} (the agent_branch field). Branch
names use : in place of / in URL path segments
(e.g. agent:hostname-abc123).
A lens — one write repo federated with N read mounts — has its own endpoint and no branch segment, because each mount resolves its own branch:
POST /api/v1/lenses/{lens}/mcpA ?profile= query parameter on either endpoint is deprecated: the value is
logged at debug level and discarded.
Session instructions
Section titled “Session instructions”There is a single MCP server instance, shared across every repo and lens;
each tool handler resolves the target binding from the request context. The
instructions sent to the calling model are computed per session from that
binding via an AfterInitialize hook, so the model is told the exact topics and
categories the repo accepts. The topic list it receives is the repo’s
ontology, each topic
carried by its description — which makes those descriptions prompt text rather
than documentation.
For a lens binding, the instructions also carry a lens addendum: the mounts
table, the kb:// path convention, and read coverage per mount. A single-repo
binding emits no addendum, so those sessions are byte-identical to before
lenses existed.
The stdio bridge
Section titled “The stdio bridge”stdio-only MCP clients (Claude Code, Claude Desktop, Claude Cowork, VS Code,
Cursor, …) use knomit-bridge, the stdio↔HTTP adapter built by make build. It
auto-discovers the agent branch from the running server, so you never look it up
by hand:
{ "mcpServers": { "knomit": { "command": "dist/knomit-bridge", "args": ["--repo", "core"] } }}--lens <name> connects to a lens endpoint instead; it is mutually exclusive
with --repo and skips branch discovery, since each mount resolves its own
branch server-side.
See the Quick start for the full connection walkthrough and the streamable-HTTP alternative, and the CLI reference for every bridge flag.
Tool reference
Section titled “Tool reference”knomit_learn — write facts
Section titled “knomit_learn — write facts”Write one or more facts in a single git commit.
| Param | Type | Req | Default | Notes |
|---|---|---|---|---|
moment_name | string | ✓ | Short label for this learning moment (becomes part of the commit) | |
facts | array | ✓ | One or more fact objects (below) |
Each fact object:
| Field | Type | Req | Default | Notes |
|---|---|---|---|---|
topic | string | ✓* | Top-level ontology topic. A closed set — an undeclared topic is rejected | |
category | string | ✓* | Category path within the topic (e.g. languages/go/concurrency). Not a closed set — declared children guide placement, but anything below a topic is accepted | |
path | string | ✓* | Private state only. An explicit repo-relative path under .knomit/<area>/, used instead of topic/category — see private state | |
title | string | ✓ | Short descriptive title | |
body | string | ✓ | Fact body, natural language | |
kind | string | epistemic | epistemic (descriptive) or pragmatic (prescriptive) | |
type | string | observation | Leaf type — depends on kind (see below). A JSON-schema enum, so a typo’d type is rejected at the protocol layer | |
origin | string | authored · distilled · discovered. Omit for a fact you write yourself — authored is the default for every type except synthesis, where omitting origin means distilled, so set it explicitly on a synthesis fact you wrote. Immutable after the write | ||
domain | string[] | Cross-cutting domain tags | ||
entities | string[] | Entities the fact mentions | ||
confidence | number | 0.7 | 0.0–1.0 | |
sources | integer | 1 | Independent sources | |
refs | string[] | External URLs or source refs |
* Either topic and category, or path — never both. Supplying path
alongside either is an error, not a precedence rule.
Types — epistemic: observation, concept, process, principle,
pattern, reference, synthesis, insight, hypothesis, methodology.
Pragmatic: policy, heuristic. A single batch may not mix observed types
with inferred types (hypothesis / methodology). The kind, type, and
origin vocabularies are generated from one source, so the learn schema, the
update schema, and the session instructions cannot disagree.
Schema conformance is not the only gate. If the repo’s ontology declares
validation rules, each written fact — and
each merged fact after a subsumption — is evaluated against them, and the first
rule to fail rejects the write with that rule’s message. knomit_update runs
the same check on the reassembled fact.
knomit_query — search
Section titled “knomit_query — search”Returns lightweight rows (title, type, domain, score, ~400-char snippet) — not
full bodies. Paginated via an opaque cursor. At least one of text,
entities, domain, applies_to, path, type, origin, or
min_confidence is required (unless paging with cursor, or using
sort=recent).
| Param | Type | Notes |
|---|---|---|
text | string | Full-text / semantic query |
entities | string[] | All must be present (AND) |
domain | string[] | Domain tags |
applies_to | string[] | Ancestor-or-equal domain match (scope includes these) |
domain_exact | bool | Exact canonical tag match only (default false) |
path | string | Path prefix |
type | string[] | Filter to epistemic types |
origin | string[] | authored · distilled · discovered |
min_confidence | number | 0–1 |
min_similarity | number | Cosine floor for text search; 0 = model’s calibrated recall floor |
sort | string | relevance (default) or recent (browse, needs no other filter) |
limit | number | Default 20, max 100 (snippet mode); 3/5 with include_body |
max_results | number | Maximum total results materialised across all pages (snapshot depth). Default 500; higher values are clamped to 500. Page size is limit, not this. A value ≤ 0 is an error |
include_body | bool | Full bodies (small pages only) |
cursor | string | Page token; freezes filters |
knomit_explain — walk provenance
Section titled “knomit_explain — walk provenance”Explains a fact by walking its versioned provenance graph, anchored at a
commit. The root fact is returned in full (with revision history); every
referenced fact is returned as a lean summary read at the exact version the
referrer pointed to. Summaries may be flagged deleted (retracted since) or
superseded (newer at HEAD).
| Param | Type | Req | Notes |
|---|---|---|---|
file | string | ✓ | Fact path — bare (kb/technology/go/abc123.md) for the write repo, or qualified (kb://<id12>/kb/…) for a fact on a read mount |
commit | string | Anchor commit; omit for HEAD. Drill into a summary by re-calling with its path + commit | |
cursor | string | Session id to page the walk |
knomit_explain never fans out across mounts: the fact’s own path fixes the
mount, and the whole provenance walk stays inside it. A kb:// ref is
classified as External, not a local fact edge — it carries no evidence
weight and creates no local provenance edge.
knomit_update — revise a fact in place
Section titled “knomit_update — revise a fact in place”| Param | Type | Req | Notes |
|---|---|---|---|
file | string | ✓ | Fact path |
moment_name | string | ✓ | Label for the update |
updates | object | ✓ | Only the fields to change |
updates fields: title, body, kind, type, confidence, sources,
domain (replaces), entities (replaces), refs (replaces).
type carries the same enum as knomit_learn, but the update schema
deliberately declares no defaults — an omitted field means “leave it alone”, not
“reset it”. origin is not updatable.
refs replaces the entire list — it is not appended to. Every ref you leave
out of the array is dropped from the fact. To add or refresh one ref, read the
fact’s current refs first and resend the full merged list. Omit refs entirely
to leave the existing refs untouched. domain and entities behave the same
way.
knomit_retract — remove a fact
Section titled “knomit_retract — remove a fact”| Param | Type | Req | Notes |
|---|---|---|---|
file | string | ✓ | Fact path to retract |
moment_name | string | ✓ | Label for the retraction |
Retraction is a commit, not a hard delete — the fact remains in history and
resolves under commit-anchored reads (e.g. ?fallback=before).
Both knomit_update and knomit_retract reject a kb://<read-mount-id>/…
path: read-only mount: repo X is not this binding’s write repo — facts there
can only be changed through their own endpoint.
Every write says where it landed
Section titled “Every write says where it landed”The write destination is a property of the connection — fixed when the
bridge starts, resolved from the request, and deliberately not expressible as a
tool argument. (A repo: argument would relocate the choice into model output,
where a model invents a plausible value rather than asking.) The cost of that
design is that a fact written to an unintended repo used to return success
identical to one written to the intended repo.
So knomit_learn, knomit_update and knomit_retract each stamp the
destination onto their result:
{ "commits": [{"file": "kb/gotchas/x/1a2b3c4d.md", "hash": "abc1234"}], "written_to": { "repo": "knomit-kb", "repo_id": "3ec012f5b4d2", "branch": "agent/main", "lens": "knomit-dev" }, "summary": "wrote 2 facts to repo \"knomit-kb\" (3ec012f5b4d2) on branch \"agent/main\" — the write repo of lens \"knomit-dev\""}repois always the write repo, never the lens: a lens reads a union but writes to exactly one member, so naming the lens would misdescribe where the bytes are.lensis reported alongside as the reason that repo was chosen, and is omitted on a repo-bound binding rather than carried as an empty field.repo_idis the 12-hex root-commit id — the same identifier that appears inkb://<id>/…paths and theknomit_reposmount table. It is what makes the stamp discriminating: a repo name is per-machine, mutable, and unique only among one server’s active repos, so two connected knomit servers each holding a repo calledknomitonagent/mainwould otherwise render byte-identically and the stamp would detect nothing. It is omitted when the store is unavailable and the identity is genuinely unknown.- The prose
summaryis not redundant with the struct — a caller reacts to prose, and the point of stamping is that a wrong destination gets noticed in the same turn it happens. The id rides in the sentence for the same reason it is in the struct.
knomit_update and knomit_retract carry the same two fields alongside their
own file and commit.
Private state
Section titled “Private state”Facts are knowledge. A periodic job’s bookkeeping is machinery, and putting it
in kb/ means it gets indexed, embedded, ranked in knomit_query, browsed in
the UI, exported by OKF and swept by synthesis.
knomit_learn therefore accepts an explicit path instead of
topic/category, and knomit_update, knomit_retract and the equivalent
PUT/DELETE /facts/{path} accept the same paths:
Agent-writable =
.knomit/<area>/…— at least one subdirectory deep, with<area>a dotless directory name, minus a small reserved list.
- It is an allow-list rooted in ownership, not a deny-list of foreign dot
directories. A deny-list fails open: the moment a KB repo grows
.vscode/, knomit would be authorized to write another tool’s territory. <area>is any name the caller picks — nothing in knomit knows what your areas are called, so a new category of private state needs no release.- Loose files at the root of
.knomit/are knomit’s own config (the ontology) and stay refused.<area>must be dotless so that a server-owned file’s name cannot be reused as a directory. - A writable private path is still private: excluded from discovery at every walker. That is the whole point — job state stays invisible to readers while remaining writable by its job.
- Ontology validation rules are skipped for these writes, root rules included. Private state has no ontology placement, and without the exemption a root rule would let a job create its slot and then refuse every update to it.
knomit_review — maintain the KB
Section titled “knomit_review — maintain the KB”Prune redundant facts, distill clusters into synthesis facts, and reflect on
hypothesis transitions to record methodology. Does not generate new
hypotheses. A session loop: call with no args to start, then with session_id +
response (your JSON decisions for the previous work item) to continue.
See Synthesis & hypotheses and Emergent discovery.
| Param | Type | Notes |
|---|---|---|
session_id | string | Omit to start a new session |
response | string | JSON decisions for the previous work item |
page | number | Fetch another page of the current item without answering it. Omit when submitting a response; paging advances nothing, so pages may be re-fetched freely |
completion_token | string | Echo back the token from the final page of a multi-page item. Required to answer one — see below. Single-page items carry no token |
item_id | number | Echo back item.id from the work item you are answering, so the server can reject a stale answer. Optional and additive — omitting it keeps the old “answer whatever is current” behaviour. It matters because applying a distill item enqueues RAPTOR follow-up items, so the current item can change between render and answer |
effort | string | normal (default) · medium · high — medium/high engage the bridge discovery engine |
domain | string[] | Scope filter on the seed pool (empty = whole corpus) |
entities | string[] | Scope filter on the seed pool |
knomit_hypothesize — generate predictions
Section titled “knomit_hypothesize — generate predictions”Generate new hypothesis facts from synthesis facts on the agent branch.
Distinct from review and only to be invoked on explicit user request (never
as an auto-follow-up). Per work item the model decides whether to write a
hypothesis; skipping is the expected outcome for most.
| Param | Type | Notes |
|---|---|---|
session_id | string | Omit to start |
response | string | Acknowledgement for the previous work item |
item_id | number | Echo back item.id from the work item you are answering, so the server can reject a stale answer. Optional and additive |
effort | string | normal (default) · medium · high — medium/high engage the backward (keystone) bridge engine |
domain | string[] | Scope filter (empty = whole corpus) |
entities | string[] | Scope filter |
knomit_repos — list the mounts behind this endpoint
Section titled “knomit_repos — list the mounts behind this endpoint”Read-only and parameterless. Returns the binding and one row per mount:
{ "binding": "core", "mounts": [ {"name": "core", "id": "a1b2c3d4e5f6", "branch": "main", "role": "read+write", "write_branch": "agent/hostname-abc123"}, {"name": "vendor-kb", "id": "0f9e8d7c6b5a", "branch": "main", "role": "read"} ]}| Field | Notes |
|---|---|
name | Repo name |
id | 12-hex prefix of the repo’s root commit — the same id that appears in kb://<id>/… paths |
branch | The branch this mount is read at |
role | read+write on the write mount when writes are permitted; read otherwise |
source | Source slug, when the mount has one |
write_branch | Present only on the read+write row — where writes actually commit, which may differ from branch |
This is how a session discovers its source slug and its repo ids; it replaces
the bridge’s removed --source flag.