Class: Pikuri::Mcp::Extension

Inherits:
Object
  • Object
show all
Includes:
Agent::Extension
Defined in:
lib/pikuri/mcp/extension.rb

Overview

An Agent::Extension that wires MCP (Model Context Protocol) support onto an agent: builds an Servers runtime from a Registry, appends the <available_mcps> block to the system prompt, registers an on_close handler that tears down the live MCP clients, and (in bind) installs a per-agent mcp_connect tool so the LLM can pull MCP-exposed tools into its toolset on demand.

Configure / bind split

configure runs once on the parent’s Configurator and creates the shared Servers runtime — that’s the resource extensions own. bind fires per agent (parent + each sub-agent in Step 4’s world) and creates a fresh Connect tool keyed to whichever agent is being bound, registered via Agent#internal_add_tool so it lands only on that agent’s RubyLLM::Chat (not in pikuri’s @tools list — sub-agents therefore don’t inherit the parent’s connect tool through the snapshot, and each agent’s activation Set stays per-agent).

Usage

registry = Pikuri::Mcp::Registry.new(entries: [
  Pikuri::Mcp::Registry::StdioEntry.new(id: 'gmail', command: %w[gmail-mcp])
])
Pikuri::Agent.new(transport: ..., system_prompt: ...) do |c|
  c.add_extension Pikuri::Mcp::Extension.new(registry: registry)
end

Empty registry

When the registry is Registry#empty?, the extension is a no-op — no Servers, no snippet, no tool, no on_close. Same semantics as the legacy mcp_registry: kwarg on Agent#initialize, which routes through this extension as a transition layer.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry: Registry::EMPTY, synthesize_descriptions: true, verify_mcp_servers: true) ⇒ Extension

Returns a new instance of Extension.

Parameters:

  • registry (Mcp::Registry) (defaults to: Registry::EMPTY)

    configured MCP servers. Defaults to Registry::EMPTY — extension is a no-op then.

  • synthesize_descriptions (Boolean) (defaults to: true)

    threads a Synthesizer into Servers so servers without an instructions / serverInfo.title field get an LLM-synthesized description for the <available_mcps> block. On by default — see the synthesize_descriptions: kwarg on Agent#initialize for the full rationale.

  • verify_mcp_servers (Boolean) (defaults to: true)

    threads a Verifier into Servers so every server’s handshake + tool surface is checked for prompt-injection patterns before tools can register. On by default — see the verify_mcp_servers: kwarg on Agent#initialize.



57
58
59
60
61
62
63
64
# File 'lib/pikuri/mcp/extension.rb', line 57

def initialize(registry: Registry::EMPTY,
               synthesize_descriptions: true,
               verify_mcp_servers: true)
  @registry = registry
  @synthesize_descriptions = synthesize_descriptions
  @verify_mcp_servers = verify_mcp_servers
  @servers = nil
end

Instance Attribute Details

#serversMcp::Servers? (readonly)

Returns the runtime built in configure, or nil when the registry was empty (extension is a no-op).

Returns:

  • (Mcp::Servers, nil)

    the runtime built in configure, or nil when the registry was empty (extension is a no-op).



69
70
71
# File 'lib/pikuri/mcp/extension.rb', line 69

def servers
  @servers
end

Instance Method Details

#bind(agent) ⇒ void

This method returns an undefined value.

Register a per-agent mcp_connect tool on the agent’s chat. The tool’s execute closure captures the agent reference so activations register their tools on the correct chat — see IDEAS.md §“Two invariants worth recording” for the static-vs-dynamic tool boundary.

Parameters:

  • agent (Pikuri::Agent)


103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/pikuri/mcp/extension.rb', line 103

def bind(agent)
  return if @servers.nil? || @servers.empty?

  if agent.tools.any?(Mcp::Servers::Connect)
    raise 'Mcp::Servers::Connect cannot be passed in tools: when an MCP runtime is wired; ' \
          'Agent auto-registers it.'
  end

  connect = @servers.build_mcp_connect_tool(agent)
  agent.internal_add_tool(connect.to_ruby_llm_tool)
  nil
end

#configure(c) ⇒ void

This method returns an undefined value.

Build the shared Servers runtime, append the <available_mcps> block to the system prompt, and register the close handler. The Synthesizer / Verifier thinker closure captures the Configurator’s transport + cancellable so the LLM-driven boot passes run against the same model the agent itself uses and honor the same cancel flag.

Parameters:

  • c (Pikuri::Agent::Configurator)


80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/pikuri/mcp/extension.rb', line 80

def configure(c)
  return if @registry.empty?

  if @synthesize_descriptions || @verify_mcp_servers
    thinker = build_thinker(c.transport, c.cancellable)
  end
  synthesizer = build_synthesizer(thinker, c.transport.model) if @synthesize_descriptions
  verifier    = build_verifier(thinker, c.transport.model)    if @verify_mcp_servers

  @servers = Mcp::Servers.new(@registry, synthesizer: synthesizer, verifier: verifier)
  c.append_system_prompt(@servers.system_prompt_snippet.lstrip) unless @servers.empty?
  c.on_close { @servers.close }
  nil
end