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_tags count 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 total counting files made that read as bounded. Three is what "recently" means when the entries are date-grouped; limit asks 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 — an okf log verb is the obvious one — the split moves to the kernel and both read it from there.

/^(?=## )/.freeze
LOG_BUDGET =

Bytes per limit unit — the size cap every log answer is cut to, announced with truncated: 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 fields selects 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 resources declaration while resources/list answered [], plus a logging one nothing ever emits through. That is the same class of dishonesty the readOnlyHint annotations are careful to avoid, so the hash below names exactly what is served and nothing else.

No listChanged on any of them and no subscribe: 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. Declaring listChanged is 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

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

.promptsObject



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