feat(mcp): serve OKF bundles over MCP in two shapes, plus the generic skill
The eval was written RED at `5f1772e` with no server in the tree. This is the
capability it was written against.
`okf mcp --bundle <dir>` serves exactly one bundle, whose tools take no bundle
argument. `okf mcp --root <dir>` (repeatable) serves every bundle under the
roots and knows NONE of them by name. Four tools -- `okf_list`,
`okf_describe`, `okf_ask`, `okf_fetch` -- each carrying its reason in the
description a client actually reads.
Gate today: 1 (7/7) - 2 (83/181) - 3 (4/4) - 4 (9/9) - 5 (3/3) - 6 (6/6),
`GATE RED: rows 2`, exit 1.
THE PROTOCOL IS STDLIB, AND THAT IS THE PACKAGING INVARIANT KEPT RATHER THAN
A TASTE. An MCP SDK would be this package's second runtime dependency on the
DEFAULT install path, for four JSON-RPC methods and a newline framing, and
`test_the_only_runtime_dependency_is_the_security_boundary` pins that list
literally. Chosen hand-written because the surface needed is `initialize`,
`notifications/initialized`, `tools/list` and `tools/call`; `uv.lock` is
untouched.
NOTHING IS CACHED ACROSS CALLS, and row 3 is why. Every call re-walks the
roots and recomputes `bundle_ref`, so a bundle added, removed or rebuilt while
the process runs is seen by the next call with no restart, no configuration
edit and no code change -- 9 of 9 discovery checks over three bundles written
while the server was serving. The cost is paid per call and is published
rather than hidden: 0.75 s for the identity of a 2 756-concept bundle, 5.6 s
for one ask, 4 min 13 s for row 2's full run over four bundles.
CONTAINMENT IS TWO INDEPENDENT CHECKS: the bundle's own index must name the
concept, AND `connectors.safe_resolve` must place it inside the bundle. A
mutant removing either one alone still refuses -- with a DIFFERENT code, which
row 6 asserts by name -- and one removing both is killed. Row 6 declares a
code set per case because its first run had the 10 MB concept refused as
`concept_unknown`: the fixture had not named the file in the index, so the
size ceiling never ran and the row was green for a reason unrelated to the
attack.
`okf card <bundle>` and `okf skill --generic` are the one-to-many skill
candidate. The card is DERIVED on every run and never written into the bundle:
storing it would move the bytes of all six `examples/*/expected-bundle` trees
(23 files compared byte-for-byte) and of the pinned reference bundle, to keep
something recomputable in under a second, and a stored card is one more
artefact that can disagree with what is beside it. Measured here rather than
taken from the order: two per-bundle skills are identical on 280 of 312 and
310 lines; the 62 that differ are identity, concept count, the
conditional-field table, the whole-bundle cost and the breaking point. The
generic skill carries none of them, and `render_generic()` takes no argument,
so there is no bundle it could have read.
Row 2 decomposes into three numbers and the middle one is the finding: 99 of
181 (bundle, anchor) pairs are present in the bundles at all, 83 of those 99
were reached, and 0 of 83 were met by `okf_fetch` on the anchor as a concept
id. The set's anchors and this library's concept ids are different
vocabularies, so every pair met was met through the ranker -- 83 is a FLOOR on
the ceiling, never the ceiling.
13 mutants in a scratch copy, never in the working tree: 12 killed, 1 survived
with its mechanism printed, 0 errors, control green first. Suite 2323 passed,
2 skipped. The architecture choice between the two shapes is the OPERATOR's;
these rows are its input. Report: docs/2026-09-20-mcp-to-varianter.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f1772e832
commit
df5a1183c9
10 changed files with 1823 additions and 59 deletions
75
README.md
75
README.md
|
|
@ -1155,6 +1155,81 @@ the summary back. Install it for your user account after cloning:
|
|||
mkdir -p ~/.claude/skills && cp -R skills/okf-prosjekt ~/.claude/skills/
|
||||
```
|
||||
|
||||
## Serve a bundle over MCP: `okf mcp`
|
||||
|
||||
Two shapes, one implementation, and the difference is what an agent has to be
|
||||
told in advance.
|
||||
|
||||
```sh
|
||||
okf mcp --bundle .okf/my-bundle # one server, one bundle
|
||||
okf mcp --root ~/bundles --root ./.okf # one server, every bundle under the roots
|
||||
```
|
||||
|
||||
`--bundle` serves exactly one bundle, fixed at startup; its tools take no
|
||||
bundle argument, because there is nothing to choose. `--root` (repeatable)
|
||||
serves every bundle found under the given directories and **knows none of them
|
||||
by name**: it discovers them per call, so a bundle you add, remove or rebuild
|
||||
while the server is running is picked up by the next call. No restart, no
|
||||
configuration edit, no code change.
|
||||
|
||||
Four tools, and each one's description says why it exists:
|
||||
|
||||
| tool | what it answers |
|
||||
|---|---|
|
||||
| `okf_list` | which bundles are reachable right now, with each one's content identity and concept count (multi-bundle servers only) |
|
||||
| `okf_describe` | what one bundle is: id, ref, concept count, source documents, and how many concepts carry each conditionally-written field |
|
||||
| `okf_ask` | one question, one bounded payload of excerpts, each with its bundle id, concept id, title and provenance locators. Omitting `bundle_id` on a multi-bundle server asks them all and splits the budget |
|
||||
| `okf_fetch` | one named concept, verbatim, with its frontmatter and locators |
|
||||
|
||||
**Nothing is cached between calls, and that is the design.** Every call
|
||||
re-reads the directories and recomputes the bundle's content identity, so the
|
||||
identity in an answer is a fact about the bytes at the moment of the call
|
||||
rather than at startup — a server that answered from yesterday's bundle is the
|
||||
one failure you cannot see from the outside. The cost is real and is paid per
|
||||
call: on a 2 756-concept bundle the identity is a 0.75 s hash of the whole
|
||||
concept tree, and one `okf_ask` is 5.6 s.
|
||||
|
||||
**Refusals are loud.** A path climbing out of the bundle, a symlink leaving the
|
||||
served root, a bundle id nobody answers to, a directory whose manifest cannot be
|
||||
read, and a concept above the server's size ceiling each come back as an error
|
||||
with a code — never as a plausible-looking empty answer. A concept over the
|
||||
ceiling is refused whole rather than truncated: a truncated concept read as
|
||||
whole is a wrong answer that looks right. A directory that cannot be read as a
|
||||
bundle is **reported** in `okf_list`'s `unreadable`, not skipped.
|
||||
|
||||
The protocol is written with the standard library only. An MCP SDK would be
|
||||
this package's second runtime dependency on the default install path, for four
|
||||
JSON-RPC methods and a newline framing — see
|
||||
[Requirements](#requirements).
|
||||
|
||||
`tools/okf_mcp_gate.py` is the eval: it starts the server as a subprocess,
|
||||
speaks real stdio to it, and measures six rows. It was written red before the
|
||||
server existed, and it is red today on row 2. The measurements, the update
|
||||
drill and the limits are in
|
||||
[`docs/2026-09-20-mcp-to-varianter.md`](docs/2026-09-20-mcp-to-varianter.md).
|
||||
|
||||
### One skill for every bundle: `okf card` and `okf skill --generic`
|
||||
|
||||
`okf skill <bundle>` writes a consumption skill for **that** bundle, with its
|
||||
identity and its numbers measured into the text — which is what makes the file
|
||||
stale the moment the bundle is rebuilt. `okf skill --generic` writes one
|
||||
installable skill for **any** bundle instead:
|
||||
|
||||
```sh
|
||||
okf skill --generic --out ~/.claude/skills/okf-consume-any
|
||||
okf card .okf/my-bundle # the per-bundle numbers, as JSON, on demand
|
||||
```
|
||||
|
||||
The generic skill carries no bundle's id, no ref and no count; it tells its
|
||||
reader to run `okf card <bundle>` first. The card is **derived on every run and
|
||||
never written into the bundle**, so there is no second artefact that can
|
||||
disagree with the bytes beside it.
|
||||
|
||||
Measured on two unrelated bundles: two per-bundle skills are identical on 280
|
||||
of 312 and 310 lines. The 62 lines that differ are exactly identity, concept
|
||||
count, the conditional-field table, the whole-bundle cost and the breaking
|
||||
point — the five things a rebuild invalidates.
|
||||
|
||||
## Implemented scope (v1)
|
||||
|
||||
The library provides three entry points for getting content into an OKF
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue