okf-mcp 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a78bd37b6fbbf5ae6740454269f733373560b8dd2e700f97bfaee18703e22c7c
4
- data.tar.gz: f891e4a6918915312f6dd5b059c962ed90f35cb4326596a51613e6b600661b4c
3
+ metadata.gz: 6f19f69227417cc91ee456e6fca9f517ae1d8f4d571bb7423c7d76d4e38df238
4
+ data.tar.gz: dcd1b33fb97cb5eb47bbf01681b2b291d26b035c33d9dc5585422edd3f00e165
5
5
  SHA512:
6
- metadata.gz: 174754a21311bd18764378a70211f0ff4a17c1a141573cf765fcf5ec80d28cef4d6f789c3c2c5b30e756a403af64409f190194c835816a170d43e42f50c293a0
7
- data.tar.gz: 34c4ecb1dc3e665af8b7f93a8b524e0c9d705c785f70596dfb1e138253825f7ee89536e9a2cc8777c963d49d4e023511dbd6a8f3c4d20c33fe1ec3daf5cbcd90
6
+ metadata.gz: 1821ef4c6684ec3ca30d3368f1c4ee07c308f24693278b38866e91f2ad1b12b8f742d45cc42c02d002c5f75bd9cd007e64aec155de439c1f2a47d9c12bedc0f8
7
+ data.tar.gz: 399fdd6c71fad193ce28256b5982d6de87fe547ac818aa4813a72981f8afe8733488f91a420c0232dc58221bcd29dd31f2a8fa318ffc584af521fd8d1d9d0279
data/CHANGELOG.md CHANGED
@@ -1,5 +1,296 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2026-07-13
3
+ All notable changes to this project are documented in this file.
4
4
 
