Class: Phronomy::Tool::McpTool::StdioTransport

Inherits:
Object
  • Object
show all
Defined in:
lib/phronomy/tool/mcp_tool.rb

Overview

Minimal stdio transport implementing a subset of the MCP JSON-RPC protocol. Keeps the child process alive for the lifetime of this transport instance so that session state (registered resources, tool context, etc.) is preserved across multiple calls.

Instance Method Summary collapse

Constructor Details

#initialize(command, read_timeout: 30, env: nil, cwd: nil, startup_timeout: nil) ⇒ StdioTransport

Returns a new instance of StdioTransport.

Parameters:

  • command (String)

    shell command to spawn the MCP server process

  • read_timeout (Integer) (defaults to: 30)

    seconds to wait for the server's JSON-RPC response before raising Phronomy::ToolError. Mirrors the +read_timeout+ option on HttpTransport. Defaults to 30 seconds.

  • env (Hash, nil) (defaults to: nil)

    environment variable overrides for the subprocess. When provided, only these variables are added/overridden; the parent environment is still inherited. Use +nil+ as a value to unset a variable in the child process (e.g. +{ "SECRET" => nil }+). An empty string value (+""+ ) sets the variable to an empty string — it does NOT unset it.

  • cwd (String, nil) (defaults to: nil)

    working directory for the subprocess. Defaults to the current process's working directory.

  • startup_timeout (Numeric, nil) (defaults to: nil)

    seconds to wait for the server to emit its first line on stdout before raising Phronomy::ToolError. When nil (default), no startup check is performed.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/phronomy/tool/mcp_tool.rb', line 129

def initialize(command, read_timeout: 30, env: nil, cwd: nil, startup_timeout: nil)
  # Split the command string into an argv array so that Open3 executes
  # it directly without going through the shell, preventing injection.
  @command = Shellwords.split(command)
  @read_timeout = read_timeout
  @env = env
  @cwd = cwd
  @startup_timeout = startup_timeout
  @stdin = nil
  @stdout = nil
  @stderr = nil
  @wait_thr = nil
  @stderr_thread = nil
  @stderr_op = nil
end

Instance Method Details

#call_tool(tool_name, args) ⇒ Object

Call a tool on the MCP server using the tools/call method.

Parameters:

  • tool_name (String)
  • args (Hash)

Returns:

  • (Object)

    the tool result



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/phronomy/tool/mcp_tool.rb', line 190

def call_tool(tool_name, args)
  response = rpc_call("tools/call", {name: tool_name, arguments: args})
  if response["error"]
    err_msg = response.dig("error", "message") || response["error"].to_s
    raise Phronomy::ToolError, "MCP server returned error: #{err_msg}"
  end
  content = response.dig("result", "content")

  # MCP content is an array of content blocks; extract text blocks.
  if content.is_a?(Array)
    texts = content.select { |c| c["type"] == "text" }.map { |c| c["text"] }
    (texts.length == 1) ? texts.first : texts
  else
    content
  end
end

#closeObject

Shut down the child process and close its IO streams.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/phronomy/tool/mcp_tool.rb', line 146

def close
  @stdin&.close
  @stdout&.close
  @stderr&.close
  @stdin = nil
  @stdout = nil
  @stderr = nil
  stderr_thread = @stderr_thread
  stderr_op = @stderr_op
  wait_thr = @wait_thr
  @stderr_thread = nil
  @stderr_op = nil
  @wait_thr = nil
  stderr_thread&.join(1)
  begin
    stderr_op&.await(timeout: 1.0)
  rescue
    nil
  end
  wait_thr&.join(5)
end

#fetch_tool(tool_name) ⇒ Hash

Retrieve the tool definition from the server using the MCP tools/list method.

Parameters:

  • tool_name (String)

Returns:

  • (Hash)

    { description:, parameters: }

Raises:

  • (ArgumentError)


172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/phronomy/tool/mcp_tool.rb', line 172

def fetch_tool(tool_name)
  response = rpc_call("tools/list", {})
  tools = response.dig("result", "tools") || []
  defn = tools.find { |t| t["name"] == tool_name }
  raise ArgumentError, "Tool #{tool_name.inspect} not found on MCP server #{@command.inspect}" unless defn

  required_names = defn.dig("inputSchema", "required") || []
  {
    description: defn["description"],
    parameters: parse_schema_params(defn.dig("inputSchema", "properties") || {}, required_names: required_names)
  }
end