Class: RubynCode::MCP::StdioTransport
- Inherits:
-
Object
- Object
- RubynCode::MCP::StdioTransport
- Defined in:
- lib/rubyn_code/mcp/stdio_transport.rb
Overview
Communicates with an MCP server via subprocess stdin/stdout using JSON-RPC 2.0.
The server process is spawned with Open3.popen3 and kept alive for the duration of the session. Requests are written as newline-delimited JSON to stdin, and responses are read line-by-line from stdout.
Defined Under Namespace
Classes: TimeoutError, TransportError
Constant Summary collapse
- DEFAULT_TIMEOUT =
seconds
30
Instance Method Summary collapse
-
#alive? ⇒ Boolean
Checks whether the subprocess is still running.
-
#initialize(command:, args: [], env: {}, timeout: DEFAULT_TIMEOUT) ⇒ StdioTransport
constructor
A new instance of StdioTransport.
-
#send_notification(method, params = {}) ⇒ void
Sends a JSON-RPC 2.0 notification (no response expected).
-
#send_request(method, params = {}) ⇒ Hash
Sends a JSON-RPC 2.0 request and waits for the correlated response.
-
#start! ⇒ void
Spawns the MCP server subprocess.
-
#stop! ⇒ void
Gracefully shuts down the MCP server and cleans up resources.
Constructor Details
#initialize(command:, args: [], env: {}, timeout: DEFAULT_TIMEOUT) ⇒ StdioTransport
Returns a new instance of StdioTransport.
27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 27 def initialize(command:, args: [], env: {}, timeout: DEFAULT_TIMEOUT) @command = command @args = args @env = env @timeout = timeout @request_id = 0 @mutex = Mutex.new @stdin = nil @stdout = nil @stderr = nil @wait_thread = nil end |
Instance Method Details
#alive? ⇒ Boolean
Checks whether the subprocess is still running.
118 119 120 121 122 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 118 def alive? return false unless @wait_thread @wait_thread.alive? end |
#send_notification(method, params = {}) ⇒ void
This method returns an undefined value.
Sends a JSON-RPC 2.0 notification (no response expected).
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 79 def send_notification(method, params = {}) raise TransportError, 'Transport is not running' unless alive? notification = { jsonrpc: '2.0', method: method, params: params } write_request(notification) end |
#send_request(method, params = {}) ⇒ Hash
Sends a JSON-RPC 2.0 request and waits for the correlated response.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 59 def send_request(method, params = {}) raise TransportError, 'Transport is not running' unless alive? id = next_request_id request = { jsonrpc: '2.0', id: id, method: method, params: params } write_request(request) read_response(id) end |
#start! ⇒ void
This method returns an undefined value.
Spawns the MCP server subprocess.
44 45 46 47 48 49 50 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 44 def start! raise TransportError, 'Transport already started' if alive? @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@env, @command, *@args) rescue Errno::ENOENT => e raise TransportError, "Failed to start MCP server: #{e.}" end |
#stop! ⇒ void
This method returns an undefined value.
Gracefully shuts down the MCP server and cleans up resources.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/rubyn_code/mcp/stdio_transport.rb', line 94 def stop! return unless alive? begin send_notification('notifications/cancelled') @stdin&.close rescue IOError, Errno::EPIPE # Process may already be gone end begin @wait_thread&.join(5) rescue StandardError # Best-effort wait end force_kill if alive? ensure close_streams end |