5
- - Initial release
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2026-08-14
9
+
10
+ ### Added
11
+
12
+ - **`tags`, `types` and `stats` — the CLI's remaining read views, as tools.** `tags`
13
+ ships with the `by: "dir" | "type"` curation view — the kernel's
14
+ `Bundle#tag_groups`, extracted so the within-group-beside-total counting
15
+ has one home — and `stats` reads the kernel's new `Bundle#stats`, whose
16
+ `by_dir` keeps the honest zero a directory holding nothing directly
17
+ reports. `files` is deliberately not a tool: `index`'s per-directory
18
+ listing and `catalog`'s projection already carry its whole answer, and
19
+ tool-list weight is a real cost on hosts.
20
+ - **`lint` takes `today`** — the CLI's `--today`, in the same deliberately
21
+ narrow calendar-day grammar (a reinterpretable `20260101` or a
22
+ `2026-02-30` is refused by name), so an `expired` report is reproducible
23
+ and citable. The wall clock stays the default.
24
+ - **`fields`/`except` projection on `search`, and `except` on `catalog`** —
25
+ the row-key vocabulary is checked either way, so a typo is refused by
26
+ name even when a query matches nothing; the pair is mutually exclusive.
27
+ - **`references`, the eleventh tool** — the kernel's §6.3 inventory over MCP,
28
+ and the one lens that sees a bundle's non-markdown files: every
29
+ `references/` entry with the concepts citing it through the §6.2
30
+ path-valued fields, plus every pointer that resolves to nothing (a bare
31
+ `references/…` from a subdirectory is the classic miss; the dangling entry
32
+ names the leading-slash fix). Advisory like every read tool — dangling
33
+ pointers are data, never a tool error.
34
+ - **`graph`'s traffic view takes `cut`** — the CLI's `--cut N`, so a client
35
+ can widen past the fitted threshold or narrow below it. Outside
36
+ `view: "traffic"` it is refused: a half-honored argument is the
37
+ silent-wrong-answer shape.
38
+
39
+ ### Changed
40
+
41
+ - **`search` narrows by `status` and `trust`**, the two filters `catalog` and
42
+ `okf search` already took. An agent that learned the vocabulary from either
43
+ one and brought it here got an invalid-argument error. They resolve through
44
+ the catalog rather than the engine's rows — a search row carries what the
45
+ engine matched on, and the §5 families are not among it.
46
+
47
+ - **`lint`'s `stale_after` takes the kernel's cutoff grammar** — a
48
+ `YYYY-MM-DD` date or a full timestamp — rather than everything
49
+ `Date.iso8601` parses. `20260101` and `2026-W01-1` are now tool errors
50
+ naming the accepted shapes; a `generated.at`-shaped timestamp is accepted,
51
+ as before, and reduced to its date. They were accepted before
52
+ and silently reinterpreted — `okf lint --stale-after 20260101` exits 2, so
53
+ the shell and its own CLI answered differently about one bundle's staleness.
54
+
55
+ - **The okf floor must move to okf's v0.2 release number in the release that
56
+ ships this** — the shell now reads v0.2 surfaces (the trust catalog column,
57
+ `Concept.effective_status`/`fold_tier`, the linter's clock contract), none
58
+ of which exist in published okf 1.13.0; the gemspec carries the same
59
+ RELEASE OBLIGATION note, and the floor cannot move sooner because the
60
+ monorepo resolves against the path-sourced okf still versioned 1.13.0.
61
+ `rake release` now refuses while the floor still admits 1.13.0 (the guard
62
+ hangs off `release:guard_clean`, ahead of both pushes, and deliberately not
63
+ off `build` — `rake install` depends on that), so the obligation stops a
64
+ release rather than riding inside one.
65
+ - The catalog tool speaks v0.2: `CATALOG_FIELDS` matches the new row
66
+ (`timestamp` is gone — the vocabulary refuses it by name), `status` narrows
67
+ on the effective value through the kernel's own fold, and a `trust` filter
68
+ lands beside it. `lint` supplies the clock the `expired` check needs, the
69
+ way the CLI does, and the client-visible descriptions and prompts cite
70
+ v0.2's chapter numbers.
71
+
72
+ ### Fixed
73
+
74
+ - **`search`'s declared output schema now carries `skipped`** — the field the
75
+ tool emits when `"*"` forgives a vanished bundle. A host typing its
76
+ structured-content handling off `outputSchema` dropped it, and the SDK's
77
+ result validation (on in the test suite) failed a payload the tool
78
+ genuinely produces; slugs, not rows, and never required.
79
+ - **An out-of-range `--port` or unresolvable `--bind` is a usage error.** The
80
+ socket layer raises `SocketError` (Socket::ResolutionError on newer
81
+ Rubies), which the boot rescue did not name, so a typo came back as a
82
+ backtrace and exit 1 instead of the one readable line and exit 2 every
83
+ other boot failure earns.
84
+
85
+
86
+ ## [1.0.0] - 2026-08-07
87
+
88
+ The first functional release: an MCP server over the okf kernel, judged by
89
+ the kernel's own contracts. Nothing functional precedes it — `0.0.0` below is
90
+ the empty name reservation, and a `0.1.0` section stood here for a version
91
+ that was cut in this file and never published, so its entries have folded in
92
+ rather than pretending to be changes somebody could have seen.
93
+
94
+ ### Added
95
+
96
+ - **`okf mcp`** — the entry point, and the only one. The gem ships no
97
+ executable: it registers a verb with the kernel CLI through the
98
+ `okf/plugin.rb` seam, so installing it is the whole installation and the
99
+ server appears in `okf help` under *installed extensions*. The MCP SDK loads
100
+ only when the verb runs, so `okf help` never pays for it.
101
+ - **Ten read-only tools** mapped straight onto the kernel's library API:
102
+ `list_bundles`, `dirs`, `index`, `search`, `read_concept`, `catalog`, `log`,
103
+ `validate`, `lint`, `graph`. Every list output is bounded with a visible
104
+ `total`, and the bound is on the thing the `total` counts — `log` returns the
105
+ newest three date-grouped entries per file and names how many each holds,
106
+ after the pre-release eval found it answering "what changed recently" with a
107
+ whole 119,863-byte history under a `total` that was counting *files*. Every
108
+ annotation is honest (`readOnlyHint` on all ten, and the handshake declares
109
+ only capabilities that answer); domain failures surface as tool errors
110
+ carrying the kernel's own sentences.
111
+ - **Structured output**: nine of the ten declare an `outputSchema` and emit
112
+ `structuredContent` beside the JSON text, so a host consumes a result instead
113
+ of parsing a blob and guessing. `read_concept` returns markdown, which has no
114
+ object shape to declare.
115
+ - **Bundle identity delegated to the kernel registry**: every `bundle` argument
116
+ is a registry slug — the identity `@slug` resolves at the CLI and
117
+ `/b/<slug>/` mounts on the hub. One argument name on all ten tools, including
118
+ `search`, which is the one that accepts a *set* and briefly said so by
119
+ spelling itself `bundles`: the eval found that the plural's only signal
120
+ arrived after a failed call, because an unknown property is refused by the
121
+ schema before any okf sentence can be written. The type already says it takes
122
+ an array, and the kernel spells the identity slot the same way for every verb. Argv roots are the allowlist (refs and plain
123
+ dirs mix, registered slugs reserved before basenames are deduped); no argv
124
+ serves the active registry, project-local discovery and `OKF_NO_DISCOVERY`
125
+ included. The allowlist is closed at boot as a property of the object rather
126
+ than a promise: argv mode does not retain the kernel registry, so no tool
127
+ argument and no resource URI can widen the served set. Groups fan out for
128
+ `search` and are refused by every single-bundle tool; `"*"` tolerates a
129
+ vanished directory (`skipped` named in the payload) while naming one demands
130
+ it.
131
+ - **The residency layer**: one parsed bundle per root, re-read only when the
132
+ on-disk fingerprint (mtime *and* size) moves, so bodies are always live and
133
+ canonical. Index queries hold one shared corpus per *searched* set — the
134
+ bundles a query names, so federated BM25 scores are comparable by
135
+ construction — built on first use, bounded by an LRU of four, and dropped
136
+ when any member changes. **The identity map
137
+ follows the same rule**: served by the registry, the set of bundles tracks
138
+ the registry file rather than a boot snapshot, so a `set`/`rename`/`del` in
139
+ another terminal lands on the next tool call — and the `resources/list` set
140
+ moves with it, since a resource list derived once at boot drifts out of step
141
+ with the served set it is supposed to describe. A file that cannot be read or
142
+ parsed keeps the last good set and retries, so a server with a working set is
143
+ never taken down by a file it does not own. Argv mode does not follow
144
+ anything — it never carried the registry, so its set cannot widen.
145
+ Two consequences the re-read carries with it, both handled at the request
146
+ seam rather than in each tool: the residency is **pruned to the served
147
+ set** whenever the set moves — the corpus cache with it, since it pins the
148
+ same parsed bundles plus a prepared index over each and its own LRU evicts
149
+ only on an index query, which a scan-only workload never sends — because a
150
+ movable set was the only thing left bounding a cache that never evicted.
151
+ Pruning on *every* request instead took the residency and corpus locks each
152
+ time, queuing an unrelated `list_bundles` — or an initialize handshake —
153
+ behind whatever corpus build another host's index query was holding them
154
+ for; an unchanged set has nothing to prune and takes no locks at all. And
155
+ the fingerprint is **computed once per root per request**, because freshness is
156
+ a question between requests and asking it three times inside one cost three
157
+ full-tree walks under the residency lock (measured: `list_bundles`, `catalog`
158
+ and `search` each went from two walks to one, `search` on the index engine from
159
+ three).
160
+ - **The engine doctrine, the CLI's exactly**: raw-text scan by default,
161
+ `engine: "index"` opt-in for BM25+/page parity, `fuzzy` implying the index,
162
+ `regexp` staying on the scan, incompatible pairs refused with the fix named.
163
+ The payload names the engine that **answered**, resolved rather than echoed —
164
+ `fuzzy` switches engines without being asked, and a caller cannot infer which
165
+ ran, so a miss under the index's tokenizer was indistinguishable from a fact
166
+ the bundle does not carry.
167
+ - **Resources**: every bundle with a root `index.md` at `okf://<slug>`, and
168
+ every concept under the template `okf://{bundle}/{id}` — the affordance no
169
+ tool call has, since a host can attach a document to the context without the
170
+ model deciding to fetch it. Read live through the same residency layer
171
+ `read_concept` uses, and closed by the same allowlist: a URI is not a path.
172
+ Concepts are deliberately not enumerated in `resources/list`, which would
173
+ mean reading every bundle at boot and freezing a list the fingerprint check
174
+ exists to keep honest. The bundle list is recomputed per `resources/list` for
175
+ the same reason — one `stat` each, no bundle read — so *listed implies
176
+ readable* survives the registry moving.
177
+ - **Completions** for the template's `bundle` and `id`, so it is browsable
178
+ rather than a shape you have to know. An unserved bundle, an unknown argument
179
+ and a missing context all complete to nothing — a completion cannot be used
180
+ to probe what argv did not serve.
181
+ - **Two prompts, the consuming pair**: `okf-search` — retrieval as progressive
182
+ disclosure (map, then search, then only the winning bodies, with the engine
183
+ doctrine per query shape) — and `okf-consume` — a bundle as working context
184
+ without reading it whole. Both are this gem's own text, written against the
185
+ tools rather than the CLI. An earlier cut served all eight of the skill's
186
+ playbooks verbatim from the installed kernel, on the argument that a prompt
187
+ is instructions rather than a capability; what that argument missed is
188
+ *whose* instructions they were. Every playbook speaks in `okf …` invocations
189
+ and half dead-end a CLI-less host at "install the CLI first", and five teach
190
+ authoring — a mission every tool here refuses. The consuming doctrine is
191
+ restated in tool vocabulary and shipped in this gem, which is also the cost
192
+ accepted: a doctrine change in the skill must be carried here by hand.
193
+ - **Two transports over one server definition**: stdio (default) and `--http` —
194
+ Streamable HTTP in stateless JSON mode on the WEBrick the kernel already
195
+ ships. A non-loopback bind works (a wildcard `--bind 0.0.0.0` admits this
196
+ machine's own addresses and hostname; `--allow-host` adds a name only a proxy
197
+ or DNS knows) **and warns at boot in plain words**: every served bundle
198
+ becomes readable, without authentication, by anything that can reach the
199
+ port. The Host allowlist those flags feed is a DNS-rebinding defence, not
200
+ access control — a client that is not a browser sets `Host` to whatever it
201
+ likes — and the docs no longer imply otherwise. The bridge caps a request
202
+ body at 4 MiB before allocating it.
203
+ - **Every failure reads as one actionable line.** A boot that cannot bind —
204
+ `okf mcp --http` on a port already serving, which is the likeliest mistake on
205
+ the flag that exists so one warm process is shared — printed an eleven-frame
206
+ backtrace and exited 1 while every other boot failure exited 2 with a sentence;
207
+ `SystemCallError` is rescued with the rest now — around *boot* only: past
208
+ it, the likeliest errno is the host closing its pipes, a stdio session's
209
+ normal end, and routing that through the boot rescue printed the usage
210
+ banner and exited 2, misfiling a shutdown as an operator mistake for any
211
+ supervisor keyed on the exit status. A stdio host hanging up mid-serve exits
212
+ 0 — exactly the two hang-up errnos, on stdio alone; any other mid-serve
213
+ errno propagates as the crash it is rather than borrowing the usage
214
+ banner's exit 2. The split is structural — the HTTP bind happens in boot,
215
+ so EADDRINUSE stays a sentence and exit 2 — and diagnostics are best-effort
216
+ throughout: a closed stderr loses the boot line, never the outcome, where
217
+ it used to re-raise EPIPE out of the boot rescue's own print on stdio and
218
+ to file a lost `--http` boot line as a clean exit 0 for a server that never
219
+ started accepting.
220
+ - **The `okf-sqlite3` seam**: `Backend.detect` soft-requires the engine and
221
+ duck types it; a missing gem, an unloadable native extension and an engine
222
+ that will not construct all degrade to memory, silently except in the boot
223
+ line and `list_bundles.backend`.
224
+ - **The `dir` vocabulary in one place**, since two copies of a rule are two
225
+ answers waiting to disagree: `"."` and `"/"` both name the bundle root, a
226
+ trailing slash is ignored, matching folds case, and a blank value is "no
227
+ filter" rather than a third spelling of the root — a client that fills every
228
+ declared optional property with `""` is doing something routine and must not
229
+ be read as asking for the root. Not the CLI's other spelling either: `--dir
230
+ root` exists there because a shell needs no quoting for it, and importing that
231
+ into a JSON argument would cost any bundle with a real `root/` directory the
232
+ ability to name it.
233
+ - **A `dir` that names no directory is refused by every tool that takes one.**
234
+ `dirs` and `index` have always said so; `catalog` and `search` answered
235
+ `total: 0` and no error, which is the same empty-answer-that-reads-real this
236
+ gem refuses everywhere else. It was worst for the spelling the CLI and the
237
+ bundled skill both teach: an agent asked `catalog` for `root`, was told zero,
238
+ and reported that the bundle root holds no concepts. Across bundles the
239
+ refusal is a fact about the *searched set* — a directory one of three bundles
240
+ has still filters and does not refuse. The set consulted is
241
+ `Bundle#directories` — the same list the `dirs` rows are built from, so the
242
+ refusal and the tool its message points at ("orient with dirs") can never
243
+ disagree; a first cut derived it from the raw file list and accepted a
244
+ directory holding only a file the reader skipped, which `dirs` refuses to
245
+ list. The message carries the one nuance the source cannot: a directory
246
+ standing on disk but holding only unparseable files is refused as exactly
247
+ that — "holds only files the reader could not parse", pointing at validate
248
+ — because "no directory" would be false about the filesystem and sends the
249
+ caller off to re-spell a name that was correct.
250
+ - **`total` means one thing on every tool**: how many rows the request matched,
251
+ before any `limit`. `dirs` and `index` reported the whole bundle's directory
252
+ count so a narrowing stayed visible — defensible alone, wrong as a set, and
253
+ against this gem's own "no silent truncation" promise it read as rows withheld
254
+ by a tool that takes no limit at all.
255
+ - **Every log answer is held to a byte budget `limit` scales.** §7 fixes no
256
+ heading level, so `###` date groups are conformant and the entry split
257
+ cannot see them: the file came back entire under `total: 0, returned: 0` —
258
+ the one unbounded read on this surface surviving the change that was meant
259
+ to close it, and reporting itself as empty. The first fix counted it as one
260
+ indivisible entry cut by size, and left the same read alive one shape over:
261
+ a whole history under a single `## ` heading split into one "entry" and came
262
+ back whole behind a `total: 1` that read as bounded. The budget caps every
263
+ answer now, announced with `truncated: true` — and a scaffolded title with
264
+ no entries yet reports the zero it holds, where counting it as one entry
265
+ told an agent there is history where there is none. Held to its word twice
266
+ over since: the budget is enforced in **bytes** as announced (a character
267
+ count let a multibyte log through at up to 4x the cap, `truncated` silent),
268
+ `returned` is recounted from what survived the cut rather than claiming
269
+ entries whose very headings it removed, and the title's zero holds wherever
270
+ whitespace put the title.
271
+
272
+ ### Fixed
273
+
274
+ - **Only the root path speaks MCP; every path beside it answers 404.** The SDK
275
+ transport routes on method alone, so a bridge that handed it every path
276
+ answered the OAuth-discovery probes a host sends first (`GET /.well-known/*`,
277
+ `POST /register`) with a 405 and a 200-wrapped JSON-RPC parse error — a broken
278
+ sign-in service instead of an absent one, and the host refused the connector
279
+ on it. 404 is the answer a discovering client reads as "no auth here".
280
+
281
+ ### Security
282
+
283
+ - **A symlinked `index.md` can no longer export an arbitrary file.** The kernel's
284
+ read guards (okf 1.13.0) protect every concept, but the root-index resource
285
+ (`okf://<slug>`) read `index.md` directly, outside them — so a bundle whose
286
+ `index.md` was a symlink out of the root served the target verbatim, and listed
287
+ the URI besides. That read is now realpath-checked against the root, and a
288
+ bundle with an escaping index is not listed at all.
289
+
290
+ ## [0.0.0] - 2026-07-14
291
+
292
+ The name reservation on RubyGems: an empty gem, no functionality.
293
+
294
+ [1.1.0]: https://github.com/serradura/okf-gem/compare/okf-mcp/v1.0.0...okf-mcp/v1.1.0
295
+ [1.0.0]: https://github.com/serradura/okf-gem/releases/tag/okf-mcp/v1.0.0
296
+ [0.0.0]: https://rubygems.org/gems/okf-mcp/versions/0.0.0
data/README.md CHANGED
@@ -1,12 +1,191 @@
1
- # OKF::MCP
1
+ # okf-mcp
2
2
 
3
- A Model Context Protocol server for [OKF](https://github.com/serradura/okf-gem)
4
- it lets any MCP-capable agent host discover, search, and read Open Knowledge
5
- Format bundles through the `okf` kernel.
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ [OKF](https://okfgem.com) bundles: any MCP-capable agent host Claude Desktop,
5
+ Claude Code, anything speaking the protocol — can discover, orient in, search,
6
+ and read the Open Knowledge Format bundles on a machine.
6
7
 
7
- > **Status:** placeholder release (`0.0.0`) reserving the gem name. The MCP tool
8
- > surface lands in a later version.
8
+ It is a thin shell over the [`okf`](https://rubygems.org/gems/okf) kernel, the
9
+ fourth surface beside its CLI, HTTP server and Ruby library: every tool maps
10
+ onto the kernel's library API, bundles are named by the same registry slugs
11
+ `@slug` resolves at the CLI, and nothing is reimplemented here.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ gem install okf-mcp
17
+ ```
18
+
19
+ Ruby >= 2.7 — the official `mcp` gem's own floor, inherited rather than chosen.
20
+
21
+ Installing the gem is the whole installation: the
22
+ [`okf`](https://rubygems.org/gems/okf) kernel arrives as a dependency, and the
23
+ gem registers an `okf mcp` verb with its CLI through the plugin seam, so
24
+ the server appears in `okf help` under *installed extensions*. There is no
25
+ second binary — this gem ships no executable of its own.
26
+
27
+ ## Run
28
+
29
+ ```bash
30
+ okf mcp <bundle-dir> [<bundle-dir>…] # serve exactly these directories
31
+ okf mcp @handbook ./scratch # registry refs and plain dirs mix
32
+ okf mcp # no args: serve the registered bundles (okf registry set <dir> adds one)
33
+ okf mcp --http # one warm HTTP process instead of stdio-per-host
34
+ ```
35
+
36
+ Stdio is the default — each host spawns its own process, zero config. `--http`
37
+ (`--bind`, default 127.0.0.1; `--port`, default 9134) serves Streamable HTTP in
38
+ stateless JSON mode for every agent at once. The boot line goes to stderr and
39
+ names the backend, the served bundles, and the registry file the slugs came
40
+ from.
41
+
42
+ `--bind` beyond loopback works — a wildcard `0.0.0.0` admits this machine's own
43
+ addresses and hostname, and `--allow-host HOST` (repeatable) adds a name only a
44
+ proxy or DNS knows — and it publishes your bundles. **There is no
45
+ authentication.** Anything that can reach the port can read every served
46
+ bundle, so the boot line says so.
47
+
48
+ The Host allowlist those flags feed is a defence against **DNS rebinding**: a
49
+ browser walked into this port by a page you never meant to give it to. It is
50
+ not access control, and it cannot be — a client that is not a browser sets
51
+ `Host` to whatever it likes. Treat a non-loopback bind the way you would treat
52
+ serving your notes directory over HTTP, because that is what it is.
53
+
54
+ Whatever argv names is the whole served set. A registry ref is resolved once, at
55
+ boot; no tool argument can widen the set afterwards, so a group slug reaches
56
+ bundles only when the registry itself is what is being served.
57
+
58
+ With no arguments the registry *is* what is served, so it is followed rather
59
+ than snapshotted: `okf registry set`, `rename` or `del` in another terminal
60
+ shows up on the next tool call, without a restart. The file is re-read only
61
+ when its fingerprint moves, so the cost is a `stat`. Bundle contents are never
62
+ snapshotted either — bodies are read live, and a bundle is re-parsed whenever
63
+ one of its files changes.
64
+
65
+ ## Host configuration
66
+
67
+ **Run `which okf` and paste the absolute path.** A desktop app is launched by
68
+ the window manager, not by your shell, so it never runs the rc file that puts
69
+ a version manager's Ruby on `PATH` — and if your Ruby came from mise, rbenv,
70
+ asdf or chruby, a bare `"okf"` is unresolvable there. The binstub's shebang
71
+ pins its own Ruby, so the absolute path needs nothing else set up.
72
+
73
+ Claude Desktop (`claude_desktop_config.json`):
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "okf": { "command": "/absolute/path/from/which/okf", "args": ["mcp"] }
79
+ }
80
+ }
81
+ ```
82
+
83
+ Claude Code inherits your shell, so the bare name is fine there:
84
+
85
+ ```bash
86
+ claude mcp add okf -- okf mcp
87
+ ```
88
+
89
+ Both serve whatever `okf registry` lists; add directories or `@slug`s to `args`
90
+ to pin the set instead.
91
+
92
+ ## Tools
93
+
94
+ Fourteen read-only tools. Every list answer is bounded and names the full
95
+ count it was cut from — the rows the request matched, before any `limit`:
96
+
97
+ | Tool | What it answers |
98
+ |---|---|
99
+ | `list_bundles` | what exists: slug, title, root, concept count, type/tag rollups, the default, the groups, the backend |
100
+ | `dirs` | the shape — one row per directory with direct and subtree counts; **the first move** |
101
+ | `index` | the index map: authored index bodies, rollups, listings, one directory at a time (depth 1 by default) |
102
+ | `search` | pointed questions: ANDed terms, scored rows carrying the fields they matched, across several bundles at once; narrows by `type`, `dir`, `tag`, `status`, `trust`; `fields`/`except` project the rows |
103
+ | `read_concept` | one concept's file, verbatim and live from disk; ids are exact |
104
+ | `catalog` | per-concept metadata with link degrees; the same five filters, paging, `fields`/`except` projection |
105
+ | `log` | every `log.md`, root first, live — the newest 3 dated entries per file, each answer held to a byte budget `limit` scales; a cut says `truncated` |
106
+ | `validate` | the spec §11 conformance verdict |
107
+ | `lint` | the curation-quality report; `group: "folder"` lists the unlinked files by folder; `today` pins the clock for a reproducible `expired` report |
108
+ | `graph` | the knowledge graph in three bounded views: minimal, hubs, traffic (`cut` widens or narrows the arcs) — never with bodies |
109
+ | `references` | the §6.3 inventory: every `references/` file — `.py` attesters and `.sql` computations included — with its citing concepts, and the pointers that resolve to nothing |
110
+ | `tags` | the tag index by count; `by: "dir"` or `"type"` regroups for vocabulary curation, within-group counts beside cross-bundle totals |
111
+ | `types` | the type index by count — how you learn what a producer meant by its open §4.1 vocabulary |
112
+ | `stats` | the sizing rollup: concepts, dirs, cross-links, distinct tags, and the by_type/by_dir/by_top_dir distributions |
113
+
114
+ Each returns its JSON twice: as text, and as `structuredContent` against a
115
+ declared `outputSchema`, so a host consumes a result without parsing a blob and
116
+ guessing at its shape. `read_concept` is the exception — markdown has no object
117
+ shape to declare.
118
+
119
+ The `status` and `trust` filters answer with the §5 families' vocabulary
120
+ (`draft | stable | deprecated`; `unverified | machine-confirmed |
121
+ human-reviewed`, either spelling). On `search` they narrow through the
122
+ catalog rather than the engine's rows — a search row carries what the engine
123
+ matched on, and the §5 families are not among it — so a `trust` that narrows
124
+ `catalog` narrows `search` identically.
125
+
126
+ ## Prompts
127
+
128
+ Two prompts, the consuming pair: `okf-search` — retrieval as progressive
129
+ disclosure (map, then search, then only the winning bodies), with the engine
130
+ doctrine per query shape — and `okf-consume` — using a bundle as working
131
+ context without reading it whole. Both are this gem's own text, written
132
+ against the tools above rather than the CLI, so they work on a host with no
133
+ shell and no filesystem.
134
+
135
+ The okf skill's playbooks are deliberately not served. They speak in `okf …`
136
+ invocations, and the authoring ones — produce, migrate, maintain, refine,
137
+ curate — carry a mission every tool here refuses: this server makes a client
138
+ an expert *consumer* of bundles. Authoring stays with the skill
139
+ (`okf skill <dest>`), installed where a filesystem and the CLI actually are.
140
+
141
+ ## Resources
142
+
143
+ Every bundle with a root `index.md` is a resource at `okf://<slug>`, and every
144
+ concept is one under the template `okf://{bundle}/{id}` — so a host can *attach*
145
+ a document to the context directly, instead of the model having to decide to
146
+ fetch it. Both are read live from disk, and `bundle` and `id` complete, so the
147
+ template is browsable rather than a shape you have to know.
148
+
149
+ Concepts are not enumerated in `resources/list`: that would mean reading every
150
+ bundle at boot — exactly the eager work this server avoids, since bundles are
151
+ parsed on first use and re-read only when their files change — and it would
152
+ freeze a list that live reading is meant to keep honest.
153
+
154
+ ## Engines
155
+
156
+ Search matches raw text by default — exact, no tokenizer, milliseconds over an
157
+ already-parsed bundle. `engine: "index"` opts into BM25+ ranking on a held corpus,
158
+ the same engine and version the `okf server` page runs, so the two rank
159
+ identically. `fuzzy` implies the index and its tokenizer; `regexp` stays on the
160
+ raw-text scan; incompatible pairs are refused with the fix named.
161
+
162
+ Every result names the engine that **answered** it, because `fuzzy` picks one
163
+ without being asked and no score tells you which ran. It matters when something
164
+ is missing: under the index's tokenizer a shattered identifier and an absent
165
+ fact look identical, so knowing you were on the index is what turns "not here"
166
+ back into "try the scan". The doctrine runs the other way too — the scan is
167
+ exact, so a typo or a stemmed form misses on it, and that miss is what
168
+ `engine: "index"` and `fuzzy` are for.
169
+
170
+ ## Beside a browser
171
+
172
+ The MCP surface is for agents; humans get the same bundles as an interactive
173
+ graph:
174
+
175
+ ```bash
176
+ okf server # the registered bundles, one hub, http://127.0.0.1:8808
177
+ ```
178
+
179
+ Run it beside the MCP config and both read the same live files.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ bin/setup # install dependencies
185
+ bundle exec rake # tests + RuboCop — what CI runs
186
+ bundle exec rake test:integration # the critical layer alone + coverage/integration/
187
+ ```
9
188
 
10
189
  ## License
11
190
 
12
- Apache-2.0. See [LICENSE.txt](LICENSE.txt) and [NOTICE](NOTICE).
191
+ Apache-2.0, see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OKF
4
+ module MCP
5
+ # The seam between the shell and the optional okf-sqlite3 engine. One
6
+ # `require` answers both "is it installed?" and "does the native extension
7
+ # load?" — a broken sqlite3 build degrades to the memory backend instead
8
+ # of crashing the server, silently except in the boot line and
9
+ # list_bundles.backend. Feature detection, not version pinning: the engine
10
+ # only has to answer the duck type below, so the gems release
11
+ # independently. Backends speak symbol-keyed rows in the kernel's catalog
12
+ # and search shapes; `score` and `snippet` are optional extras.
13
+ module Backend
14
+ ENGINE_METHODS = %i[refresh search catalog capabilities].freeze
15
+
16
+ def self.detect
17
+ engine = build_engine
18
+ suitable?(engine) ? engine : MemoryBackend.new
19
+ rescue LoadError, StandardError
20
+ MemoryBackend.new
21
+ end
22
+
23
+ # Both ways the optional engine can fail are on this side of the seam, so
24
+ # the one rescue above covers both: the `require` (absent gem, native
25
+ # extension that will not load) and the construction (a connection it
26
+ # cannot open, a schema check, a constant renamed out from under us).
27
+ # Catching only LoadError left the second half fatal, which is exactly
28
+ # what "degrades instead of crashing the server" promises it is not.
29
+ def self.build_engine
30
+ require "okf/sqlite3"
31
+ OKF::Sqlite3::Backend.new
32
+ end
33
+
34
+ def self.suitable?(engine)
35
+ ENGINE_METHODS.all? { |name| engine.respond_to?(name) }
36
+ end
37
+ end
38
+ end
39
+ end