Lang on Rock
Guides

MCP server

Six tools for Claude Code, Cursor, or anything else that speaks MCP.

langonrock mcp "okf://$PWD/data?tenant=acme"

The MCP server speaks stdio on top of a Connection, so it works against an embedded path, a local daemon or a remote server without knowing which.

Registering it with Claude Code

claude mcp add langonrock -- langonrock mcp "okf:///abs/path/to/data?tenant=acme"

Any connection string works, so point it at a running daemon instead and every agent invocation shares one process with warm indexes rather than paying cold start:

claude mcp add langonrock -- langonrock mcp "okf+unix:///tmp/okf.sock?tenant=acme"

The six tools

ToolInputOutput
manifestbundle?The manifest as TSV
searchquery, k?, bundle?Ranked manifest rows plus a pos cell, never bodies
getids[], section?, offset?, limit?, find?Framed slices, one @@ id block each
snapshotnoneThe current digest
writebundle, path, content, replaces?The new digest, plus that file's compiler warnings
deletebundle, path, replaces?The new digest

k is capped at 50 by the schema. get requires at least one id, and reports anything it could not find as a trailing @@ missing block rather than failing the call.

get returns at most 15,000 characters per concept by default. A partial slice frames itself as @@ id [start..end of total], so the model continues with offset or raises limit when it truly needs more at once. find takes a literal case-insensitive phrase and answers with a window around the first occurrence plus the offset of every one — the cheap way to answer "where does the text say this" without paying for the document.

When there is no phrase to quote, search itself points the way: every direct hit ends in a pos cell, the character offset where the query's words cluster densest in that concept's body, - when they only match its manifest row. The follow-up is get with {offset: pos, limit: 2000} — a located passage instead of a capped document, for descriptive questions find cannot anchor.

Six verbs, and no more, because every tool definition costs tokens in the client's system prompt. The four read tools measure 1,122, write adds 413 and delete 206, so a session that never touches knowledge still pays 1,741. manifest and search narrow, get fetches, snapshot invalidates, write persists, delete retracts. Nothing else earned a place.

The manifest description carries the tenant's own advice

At startup the server measures the manifest and appends one sentence to the manifest tool's description: prefer search when the manifest costs more than about twenty times one search result, read it whole when it does not. The crossover comes from the benchmarks, the arithmetic comes from the manifest itself, and the sentence is deterministic — same snapshot, same advice. The advice is read once per client session, so a tenant that changes shape under a running server keeps the old sentence until the next session; corpus shape moves slowly enough for that to be fine.

The manifest is exposed twice

As a tool for every client, and as an MCP resource at okf://manifest for clients that preload resources into context.

The resource path is the one that delivers the central claim of the design. A preloaded manifest lands in the cacheable prompt prefix, where it costs roughly a tenth as much on every later turn. A manifest arriving mid-conversation as a tool result does not.

okf://manifest    mimeType: text/tab-separated-values

Tool descriptions state when to call

Not just what the tool does. manifest says "call this before anything else whenever you need to know what knowledge exists" and "never guess an id". get says "pass every id you need in one call rather than calling repeatedly; the batch costs one round trip regardless of size".

A description that only describes gets under-triggered. That is a real effect and worth the extra tokens in the schema.

The manifest description also explains the status column, so an agent knows that a cell other than - means the concept is deprecated, draft, or stale — the reader demotes a concept past its stale_after date at read time — and that it should say so if it uses one anyway.

The session an agent should have

  1. Read the manifest, ideally from the cached prefix rather than by calling the tool.
  2. Pick ids from the rows it already holds.
  3. Call get once with all of them — with section when it only needs the schema, with find when it only needs a quoted passage, with offset: pos from a search hit when the passage was described rather than quoted.

Two round trips, and the second one is batched. On a large tenant, replace step 1 with search, or with manifest narrowed to a bundle — the manifest tool's own description says which.

Writing goes through the source, never the snapshot

write and delete change the source Markdown and then recompile, which is the only shape that survives the watcher. A tool that wrote a snapshot directly would have it silently replaced by the next filesystem event, at most 30 seconds later; the write would look like it succeeded and then vanish. Writing the file the watcher already reads keeps one writer and one source of truth instead of two that race, and it is the same path the source API takes.

The precondition, for a caller with no hash

Replacing a concept needs the hash of the version being replaced, or two editors silently lose each other's work. A model has no hash. Fetching one first would cost a round trip on every write, and a separate read-the-hash tool would cost a whole definition in every session's prompt, so the refusal carries what the retry needs:

write {bundle: "inbox", path: "idea.md", content: "..."}
  → concept already exists: re-read the concept and retry with its new hash
    retry with replaces: "968f33390cab..."

write {bundle: "inbox", path: "idea.md", content: "...", replaces: "968f33390cab..."}
  → wrote inbox/idea.md (hash 4c1e...), snapshot 19e8c4026a96, 1 concepts

Creating costs one call, because omitting replaces is what asserts the concept is new. Replacing costs two the first time. Neither can silently overwrite.

delete works the same way without the create case, so its hash is not optional and a deletion is always two calls. Aimed at a concept that is not there, it answers concept does not exist and offers no hash, rather than inviting a retry that cannot succeed.

A tenant is created by the first write

Asking an agent to persist something for a tenant that does not exist yet is an ordinary first request. write creates the directory, registers it in sources.json and starts watching it, with no restart and no setup. Only the tenant the connection was already scoped to is ever created, so this reaches nothing the caller could not already write.

A tenant with knowledge is never bootstrapped over

A tenant that already has snapshots but no sources.json entry is refused instead. That is what langonrock sync on the command line leaves behind, and pointing it at a fresh empty directory would replace its whole manifest with a single file. Register the real directory before writing to it.

The recompile happens before the tool answers, so the concept is visible to manifest, search and get on the very next call. The response carries the new digest, the concept count, and the compiler's warnings for the file just written — a missing type, a link resolving to nothing — so an agent learns its frontmatter was wrong at the moment it wrote it.

Ids are the shortest unambiguous form of their path, so creating a file can rename a concept nobody touched. The tool description says so, and an agent should re-read the manifest after writing rather than reuse ids it saw before.

Debugging it

The stdio transport owns stdout. Anything else written there corrupts the JSON-RPC framing, so all diagnostics go to stderr:

langonrock mcp serving okf:///abs/path/to/data?tenant=acme over stdio

A tool that throws returns its message as an error result rather than killing the connection, so a bad tenant or a missing store shows up in the client as a readable failure.

On this page