Class: Insika::McpStore
- Inherits:
-
Object
- Object
- Insika::McpStore
- Includes:
- Coercion
- Defined in:
- lib/insika/mcp_store.rb
Overview
MCP instances authored at runtime. One
record per instance in the ConfigStore (scope "mcp"), keyed by the name slug
(tavily, github, ...). Holds transport/command/args/url, the enabled
flag, and two credential Hashes: env (stdio child-process environment) and
headers (http/sse request headers). env used to double as
"HTTP headers" for http/sse instances; a record written under that old
meaning is migrated ON READ (see migrate_legacy_headers!), never rewritten
silently (insika doctor flags it so the operator re-saves it explicitly).
Credentials (env/headers) NEVER leave here in plaintext to the UI: the
display reads (get/all) mask EACH value with the __OCULTO__ sentinel.
Only get_raw/all_raw (consumed by Insika::McpClient, never by the
screen) return the real values. On write, the sentinel coming back
preserves the value; a new string replaces it; "" clears it (see
Insika::SecretMasking, the same pattern as the LLM api_keys).
Also holds tools_cache — the last live discovery result
(Insika::McpToolRegistry#refresh writes it; entries/Studio/doctor read
it for cheap display; tool EXECUTION never depends on it.
Not a credential -> never masked.
Durable config CRUD (the instances UI) + the shape Insika::McpClient.for/Insika::McpToolRegistry read.
Constant Summary collapse
- SCOPE =
"mcp"
Constants included from Coercion
Instance Method Summary collapse
-
#all ⇒ Object
-> [Hash] all MASKED (for the UI).
-
#all_raw ⇒ Object
-> [Hash] all with REAL env (for an MCP client).
-
#delete(name) ⇒ Object
-> bool (did it exist?).
-
#get(name) ⇒ Object
-> MASKED Hash (env with sentinel) | nil.
-
#get_raw(name) ⇒ Object
-> Hash with REAL env | nil.
-
#initialize(config_store:) ⇒ McpStore
constructor
A new instance of McpStore.
-
#legacy_header_names ⇒ Object
-> [String] names of http/sse instances still stored under the pre- Meaning of
env(insika doctor's "mcp" check;raw/all_rawalready read them correctly — this is only to flag the ones that still need a re-save so the stored record itself catches up). -
#names ⇒ Object
-> [String] slugs, lexicographic order.
-
#set_tools_cache(name, tools) ⇒ Object
System-written cache of the instance's discovered tools: Insika::McpToolRegistry#refresh connects live and writes the result here for cheap display (Studio/doctor/
entries) — execution never reads it back, only the live client. -
#upsert(attrs) ⇒ Object
Upsert with per-key secret reconciliation.
Methods included from Coercion
blank?, deep_stringify, presence, present?, truthy?, utf8
Constructor Details
#initialize(config_store:) ⇒ McpStore
Returns a new instance of McpStore.
32 33 34 |
# File 'lib/insika/mcp_store.rb', line 32 def initialize(config_store:) @cs = config_store end |
Instance Method Details
#all ⇒ Object
-> [Hash] all MASKED (for the UI).
50 51 52 |
# File 'lib/insika/mcp_store.rb', line 50 def all names.filter_map { |n| get(n) } end |
#all_raw ⇒ Object
-> [Hash] all with REAL env (for an MCP client). Never goes to the screen.
55 56 57 |
# File 'lib/insika/mcp_store.rb', line 55 def all_raw names.filter_map { |n| raw(n) } end |
#delete(name) ⇒ Object
-> bool (did it exist?).
104 |
# File 'lib/insika/mcp_store.rb', line 104 def delete(name) = @cs.delete(SCOPE, name.to_s) |
#get(name) ⇒ Object
-> MASKED Hash (env with sentinel) | nil.
37 38 39 |
# File 'lib/insika/mcp_store.rb', line 37 def get(name) mask(raw(name)) end |
#get_raw(name) ⇒ Object
-> Hash with REAL env | nil. Internal use (MCP client), never the screen.
42 43 44 |
# File 'lib/insika/mcp_store.rb', line 42 def get_raw(name) raw(name) end |
#legacy_header_names ⇒ Object
-> [String] names of http/sse instances still stored under the pre-
Meaning of env (insika doctor's "mcp" check; raw/all_raw
already read them correctly — this is only to flag the ones that still
need a re-save so the stored record itself catches up).
110 111 112 |
# File 'lib/insika/mcp_store.rb', line 110 def legacy_header_names names.select { |n| needs_header_migration?(@cs.get(SCOPE, n)) } end |
#names ⇒ Object
-> [String] slugs, lexicographic order.
47 |
# File 'lib/insika/mcp_store.rb', line 47 def names = @cs.keys(SCOPE) |
#set_tools_cache(name, tools) ⇒ Object
System-written cache of the instance's discovered tools:
Insika::McpToolRegistry#refresh connects live and writes the result here
for cheap display (Studio/doctor/entries) — execution never reads it
back, only the live client. NOT a credential -> never masked. -> the
MASKED record (mirrors upsert's return).
64 65 66 67 68 69 70 71 |
# File 'lib/insika/mcp_store.rb', line 64 def set_tools_cache(name, tools) existing = raw(name) raise Insika::NotFoundError, "MCP instance '#{name}' not found" if existing.nil? record = existing.merge("tools_cache" => normalize_tools_cache(tools)) @cs.put(SCOPE, name, record) mask(record) end |
#upsert(attrs) ⇒ Object
Upsert with per-key secret reconciliation. attrs
(string|symbol keys):
name (required), transport, command, args ([String, ...]), url,
description, enabled (bool), env ({ "KEY" => value|sentinel|"" }),
headers ({ "Header-Name" => value|sentinel|"" })
-> MASKED Hash (the stored record).
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/insika/mcp_store.rb', line 79 def upsert(attrs) h = symbolize(attrs) name = presence(h[:name]) raise Insika::ValidationError, "name is required" if name.nil? existing = raw(name) record = { "name" => name, "transport" => presence(h[:transport]) || "stdio", "command" => presence(h[:command]), "args" => Array(h[:args]).map(&:to_s).reject(&:empty?), "url" => presence(h[:url]), "description" => presence(h[:description]), "enabled" => h.fetch(:enabled, true) ? true : false, "env" => reconcile_hash(h[:env], existing && existing["env"]), "headers" => reconcile_hash(h[:headers], existing && existing["headers"]), # editing an instance's connection details doesn't change its discovered # tools -> preserved across upsert; only set_tools_cache writes it. "tools_cache" => (existing && existing["tools_cache"]) || [] } @cs.put(SCOPE, name, record) mask(record) end |