Class: RubynCode::MCP::StdioTransport

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(command:, args: [], env: {}, timeout: DEFAULT_TIMEOUT) ⇒ StdioTransport

Returns a new instance of StdioTransport.

Parameters:

  • command (String)

    executable to spawn

  • args (Array<String>) (defaults to: [])

    arguments for the command

  • env (Hash<String, String>) (defaults to: {})

    additional environment variables

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    default timeout in seconds per request



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.

Returns:

  • (Boolean)


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).

Parameters:

  • method (String)

    the JSON-RPC method name

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

    parameters for the notification

Raises:



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.

Parameters:

  • method (String)

    the JSON-RPC method name

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

    parameters for the request

Returns:

  • (Hash)

    the parsed JSON-RPC response result

Raises:



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.

Raises:



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.message}"
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