Class: Rubino::MCP::Manager

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/mcp/manager.rb

Overview

Manages multiple MCP client connections. Reads server definitions from config, starts clients, and registers their tools into the agent’s tool registry.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config: nil) ⇒ Manager

Returns a new instance of Manager.



17
18
19
20
21
22
# File 'lib/rubino/mcp/manager.rb', line 17

def initialize(config: nil)
  @config = config || Rubino.configuration
  @clients = {}
  @last_errors = {}
  route_mcp_logging!
end

Instance Attribute Details

#clientsObject (readonly)

clients: name => live RubyLLM::MCP client. last_errors: name => the most recent start failure message (cleared on a successful start) — the “why is my server missing?” answer /mcp’s drill-in shows (#182).



15
16
17
# File 'lib/rubino/mcp/manager.rb', line 15

def clients
  @clients
end

#last_errorsObject (readonly)

clients: name => live RubyLLM::MCP client. last_errors: name => the most recent start failure message (cleared on a successful start) — the “why is my server missing?” answer /mcp’s drill-in shows (#182).



15
16
17
# File 'lib/rubino/mcp/manager.rb', line 15

def last_errors
  @last_errors
end

Instance Method Details

#configured?Boolean

Returns true if any MCP servers are configured

Returns:

  • (Boolean)


114
115
116
117
# File 'lib/rubino/mcp/manager.rb', line 114

def configured?
  servers = @config.dig("mcp", "servers")
  servers.is_a?(Hash) && !servers.empty?
end

#health_checkObject

Checks health of all connected servers



102
103
104
105
106
107
108
109
110
111
# File 'lib/rubino/mcp/manager.rb', line 102

def health_check
  @clients.map do |name, client|
    alive = begin
      client.alive?
    rescue StandardError
      false
    end
    { name: name, alive: alive }
  end
end

#register_all_tools!Object

Registers all MCP tools into the agent’s tool registry. Per-agent mcp_servers scoping is NOT applied here — it lives in Agent::Definition#resolved_tools (#173), the single seam every consumer of an agent’s tool set goes through.



82
83
84
# File 'lib/rubino/mcp/manager.rb', line 82

def register_all_tools!
  @clients.each_key { |server_name| register_server_tools(server_name) }
end

#register_server_tools(name) ⇒ Object

Registers ONE started server’s tools — the ‘/mcp <server> on` path (#182) re-registers only that server instead of re-reading every client’s tool list.



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/rubino/mcp/manager.rb', line 89

def register_server_tools(name)
  client = @clients[name.to_s]
  return unless client

  client.tools.each do |mcp_tool|
    wrapped = MCPToolWrapper.new(mcp_tool, server_name: name.to_s)
    Tools::Registry.register(wrapped)
  end
rescue StandardError => e
  Rubino.ui.warning("Failed to load tools from '#{name}': #{e.message}")
end

#start_all!Object

Initializes all configured MCP servers



25
26
27
28
29
30
31
32
33
34
# File 'lib/rubino/mcp/manager.rb', line 25

def start_all!
  server_configs = @config.dig("mcp", "servers") || {}

  server_configs.each do |name, server_config|
    start_server(name, server_config)
  end

  register_all_tools!
  @clients
end

#start_server(name, server_config) ⇒ Object

Starts a single MCP server by name



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/rubino/mcp/manager.rb', line 37

def start_server(name, server_config)
  transport = server_config["transport"] || "stdio"
  client_opts = build_client_options(name, transport, server_config)

  client = RubyLLM::MCP.client(**client_opts)
  @clients[name.to_s] = client
  @last_errors.delete(name.to_s)

  Rubino.event_bus.emit(:mcp_server_started, name: name)
  client
rescue StandardError => e
  @last_errors[name.to_s] = e.message
  Rubino.ui.warning("MCP server '#{name}' failed to start: #{e.message}")
  nil
end

#stop_all!Object

Stops all MCP clients (deregistering their tools — see #stop_server). ‘keys.each`, NOT `each_key`: stop_server deletes from @clients, which would raise mid-iteration without the snapshot.



56
57
58
# File 'lib/rubino/mcp/manager.rb', line 56

def stop_all!
  @clients.keys.each { |name| stop_server(name) } # rubocop:disable Style/HashEachMethods
end

#stop_server(name) ⇒ Object

Stops a specific MCP client AND deregisters its MCPToolWrapper instances from Tools::Registry (#182) — before, nothing ever unregistered them, so a stopped server left dead tools the model could still call.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/rubino/mcp/manager.rb', line 64

def stop_server(name)
  client = @clients.delete(name.to_s)
  return nil unless client

  deregister_tools(name.to_s)
  begin
    client.stop
  rescue StandardError => e
    Rubino.ui.warning("Error stopping MCP '#{name}': #{e.message}")
  end
  Rubino.event_bus.emit(:mcp_server_stopped, name: name)
  client
end