insika 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -2
  3. data/README.md +10 -6
  4. data/bin/insika +4 -1
  5. data/bin/insika-router +0 -1
  6. data/docs/AGENTS.md +142 -18
  7. data/docs/API.md +73 -0
  8. data/docs/ARCHITECTURE.md +56 -50
  9. data/docs/ARTIFACTS.md +33 -3
  10. data/docs/BENCHMARK.md +50 -7
  11. data/docs/CHANNELS.md +19 -2
  12. data/docs/CONTEXT.md +96 -27
  13. data/docs/CROSS-HARNESS-BENCH.md +157 -0
  14. data/docs/DEPLOY.md +70 -13
  15. data/docs/EMBEDDING.md +10 -0
  16. data/docs/EVALS.md +129 -10
  17. data/docs/FACTS.md +12 -6
  18. data/docs/HARVEST.md +8 -3
  19. data/docs/KNOWLEDGE.md +14 -0
  20. data/docs/MEDIA.md +6 -2
  21. data/docs/OBSERVABILITY.md +164 -28
  22. data/docs/PERFORMANCE.md +110 -0
  23. data/docs/PLUGINS.md +4 -4
  24. data/docs/POLICY.md +26 -14
  25. data/docs/RELEASING.md +9 -0
  26. data/docs/ROUTER.md +7 -0
  27. data/docs/RUBYLLM_2_MIGRATION.md +307 -0
  28. data/docs/SECURITY.md +34 -1
  29. data/docs/SHARED-CONVERSATIONS.md +40 -0
  30. data/docs/SKILLS.md +5 -3
  31. data/docs/STUDIO.md +122 -0
  32. data/docs/TOOLS.md +238 -42
  33. data/docs/operate.md +3 -0
  34. data/docs/prompts/ADD-TOOL.md +12 -2
  35. data/docs/prompts/DIAGNOSE-TURN.md +3 -0
  36. data/docs/prompts/GO-LIVE.md +3 -1
  37. data/lib/insika/agent_defaults.rb +140 -0
  38. data/lib/insika/agent_file_store.rb +12 -0
  39. data/lib/insika/agent_profile.rb +99 -17
  40. data/lib/insika/artifact_signing.rb +11 -0
  41. data/lib/insika/channels/web/widget.js +33 -0
  42. data/lib/insika/channels/web.rb +5 -2
  43. data/lib/insika/chat_builder.rb +124 -28
  44. data/lib/insika/checkpoint.rb +6 -1
  45. data/lib/insika/checkpoint_store.rb +59 -7
  46. data/lib/insika/commands/agent_payload.rb +3 -3
  47. data/lib/insika/commands/delete_kit.rb +26 -0
  48. data/lib/insika/commands/gate_harvest.rb +3 -0
  49. data/lib/insika/commands/review_guardrail_flag.rb +38 -0
  50. data/lib/insika/commands/run_distillation.rb +5 -8
  51. data/lib/insika/commands/run_harvest.rb +17 -1
  52. data/lib/insika/commands/seed_session.rb +128 -0
  53. data/lib/insika/commands/send_message.rb +9 -3
  54. data/lib/insika/commands/update_settings.rb +23 -1
  55. data/lib/insika/commands/write_kit.rb +27 -0
  56. data/lib/insika/compaction.rb +33 -7
  57. data/lib/insika/config_store.rb +87 -7
  58. data/lib/insika/context/builder.rb +29 -9
  59. data/lib/insika/context/priority.rb +5 -0
  60. data/lib/insika/context/provider.rb +7 -2
  61. data/lib/insika/context/providers/briefing.rb +11 -8
  62. data/lib/insika/context/providers/fence_notice.rb +27 -0
  63. data/lib/insika/context/providers/knowledge.rb +32 -6
  64. data/lib/insika/context/providers/memory.rb +61 -8
  65. data/lib/insika/context/providers/pending_confirmation.rb +52 -0
  66. data/lib/insika/context/providers/prompt.rb +17 -4
  67. data/lib/insika/context/providers/session.rb +46 -6
  68. data/lib/insika/context/reranker.rb +75 -0
  69. data/lib/insika/distill.rb +22 -13
  70. data/lib/insika/doctor.rb +142 -4
  71. data/lib/insika/dsl/runtime.rb +37 -4
  72. data/lib/insika/dsl/server_boot.rb +4 -12
  73. data/lib/insika/dsl.rb +19 -5
  74. data/lib/insika/edge_limiter.rb +27 -16
  75. data/lib/insika/egress_guard.rb +11 -3
  76. data/lib/insika/env_schema.rb +33 -73
  77. data/lib/insika/errors.rb +1 -0
  78. data/lib/insika/evals/assertions.rb +259 -18
  79. data/lib/insika/evals/golden.rb +230 -4
  80. data/lib/insika/evals/judge.rb +19 -10
  81. data/lib/insika/evals/moderator_compare.rb +47 -0
  82. data/lib/insika/evals/report.rb +61 -9
  83. data/lib/insika/evals/runner.rb +64 -4
  84. data/lib/insika/evals/simulator.rb +21 -3
  85. data/lib/insika/evals/transport.rb +317 -15
  86. data/lib/insika/event_stream.rb +63 -7
  87. data/lib/insika/evidence.rb +105 -17
  88. data/lib/insika/executor.rb +698 -164
  89. data/lib/insika/fence.rb +96 -0
  90. data/lib/insika/golden_store.rb +3 -0
  91. data/lib/insika/harvest.rb +26 -14
  92. data/lib/insika/harvest_engine.rb +1 -0
  93. data/lib/insika/harvest_store.rb +12 -0
  94. data/lib/insika/http_client.rb +87 -4
  95. data/lib/insika/kits.rb +54 -0
  96. data/lib/insika/knowledge.rb +34 -20
  97. data/lib/insika/llm_configurator.rb +46 -4
  98. data/lib/insika/llm_provider_store.rb +1 -3
  99. data/lib/insika/llm_trace_store.rb +54 -0
  100. data/lib/insika/mcp_client.rb +27 -63
  101. data/lib/insika/mcp_live_tool.rb +55 -9
  102. data/lib/insika/mcp_store.rb +37 -3
  103. data/lib/insika/mcp_tool_registry.rb +76 -26
  104. data/lib/insika/media.rb +38 -25
  105. data/lib/insika/memory_store.rb +16 -3
  106. data/lib/insika/memory_trim.rb +60 -0
  107. data/lib/insika/middleware.rb +12 -3
  108. data/lib/insika/model_metrics_store.rb +285 -0
  109. data/lib/insika/model_registry.rb +110 -0
  110. data/lib/insika/model_resolver.rb +1 -1
  111. data/lib/insika/model_selection.rb +14 -21
  112. data/lib/insika/model_visible.rb +17 -6
  113. data/lib/insika/onboarding.rb +1 -0
  114. data/lib/insika/outcome_store.rb +1 -1
  115. data/lib/insika/overlay_tool_registry.rb +30 -2
  116. data/lib/insika/pack_importer.rb +54 -9
  117. data/lib/insika/packaging.rb +14 -5
  118. data/lib/insika/pending_action_store.rb +45 -18
  119. data/lib/insika/plugin/loader.rb +4 -2
  120. data/lib/insika/prefix_fingerprint.rb +32 -27
  121. data/lib/insika/profile_source.rb +5 -0
  122. data/lib/insika/provider_error_classifier.rb +18 -3
  123. data/lib/insika/queue_policy.rb +7 -2
  124. data/lib/insika/refinement/proposer.rb +42 -17
  125. data/lib/insika/reliability.rb +12 -3
  126. data/lib/insika/router/app.rb +14 -5
  127. data/lib/insika/router/session_key.rb +35 -5
  128. data/lib/insika/safety/config.rb +20 -3
  129. data/lib/insika/safety/corpus.rb +10 -3
  130. data/lib/insika/safety/factory.rb +39 -1
  131. data/lib/insika/safety/input_guardrail.rb +21 -1
  132. data/lib/insika/safety/judge_moderator.rb +70 -0
  133. data/lib/insika/safety/output_validator.rb +29 -5
  134. data/lib/insika/safety/prompt_echo.rb +89 -0
  135. data/lib/insika/schema_guard.rb +53 -76
  136. data/lib/insika/server/app.rb +58 -7
  137. data/lib/insika/server/rack_app.rb +2 -0
  138. data/lib/insika/server/responses.rb +50 -5
  139. data/lib/insika/session_actor.rb +58 -28
  140. data/lib/insika/session_store.rb +163 -11
  141. data/lib/insika/settings_store.rb +57 -2
  142. data/lib/insika/shared_conversations.rb +305 -0
  143. data/lib/insika/skill_catalog.rb +28 -2
  144. data/lib/insika/skill_store.rb +6 -0
  145. data/lib/insika/soak/runner.rb +1 -1
  146. data/lib/insika/spoken_transcript.rb +31 -0
  147. data/lib/insika/steer_injector.rb +7 -48
  148. data/lib/insika/store.rb +29 -1
  149. data/lib/insika/stores/json_value.rb +37 -0
  150. data/lib/insika/stores/memory.rb +22 -35
  151. data/lib/insika/stores/postgres.rb +295 -0
  152. data/lib/insika/stores/sqlite.rb +257 -43
  153. data/lib/insika/stores/turn_counter.rb +25 -0
  154. data/lib/insika/studio/app.rb +1459 -163
  155. data/lib/insika/studio/assets/dist/application.css +1 -1
  156. data/lib/insika/studio/assets/dist/application.js +25 -25
  157. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  158. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  159. data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  160. data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  161. data/lib/insika/studio/forms.rb +189 -12
  162. data/lib/insika/studio/nav_icons.rb +3 -0
  163. data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
  164. data/lib/insika/studio/views/_agent_picker.erb +15 -0
  165. data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
  166. data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
  167. data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
  168. data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
  169. data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
  170. data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
  171. data/lib/insika/studio/views/_agents_master.erb +8 -12
  172. data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
  173. data/lib/insika/studio/views/_cfg_budget.erb +20 -0
  174. data/lib/insika/studio/views/_cfg_distill.erb +26 -0
  175. data/lib/insika/studio/views/_cfg_followup.erb +30 -0
  176. data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
  177. data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
  178. data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
  179. data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
  180. data/lib/insika/studio/views/_chats_master.erb +66 -0
  181. data/lib/insika/studio/views/_customers_master.erb +37 -0
  182. data/lib/insika/studio/views/_empty.erb +15 -0
  183. data/lib/insika/studio/views/_facts_master.erb +42 -0
  184. data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
  185. data/lib/insika/studio/views/_identity.erb +16 -0
  186. data/lib/insika/studio/views/_kpi.erb +17 -0
  187. data/lib/insika/studio/views/_message.erb +40 -2
  188. data/lib/insika/studio/views/_model_chart.erb +82 -0
  189. data/lib/insika/studio/views/_refinement_master.erb +38 -0
  190. data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
  191. data/lib/insika/studio/views/_tool_users.erb +17 -0
  192. data/lib/insika/studio/views/_why.erb +53 -0
  193. data/lib/insika/studio/views/agent_detail.erb +17 -10
  194. data/lib/insika/studio/views/agents.erb +28 -16
  195. data/lib/insika/studio/views/approvals.erb +16 -11
  196. data/lib/insika/studio/views/artifact.erb +20 -7
  197. data/lib/insika/studio/views/artifacts.erb +19 -14
  198. data/lib/insika/studio/views/chats.erb +22 -29
  199. data/lib/insika/studio/views/customer.erb +88 -43
  200. data/lib/insika/studio/views/customers.erb +16 -26
  201. data/lib/insika/studio/views/evals.erb +49 -31
  202. data/lib/insika/studio/views/fact_detail.erb +114 -0
  203. data/lib/insika/studio/views/facts.erb +15 -116
  204. data/lib/insika/studio/views/followups.erb +63 -52
  205. data/lib/insika/studio/views/funnel.erb +104 -43
  206. data/lib/insika/studio/views/harvest.erb +85 -59
  207. data/lib/insika/studio/views/home.erb +138 -23
  208. data/lib/insika/studio/views/inbox.erb +36 -0
  209. data/lib/insika/studio/views/kits.erb +176 -0
  210. data/lib/insika/studio/views/knowledge.erb +92 -80
  211. data/lib/insika/studio/views/layout.erb +42 -11
  212. data/lib/insika/studio/views/live.erb +45 -0
  213. data/lib/insika/studio/views/mcp.erb +43 -13
  214. data/lib/insika/studio/views/models.erb +187 -0
  215. data/lib/insika/studio/views/parity.erb +38 -32
  216. data/lib/insika/studio/views/playground.erb +13 -9
  217. data/lib/insika/studio/views/refinement.erb +15 -224
  218. data/lib/insika/studio/views/refinement_detail.erb +243 -0
  219. data/lib/insika/studio/views/session.erb +153 -118
  220. data/lib/insika/studio/views/settings.erb +86 -22
  221. data/lib/insika/studio/views/skills.erb +53 -19
  222. data/lib/insika/studio/views/system_files.erb +7 -7
  223. data/lib/insika/studio/views/task.erb +45 -15
  224. data/lib/insika/studio/views/tasks.erb +35 -18
  225. data/lib/insika/studio/views/tools.erb +62 -73
  226. data/lib/insika/task_actor.rb +4 -1
  227. data/lib/insika/task_store.rb +67 -1
  228. data/lib/insika/telemetry/pricing.rb +16 -14
  229. data/lib/insika/telemetry/recorder.rb +115 -9
  230. data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
  231. data/lib/insika/telemetry.rb +12 -2
  232. data/lib/insika/templates/daily-digest/README.md +13 -0
  233. data/lib/insika/templates/daily-digest/agent.rb +22 -11
  234. data/lib/insika/testing/store_contract.rb +65 -0
  235. data/lib/insika/text_tool_call.rb +66 -0
  236. data/lib/insika/tick.rb +7 -1
  237. data/lib/insika/token_store.rb +5 -0
  238. data/lib/insika/tool_assembly.rb +21 -13
  239. data/lib/insika/tool_batch.rb +1 -1
  240. data/lib/insika/tool_catalog.rb +12 -2
  241. data/lib/insika/tool_definition.rb +82 -13
  242. data/lib/insika/tool_envelope.rb +266 -29
  243. data/lib/insika/tool_manifest.rb +1 -0
  244. data/lib/insika/tool_store.rb +12 -4
  245. data/lib/insika/tool_trace_store.rb +15 -8
  246. data/lib/insika/tool_usage_report.rb +14 -4
  247. data/lib/insika/tools/a2a_remote.rb +1 -1
  248. data/lib/insika/tools/agent_enum.rb +1 -1
  249. data/lib/insika/tools/confirm_pending.rb +112 -0
  250. data/lib/insika/tools/data_defined_tool.rb +24 -15
  251. data/lib/insika/tools/generate_image.rb +2 -2
  252. data/lib/insika/tools/load_knowledge.rb +7 -3
  253. data/lib/insika/tools/load_skill.rb +3 -3
  254. data/lib/insika/tools/present.rb +115 -0
  255. data/lib/insika/tools/remember.rb +7 -2
  256. data/lib/insika/tools/run_persona_eval.rb +9 -6
  257. data/lib/insika/tools/save_artifact.rb +16 -6
  258. data/lib/insika/tools/schedule_followup.rb +3 -3
  259. data/lib/insika/tools/stuck_signal.rb +3 -3
  260. data/lib/insika/tools/subagent.rb +5 -5
  261. data/lib/insika/tools/subagents.rb +3 -3
  262. data/lib/insika/tools/tool_search.rb +11 -7
  263. data/lib/insika/tools/tts.rb +2 -2
  264. data/lib/insika/tools/update_briefing.rb +4 -4
  265. data/lib/insika/turn_output.rb +2 -2
  266. data/lib/insika/turn_state.rb +31 -1
  267. data/lib/insika/turn_timing.rb +51 -14
  268. data/lib/insika/version.rb +1 -1
  269. data/lib/insika/wiring/graph.rb +88 -16
  270. data/lib/insika/wiring/graph_chat.rb +4 -0
  271. data/lib/insika/workflow.rb +25 -64
  272. data/lib/insika.rb +23 -3
  273. metadata +74 -13
  274. data/lib/insika/commands/import_mcp_tools.rb +0 -48
  275. data/lib/insika/mcp_http_client.rb +0 -67
  276. data/lib/insika/mcp_tool_ingestor.rb +0 -165
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Insika
6
- # MINIMAL MCP client over HTTP JSON-RPC. Discovers the tools
7
- # of an MCP instance with HTTP transport by making a JSON-RPC 2.0 `tools/list`
8
- # POST to the instance endpoint, behind the EgressGuard (SSRF — the url comes
9
- # from editable config). It is the DEFAULT client injected into the
10
- # McpToolIngestor; tests pass a Fake (duck-typed) in its place.
11
- #
12
- # Contract (MCP client duck-type): `#list_tools -> [{name, description,
13
- # inputSchema}]` — the same MCP envelope that the ToolManifest adapter normalizes.
14
- #
15
- # SCOPE (bounded): only the minimal handshake of ONE stateless `tools/list`
16
- # POST. Does NOT implement the full MCP session lifecycle (initialize/protocol
17
- # negotiation/session-id/notifications) nor the stdio transport — that is the
18
- # "real MCP transport", later work (out-of-scope, see spec). It serves
19
- # simple HTTP MCP servers (direct JSON-RPC) and proves the ingestion seam.
20
- class McpHttpClient
21
- JSONRPC_VERSION = "2.0"
22
-
23
- def initialize(url:, http: Insika::HttpClient.new, egress: Insika::EgressGuard,
24
- egress_options: {}, headers: {}, timeout: nil)
25
- @url = url.to_s
26
- @http = http
27
- @egress = egress
28
- @egress_options = egress_options
29
- @headers = { "Content-Type" => "application/json", "Accept" => "application/json" }.merge(headers || {})
30
- @timeout = timeout
31
- end
32
-
33
- # -> [ { "name", "description", "inputSchema" } ]. Raises Insika::Error on
34
- # blocked egress, HTTP != 2xx, invalid JSON or a JSON-RPC error.
35
- def list_tools
36
- reason = @egress.violation(@url, **@egress_options)
37
- raise Insika::Error, "MCP target blocked: #{reason}" if reason
38
-
39
- result = @http.request(method: "POST", url: @url, headers: @headers,
40
- body: request_body("tools/list", {}), timeout: @timeout)
41
- Array(rpc_result(result).fetch("tools", []))
42
- end
43
-
44
- private
45
-
46
- def request_body(method, params)
47
- JSON.generate(jsonrpc: JSONRPC_VERSION, id: 1, method: method, params: params)
48
- end
49
-
50
- # Validates the JSON-RPC envelope and returns `result` (Hash). HTTP/parse/error -> Error.
51
- def rpc_result(result)
52
- status = result[:status].to_i
53
- raise Insika::Error, "MCP HTTP #{status}: #{result[:body].to_s[0, 200]}" if status >= 400
54
-
55
- parsed = begin
56
- JSON.parse(result[:body].to_s)
57
- rescue JSON::ParserError => e
58
- raise Insika::Error, "MCP response is not JSON: #{e.message}"
59
- end
60
- if (err = parsed["error"])
61
- raise Insika::Error, "MCP JSON-RPC error: #{err["message"] || err.inspect}"
62
- end
63
-
64
- parsed["result"] || {}
65
- end
66
- end
67
- end
@@ -1,165 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Insika
6
- # LIVE MCP ingestion (/ spec): discovers the tools of an
7
- # MCP instance at RUNTIME (no hand-written manifest) and ingests them as
8
- # data-tools. Given an McpStore instance + an INJECTABLE MCP client
9
- # (duck-typed: `#list_tools -> [{name, description, inputSchema}]`), it builds a
10
- # ToolManifest and REUSES the ingestion path (the:import_tools Command:
11
- # batch upsert into the ToolStore + hot reload + per-tool report + partial-
12
- # failure isolation R4). The ToolManifest MCP adapter (`inputSchema`) is reused
13
- # — no schema parsing here.
14
- #
15
- # GENERIC: nothing here mentions a consumer/gateway. The MCP instance is DATA in the store.
16
- #
17
- # BINDING STRATEGY (this stage's choice, bounded):
18
- # Each discovered tool becomes an HTTP data-tool that makes a JSON-RPC 2.0
19
- # `tools/call` POST to the instance endpoint (url). The tool name is resolved
20
- # at INGESTION (literal in the body); the model's arguments come in as `{{param}}`
21
- # per TOP-level property of the inputSchema (with quoting by type — strings
22
- # quoted, others raw, via the DataDefinedTool :body encode). This way the tool
23
- # runs through the SAME HTTP path as the other data-tools (egress guard, secret
24
- # headers, hot reload) — no new execution code.
25
- #
26
- # Each tool gets `group: "mcp:<instance>"` so the per-group gating
27
- # (tools_allow_groups) works for free.
28
- #
29
- # DEFERRED / OUT-OF-SCOPE (documented — spec):
30
- # - Real MCP transport: only instances with a `url` (http transport) are ingestible;
31
- # stdio has no HTTP endpoint -> raises a clear error (later work).
32
- # - MCP session lifecycle (initialize/negotiation/session-id/notifications) and the
33
- # UNWRAP of the `tools/call` response (`{content:[{type,text}]}`) — the binding
34
- # makes a stateless POST and returns the raw body (extract body_raw). A server
35
- # that requires the handshake before answering `tools/list`/`tools/call` is not
36
- # reachable yet — that IS the real transport, not this minimal client.
37
- # - Tools whose name/top-level property is outside the ToolDefinition NAME_RE
38
- # (uppercase/hyphens) are ISOLATED into `errors[]` by the import (R4).
39
- #
40
- # CREDENTIAL INJECTION: the instance `env` (Hash) is sent verbatim as HTTP
41
- # headers, on BOTH the discovery request (`tools/list`, this file) and every
42
- # ingested tool's `tools/call` binding (`build_manifest`'s `defaults.headers`)
43
- # — literal header-name -> value, the same convention as a data-tool's own
44
- # `secret_headers` (the operator types the full value, "Bearer xxx" included;
45
- # no magic prefixing). An instance with no `env` sends the bare
46
- # `Content-Type` header only, byte-for-byte what shipped before this.
47
- class McpToolIngestor
48
- def initialize(mcp_store:, import_tools:, client_factory: nil)
49
- @mcp_store = mcp_store
50
- @import_tools = import_tools
51
- # Per-instance client factory (default: minimal JSON-RPC HTTP client).
52
- # Injectable for tests (Fake) and to swap for a real transport later.
53
- @client_factory = client_factory || method(:default_client)
54
- end
55
-
56
- # Discovers + ingests the tools of instance `name`. `client` is injectable
57
- # (Fake in tests); absent -> the factory builds one from the record. -> the
58
- # import_tools report + `instance:` ({ instance:, version:, created:, updated:, errors: }).
59
- def ingest(name, client: nil)
60
- manifest = manifest_for(name, client: client)
61
- report = @import_tools.call(Insika::Command.build(:import_tools, manifest, transport: :internal))
62
- report.merge(instance: name.to_s)
63
- end
64
-
65
- # Discovers the tools and builds the manifest Hash (without ingesting) — isolable for testing.
66
- def manifest_for(name, client: nil)
67
- record = @mcp_store.get_raw(name.to_s)
68
- raise Insika::NotFoundError, "MCP instance '#{name}' not found" if record.nil?
69
- raise Insika::ValidationError, "MCP instance '#{name}' is disabled" unless record["enabled"]
70
-
71
- url = presence(record["url"])
72
- if url.nil?
73
- raise Insika::ValidationError,
74
- "MCP instance '#{name}' has no url: live ingestion requires HTTP transport " \
75
- "(stdio is later work)"
76
- end
77
-
78
- tools = Array((client || @client_factory.call(record)).list_tools)
79
- build_manifest(name.to_s, url, tools, env_headers(record))
80
- end
81
-
82
- private
83
-
84
- # `headers` (an http/sse instance's credentials — `env` reads
85
- # as `headers` for these transports via McpStore's migration-on-read, see
86
- # McpStore#migrate_legacy_headers) -> literal HTTP headers (header-NAME ->
87
- # value; the value carries its own scheme, e.g. "Bearer xxx" — no magic
88
- # prefixing). NOT `secret_headers`: ToolManifest's own rule for a secret
89
- # header requires a `{{secret.*}}` reference, which is the opposite of what
90
- # a literal credential is — so an MCP instance's headers are NOT masked in
91
- # a tool's edit page in /studio/tools, unlike a hand-authored data-tool's
92
- # Authorization header. Acceptable for now (Studio is already
93
- # operator-gated); a real fix routes this through the deployment's own
94
- # {{secret.*}}/{{env.*}} resolution instead — later work.
95
- def env_headers(record)
96
- (record["headers"] || {}).each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
97
- end
98
-
99
- def build_manifest(name, url, tools, headers)
100
- {
101
- "version" => 1,
102
- "defaults" => {
103
- "method" => "POST",
104
- "headers" => { "Content-Type" => "application/json" }.merge(headers),
105
- "response" => { "extract" => "body_raw" },
106
- "group" => "mcp:#{name}"
107
- },
108
- "tools" => tools.map { |raw| tool_entry(name, url, stringify(raw)) }
109
- }
110
- end
111
-
112
- # Raw MCP entry -> manifest entry (MCP `inputSchema` envelope + JSON-RPC
113
- # binding). ToolManifest normalizes the `inputSchema` (MCP adapter) and
114
- # inherits the defaults; `group` falls through from the defaults.
115
- def tool_entry(name, url, raw)
116
- tool_name = raw["name"]
117
- input_schema = raw["inputSchema"] || {}
118
- {
119
- "name" => tool_name,
120
- "description" => presence(raw["description"]) || "Tool '#{tool_name}' from MCP server '#{name}'.",
121
- "inputSchema" => input_schema,
122
- "url" => url,
123
- "side_effect" => true, # a tools/call is a side effect (checkpoint/skip-on-resume)
124
- "body" => jsonrpc_call_body(tool_name, input_schema)
125
- }
126
- end
127
-
128
- # JSON-RPC 2.0 `tools/call` body. `name` literal (resolved at ingestion);
129
- # `arguments` per TOP-level property of the inputSchema, with `{{param}}` that
130
- # the DataDefinedTool interpolates at TURN time.
131
- def jsonrpc_call_body(tool_name, input_schema)
132
- %({"jsonrpc":"2.0","id":1,"method":"tools/call",) +
133
- %("params":{"name":#{JSON.generate(tool_name)},"arguments":#{arguments_fragment(input_schema)}}})
134
- end
135
-
136
- # -> "{...}" JSON with one placeholder per top-level property. Strings are
137
- # quoted (the DataDefinedTool :body encode returns the escaped content WITHOUT
138
- # quotes); other types raw (the encode returns value.to_json).
139
- def arguments_fragment(input_schema)
140
- props = (input_schema["properties"] || input_schema[:properties] || {})
141
- return "{}" if props.nil? || props.empty?
142
-
143
- pairs = props.map do |key, spec|
144
- type = stringify(spec)["type"].to_s
145
- placeholder = type == "string" ? %("{{#{key}}}") : "{{#{key}}}"
146
- %(#{JSON.generate(key.to_s)}:#{placeholder})
147
- end
148
- "{#{pairs.join(',')}}"
149
- end
150
-
151
- def default_client(record)
152
- Insika::McpHttpClient.new(url: record["url"], headers: env_headers(record))
153
- end
154
-
155
- def presence(str) = Insika::Coercion.presence(str)
156
-
157
- def stringify(obj)
158
- case obj
159
- when Hash then obj.each_with_object({}) { |(k, v), acc| acc[k.to_s] = stringify(v) }
160
- when Array then obj.map { |v| stringify(v) }
161
- else obj
162
- end
163
- end
164
- end
165
- end