Class: MCPClient::ServerStdio
- Inherits:
-
ServerBase
- Object
- ServerBase
- MCPClient::ServerStdio
- 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
-
#command ⇒ Object
readonly
Returns the value of attribute command.
Instance Method Summary collapse
-
#call_tool(tool_name, parameters) ⇒ Object
Call a tool with the given parameters.
-
#cleanup ⇒ Object
Clean up the server connection Closes all stdio handles and terminates any running processes and threads.
-
#connect ⇒ Boolean
Connect to the MCP server by launching the command process via stdout/stdin.
-
#handle_line(line) ⇒ Object
Handle a line of output from the stdio server Parses JSON-RPC messages and adds them to pending responses.
-
#initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, logger: nil) ⇒ ServerStdio
constructor
A new instance of ServerStdio.
-
#list_tools ⇒ Array<MCPClient::Tool>
List all tools available from the MCP server.
-
#start_reader ⇒ Object
Spawn a reader thread to collect JSON-RPC responses.
Methods inherited from ServerBase
Constructor Details
#initialize(command:, retries: 0, retry_backoff: 1, read_timeout: READ_TIMEOUT, logger: nil) ⇒ ServerStdio
Returns a new instance of ServerStdio.
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
#command ⇒ Object (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
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.}" end |
#cleanup ⇒ Object
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 |
#connect ⇒ Boolean
Connect to the MCP server by launching the command process via stdout/stdin
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.}" end |
#handle_line(line) ⇒ Object
Handle a line of output from the stdio server Parses JSON-RPC messages and adds them to pending responses
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_tools ⇒ Array<MCPClient::Tool>
List all tools available from the MCP server
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.}" end |
#start_reader ⇒ Object
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 |