Module: OKF::MCP::Server
- Defined in:
- lib/okf/mcp/server.rb
Overview
Builds the MCP::Server definition shared by every transport: ten read-only tools mapped straight onto the kernel's library API, plus the two consuming prompts. The surface is deliberately minimal — agents do better with few, well-described tools, and dirs + index + search compose to most retrieval — so adding a tool here is a design decision, not a convenience.
Every list output is bounded with a visible total; no silent
truncation, ever. total means one thing on every tool: how many rows
the request matched, before any limit cut them. dirs and index used to
report the whole bundle's directory count instead, so a narrowing stayed
visible — defensible alone, wrong as a set, and against the promise above
it read as rows withheld by a tool that takes no limit at all.
The descriptions carry the skill's doctrine, because they are the only playbook a Desktop host ever sees.
Defined Under Namespace
Classes: Context, Definition
Constant Summary collapse
- ROLLUP_LIMIT =
Cap on the distinct type/tag values list_bundles rolls up per bundle; the remainder is reported as an
other_types/other_tagscount so a huge bundle cannot flood a context window with a long-tail histogram. 25- SEARCH_LIMIT =
20- CATALOG_LIMIT =
200- LOG_LIMIT =
Date-grouped entries returned per log.md. A log is the one file in a bundle that only ever grows and never gets curated, so its size tracks the project's age rather than the question asked — this repo's own answered "what changed recently" with 119,863 bytes across fourteen dates, and
totalcounting files made that read as bounded. Three is what "recently" means when the entries are date-grouped;limitasks for more. 3- LOG_ENTRY =
A §7 log is "a flat list of date-grouped entries, newest first", so a
##heading at column 0 is the entry boundary. Split kept here rather than pushed into the kernel because bounding for a context window is this surface's problem alone: the graph page's Log panel wants the whole file and scrolls it. If a second consumer ever needs the entries — anokf logverb is the obvious one — the split moves to the kernel and both read it from there. /^(?=## )/.freeze
- LOG_BUDGET =
Bytes per
limitunit — the size cap every log answer is cut to, announced withtruncated: true(see #sized_log). Sized off the measured shape rather than guessed: this repo's own three-entry answer is 13,491 bytes, so one entry is budgeted at roughly what one date group costs. The cap exists because counting entries alone bounds nothing when one entry carries the whole file — a log under a single##heading, or under headings the split cannot see. 4_500- CATALOG_FIELDS =
The catalog row, the projection vocabulary
fieldsselects from. %w[id title type description tags timestamp status backlog_ref dir top_dir links_out links_in].freeze
- GRAPH_VIEWS =
%w[minimal hubs traffic].freeze
- LINT_GROUPS =
%w[check folder].freeze
- CAPABILITIES =
Declared explicitly, because the SDK's default announces every capability it knows how to route — and this server used to inherit a
resourcesdeclaration whileresources/listanswered[], plus aloggingone nothing ever emits through. That is the same class of dishonesty thereadOnlyHintannotations are careful to avoid, so the hash below names exactly what is served and nothing else.No
listChangedon any of them and nosubscribe: nothing here sends a notification, and claiming otherwise would invite a host to wait for one. The tool and prompt lists are genuinely fixed for a process's life; the resource list is not — it tracks the registry (see Definition), so a host that re-lists sees the current set, and one that caches the boot listing is stale until it asks again. DeclaringlistChangedis what would fix that, and it stays undeclared until something actually notifies. { tools: {}, prompts: {}, resources: {}, completions: {} }.freeze
- INSTRUCTIONS =
<<~TEXT Tools take a `bundle` argument: a slug from list_bundles — the same name `@slug` resolves at the okf CLI. The retrieval discipline: orient with dirs (the shape), descend with index (a directory's map), answer pointed questions with search (omit `bundle` to search every bundle), and read only the winning concepts with read_concept — never slurp a bundle whole. Check log for recent history. validate and lint report a bundle's health: you may flag what they find, but fixing it belongs to the okf skill and CLI — the tools here never write. Bodies are read live from disk, so results always reflect the current files. TEXT
- PROMPTS =
The consuming prompts, and only those — this gem's own text, written against the tools above rather than the CLI. The skill's playbooks were served here verbatim once, all eight, on the argument that a prompt is instructions rather than a capability; what that argument missed is whose instructions they were. Every playbook speaks in
okf …invocations and half dead-end a CLI-less host at "install the CLI first", so to the host this surface exists for they taught a vocabulary it cannot use and a mission (authoring) its tools refuse. This server makes a client an expert consumer of bundles; producing, migrating, maintaining, refining and curating stay with the skill, which is installed where a filesystem and the CLI actually are.Owning the text is the cost: these two restate the skill's retrieval doctrine in tool vocabulary, so a doctrine change there must be carried here by hand. The pair keeps SKILL.md's Commands-table order (search before consume).
{ "okf-search" => [ "search", "find the concepts that answer a pointed question, without paying for the whole bundle" ], "okf-consume" => [ "consume", "answer questions from an OKF bundle without reading it whole" ] }.freeze
Class Method Summary collapse
-
.build(registry, engine: Backend.detect, configuration: nil) ⇒ Object
configuration:is the seam the output-schema suite needs: it turns on the SDK's result validation so a schema that has drifted from its payload fails a test. -
.prompt_text(name) ⇒ Object
This gem's own prompts/ tree, read at get-time so booting never pays for it.
- .prompts ⇒ Object
- .tools_for(context) ⇒ Object
Class Method Details
.build(registry, engine: Backend.detect, configuration: nil) ⇒ Object
configuration: is the seam the output-schema suite needs: it turns
on the SDK's result validation so a schema that has drifted from its
payload fails a test. Left nil in production deliberately — a schema
bug should not turn a working tool into a runtime error.
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/okf/mcp/server.rb', line 195 def build(registry, engine: Backend.detect, configuration: nil) memory = engine.is_a?(MemoryBackend) ? engine : MemoryBackend.new context = Context.new(registry, engine, memory) server = Definition.new( registry: registry, context: context, name: "okf", title: "OKF knowledge bundles", version: VERSION, instructions: INSTRUCTIONS, capabilities: CAPABILITIES, configuration: configuration, tools: tools_for(context), prompts: prompts, resources: [], resource_templates: Resources.templates ) # Replaces the SDK's URI matching wholesale, because an OKF id below # the root carries slashes and the template matcher binds a variable # to `[^/]+`. Resources owns the parsing; the template is signage. server.resources_read_handler { |params| Resources.read(context, params[:uri]) } server.completion_handler { |params| { completion: { values: Resources.complete(context, params) } } } server end |
.prompt_text(name) ⇒ Object
This gem's own prompts/ tree, read at get-time so booting never pays for it. No missing-file rescue like the tools carry: the old path read from the installed kernel's skill tree, where version skew could orphan a name, but a file shipped in this gem beside this file can only be absent when the package itself is broken.
273 274 275 |
# File 'lib/okf/mcp/server.rb', line 273 def prompt_text(name) ::File.read(::File.join(__dir__, "prompts", "#{name}.md"), encoding: "UTF-8") end |
.prompts ⇒ Object
256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/okf/mcp/server.rb', line 256 def prompts PROMPTS.map do |name, (file, description)| # `**` absorbs the server_context: the SDK's template passes. ::MCP::Prompt.define(name: name, description: description) do |_args, **| ::MCP::Prompt::Result.new( description: description, messages: [ ::MCP::Prompt::Message.new(role: "user", content: ::MCP::Content::Text.new(Server.prompt_text(file))) ] ) end end end |
.tools_for(context) ⇒ Object
220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/okf/mcp/server.rb', line 220 def tools_for(context) [ list_bundles_tool(context), dirs_tool(context), index_tool(context), search_tool(context), read_concept_tool(context), catalog_tool(context), log_tool(context), validate_tool(context), lint_tool(context), graph_tool(context) ] end |