Class: MCPClient::ServerStdio

Inherits:
ServerBase show all
Defined in:
lib/mcp_client/server_stdio.rb

Overview

JSON-RPC implementation of MCP server over stdio.

Constant Summary collapse

READ_TIMEOUT =

Timeout in seconds for responses

15

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, retries: 0, retry_backoff: 1, logger: nil) ⇒ ServerStdio

Returns a new instance of ServerStdio.

Parameters:

  • command (String, Array)

    the stdio command to launch the MCP JSON-RPC server

  • retries (Integer) (defaults to: 0)

    number of retry attempts on transient errors

  • retry_backoff (Numeric) (defaults to: 1)

    base delay in seconds for exponential backoff

  • logger (Logger, nil) (defaults to: nil)

    optional logger



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/mcp_client/server_stdio.rb', line 20

def initialize(command:, retries: 0, retry_backoff: 1, logger: nil)
  super()
  @command = command.is_a?(Array) ? command.join(' ') : command
  @mutex = Mutex.new
  @cond = ConditionVariable.new
  @next_id = 1
  @pending = {}
  @initialized = false
  @logger = logger || Logger.new($stdout, level: Logger::WARN)
  @max_retries = retries
  @retry_backoff = retry_backoff
end

Instance Attribute Details

#commandObject (readonly)

Returns the value of attribute command.



11
12
13
# File 'lib/mcp_client/server_stdio.rb', line 11

def command
  @command
end

Instance Method Details

#call_tool(tool_name, parameters) ⇒ Object

Call a tool with the given parameters

Parameters:

  • tool_name (String)

    the name of the tool to call

  • parameters (Hash)

    the parameters to pass to the tool

Returns:

  • (Object)

    the result of the tool invocation

Raises:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/mcp_client/server_stdio.rb', line 97

def call_tool(tool_name, parameters)
  ensure_initialized
  req_id = next_id
  # JSON-RPC method for calling a tool
  req = {
    'jsonrpc' => '2.0',
    'id' => req_id,
    'method' => 'tools/call',
    'params' => { 'name' => tool_name, 'arguments' => parameters }
  }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  res['result']
rescue StandardError => e
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
end

#cleanupObject

Clean up the server connection Closes all stdio handles and terminates any running processes and threads



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/mcp_client/server_stdio.rb', line 120

def cleanup
  return unless @stdin

  @stdin.close unless @stdin.closed?
  @stdout.close unless @stdout.closed?
  @stderr.close unless @stderr.closed?
  if @wait_thread&.alive?
    Process.kill('TERM', @wait_thread.pid)
    @wait_thread.join(1)
  end
  @reader_thread&.kill
rescue StandardError
  # Clean up resources during unexpected termination
ensure
  @stdin = @stdout = @stderr = @wait_thread = @reader_thread = nil
end

#connectBoolean

Connect to the MCP server by launching the command process via stdout/stdin

Returns:

  • (Boolean)

    true if connection was successful

Raises:



36
37
38
39
40
41
# File 'lib/mcp_client/server_stdio.rb', line 36

def connect
  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@command)
  true
rescue StandardError => e
  raise MCPClient::Errors::ConnectionError, "Failed to connect to MCP server: #{e.message}"
end

#handle_line(line) ⇒ Object

Handle a line of output from the stdio server Parses JSON-RPC messages and adds them to pending responses

Parameters:

  • line (String)

    line of output to parse



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/mcp_client/server_stdio.rb', line 57

def handle_line(line)
  msg = JSON.parse(line)
  @logger.debug("Received line: #{line.chomp}")
  id = msg['id']
  return unless id

  @mutex.synchronize do
    @pending[id] = msg
    @cond.broadcast
  end
rescue JSON::ParserError
  # Skip non-JSONRPC lines in the output stream
end

#list_toolsArray<MCPClient::Tool>

List all tools available from the MCP server

Returns:

Raises:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/mcp_client/server_stdio.rb', line 75

def list_tools
  ensure_initialized
  req_id = next_id
  # JSON-RPC method for listing tools
  req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'tools/list', 'params' => {} }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  (res.dig('result', 'tools') || []).map { |td| MCPClient::Tool.from_json(td) }
rescue StandardError => e
  raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
end

#start_readerObject

Spawn a reader thread to collect JSON-RPC responses



44
45
46
47
48
49
50
51
52
# File 'lib/mcp_client/server_stdio.rb', line 44

def start_reader
  @reader_thread = Thread.new do
    @stdout.each_line do |line|
      handle_line(line)
    end
  rescue StandardError
    # Reader thread aborted unexpectedly
  end
end