Class: Insika::McpToolRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/mcp_tool_registry.rb

Overview

LIVE MCP tools — kills the McpToolIngestor snapshot.

entries is CHEAP: it reads only McpStore#tools_cache (no I/O), the same contract the ToolStore-backed dynamic entries already give OverlayToolRegistry — Policy recomputes candidate_tools every turn, so an unreachable MCP server must never add real latency there.

refresh is the ONLY thing that talks to a server ahead of time: it connects, lists live, and writes the result back to tools_cache (display/ doctor). Calling a tool never depends on that cache — Insika::McpLiveTool always goes through the live, memoized client (and RubyLLM::MCP::Client does its own real tools/list on ITS first use per instance, regardless of whether refresh ever ran).

Instance Method Summary collapse

Constructor Details

#initialize(mcp_store:, client_factory: Insika::McpClient.method(:for)) ⇒ McpToolRegistry

Returns a new instance of McpToolRegistry.



18
19
20
21
22
23
# File 'lib/insika/mcp_tool_registry.rb', line 18

def initialize(mcp_store:, client_factory: Insika::McpClient.method(:for))
  @mcp_store = mcp_store
  @client_factory = client_factory
  @clients = {}
  @mutex = Mutex.new
end

Instance Method Details

#entriesObject

-> [Registry::Entry] one per cached tool of every ENABLED instance.



26
27
28
# File 'lib/insika/mcp_tool_registry.rb', line 26

def entries
  @mcp_store.all_raw.select { |r| r["enabled"] }.flat_map { |r| entries_for(r) }
end

#evict(name) ⇒ Object

Stops and drops any memoized client for name (no-op if none), so the next client_for builds a fresh one off the current record. Public: called by refresh above AND by UpsertMcp/DeleteMcp right after they write the store — a Studio/CLI edit takes effect on that instance's NEXT tool call or refresh, no process restart required (there's no way to restart a process by hand on a Railway dyno).



57
58
59
60
61
62
# File 'lib/insika/mcp_tool_registry.rb', line 57

def evict(name)
  client = @mutex.synchronize { @clients.delete(name.to_s) }
  client&.stop
rescue StandardError
  nil # best-effort teardown of a possibly already-dead process
end

#refresh(name) ⇒ Object

Connects to name LIVE, lists its tools, and writes McpStore#tools_cache. ALWAYS drops any memoized client first and rebuilds from the current record: a stdio process can be alive? (still running) yet permanently broken (e.g. wrong transport args — see grafana-stg gotcha), and an edited command/url/env must take effect without a process restart — there's no SSH into a Railway dyno to do that by hand. -> the discovered ["name","description","inputSchema"]. Raises on a missing/disabled instance or a transport failure — the caller (a CLI verb/API route/UI button, PR3/PR4) decides how to surface it.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/insika/mcp_tool_registry.rb', line 39

def refresh(name)
  record = @mcp_store.get_raw(name.to_s)
  raise Insika::NotFoundError, "MCP instance '#{name}' not found" if record.nil?
  raise Insika::ValidationError, "MCP instance '#{name}' is disabled" unless record["enabled"]

  evict(record["name"])
  client = client_for(record)
  discovered = client.tools.map { |t| { "name" => t.name, "description" => t.description, "inputSchema" => t.params_schema } }
  @mcp_store.set_tools_cache(name, discovered)
  discovered
end