# Knowledge Sources

> The org-scoped knowledge catalog: url, text, and file sources, how they are stored, and how the MCP server catalog differs from a per-tool MCP spec.

The Build screen of the Voice AI console has three cards: **Tools**,
**Knowledge**, and **MCP servers**. Tools bind to one `agent_config`.
Knowledge sources and MCP servers do not — they are **org-scoped
catalogs** backed by the `knowledge_source` and `mcp_server` tables and
served by the `adminKnowledge` and `adminMcp` routers.

This page covers the knowledge catalog and how the MCP catalog relates
to the `mcp`-kind tool spec. For the tool spec shapes themselves, see
[Tool calling](/modalities/voice/runtime/tool-calling).

## The three source kinds

A `knowledge_source` row carries a `name`, a `kind`, an optional
`description`, and exactly one payload field determined by the kind.

| Kind   | Payload field | What you supply |
| ------ | ------------- | --------------- |
| `url`  | `url`         | An absolute URL. The console validates it as a URL before saving. |
| `text` | `content`     | The text pasted into the editor, stored on the row itself. |
| `file` | `file_key`    | A file uploaded through the BFF. The row stores the resulting object key, not the bytes. |

The console sends the same field set for every kind and lets the unused
payload fields go out as `null`:

```ts
const body = { name: f.name.trim(), kind: f.kind, description: f.description || null };
if (f.kind === 'url')  body.url = f.url?.trim() || null;
if (f.kind === 'text') body.content = f.content ?? null;
```

Writes go through `adminKnowledge.create` for a new source and
`adminKnowledge.update` for an existing one.

## Required fields by kind

The editor validates before it calls the mutation, so a malformed source
never reaches the catalog:

| Field | When required | Extra check |
| ----- | ------------- | ----------- |
| `name` | Always | — |
| `url` | `kind = url` | Must parse as a URL. |
| `content` | `kind = text` | Must be non-empty after trimming. |
| file | `kind = file` | Only if the row does not already have a `file_key`. |

Because the file check falls back to the existing `file_key`, you can
re-open a `file` source to change its name or description without
re-uploading the blob.

## Org-scoped catalog, not agent-bound

Tool mutations (`admin.upsertAgentTool`, `admin.deleteAgentTool`) look up
the agent in the caller's org, write to `agent_tool` keyed on
`(agent_id, name)`, and then call `hydrateAndPublish` so the change is
live on the next call. Knowledge and MCP entries are different: they are
addressed by `orgId` alone, and the `adminKnowledge` mutations the
console calls do not republish an agent config. Editing a knowledge
source is a catalog edit, not an agent deploy.

Practically, that means the Knowledge card on the Build screen answers
"what does this org have on file", while the Tools card answers "what can
*this* agent do mid-call".

## URL sources

For a `url` source, the console records the URL on the row and nothing
else. The `create` / `update` bodies it sends carry only `name`, `kind`,
`description`, and `url` — there is no refresh-interval, schedule, or
crawl-depth field in the editor, and the screen surfaces no
last-fetched timestamp. Fetching is handled outside the console; the
catalog row is just the pointer.

Keep the URL stable and publicly resolvable from TeleQuick's side.
If a document moves, edit the source rather than creating a second row
with the same name.

## File upload and storage

Self-hosted deployments store blobs in MinIO, not Supabase Storage, and
only the BFF holds the root credentials. So the file does **not** go up
with the tRPC mutation. The console:

1. Creates (or updates) the `knowledge_source` row first, to obtain a
   `sourceId`.
2. `POST`s the blob as `multipart/form-data` to
   `/api/knowledge/upload` on the same origin.
3. The BFF writes the object under `<org>/<sourceId>/…` and persists
   `file_key` on the row.

The upload is authenticated with the signed-in user's Supabase session
access token, not an API key:

```ts
const { data: sess } = await supabase.auth.getSession();
const token = sess.session?.access_token;
if (!token) throw new Error('No active session — sign in again');

const form = new FormData();
form.append('orgId', orgId);
form.append('sourceId', sourceId);
form.append('file', file);

const res = await fetch(`${window.location.origin}/api/knowledge/upload`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
  body: form,
});
const json = await res.json(); // { ok, file_key, error }
```

The endpoint is considered successful only when the response is `2xx`
**and** the body has `ok: true` **and** a `file_key`. Anything else is
reported in the modal as the returned `error`, or as
`Upload failed (HTTP <status>)` when the body carries no message. The
row-then-upload ordering is why a failed upload can leave a `file`
source with no `file_key`: re-open it and pick the file again.

## MCP server catalog vs per-tool MCP specs

There are two distinct MCP concepts on this screen, and they are not
wired to each other in the shapes the console edits:

| | MCP server catalog | `mcp`-kind tool |
| --- | --- | --- |
| Table | `mcp_server` | `agent_tool` |
| Scope | Org | One `agent_config` |
| Router | `adminMcp` | `admin.upsertAgentTool` / `deleteAgentTool` |
| Republishes the agent on write | No | Yes (`hydrateAndPublish`) |
| Connection details live in | The catalog row | The tool's `spec` |

An `mcp`-kind tool carries its own connection details in its `spec`, and
the engine is the authority on that shape:

- `server_url` — required, validated as a URL by the editor.
- `tool_name` — the tool's name on the server; defaults to the tool's
  own `name` when omitted.
- `transport` — `http`.
- `auth` — written by the editor as `{ type: 'bearer', token }` when you
  fill in the bearer token field, and removed from the spec when you
  clear it.

So adding a server to the org catalog does not by itself give an agent a
tool, and an `mcp` tool does not reference a catalog entry by id — you
still supply `server_url` on the tool. Treat the catalog as the org's
inventory of servers and the tool spec as the binding that the runtime
actually calls.

## Why a save fails

The Knowledge editor shows errors in two places: inline under the field
that failed validation, and in the modal footer for anything the server
or the upload returned.

| Symptom | Cause |
| ------- | ----- |
| Inline error under Name / URL | Required field empty, or the URL did not parse. |
| `Paste the content the agent should know` | `kind = text` with empty `content`. |
| `Choose a file to upload` | `kind = file` and the row has no `file_key` yet. |
| `No active session — sign in again` | The Supabase session expired before the multipart upload. |
| `Upload failed (HTTP …)` | The BFF rejected the upload and returned no error message. |
| Footer message from the mutation | `adminKnowledge.create` / `update` returned a tRPC error. |

## Related

- [Tool calling](/modalities/voice/runtime/tool-calling) — the `http`,
  `client`, and `mcp` tool spec shapes and the `silent` flag
- [Authentication](/concepts/authentication) — API keys, relay tokens,
  and why the browser never holds a long-lived credential
