Class: Insika::McpToolIngestor
- Inherits:
-
Object
- Object
- Insika::McpToolIngestor
- Defined in:
- lib/insika/mcp_tool_ingestor.rb
Overview
LIVE MCP ingestion (Phase 7, Stage E / spec §4 D8): 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 Stage B 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 (NF1): nothing here mentions achei/openclaw. 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 Stage C per-group gating
(tools_allow_groups) works for free.
DEFERRED / OUT-OF-SCOPE (documented — spec §4 D8):
- 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).
- Credential injection (the instance `env`) as an auth header in the HTTP
binding: the `env` is consumed by a real MCP client (deferred), not mapped
to a header here.
- Tools whose name/top-level property is outside the ToolDefinition NAME_RE
(uppercase/hyphens) are ISOLATED into `errors[]` by the import (R4).
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.
41 42 43 44 45 46 47 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 41 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: }).
52 53 54 55 56 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 52 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.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/insika/mcp_tool_ingestor.rb', line 59 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 — D8)" end tools = Array((client || @client_factory.call(record)).list_tools) build_manifest(name.to_s, url, tools) end |