Class: Pikuri::Mcp::ClientWrapper

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/mcp/client_wrapper.rb

Overview

Wraps one MCP::Client + its transport with retry-and-restart on stdio-subprocess death. On construction it builds a fresh transport and runs the initialize handshake; on a subprocess-died #call_tool it closes the dead transport, respawns, re-handshakes, and retries up to MAX_CALL_ATTEMPTS times, after which the RequestHandlerError propagates (the synthesized tool's closure turns it into an Error: ... observation).

Only subprocess-death messages trigger the restart: once an stdio subprocess dies, MCP::Client::Stdio is irreversibly broken (+@wait_thread.alive?+ stays false, every call raises from ensure_running!), so restarting is the only way back. Other +RequestHandlerError+s (protocol mismatch, JSON parse, ValidationError) leave the transport usable — a restart would just retry the same mistake. Registry::HttpEntry is accepted but no HTTP failure matches those patterns, so the wrapper is a pass-through for HTTP.

Lifecycle: if the handshake fails, #initialize closes its own half-opened transport before re-raising — a caller who saw ClientWrapper.new raise need not close anything (fully-initialized object, or no object at all).

Constant Summary collapse

MAX_CALL_ATTEMPTS =

Maximum number of call_tool attempts including the initial one, before propagating the underlying exception. 3 means: one normal try plus up to two restart-then-retry attempts.

3

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entry) ⇒ ClientWrapper

Returns a new instance of ClientWrapper.

Parameters:

Raises:

  • (StandardError)

    anything raised by the underlying transport's spawn / connect handshake. The half-opened transport is closed before the exception propagates.



63
64
65
66
67
# File 'lib/pikuri/mcp/client_wrapper.rb', line 63

def initialize(entry)
  @entry = entry
  @closed = false
  spawn_fresh!
end

Instance Attribute Details

#clientMCP::Client (readonly)

Returns the current live client. Replaced on each restart, so callers must not cache this across #call_tool invocations. Exposed for one-shot boot-time reads of server_info / tools by Servers, Verifier, Synthesizer, and Pikuri::Mcp::Cache.

Returns:



56
57
58
# File 'lib/pikuri/mcp/client_wrapper.rb', line 56

def client
  @client
end

#entryRegistry::StdioEntry, Registry::HttpEntry (readonly)

Returns the registry entry the wrapper builds (and rebuilds) transports from.

Returns:



49
50
51
# File 'lib/pikuri/mcp/client_wrapper.rb', line 49

def entry
  @entry
end

Instance Method Details

#call_tool(tool:, arguments:) ⇒ Hash

Call an MCP tool. On a SUBPROCESS_DEAD_PATTERNS failure, restart and retry up to MAX_CALL_ATTEMPTS times; other failures propagate on the first attempt (JSON-RPC errors don't reach here — they ride in the response Hash).

Parameters:

  • tool (MCP::Client::Tool)

    from #tools at boot. Only name is used at call time, so a reference captured before a restart keeps working if the new server still exposes that name.

  • arguments (Hash)

    passed verbatim to the underlying call_tool.

Returns:

  • (Hash)

    the JSON-RPC response.

Raises:

  • (MCP::Client::RequestHandlerError)

    when retries exhaust, or on the first non-recoverable failure.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pikuri/mcp/client_wrapper.rb', line 98

def call_tool(tool:, arguments:)
  attempt = 1
  begin
    @client.call_tool(tool: tool, arguments: arguments)
  rescue MCP::Client::RequestHandlerError => e
    raise unless subprocess_dead?(e)
    raise if attempt >= MAX_CALL_ATTEMPTS

    LOGGER.warn(
      "MCP server #{@entry.id.inspect} subprocess died " \
      "(#{e.message.inspect}); restarting and retrying " \
      "(attempt #{attempt + 1}/#{MAX_CALL_ATTEMPTS})."
    )
    restart!
    attempt += 1
    retry
  end
end

#closevoid

This method returns an undefined value.

Close the underlying transport. Idempotent — subsequent calls are no-ops. After close, #call_tool will fail on the dead client.



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/pikuri/mcp/client_wrapper.rb', line 122

def close
  return if @closed

  begin
    @transport&.close
  rescue StandardError => e
    LOGGER.warn(
      "Error closing MCP transport for #{@entry.id.inspect}: " \
      "#{e.class}: #{e.message}"
    )
  end
  @closed = true
end

#server_infoHash?

Server's cached InitializeResult, as exposed by the transport. Delegates to #client.

Returns:

  • (Hash, nil)


73
74
75
# File 'lib/pikuri/mcp/client_wrapper.rb', line 73

def server_info
  @client.server_info
end

#toolsArray<MCP::Client::Tool>

Enumerate the server's tools via MCP::Client#tools. Each call is a real round-trip — callers typically invoke this once at boot and cache the result.

Returns:

  • (Array<MCP::Client::Tool>)


82
83
84
# File 'lib/pikuri/mcp/client_wrapper.rb', line 82

def tools
  @client.tools
end