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

Methods inherited from ServerBase

#on_notification, #ping

Constructor Details

#initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, 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

  • read_timeout (Numeric) (defaults to: READ_TIMEOUT)

    timeout in seconds for reading responses

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

    optional logger



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

def initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, 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)
  @logger.progname = self.class.name
  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
  @max_retries = retries
  @retry_backoff = retry_backoff
  @read_timeout = read_timeout
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:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/mcp_client/server_stdio.rb', line 107

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



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/mcp_client/server_stdio.rb', line 130

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:



40
41
42
43
44
45
# File 'lib/mcp_client/server_stdio.rb', line 40

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



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/mcp_client/server_stdio.rb', line 61

def handle_line(line)
  msg = JSON.parse(line)
  @logger.debug("Received line: #{line.chomp}")
  # Dispatch JSON-RPC notifications (no id, has method)
  if msg['method'] && !msg.key?('id')
    @notification_callback&.call(msg['method'], msg['params'])
    return
  end
  # Handle standard JSON-RPC responses
  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:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/mcp_client/server_stdio.rb', line 85

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



48
49
50
51
52
53
54
55
56
# File 'lib/mcp_client/server_stdio.rb', line 48

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