Module: Kernai::MCP

Defined in:
lib/kernai/mcp.rb

Overview

Optional MCP (Model Context Protocol) adapter.

Kernai’s core stays fully protocol-agnostic. This file is loaded only when the user explicitly ‘require ’kernai/mcp’‘ — at which point we pull in the `mcp_client` gem (soft dependency) and register a handler for the `:mcp` block type via Kernai::Protocol.

The agent then speaks MCP’s native verbs directly:

<block type="mcp">{"method":"tools/list","params":{"server":"fs"}}</block>

There is no Kernai-invented wrapping around MCP primitives: method names come from the MCP spec, plus one Kernai extension (‘servers/list`) that exposes the multiplexing layer the adapter adds on top.

Defined Under Namespace

Classes: Adapter, ConfigError, DependencyMissingError

Constant Summary collapse

MCP_DOCUMENTATION =
<<~DOC
  MCP (Model Context Protocol) external access.

  ## Two vocabularies — do not confuse them

  There are exactly TWO kinds of names you will encounter, and they
  live in different layers:

  1. PROTOCOL METHODS — a fixed, closed set defined by the MCP spec
     (plus one Kernai extension). These go in the "method" field of
     your request. You cannot invent new ones.

  2. TOOL NAMES — dynamic, server-specific identifiers like
     "read_file", "list_directory", "search_files", etc. Each MCP
     server publishes its own catalog. Tool names are NOT protocol
     methods. You CANNOT use a tool name in the "method" field.

  To invoke a tool you always go through the protocol method
  `tools/call`, passing the tool name in `params.name`. There is no
  other entry point for invoking tools. Ever.

  ## Request shape

  Emit a <block type="mcp"> whose content is a JSON request:
    {"method":"<PROTOCOL_METHOD>","params":{...}}

  ## The complete, closed set of PROTOCOL METHODS

    servers/list                                        [Kernai ext] list configured MCP servers
    tools/list     {"server":"..."}                     list tools on a server (omit server to list all)
    tools/describe {"server":"...","name":"..."}        full JSON schema for one tool
    tools/call     {"server":"...","name":"...","arguments":{...}}
    resources/list {"server":"..."}                     list resources (server optional)
    resources/read {"server":"...","uri":"..."}         read a resource by URI
    prompts/list   {"server":"..."}                     list prompts (server optional)
    prompts/get    {"server":"...","name":"...","arguments":{...}}

  If a "method" value is not in this list, the request is rejected —
  including every tool name you see in tools/list output.

  ## Worked example: calling a tool named "list_directory"

  WRONG (tool name treated as a protocol method — rejected):
    <block type="mcp">{"method":"list_directory","params":{"path":"/tmp"}}</block>

  RIGHT (protocol method tools/call, tool name in params.name):
    <block type="mcp">{"method":"tools/call","params":{"server":"filesystem","name":"list_directory","arguments":{"path":"/tmp"}}}</block>

  The same pattern applies to every tool you discover, regardless of
  the server.

  ## Responses and errors

  Responses come back in <block type="result" name="mcp">.
  Errors come back in <block type="error" name="mcp">.

  ## Typical exploratory flow

  1. servers/list    — discover what's available
  2. tools/list      — see the tools on a specific server
  3. tools/describe  — inspect a tool's input schema before calling it
  4. tools/call      — execute the tool (this is the ONLY way to invoke it)

  ## Protocols vs skills

  Protocols are distinct from skills: skills are local Ruby callables
  invoked via <block type="command" name="...">. Protocols are external
  systems invoked via their own block type (here: <block type="mcp">).
DOC

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.adapterObject (readonly)

Returns the value of attribute adapter.



509
510
511
# File 'lib/kernai/mcp.rb', line 509

def adapter
  @adapter
end

Class Method Details

.load(config_path) ⇒ Object

Load servers from a YAML config file. The file must contain a top-level ‘servers:` key; each child is a server definition.



479
480
481
482
483
484
485
486
487
488
# File 'lib/kernai/mcp.rb', line 479

def load(config_path)
  raw = File.read(config_path)
  expanded = expand_env(raw)
  config = YAML.safe_load(expanded, aliases: true, permitted_classes: [])
  unless config.is_a?(Hash) && config['servers'].is_a?(Hash) && config['servers'].any?
    raise ConfigError, "MCP config must declare at least one server under 'servers:'"
  end

  setup(config)
end

.setup(config, client: nil) ⇒ Object

Register the MCP protocol handler from an already-parsed config hash. Useful from tests or programmatic setups. Pass ‘client:` to inject a pre-built client instance (skips the upstream gem wiring).



493
494
495
496
497
498
499
500
501
# File 'lib/kernai/mcp.rb', line 493

def setup(config, client: nil)
  shutdown if @adapter
  @adapter = Adapter.new(config, client: client)
  Kernai::Protocol.register(:mcp, documentation: MCP_DOCUMENTATION) do |block, ctx|
    @adapter.handle(block, ctx)
  end
  register_shutdown
  @adapter
end

.shutdownObject



503
504
505
506
507
# File 'lib/kernai/mcp.rb', line 503

def shutdown
  @adapter&.shutdown
  Kernai::Protocol.unregister(:mcp)
  @adapter = nil
end