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
48
49
# 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
  @server_info = nil
  @capabilities = nil
  initialize_logger(logger)
  @max_retries   = retries
  @retry_backoff = retry_backoff
  @read_timeout  = read_timeout
  @env           = env || {}
end

Instance Attribute Details

#capabilitiesHash? (readonly)

Server capabilities from the initialize response

Returns:

  • (Hash, nil)

    Server capabilities



57
58
59
# File 'lib/mcp_client/server_stdio.rb', line 57

def capabilities
  @capabilities
end

#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

#server_infoHash? (readonly)

Server info from the initialize response

Returns:

  • (Hash, nil)

    Server information



53
54
55
# File 'lib/mcp_client/server_stdio.rb', line 53

def server_info
  @server_info
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:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/mcp_client/server_stdio.rb', line 313

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



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/mcp_client/server_stdio.rb', line 337

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:



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

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

#get_prompt(prompt_name, parameters) ⇒ Object

Get a prompt with the given parameters

Parameters:

  • prompt_name (String)

    the name of the prompt to get

  • parameters (Hash)

    the parameters to pass to the prompt

Returns:

  • (Object)

    the result of the prompt interpolation

Raises:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/mcp_client/server_stdio.rb', line 140

def get_prompt(prompt_name, parameters)
  ensure_initialized
  req_id = next_id
  # JSON-RPC method for getting a prompt
  req = {
    'jsonrpc' => '2.0',
    'id' => req_id,
    'method' => 'prompts/get',
    'params' => { 'name' => prompt_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::PromptGetError, "Error calling prompt '#{prompt_name}': #{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



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

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_promptsArray<MCPClient::Prompt>

List all prompts available from the MCP server

Returns:

Raises:



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

def list_prompts
  ensure_initialized
  req_id = next_id
  req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'prompts/list', 'params' => {} }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  (res.dig('result', 'prompts') || []).map { |td| MCPClient::Prompt.from_json(td, server: self) }
rescue StandardError => e
  raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
end

#list_resource_templates(cursor: nil) ⇒ Hash

List all resource templates available from the MCP server

Parameters:

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

    optional cursor for pagination

Returns:

  • (Hash)

    result containing resourceTemplates array and optional nextCursor

Raises:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/mcp_client/server_stdio.rb', line 218

def list_resource_templates(cursor: nil)
  ensure_initialized
  req_id = next_id
  params = {}
  params['cursor'] = cursor if cursor
  req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/templates/list', 'params' => params }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  result = res['result'] || {}
  templates = (result['resourceTemplates'] || []).map { |td| MCPClient::ResourceTemplate.from_json(td, server: self) }
  { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
end

#list_resources(cursor: nil) ⇒ Hash

List all resources available from the MCP server

Parameters:

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

    optional cursor for pagination

Returns:

  • (Hash)

    result containing resources array and optional nextCursor

Raises:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/mcp_client/server_stdio.rb', line 166

def list_resources(cursor: nil)
  ensure_initialized
  req_id = next_id
  params = {}
  params['cursor'] = cursor if cursor
  req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => params }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  result = res['result'] || {}
  resources = (result['resources'] || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
  { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
end

#list_toolsArray<MCPClient::Tool>

List all tools available from the MCP server

Returns:

Raises:



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/mcp_client/server_stdio.rb', line 291

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

#read_resource(uri) ⇒ Array<MCPClient::ResourceContent>

Read a resource by its URI

Parameters:

  • uri (String)

    the URI of the resource to read

Returns:

Raises:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/mcp_client/server_stdio.rb', line 190

def read_resource(uri)
  ensure_initialized
  req_id = next_id
  # JSON-RPC method for reading a resource
  req = {
    'jsonrpc' => '2.0',
    'id' => req_id,
    'method' => 'resources/read',
    'params' => { 'uri' => uri }
  }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  result = res['result'] || {}
  contents = result['contents'] || []
  contents.map { |content| MCPClient::ResourceContent.from_json(content) }
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
end

#start_readerThread

Spawn a reader thread to collect JSON-RPC responses

Returns:

  • (Thread)

    the reader thread



81
82
83
84
85
86
87
88
89
# File 'lib/mcp_client/server_stdio.rb', line 81

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

#subscribe_resource(uri) ⇒ Boolean

Subscribe to resource updates

Parameters:

  • uri (String)

    the URI of the resource to subscribe to

Returns:

  • (Boolean)

    true if subscription successful

Raises:



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/mcp_client/server_stdio.rb', line 242

def subscribe_resource(uri)
  ensure_initialized
  req_id = next_id
  req = {
    'jsonrpc' => '2.0',
    'id' => req_id,
    'method' => 'resources/subscribe',
    'params' => { 'uri' => uri }
  }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  true
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
end

#unsubscribe_resource(uri) ⇒ Boolean

Unsubscribe from resource updates

Parameters:

  • uri (String)

    the URI of the resource to unsubscribe from

Returns:

  • (Boolean)

    true if unsubscription successful

Raises:



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/mcp_client/server_stdio.rb', line 267

def unsubscribe_resource(uri)
  ensure_initialized
  req_id = next_id
  req = {
    'jsonrpc' => '2.0',
    'id' => req_id,
    'method' => 'resources/unsubscribe',
    'params' => { 'uri' => uri }
  }
  send_request(req)
  res = wait_response(req_id)
  if (err = res['error'])
    raise MCPClient::Errors::ServerError, err['message']
  end

  true
rescue StandardError => e
  raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
end