Class: Silas::Mcp::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/silas/mcp/server.rb

Overview

A minimal HTTP/1.1 server hosting the MCP Handler for the lifetime of one claude -p subprocess. Deliberately NOT Puma/Rack: the transport claude's MCP client needs (verified in the spike) is plain request/response JSON — one request per connection, application/json, no SSE — so a raw threaded TCPServer is fewer moving parts than embedding an app server in a worker.

In-process: the Handler has direct access to the Ledger/models, so no cross-service call is needed and it works on any worker box.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(handler:, turn:, token:, host:) ⇒ Server

Returns a new instance of Server.



23
24
25
26
27
28
# File 'lib/silas/mcp/server.rb', line 23

def initialize(handler:, turn:, token:, host:)
  @handler = handler
  @turn = turn
  @token = token
  @host = host
end

Instance Attribute Details

#portObject (readonly)

Returns the value of attribute port.



14
15
16
# File 'lib/silas/mcp/server.rb', line 14

def port
  @port
end

Class Method Details

.start(turn:, step:, tools:, resolver:, host: Silas.config.agent_sdk_mcp_host) ⇒ Object



16
17
18
19
20
21
# File 'lib/silas/mcp/server.rb', line 16

def self.start(turn:, step:, tools:, resolver:, host: Silas.config.agent_sdk_mcp_host)
  token = SecureRandom.hex(16)
  turn.update_columns(mcp_token: token)
  handler = Handler.new(turn: turn, step: step, tools: tools, resolver: resolver, token: token)
  new(handler: handler, turn: turn, token: token, host: host).tap(&:boot)
end

Instance Method Details

#await_ready!(timeout: 5.0) ⇒ Object

Health-check before spawning claude: a booting server that refuses the first connection gets marked "pending" by the CLI with no retry (spike trap).



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/silas/mcp/server.rb', line 41

def await_ready!(timeout: 5.0)
  deadline = clock + timeout
  loop do
    TCPSocket.new(@host, @port).close
    return true
  rescue SystemCallError
    raise Silas::Error, "MCP server did not become ready" if clock > deadline

    sleep 0.02
  end
end

#bootObject



30
31
32
33
34
35
# File 'lib/silas/mcp/server.rb', line 30

def boot
  @socket = TCPServer.new(@host, 0)
  @port = @socket.addr[1]
  @thread = Thread.new { accept_loop }
  @thread.report_on_exception = false
end

#mcp_urlObject



37
# File 'lib/silas/mcp/server.rb', line 37

def mcp_url = "http://#{@host}:#{@port}/mcp/#{@turn.id}?t=#{@token}"

#stopObject



53
54
55
56
57
58
59
# File 'lib/silas/mcp/server.rb', line 53

def stop
  @stopping = true
  @socket&.close
  @thread&.kill
rescue IOError
  # already closed
end