Class: LLM::MCP::Command

Inherits:
Object
  • Object
show all
Defined in:
lib/llm/mcp/command.rb

Overview

The Command class manages the lifecycle of an MCP process by wrapping a system command. It provides methods to start the process, write to its stdin, read from its stdout and stderr, and wait for it to exit.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv:, env: {}, cwd: nil) ⇒ LLM::MCP::Command

Returns A new Command instance.

Parameters:

  • argv (Array<String>)

    The command to run for the MCP process

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

    The environment variables to set for the MCP process

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

    The working directory for the MCP process



22
23
24
25
26
27
28
# File 'lib/llm/mcp/command.rb', line 22

def initialize(argv:, env: {}, cwd: nil)
  @argv = argv
  @env = env
  @cwd = cwd
  @pid = nil
  @buffers = {}
end

Instance Attribute Details

#pidInteger? (readonly)

Returns The PID of the running command, or nil if it's not running.

Returns:

  • (Integer, nil)

    The PID of the running command, or nil if it's not running



13
14
15
# File 'lib/llm/mcp/command.rb', line 13

def pid
  @pid
end

#stdinObject (readonly)

Returns the value of attribute stdin.



15
16
17
# File 'lib/llm/mcp/command.rb', line 15

def stdin
  @stdin
end

#stdoutObject (readonly)

Returns the value of attribute stdout.



15
16
17
# File 'lib/llm/mcp/command.rb', line 15

def stdout
  @stdout
end

#stderrObject (readonly)

Returns the value of attribute stderr.



15
16
17
# File 'lib/llm/mcp/command.rb', line 15

def stderr
  @stderr
end

Instance Method Details

#alive?Boolean

Returns true when command is running.

Returns:

  • (Boolean)


57
58
59
# File 'lib/llm/mcp/command.rb', line 57

def alive?
  !@pid.nil?
end

#startvoid

This method returns an undefined value.

Starts a command.

Raises:

  • (LLM::Error)

    When the command is already running



35
36
37
38
39
40
41
# File 'lib/llm/mcp/command.rb', line 35

def start
  raise LLM::MCP::Error, "MCP command is already running" if alive?
  @stdout, @stderr, @stdin = 3.times.map { Pipe.new }
  @buffers.clear
  @pid = Process.spawn(env.to_h, *argv, {chdir: cwd, out: stdout.w, err: stderr.w, in: stdin.r}.compact)
  [stdin.close_reader, [stdout, stderr].each(&:close_writer)]
end

#stopvoid

This method returns an undefined value.

Stops the command if it's running.



46
47
48
49
50
51
52
# File 'lib/llm/mcp/command.rb', line 46

def stop
  return nil unless alive?
  [stdin.close_writer, [stdout, stderr].each(&:close_reader)]
  Process.kill("TERM", pid)
  @buffers.clear
  wait
end

#write(message) ⇒ void

This method returns an undefined value.

Writes to the command's stdin

Parameters:

  • message (String)

    The message to write



65
66
67
68
69
# File 'lib/llm/mcp/command.rb', line 65

def write(message)
  stdin.write(message)
  stdin.write("\n")
  stdin.flush
end

#read_nonblock(io = :stdout) ⇒ String

Reads from the command's IO without blocking.

Parameters:

  • io (Symbol) (defaults to: :stdout)

    The IO stream to read from (:stdout, :stderr)

Returns:

  • (String)

    The next complete line from the specified IO stream

Raises:

  • (LLM::Error)

    When the command is not running

  • (IO::WaitReadable)

    When no complete message is available to read



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/llm/mcp/command.rb', line 81

def read_nonblock(io = :stdout)
  raise LLM::MCP::Error, "MCP command is not running" unless alive?
  io = public_send(io)
  @buffers[io] ||= +""
  loop do
    if (index = @buffers[io].index("\n"))
      return @buffers[io].slice!(0, index + 1)
    end
    @buffers[io] << io.read_nonblock(4096)
  end
end

#waitProcess::Status?

Waits for the command to exit and returns its exit status.

Returns:

  • (Process::Status, nil)

    The exit status of the command, or nil



97
98
99
100
101
102
# File 'lib/llm/mcp/command.rb', line 97

def wait
  Process.wait(pid)
  @pid = nil
rescue Errno::ECHILD
  nil
end