Class: SolidLoop::Mcp::StdioTransport

Inherits:
Object
  • Object
show all
Includes:
Transport
Defined in:
lib/solid_loop/mcp/stdio_transport.rb

Overview

Child-process MCP server over stdin/stdout — restricted contract.

SolidLoop executes each tool call in an independent job (any worker, any machine), so no process could own a long-lived child across rounds. Therefore the lifecycle is spawn-per-call: spawn -> initialize handshake -> request -> graceful shutdown (kill on timeout), all inside one deliver. Stateless servers only: the session id is synthetic and implies no server-side continuity; stateful stdio servers (a running shell, a browser session, a REPL sandbox) are unsupported — their in-memory state would be lost at every job boundary. This is a deliberate consequence of the durable-async core, not a transport bug.

Options: command: (argv array, never a shell string), env:, cwd:, timeout: (per call, includes spawn+handshake). stderr is captured into the wire log. Spawn+handshake is typically 50-500 ms for Node/Python servers — prefer HTTP or a toolset for hot paths.

Defined Under Namespace

Classes: Diagnostics

Instance Method Summary collapse

Constructor Details

#initialize(command:, env: {}, cwd: nil, timeout: 30) ⇒ StdioTransport

Returns a new instance of StdioTransport.



43
44
45
46
47
48
49
50
51
52
# File 'lib/solid_loop/mcp/stdio_transport.rb', line 43

def initialize(command:, env: {}, cwd: nil, timeout: 30)
  unless command.is_a?(Array) && command.any? && command.all?(String)
    raise ArgumentError, "command: must be a non-empty argv Array of Strings (never a shell string)"
  end

  @command = command
  @env = env.transform_keys(&:to_s).transform_values(&:to_s)
  @cwd = cwd
  @timeout = timeout
end

Instance Method Details

#deliver(payload, session_id: nil, context: nil) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/solid_loop/mcp/stdio_transport.rb', line 54

def deliver(payload, session_id: nil, context: nil)
  request     = JSON.parse(JSON.generate(payload))
  deadline    = now + @timeout
  diagnostics = Diagnostics.new

  body = with_server(deadline, diagnostics) do |stdin, stdout|
    handshake(stdin, stdout, deadline, diagnostics) unless request["method"] == "initialize"
    write_message(stdin, request, deadline, diagnostics)
    read_response(stdout, request["id"], deadline, diagnostics)
  end

  Result.new(
    body:         body,
    session_id:   nil, # stateless-only: no server-side continuity between calls
    raw_request:  payload,
    raw_response: body,
    status:       200,
    stderr:       diagnostics.to_s.strip.presence
  )
end