Class: Insika::McpToolIngestor
- Inherits:
-
Object
- Object
- Insika::McpToolIngestor
- Defined in:
- lib/insika/mcp_tool_ingestor.rb
Overview
LIVE MCP ingestion (/ spec): discovers the tools of an
MCP instance at RUNTIME (no hand-written manifest) and ingests them as
data-tools. Given an McpStore instance + an INJECTABLE MCP client
(duck-typed: #list_tools -> [{name, description, inputSchema}]), it builds a
ToolManifest and REUSES the ingestion path (the:import_tools Command:
batch upsert into the ToolStore + hot reload + per-tool report + partial-
failure isolation R4). The ToolManifest MCP adapter (inputSchema) is reused
— no schema parsing here.
GENERIC: nothing here mentions a consumer/gateway. The MCP instance is DATA in the store.
BINDING STRATEGY (this stage's choice, bounded):
Each discovered tool becomes an HTTP data-tool that makes a JSON-RPC 2.0
`tools/call` POST to the instance endpoint (url). The tool name is resolved
at INGESTION (literal in the body); the model's arguments come in as `{{param}}`
per TOP-level property of the inputSchema (with quoting by type — strings
quoted, others raw, via the DataDefinedTool :body encode). This way the tool
runs through the SAME HTTP path as the other data-tools (egress guard, secret
headers, hot reload) — no new execution code.
Each tool gets `group: "mcp:<instance>"` so the per-group gating
(tools_allow_groups) works for free.
DEFERRED / OUT-OF-SCOPE (documented — spec):
- Real MCP transport: only instances with a `url` (http transport) are ingestible;
stdio has no HTTP endpoint -> raises a clear error (later work).
- MCP session lifecycle (initialize/negotiation/session-id/notifications) and the
UNWRAP of the `tools/call` response (`{content:[{type,text}]}`) — the binding
makes a stateless POST and returns the raw body (extract body_raw). A server
that requires the handshake before answering `tools/list`/`tools/call` is not
reachable yet — that IS the real transport, not this minimal client.
- Tools whose name/top-level property is outside the ToolDefinition NAME_RE
(uppercase/hyphens) are ISOLATED into `errors[]` by the import (R4).
CREDENTIAL INJECTION: the instance env (Hash) is sent verbatim as HTTP
headers, on BOTH the discovery request (tools/list, this file) and every
ingested tool's tools/call binding (build_manifest's defaults.headers)
— literal header-name -> value, the same convention as a data-tool's own
secret_headers (the operator types the full value, "Bearer xxx" included;
no magic prefixing). An instance with no env sends the bare
Content-Type header only, byte-for-byte what shipped before this.
Instance Method Summary collapse
-
#ingest(name, client: nil) ⇒ Object
Discovers + ingests the tools of instance
name. -
#initialize(mcp_store:, import_tools:, client_factory: nil) ⇒ McpToolIngestor
constructor
A new instance of McpToolIngestor.
-
#manifest_for(name, client: nil) ⇒ Object
Discovers the tools and builds the manifest Hash (without ingesting) — isolable for testing.
Constructor Details
#initialize(mcp_store:, import_tools:, client_factory: nil) ⇒ McpToolIngestor
Returns a new instance of McpToolIngestor.
48 49 50 51 52 53 54 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 48 def initialize(mcp_store:, import_tools:, client_factory: nil) @mcp_store = mcp_store @import_tools = import_tools # Per-instance client factory (default: minimal JSON-RPC HTTP client). # Injectable for tests (Fake) and to swap for a real transport later. @client_factory = client_factory || method(:default_client) end |
Instance Method Details
#ingest(name, client: nil) ⇒ Object
Discovers + ingests the tools of instance name. client is injectable
(Fake in tests); absent -> the factory builds one from the record. -> the
import_tools report + instance: ({ instance:, version:, created:, updated:, errors: }).
59 60 61 62 63 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 59 def ingest(name, client: nil) manifest = manifest_for(name, client: client) report = @import_tools.call(Insika::Command.build(:import_tools, manifest, transport: :internal)) report.merge(instance: name.to_s) end |
#manifest_for(name, client: nil) ⇒ Object
Discovers the tools and builds the manifest Hash (without ingesting) — isolable for testing.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 66 def manifest_for(name, client: nil) 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"] url = presence(record["url"]) if url.nil? raise Insika::ValidationError, "MCP instance '#{name}' has no url: live ingestion requires HTTP transport " \ "(stdio is later work)" end tools = Array((client || @client_factory.call(record)).list_tools) build_manifest(name.to_s, url, tools, env_headers(record)) end |