70 added, 106 removed. Audit A to A.
# AGENTS.md
Maintainer guide for okf-mcp — `okf-mcp` on RubyGems. A Model Context Protocol
server over [Open Knowledge Format](https://github.com/serradura/okf-gem)
bundles: any MCP-capable agent host can discover, orient in, search, and read
- them, over stdio or Streamable HTTP. This file documents how to change the
- code without breaking its contracts.
+ them, over stdio or Streamable HTTP.
This gem is a sibling in the okf-gem monorepo, one directory per gem under
`gems/`, beside the baseline `gems/okf/` it depends on.
- [`../../AGENTS.md`](../../AGENTS.md) is the repo-level
- guide and owns everything above a single gem — the layout, the PR shape, the
- release-title convention, the Git attribution rule. What is here is okf-mcp's
- own: its floor, its dependency limits, the server doctrine, and the transports.
- Where the two overlap, the root is the general rule and this is the instance.
- The doctrine in full — why each tool exists, the bounded-output rules, the
- posture — lives in
- [`../../.okf/capabilities/mcp-server.md`](../../.okf/capabilities/mcp-server.md);
- this file is the working summary, that concept is the argument.
+ [`../../AGENTS.md`](../../AGENTS.md) is the repo-level guide and owns everything
+ above a single gem — the layout, the PR shape, the release-title convention, the
+ Git attribution rule. Where the two overlap, the root is the general rule and
+ this is the instance.
- ## Map
+ ## Read the bundle first
- ```
- lib/okf/mcp.rb the light entry: registry seam + backends, and the lazy
- `OKF::MCP.app` — nothing protocol-shaped loads here
- lib/okf/mcp/
- registry.rb the served set: argv refs (dirs and @slugs) or the kernel
- registry — resolved once at boot, the tools' allowlist
- filters.rb the `dir` vocabulary in one place (it lived in two, and the
- two copies disagreed about the root)
- backend.rb engine detection: memory, or okf-sqlite3 when installed
- memory_backend.rb the always-present folder cache: residency, fingerprints
- server.rb the MCP::Server definition — fourteen tools, two prompts —
- plus the per-request wrap (fingerprint memo, residency prune)
- output_schemas.rb one declared result shape per tool, looked up by name so
- an omission is deliberate (read_concept: markdown, no shape)
- resources.rb bundles + concepts as resources; owns URI parsing, because
- OKF ids carry slashes and the SDK's template matcher stops
- at `[^/]+`
- prompts/ the consuming pair, shipped in-gem and read at get-time
- app.rb the Rack seam: transport construction, in exactly one place
- http.rb the WEBrick bridge: buffered responses, the streamed
- `subscriptions/listen` adapter, and the teardown order
- cli.rb the argv shell: `okf mcp`, `--http`, exit codes
- lib/okf/plugin.rb registers `okf mcp` with the kernel's command registry —
- the gem's only entry point: there is no exe/
- ```
+ **`.okf/` is this gem's structural documentation, and this file no longer
+ restates it.** What the code is, where each responsibility lives, what each tool
+ answers, why the floor is 2.7, and how to add a tool or a test all live there —
+ once, in the concept that owns them:
- `require "okf/mcp"` loads the registry seam and the backends only. The MCP SDK,
- WEBrick and the argv shell load on demand — from `okf/mcp/server`,
- `okf/mcp/http`, `okf/mcp/cli`, or the lazy `OKF::MCP.app` —
- and `test/unit/loading_test.rb` pins it in a clean subprocess: a bare require
- defines neither `::MCP` nor `::WEBrick`.
+ | you want | read |
+ | --- | --- |
+ | the shape of the whole thing | [`.okf/overview.md`](.okf/overview.md) |
+ | what a file under `lib/` does | [`.okf/structure/`](.okf/structure/) — one concept per layer, and it names every file |
+ | whether a capability already exists | [`.okf/capabilities/tools.md`](.okf/capabilities/tools.md) — the fourteen tools, before you write a fifteenth |
+ | why a rule is a rule | [`.okf/design/`](.okf/design/) |
+ | how to test a change | [`.okf/testing/`](.okf/testing/) — the layers, and the walk a new tool owes |
- ## Hard constraints
+ `okf server .okf` from this directory reads it as a graph; `okf search @okf-mcp
+ <term>` searches it from anywhere in the checkout.
- 1. **Ruby >= 2.7** — the `mcp` SDK's floor, inherited, not okf's 2.4 (a
- sibling's floor is its own; the root's 2.4 API list does not bind here, but
- nothing past 2.7 may appear, in `lib/` or `test/`).
- 2. **Runtime dependencies are exactly `mcp` and `okf`**, and both floors are
- guarded by `test/unit/gemspec_test.rb` under one rule: **the floor tracks
- what the suite proves.** The okf floor may lead the kernel checkout but
- never lag it; the mcp pin is pessimistic (`~>`) and fails the suite the day
- the lockfile resolves past it. rack and webrick arrive via okf — never name
- them in the gemspec.
- 3. **No executable.** `okf mcp` through the kernel's plugin seam is the one
- door, and the dispatcher adds nothing but argv and the streams. A second
- binary was deliberately removed before the first release; adding one back
- needs an argument stronger than symmetry.
- 4. **Every tool is a read-only lens.** `readOnlyHint` on all fourteen, a
- `title` on all fourteen (`define_tool` requires both — a capabilities test
- pins them on the wire), `additionalProperties: false` on every input
- schema, and an output schema looked up by name. Domain failures become tool
- errors carrying the kernel's own sentences — never a bare `-32603`. Both
- channels always: the JSON text an older client reads and the same object as
- `structuredContent`.
- 5. **Bounded outputs, honest errors.** Every list answer carries `total`, and
- `total` means one thing on every tool: how many rows the request matched,
- before any `limit` cut them. No silent truncation, ever. Adding a tool is a
- design decision, not a convenience — tool-list weight is a real cost on
- hosts, and `dirs` + `index` + `search` already compose to most retrieval.
- 6. **Kernel-first.** Logic a tool needs lands in the kernel and is read from
- there (`Bundle#tag_groups`, `Bundle#stats`, the cutoff grammar) — this
- shell restates nothing it can call, so the CLI and MCP answers cannot
- drift apart.
+ The **doctrine** — why the tool set is what it is, the bounded-output argument,
+ the posture — is the repository bundle's
+ [`capabilities/mcp-server.md`](../../.okf/capabilities/mcp-server.md), and the
+ bundle here deliberately does not copy it.
- ## Transports
+ The split used to run the other way: this file carried a hand-written Map of
+ `lib/**` and nothing checked it. `test/unit/bundle_catalog_test.rb` now fails
+ when a file under `lib/` is named by no concept, when a concept names a file
+ that is gone, or when the tool catalog and `server.rb` disagree — so the
+ structural layer is pinned where it lives, rather than trusted where nobody
+ looks.
- Stdio is the default; `--http` is the WEBrick bridge in stateless JSON mode;
- `OKF::MCP.app` hands the same definition and transport to any Rack 3 server a
- config.ru names (the reader's server is the reader's dependency — the
- no-rackup position holds).
+ ## The contract
- The bridge's one subtlety is `subscriptions/listen`: the SDK answers it with a
- Rack streaming body whose callable *returns immediately*, while WEBrick ends a
- proc-body response when the proc returns — so the handler thread parks in the
- `Stream` adapter until the SDK ends the stream. Three consequences are
- load-bearing, and `test/integration/http_listen_test.rb` pins each: teardown
- closes the transport **before** WEBrick (`HTTP.stop` — WEBrick's shutdown
- joins its connection threads and hangs on any open stream); the signal trap
- hands teardown to a thread (a mutex in trap context is `ThreadError` on 2.7);
- and listens are capped at 32 on this bridge only — each holds a WEBrick
- thread and connection token, which is not true under a Rack server, where the
- SDK's default stands. A dead peer is noticed by `EPIPE` raising out of a
- keepalive write: that propagation is the SDK's cleanup signal, so the adapter
- must never swallow it.
+ Six rules. Each one's argument is in [`.okf/design/`](.okf/design/); what
+ follows is the short form a reviewer checks against.
- ## Testing
+ 1. **Ruby >= 2.7**, the `mcp` SDK's floor — inherited, not okf's 2.4. A
+ sibling's floor is its own: the root's 2.4 API list does not bind here, and
+ nothing past 2.7 may appear, in `lib/` or `test/`.
+ 2. **Runtime dependencies are exactly `mcp` and `okf`.** rack and webrick
+ arrive via okf — never name them in the gemspec. Both floors track what the
+ suite proves: `test/unit/gemspec_test.rb`.
+ 3. **No executable.** `okf mcp` through the kernel's plugin seam is the one
+ door. Adding a binary back needs an argument stronger than symmetry.
+ 4. **Every tool is a read-only lens.** `readOnlyHint` and a `title` on all
+ fourteen, `additionalProperties: false` on every input schema, an output
+ schema looked up by name. Domain failures become tool errors carrying the
+ kernel's own sentences — never a bare `-32603` — and both channels always.
+ 5. **Bounded outputs, honest errors.** Every list answer carries `total`, and
+ `total` means how many rows the request matched before any `limit` cut them.
+ No silent truncation, ever.
+ 6. **Kernel-first.** Logic a tool needs lands in the kernel and is read from
+ there, so the CLI and MCP answers cannot drift apart.
- Integration-first, the root's rule applied to this gem's surfaces: the tool
- files drive real JSON-RPC frames through `handle_json`; `cli_plugin_test.rb`
- and `http_test.rb` prove the process end-to-end (`okf mcp` spawned as a real
- process, WEBrick on a real socket), while `cli_test.rb` drives the argv shell
- in-process and deliberately spawns nothing — a claim about argv is proven
- once, cheaply, there, and a claim about the process is proven once, in the
- spawning files. The HTTP files share one harness (`test/integration/http_harness.rb`)
- so the three cannot drift in how they compose the bridge. SSE tests read the
- wire with a raw `TCPSocket` — Net::HTTP holds a chunked body until EOF, so a
- stream that stays open would read as a hang — and sequential reads must thread
- `read_until`'s returned buffer back in: one `readpartial` can carry the next
- frame along with the awaited one.
+ A change starts with a failing test, red for the reason you predicted, then the
+ code, then the same test green and unedited — the root's rule, unchanged.
- A change starts with a failing test here, same as the root: red for the
- predicted reason, then the code, then the same test green, unedited.
+ ## Commands
- From `okf-mcp/`:
+ From `gems/okf-mcp/`:
```bash
bin/setup # install dependencies
bundle exec rake # test + rubocop — the default task, what CI runs
bundle exec rake test # just the suite
```
+
+ And from the repository root, for the bundle itself:
+
+ ```bash
+ rake okf # validate + lint every registered bundle, this one included
+ ```
+
+ ## Its own bundle
+
+ `.okf/` ships inside the gem — the reject list does not name it, deliberately —
+ so an installed okf-mcp carries a real bundle, its own, for a host to read
+ through the very tools it serves. `test/unit/packaging_test.rb` pins that it
+ ships; `rake okf` at the repo root keeps it validated and lint-clean.
+
+ Maintain it in the same commit as the code it documents. A new file under
+ `lib/` without a line in the concept that owns its layer is a red suite, not a
+ stale document.