Class: Clacky::Mcp::Registry

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

#serversObject (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?

Returns:

  • (Boolean)


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.

Returns:

  • (Hash)

    MCP ‘tools/call` result

Raises:



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

Returns:

  • (Boolean)


131
132
133
# File 'lib/clacky/mcp/registry.rb', line 131

def configured?(server_name)
  @servers.key?(server_name)
end

#reloadObject

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

#shutdownObject

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.

Parameters:

  • server_name (String)

Returns:



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_skillsObject

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