Class: Insika::McpStore

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

Coercion::TRUTHY

Instance Method Summary collapse

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

#allObject

-> [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_rawObject

-> [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_namesObject

-> [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

#namesObject

-> [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