Motivation / problem
Every other connector in AskMyDocs (Google Drive, IMAP, …) does the same thing: download → chunk → embed. That is exactly right for knowledge — policies, runbooks, wikis, mail threads — content that is meaningful to vectorise and ages slowly. It is exactly wrong for transactional, fast-moving data. The status of order#10293, today’s stock for SKU A-1, the live tracking of a shipment —
embedding any of that is pointless: it is stale the moment it is indexed, and the
authoritative copy already lives behind the customer’s API.
The API connector (“Connettore API”) closes that gap. It inverts the
paradigm: instead of pulling data in, it lets the model reach out. Each
configured HTTP endpoint (Rotta) becomes a tool the LLM can call during
the chat turn. So a single question — “is order 10293 shipped, and what does
our returns policy say?” — is answered from two sources at once: RAG over the
indexed returns policy and a live get_orders call to the customer’s ERP.
Scope — Fase 1. This page covers the live-tool path. Fase 2 (bulk ingest
from the same routes, reusing the existing ingestion pipeline) is designed into
the data model via the
mode column but is not implemented yet.Theory & background
A “tool” (a.k.a. function call) is a contract the LLM understands: aname, a
description telling it when to use the tool, and an input_schema (JSON
Schema) describing the arguments. When the model decides a tool is relevant it
emits a tool call with concrete argument values; the host executes it and
feeds the result back; the model then writes the natural-language answer.
The API connector’s job is to turn a plain HTTP endpoint into that contract
without the operator hand-writing JSON Schema. It does so from a single
test call: it performs the request, reads the response, and infers both the
input schema (from the parameters the operator declared) and the output shape
(from the JSON body). The operator reviews and edits; on confirm the route goes
active and its tool is live.
The crucial safety property is the LLM never sees the wiring. It is handed
only {name, description, input_schema} and only ever supplies the values of
the parameters marked llm. The URL, the headers, the fixed params, the secrets
and the auth are all resolved server-side at call time. The model cannot
exfiltrate a key it never receives.
The two parameter axes
Every route parameter has two independent axes — this is the heart of the model:
Only
source = llm parameters enter the tool schema the model sees. fixed
carry an operator-set constant (e.g. version=v1); secret are pulled from the
encrypted auth profile and are never exposed or logged.
Design
The orchestrator gives the model both capabilities in parallel: retrieval (unchanged) and the API tools (new). The model decides, per question, whether to ground on indexed knowledge, call one or more tools for fresh data, or both. The tool-call → tool-result loop runs up to a bounded number of iterations.Services (all server-side, in padosoft/askmydocs-connector-api)
Host integration
The package cannot inject chat tools by itself — the chat loop (McpToolCallingService) is host code. The host merges
ApiToolRegistry::activeToolsForTenant() into buildToolIndex() alongside the
external MCP tools (the index entry carries an api_route_id instead of a
server) and routes those calls to ApiToolExecutor. This mirrors how
HostIngestionBridge wires the ingest connectors in — the package supplies the
capability, the host wires it. The merge is gated by
connector-api.chat_tools.enabled (safe in both states) and decoupled from
mcp.enabled, so API tools work even with no MCP server configured.
Data model
Five tenant-aware tables (every row carriestenant_id; uniques are
tenant-scoped):
api_routes.project_key is denormalised from its connector (NOT NULL, ''
when unset) so the rule “a tool slug is unique per KB” is enforceable as a DB
unique (tenant_id, project_key, slug) without the NULL-distinct gap.
Decision rationale
See ADR 0023. The load-bearing choices:- A standalone package, not a
ConnectorInterfaceimplementation. That contract is ingest-shaped (syncFull/syncIncremental/OAuth/health) and does not model “endpoint → tool”; forcing it through would distort both. The package reusesconnector-baseonly for the tenant primitives. - One chat loop, two tool sources. Rather than a parallel chat path, the
existing
McpToolCallingServicegained a second source. Less surface, uniform audit and metering. - Anthropic + Gemini joined the tool loop. ADR 0015
had left them SDK-only with no tool path; v8.27 adds a raw-
Http::with-tools branch to each (translating the OpenAI-shaped tools+history to the native Messages /generateContentprotocol and back), so all four hybrid providers can drive API tools — single-metered via the finops bridge.
Worked example — an “orders” tool
-
Connector “Gestionale Cliente X” with
base_url https://api.clientex.comand anapi_keyauth profile (the key encrypted in the vault). -
Route “Ordini”:
GET https://api.clientex.com/v1/orders, parametersorder_id(query, llm, string, optional),status(query, llm, enum),version(query, fixed,v1),api_key(header, secret,secret_ref: api_key). -
Test connessione with
order_id=10293. The response{ "orders": [ { "id": "10293", "status": "shipped", "total": 42 } ] }yields an inferred output schema and a generated tool: -
Activate. In chat, “is order 10293 shipped?” → the model calls
get_orders({order_id:"10293"}); the executor addsversion=v1, injects theapi_keyheader from the vault, checks the URL throughUrlGuard, calls the API, caps the output, logs a sanitised row (no key, noversionsecret), and returns the JSON — the model answers “Order 10293 has shipped.”
Gotchas
- JSON only in Fase 1. A non-JSON / empty response is surfaced as a structured error to the model (and shown as a diagnostic in the test panel), not silently swallowed.
- SSRF guard is on by default and load-bearing. It blocks private / loopback /
link-local targets and the cloud-metadata endpoint, enforces https, and supports
an optional per-tenant domain allowlist (
API_CONNECTOR_DOMAIN_ALLOWLIST). Do not weaken it to reach an internal host — add that host to the allowlist on a network you trust instead. - Provider matters. The tool loop runs on OpenAI, OpenRouter, Anthropic and Gemini. If your chat provider is none of these, API tools won’t fire.
- Tenant + project scope. A route is invocable only within its own tenant, and
only in conversations of its bound
project_key(or globally when unset). A route is never reachable from another tenant’s chat. - Output cap. Large responses are truncated with a note. Use the route’s field
selection (
output_transform) to return only what the model needs.