Class: Clacky::Mcp::Registry
- Inherits:
-
Object
- Object
- Clacky::Mcp::Registry
- Defined in:
- lib/clacky/mcp/registry.rb
Overview
Central registry for MCP servers configured by the user.
Responsibilities:
- Load ~/.clacky/mcp.json (or project .clacky/mcp.json) on demand.
- For each declared server, expose a VirtualSkill so the main agent
sees it as a one-line capability in the AVAILABLE SKILLS section.
No tool schemas leak into the main context.
- Lazily spawn the server process the first time invoke_skill('mcp:xxx')
happens, cache the connection, and reap idle servers after a timeout.
- Provide a single call_tool entry point for Tools::McpCall to dispatch
into.
Constant Summary collapse
- DEFAULT_IDLE_TIMEOUT =
How long an MCP server may sit idle before we reap it. Vital for the “no gateway” promise: we never keep stale processes around.
300- DESCRIPTION_FETCH_TIMEOUT =
How long to wait for tools/list during cold metadata collection.
20
Instance Attribute Summary collapse
-
#servers ⇒ Object
readonly
Returns the value of attribute servers.
Instance Method Summary collapse
-
#any? ⇒ Boolean
Has the user configured any MCP servers?.
-
#call_tool(server_name, tool_name, arguments) ⇒ Hash
Execute a tool call against an MCP server.
- #configured?(server_name) ⇒ Boolean
-
#initialize(working_dir: nil, idle_timeout: DEFAULT_IDLE_TIMEOUT) ⇒ Registry
constructor
A new instance of Registry.
-
#reload ⇒ Object
Reload mcp.json (e.g. user added a server) and invalidate caches.
-
#shutdown ⇒ Object
Stop all live MCP server processes.
-
#tool_definitions(server_name) ⇒ Object
Fetch the live tool list (and lazily cold-start the server).
-
#virtual_skill_for(server_name) ⇒ VirtualSkill?
Return a fresh VirtualSkill for a server.
-
#virtual_skills ⇒ Object
Map of server name -> VirtualSkill.
Constructor Details
#initialize(working_dir: nil, idle_timeout: DEFAULT_IDLE_TIMEOUT) ⇒ Registry
Returns a new instance of Registry.
40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/clacky/mcp/registry.rb', line 40 def initialize(working_dir: nil, idle_timeout: DEFAULT_IDLE_TIMEOUT) @working_dir = working_dir @idle_timeout = idle_timeout @servers = {} # name => spec hash @clients = {} # name => Client (only when started) @virtual_skills_cache = nil @lock = Monitor.new @reaper_thread = nil load_config start_reaper end |
Instance Attribute Details
#servers ⇒ Object (readonly)
Returns the value of attribute servers.
38 39 40 |
# File 'lib/clacky/mcp/registry.rb', line 38 def servers @servers end |
Instance Method Details
#any? ⇒ Boolean
Has the user configured any MCP servers?
127 128 129 |
# File 'lib/clacky/mcp/registry.rb', line 127 def any? !@servers.empty? end |
#call_tool(server_name, tool_name, arguments) ⇒ Hash
Execute a tool call against an MCP server. Used by Tools::McpCall.
119 120 121 122 123 124 |
# File 'lib/clacky/mcp/registry.rb', line 119 def call_tool(server_name, tool_name, arguments) client = ensure_started(server_name) raise Mcp::Client::TransportError, "MCP server '#{server_name}' is not configured" unless client client.call_tool(tool_name, arguments) end |
#configured?(server_name) ⇒ Boolean
131 132 133 |
# File 'lib/clacky/mcp/registry.rb', line 131 def configured?(server_name) @servers.key?(server_name) end |
#reload ⇒ Object
Reload mcp.json (e.g. user added a server) and invalidate caches. Existing live clients survive; only stopped/removed servers get cleaned.
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/clacky/mcp/registry.rb', line 55 def reload @lock.synchronize do old_names = @servers.keys @servers = {} load_config @virtual_skills_cache = nil (old_names - @servers.keys).each do |gone| @clients.delete(gone)&.stop end end end |
#shutdown ⇒ Object
Stop all live MCP server processes. Safe to call from at_exit hooks and on agent shutdown.
137 138 139 140 141 142 143 144 |
# File 'lib/clacky/mcp/registry.rb', line 137 def shutdown @lock.synchronize do @reaper_thread&.kill rescue nil @reaper_thread = nil @clients.each_value { |c| c.stop rescue nil } @clients.clear end end |
#tool_definitions(server_name) ⇒ Object
Fetch the live tool list (and lazily cold-start the server). Used by the HttpServer for /api/mcp/:name/tools and /api/mcp/:name/probe.
110 111 112 113 114 115 |
# File 'lib/clacky/mcp/registry.rb', line 110 def tool_definitions(server_name) client = ensure_started(server_name) return [] unless client client.tool_definitions end |
#virtual_skill_for(server_name) ⇒ VirtualSkill?
Return a fresh VirtualSkill for a server. The HttpServer’s MCP tools endpoint uses this to satisfy probe requests; tool schemas are no longer attached to the skill itself — clients fetch them separately via /api/mcp/:name/tools.
98 99 100 101 102 103 104 105 106 |
# File 'lib/clacky/mcp/registry.rb', line 98 def virtual_skill_for(server_name) return nil unless @servers.key?(server_name) spec = @servers[server_name] VirtualSkill.new( server_name: server_name, description: spec["description"] || default_description_for(server_name) ) end |
#virtual_skills ⇒ Object
Map of server name -> VirtualSkill. Cached because rebuilding it triggers tools/list against every cold server, which we want to do at most once per process.
Implementation note: we do NOT pre-spawn servers here. We need their tool list to populate the VirtualSkill body, but we only fetch it the first time the subagent actually fires up. For the system-prompt description we use the user-provided “description” field from mcp.json, falling back to a placeholder. This keeps app startup zero-cost.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/clacky/mcp/registry.rb', line 77 def virtual_skills @lock.synchronize do return @virtual_skills_cache.values if @virtual_skills_cache @virtual_skills_cache = {} @servers.each do |name, spec| @virtual_skills_cache[name] = VirtualSkill.new( server_name: name, description: spec["description"] || default_description_for(name) ) end @virtual_skills_cache.values end end |