Knowledge bases over MCP
Knowledge bases you create under Knowledge Bases in the app are searchable over the Model Context Protocol — both by your deployed voice agents (wired automatically at deployment time) and by any MCP client you connect yourself (Claude, OpenAI-compatible clients, and others).
Authentication
Use an organization API key (created under Settings → API Keys, ela_ prefix) with either header:
X-API-Key: YOUR_API_KEYAuthorization: Bearer YOUR_API_KEY
Connecting
The MCP server speaks streamable HTTP. With Claude Code:
claude mcp add elacity-kb --transport http https://YOUR_MCP_SERVER_URL/mcp \
--header "X-API-Key: YOUR_API_KEY"Or in an mcpServers JSON config (Claude Desktop and compatible clients):
{
"mcpServers": {
"elacity-kb": {
"type": "http",
"url": "https://YOUR_MCP_SERVER_URL/mcp",
"headers": { "X-API-Key": "YOUR_API_KEY" }
}
}
}The exact endpoint URL and copy-paste snippets for your environment are shown in the Developer dialog on each knowledge base's detail page.
Tools
| Tool | Description |
|---|---|
list_knowledge_bases | Lists the knowledge bases your key can search (slug, name, description) |
search_knowledge_base | Semantic search over one knowledge base — pass knowledge_base (the slug) and a natural-language query, plus optional max_results |
Your API key scopes access to your organization's knowledge bases only.
Your deployed voice agents connect over a separate, automatically-wired route that exposes one dedicated
search_<slug>tool per linked knowledge base (so the agent queries a specific KB in a single call). That route is provisioned for you at deployment time and requires a platform token — the/mcpendpoint documented above is the one to use from your own MCP clients.
Recent questions and knowledge gaps
The Gaps tab shows recent searches and unanswered questions. Empty searches
are recorded as miss; returned passages whose best distance exceeds the KB's
threshold are weak. Both feed the review queue. Results within the threshold
are hit. Missing distance metadata is treated as weak.
The Gap distance threshold in KB details is configurable per knowledge base
(default 0.4). This classification records retrieval quality; it does not change
the passages returned by search.
A scheduled job groups similar unanswered questions within the same KB and organization. A weak or empty search is only a candidate: distance measures how similar the closest passage is, not whether it answers the question. Before anyone is notified, each new gap goes through two checks:
- Is it already answered? The draft step searches the gap's own KB again and asks the organization's LLM provider to answer from those passages. When the model judges that the passages fully answer the question, the gap is marked covered: it leaves the review queue, appears under "Already answered", and nobody is emailed. A covered gap absorbs similar searches for seven days. After that a similar search opens a new gap, which is checked again, so an answer whose document was removed comes back for review. A gap whose approvers have already been emailed is never covered. Organizations without an LLM provider skip this check.
- Do people keep asking it? Approvers are notified only once the question has been asked in at least two different calls. Rewording the same question within one call counts once; searches without a call ID count separately. Single asks stay in the review queue without an email. Asks older than the query-log retention window no longer count.
Current organization owners and admins can answer or dismiss gaps even when they
are not in the gap's saved approver list. Assigned approvers retain their access.
Asking an expert remains limited to the assigned approvers. API keys act as their
owner and keep their organization and knowledge-base restrictions; an Ellavox
administrator's role does not change the connected key owner's Elacity role.
An ordinary dashboard administrator still uses the connected key owner's
permissions. A verified Ellavox superadmin can also review through a member-owned
key when both servers configure ELLAVOX_KB_GAP_ASSERTION_SECRET (the same
server-only random secret, at least 32 bytes). Ellavox signs only the exact gap
read/write after requireSuperadmin; Elacity checks the signature, key binding,
request hash and 60-second lifetime. Key organization/artifact scope and live
membership still apply. Apply 20261110114500_kb_gap_trusted_ellavox_resolution.sql
before enabling delegated answer/accept writes. Missing or invalid assertions
never elevate an ordinary key. See docs/qa/scrum-3007-local.md for reproduction.
Reviewers see the question, ask count and last-seen date:
- Answer opens the document editor with the question as a heading. Saving writes and indexes the answer, links its document, and marks the gap answered.
- Dismiss requires a reason and records the reviewer and decision time.
- Ask an expert uses the existing KB-gap notification workflow to contact the configured approvers right away, skipping both checks above. They follow the link and answer through the app.
Answered, dismissed, duplicate and covered gaps cannot silently return to open. A later unanswered search can form a new review item; an answered question is classified again against the indexed content on its next search.
Operators must schedule /api/cron/kb-gap-cluster (every five minutes) and
/api/cron/kb-query-retention (daily). Both require Authorization: Bearer <CRON_SECRET>; Vercel schedules are included. Locally, invoke those endpoints
manually with the same authorization.
KB_QUERY_LOG_RETENTION_DAYS sets the query-log retention window (default 30,
positive integer). KB_GAP_CLUSTER_DISTANCE_THRESHOLD sets clustering distance
(default 0.35). These defaults are initial settings, not calibrated guarantees
of relevance. Query logs contain verbatim caller text, are readable only by
members of their organization, and are never edited by application users.
Platform MCP tokens include agent attribution on redeploy; older tokens remain valid and produce logs without an agent ID. KB provider tools are scoped per agent so their tokens cannot overwrite another agent's attribution.
This feature does not add web fallback, automatic steward triage, or automatic publication of AI drafts.
Call context in query logs
Search logs retain optional callId and provider alongside each query, including
misses and weak matches. Gap membership links each occurrence to its query log.
MCP clients can send X-Call-Id (or X-Chat-Id / X-Conversation-Id) and
X-KB-Provider headers. API-key clients of /mcp may instead pass optional
call_id and provider arguments to search_knowledge_base; headers take
precedence. Platform deployments (the agent route and platform tokens) take call
context from headers only, so a model cannot supply an ID. Provider names are
stored lowercase. These fields are telemetry, never authorization inputs.
Do not invent IDs when no conversation context is available.
Vapi automatically includes call/chat headers (Vapi MCP documentation).
New platform deployments label Vapi and ElevenLabs requests with their provider.
ElevenLabs KB deployments forward system__conversation_id through the
X-Conversation-Id dynamic-variable header. Existing agents require a controlled
KB tool deployment update. Conversation IDs were observed in the recorded ElevenLabs
SDK and Vapi inbound phone validation; each new customer transport still needs its own validation.
Automatic conversation ID transmission for other providers is not established;
configure explicit context forwarding before expecting those IDs in logs.
Existing query logs and calls without context have null fields.
Retained conversation IDs for a gap
GET /knowledge-bases/{knowledgeBaseId}/gaps/{gapId}/query-logs returns
{ items, nextCursor }, with up to 20 retained query occurrences per page.
Each item contains id (query-log ID), createdAt, nullable callId, and nullable
provider. Pass orgSlug and the returned opaque cursor for the next page.
The caller must be a member of the gap's organization; approver rights are not
required for reading. Each occurrence is listed separately, even when calls repeat.
An empty list means no retained logs, not that no conversation ever occurred.
Retention remains controlled by KB_QUERY_LOG_RETENTION_DAYS (default 30 days).
Per-call KB scope (dynamic agents)
Use dynamic scope when calls share one deployed parent agent but need different subsets of its attached knowledge bases, for example billing versus returns.
In the agent inspector’s Knowledge tab, attach the parent KBs, enable Dynamic KB scope (per call), then choose Save KB scope. The switch is available for ElevenLabs and Vapi; other providers show a disabled switch. The agent detail header shows a Dynamic KB scope badge for the saved mode. This badge describes the saved configuration, not proof of a deployed change. You must save static scope before removing the last attached KB.
Find KB slugs in the badges beside the attached KB names in that tab. Use those slugs, not display names or database IDs. Slugs not attached to the deployed agent are ignored and can never widen access. Changing the mode or attached KBs needs a full redeploy; choosing a subset of those KBs on each call does not.
ElevenLabs initiation contract
Agents default to kbScope: "static". The agent create/update API also accepts
kbScope: "dynamic" for ElevenLabs or Vapi agents with at least one attached knowledge
base. Apply a full deployment after changing the mode or the dynamic agent's
attached KBs. Snapshots preserve the mode for redeploy and rollback.
For a dynamic deployment, Elacity adds an MCP x-kb-scope header bound to the
ElevenLabs kb_scope dynamic variable. Its comma-separated KB slugs can only
narrow the agent's attached KB allowlist. Always supply the variable on each
conversation, using an explicit empty string for no KB access. In the validated
text WebSocket flow, omitting the variable rejects startup; a stored empty
placeholder does not supply a working default. Other transports need separate
validation before rollout. Vapi uses the separate scope-store contract below.
A conversation-initiation webhook must return the explicit variable:
{
"type": "conversation_initiation_client_data",
"dynamic_variables": { "kb_scope": "billing,returns" }
}An SDK session supplies the same value with camel-case option names:
await Conversation.startSession({
agentId,
dynamicVariables: { kb_scope: "billing,returns" },
});An empty value (kb_scope: "") means no KB access for that call. Missing scope
is fail-closed at MCP, but ElevenLabs may reject startup before MCP is reached:
the tested omitted-key case closed with code 1008. Always send the key.
Default an absent selection at the caller, before starting the conversation:
const dynamicVariables = { kb_scope: selectedKbScope ?? "" };
// Pass dynamicVariables to Conversation.startSession({ ..., dynamicVariables }).
// Raw WebSocket initiation uses: dynamic_variables: dynamicVariables.This defaults to no KB access, never all attached KBs. It does not change ElevenLabs behavior for external callers that still omit the key. Elacity deploys the agent but does not start those callers' sessions. Provider testing placeholders are not a verified runtime fallback.
Dynamic deployments also instruct the agent to explain unavailable KB access immediately, without promising a search or inventing results. Apply a full deployment to pick up this prompt guidance. KBs with an explicitly empty query prompt still leave all guidance to the author.
Vapi per-call scope store
Vapi MCP headers did not substitute {{kb_scope}} in the tested chat and raw
WebSocket transports. Dynamic Vapi deployments therefore send a static
x-kb-scope-mode: dynamic marker. The MCP server reads scope by organization,
agent stable key and Vapi call ID, then intersects it with the signed allowlist.
It does not trust assistantOverrides.variableValues.kb_scope for this path.
Your customer-hosted assistant-request handler must finish this write before
returning the assistant to Vapi:
const response = await fetch(
`${elacityUrl}/api/agents/${encodeURIComponent(stableKey)}/calls/${encodeURIComponent(message.call.id)}/kb-scope`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-API-Key": elacityApiKey, // Keep this on your server.
},
body: JSON.stringify({
orgSlug,
kbSlugs: selectedKbSlugs ?? [],
ttlSeconds: 3600,
}),
},
);
if (!response.ok) throw new Error("Could not set call KB scope");
// Only after success, return this JSON response to Vapi:
const assistantResponse = { assistantId: parentAssistantId };stableKey is the Elacity agent's stable key, not its database row ID or the
Vapi assistant ID. Scope writes are authorized against the organization and
agent access, independently of undeployed draft provider/mode edits. Stored
scopes affect only dynamic Vapi MCP requests; they do not enable dynamic mode
or grant access to a static or other-provider deployment. TTL defaults to one hour and cannot exceed 24 hours. Missing,
expired or unreadable records mean no KB access. Empty lists mean no access;
foreign slugs cannot widen the parent allowlist. Cache freshness is at most
30 seconds, bounded by record expiry; updates during a call are not an instant
revocation mechanism. Write before initiation, not during the conversation.
Ordinary WebSocket/outbound clients cannot assume they can write after
create-call: Vapi can fetch tools before returning the call ID. That ordering
was observed live and yielded no tools. A caller needs a verified pre-initiation
hook with the call ID. The isolated inbound phone assistant-request flow passed
the recorded seven-case validation. SIP and ordinary outbound/web ordering remain
separate, unverified integrations; the phone result does not prove them.
Roll out the scope-store database migration and the MCP lookup implementation before enabling dynamic Vapi deployments. Existing static agents do not use this lookup and retain their existing tool config/hash.
Exact retrieval membership
Knowledge-base search verifies each returned passage's actual KB slug before it is exposed to the agent. A vector-store candidate from another KB is discarded, even if a legacy text filter matched it. Existing indexes need an operator-run exact-field migration for full server-side filtering; until then, discarded candidates can reduce the number of returned passages. Reindexing and deletion on legacy indexes use exact document/KB metadata to select chunk UUIDs.
Diagnosing effective scope
After the telemetry migration and MCP rollout, prm_kb_query_log.kbScopeStatus
records static, narrowed, or unresolved for searches with scope metadata.
narrowed means runtime scope was resolved and intersected with the signed
allowlist; the requested subset may equal the whole parent allowlist.
Historical rows and callers without status metadata retain null. Existing
callId and provider fields correlate a search with a conversation when available.
An unresolved call normally has no usable KB tools and makes no searches, so it usually creates no query-log row. Use the MCP unresolved-scope warning to diagnose missing or misconfigured scope; this column is not a per-call audit trail.
See ElevenLabs dynamic variables and Vapi MCP for provider context. The explicit-key and Vapi store requirements above reflect this project's recorded transport tests.