Skip to documentation

Ports

Tools, MCP & HITL

An agent's power comes from its tools. eli.ai ships a fixed set of read-only KB tools, adapts external tools from MCP servers, and gates anything with side effects behind human approval. A single registry enforces the allowlist and injects tenant scope into every call.

Native KB tools

All six are read-only and scope every query to the injected workspaceId. Results are trimmed hard so tool payloads stay bounded.

  • kb_search — hybrid lexical + semantic search; returns the most relevant chunks with metadata and scores, and registers each as a citation.
  • kb_doc_read — read a full document by docId (long docs truncated with a truncated flag).
  • kb_list_docs — list documents, optionally by path prefix.
  • kb_entity_lookup — resolve entities by name (exact / alias / fuzzy) and return entity cards with type, aliases, mention counts, and top relations.
  • kb_graph_query — expand the graph around seed entities. Takes a structured params object (seed names, depth 1–3, relation-type and entity-type filters) — not a string DSL — and returns nodes, typed edges, and an explicit truncated flag when per-hop or total caps cut the frontier.
  • kb_data — the semantic↔data bridge: discover the entity bindings and domain components in scope, then lookup — execute a governed, read-only named query against a scoped external connector. Every execution writes a provenance row and registers a [Dn] citation. See Entity bindings & live data.

Read-only by design

There is no fetch tool and no write tool in v1. The real injection defense is read-only tools plus governed network egress— only provider calls and the data layer's admin-registered, read-only connectors leave the box, and kb_data can only execute human-authored named queries, never model-written SQL. Assume injection succeeds; bound the blast radius. Every tool error becomes structured { ok: false, error } data the model can recover from — never a dropped result (a dropped result trains the model to stop calling tools).

Registry & scope injection

The registry merges native + graph + MCP tools, filters to the agent's exact allowlist (unknown names degrade with a warning, never crash), and adapts each to the provider tool schema. It injects the tool context per call:

scope is injected, never model-suppliedts
// The registry builds ctx; the model only supplies the tool's own inputs.
ctxFactory = () => ({
  workspaceId: env.workspaceId,   // from the run, not the prompt
  kbScope: env.config.kbScope,    // folders / tags / entityTypes
  runId: env.runId,
});
// A tool that ignored ctx.workspaceId could not compile the query under RLS.

Because scope is injected, an MCP tool can no more cross workspaces than a native tool can. The kbScope also narrows what a specific agent may see — restrict it to certain folders, tags, or entity types.

MCP servers

The Model Context Protocol lets an agent use external tools. eli.ai's default path is a host-side MCP client that works on every provider: you register a server (transport stdio, http, or sse; credentials stored in the encrypted vault), run discovery once, and its tools are cached in mcp_tools.

  • Discovered tools join the registry named mcp:<server>:<tool>, subject to the same allowlist, scoping, guardrail, and approval machinery as native tools.
  • Assembly is cache-only: agent runs read the discovered-tools rows and never open a connection. Discovery happens explicitly from settings or the API, not on every run.
  • stdio transports spawn a local process and are refused unless ELI_ALLOW_STDIO_MCP=true is set — spawning from a server is a deliberate opt-in.
  • Anthropic-backed agents may optionally use the server-side MCP connector (Tier 1) as an efficiency option — offered, never the sole path.

Human-in-the-loop (HITL)

A tool flagged requiresApproval suspends the run before it executes. In v1 all MCP tools require approval — they can have external side effects. When the model calls such a tool without a prior approval, the harness throws a control-flow signal that:

  1. Sets the run status to suspended and persists a run_approvals row with the tool name and the exact input payload.
  2. Emits an approval request (visible in the Approvals UI and over the API).
  3. On a human allow / deny decision, resumes the run from the persisted cursor — a denied call returns an error result the model can react to; an allowed call executes.

The suspend/resume record is durable by construction: the human can take hours, the process can restart, and the run continues. Approval decisions are themselves feedback signal.

Try it

Walk the full approve/deny flow in Approvals & feedback, or drive it programmatically via POST /api/v1/runs/:id/approve.