Class: MCP::Client::Stdio

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp/client/stdio.rb

Constant Summary collapse

CLOSE_TIMEOUT =

Seconds to wait for the server process to exit before sending SIGTERM. Matches the Python and TypeScript SDKs’ shutdown timeout: github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/stdio/__init__.py#L48 github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/stdio.ts#L221

2
STDERR_READ_SIZE =
4096

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, args: [], env: nil, read_timeout: nil) ⇒ Stdio

Returns a new instance of Stdio.



24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/mcp/client/stdio.rb', line 24

def initialize(command:, args: [], env: nil, read_timeout: nil)
  @command = command
  @args = args
  @env = env
  @read_timeout = read_timeout
  @stdin = nil
  @stdout = nil
  @stderr = nil
  @wait_thread = nil
  @stderr_thread = nil
  @started = false
  @initialized = false
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args.



22
23
24
# File 'lib/mcp/client/stdio.rb', line 22

def args
  @args
end

#commandObject (readonly)

Returns the value of attribute command.



22
23
24
# File 'lib/mcp/client/stdio.rb', line 22

def command
  @command
end

#envObject (readonly)

Returns the value of attribute env.



22
23
24
# File 'lib/mcp/client/stdio.rb', line 22

def env
  @env
end

Instance Method Details

#closeObject



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/mcp/client/stdio.rb', line 74

def close
  return unless @started

  @stdin.close
  @stdout.close
  @stderr.close

  begin
    Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
  rescue Timeout::Error
    begin
      Process.kill("TERM", @wait_thread.pid)
      Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
    rescue Timeout::Error
      begin
        Process.kill("KILL", @wait_thread.pid)
      rescue Errno::ESRCH
        nil
      end
    rescue Errno::ESRCH
      nil
    end
  end

  @stderr_thread.join(CLOSE_TIMEOUT)
  @started = false
  @initialized = false
end

#send_request(request:) ⇒ Object



38
39
40
41
42
43
44
# File 'lib/mcp/client/stdio.rb', line 38

def send_request(request:)
  start unless @started
  initialize_session unless @initialized

  write_message(request)
  read_response(request)
end

#startObject



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mcp/client/stdio.rb', line 46

def start
  raise "MCP::Client::Stdio already started" if @started

  spawn_env = @env || {}
  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args)
  @stdout.set_encoding("UTF-8")
  @stdin.set_encoding("UTF-8")

  # Drain stderr in the background to prevent the pipe buffer from filling up,
  # which would cause the server process to block and deadlock.
  @stderr_thread = Thread.new do
    loop do
      @stderr.readpartial(STDERR_READ_SIZE)
    end
  rescue IOError
    nil
  end

  @started = true
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOEXEC => e
  raise RequestHandlerError.new(
    "Failed to spawn server process: #{e.message}",
    {},
    error_type: :internal_error,
    original_error: e,
  )
end