Class: MCPClient::ServerStdio

Inherits:
ServerBase show all
Includes:
JsonRpcTransport
Defined in:
lib/mcp_client/server_stdio.rb,
lib/mcp_client/server_stdio/json_rpc_transport.rb

Overview

JSON-RPC implementation of MCP server over stdio.

Defined Under Namespace

Modules: JsonRpcTransport

Constant Summary collapse

READ_TIMEOUT =

Timeout in seconds for responses

15

Instance Attribute Summary collapse

Attributes inherited from ServerBase

#name

Instance Method Summary collapse

Methods included from JsonRpcTransport

#call_tool_streaming, #ensure_initialized, #next_id, #perform_initialize, #rpc_notify, #rpc_request, #send_request, #wait_response

Methods included from JsonRpcCommon

#build_jsonrpc_notification, #build_jsonrpc_request, #initialization_params, #ping, #process_jsonrpc_response, #with_retry

Methods inherited from ServerBase

#call_tool_streaming, #on_notification, #ping, #rpc_notify, #rpc_request

Constructor Details

#initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, name: nil, logger: nil, env: {}) ⇒ ServerStdio

Initialize a new ServerStdio instance

Parameters:

  • command (String, Array)

    the stdio command to launch the MCP JSON-RPC server For improved security, passing an Array is recommended to avoid shell injection issues

  • 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

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

    optional name for this server

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

    optional logger

  • env (Hash) (defaults to: {})

    optional environment variables for the subprocess



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/mcp_client/server_stdio.rb', line 33

def initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, name: nil, logger: nil, env: {})
  super(name: name)
  @command_array = command.is_a?(Array) ? command : nil
  @command = command.is_a?(Array) ? command.join(' ') : command
  @mutex = Mutex.new
  @cond = ConditionVariable.new
  @next_id = 1
  @pending = {}
  @initialized = false
  initialize_logger(logger)
  @max_retries   = retries
  @retry_backoff = retry_backoff
  @read_timeout  = read_timeout
  @env           = env || {}
end

Instance Attribute Details

#commandString, Array (readonly)

Returns the command used to launch the server.

Returns:

  • (String, Array)

    the command used to launch the server



19
20
21
# File 'lib/mcp_client/server_stdio.rb', line 19

def command
  @command
end

#envObject (readonly)

Returns the value of attribute env.



19
# File 'lib/mcp_client/server_stdio.rb', line 19

attr_reader :command, :env

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:



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

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

#cleanupvoid

This method returns an undefined value.

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



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/mcp_client/server_stdio.rb', line 155

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 stdin/stdout

Returns:

  • (Boolean)

    true if connection was successful

Raises:



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mcp_client/server_stdio.rb', line 52

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

#handle_line(line) ⇒ void

This method returns an undefined value.

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



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

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:



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

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, server: self) }
rescue StandardError => e
  raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
end

#start_readerThread

Spawn a reader thread to collect JSON-RPC responses

Returns:

  • (Thread)

    the reader thread



71
72
73
74
75
76
77
78
79
# File 'lib/mcp_client/server_stdio.rb', line 71

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