Connecting MCP servers
Point the assistant at external Model Context Protocol servers, and decide what it may run unattended.
A workspace can point the assistant at external Model Context Protocol
servers. Their tools appear alongside the built-in ones, named
connection__tool, and the assistant chooses between them like any other.
Everything below is per workspace. Two workspaces connecting the same public server hold two separate connections and two separate credentials — neither can read, spend or revoke the other's.
1. Adding a connection
Settings → MCP servers → Add MCP server.
Below the connection list is a grid of well-known servers, grouped by category, each with its icon. Choosing one opens the add dialog with the URL, transport and auth mode already filled in; every field stays editable. A server already connected shows as added rather than being offered twice.
The catalogue lives in src/lib/mcp/catalog.ts — a static file, not a table and
not a remote fetch, because it is the same for every workspace, nothing edits it
at runtime, and a suggested URL carries this app's implied endorsement, which is
not something to delegate to a CDN. Icons are static SVGs under public/mcp/,
and catalog.test.ts asserts each file exists, so a missing one fails the build
rather than rendering a broken image.
A catalogue entry is a convenience, not a guarantee. Each was checked
against the vendor's own docs on the date in its verifiedOn field, but these
endpoints move: while the list was being written, Linear had deprecated its
/sse endpoint, Atlassian was telling clients to move off /sse, and Asana's
v1 endpoint was a week from shutdown. Each entry links to the vendor's docs;
check there first when a connection stops working.
Adding a server that is not in the list is the same form — type the URL.
| Field | Notes |
|---|---|
| Name | Prefixes every tool the server contributes, so the assistant can tell two servers apart. Lowercase letters, digits and underscores. |
| Server URL | The MCP endpoint. https only, on port 443 — see What is refused below. |
| Transport | Streamable HTTP (recommended) or SSE. |
| Authentication | None, a header, or OAuth. |
| Approval policy | When the assistant may run these tools without asking. |
Then press Connect. That completes a handshake, lists the tools, and approves those definitions — nothing is offered to the model until it has.
2. Authentication
Header — for an API key or a static token. You give a header name
(Authorization by default) and its value. The value is encrypted with
CREDENTIALS_ENCRYPTION_KEY and never sent back to the browser; the settings
page shows that it is stored, not what it is.
OAuth — press Authorize and you are sent to the provider's consent screen. Most providers register this app automatically (dynamic client registration), so there is nothing to create by hand. Tokens are stored encrypted against the connection and refreshed by the SDK as they expire.
Redirect base URL. Set at the top of Settings → MCP servers. Providers
accept a redirect only over https, or over plain http on localhost — a
plain-http LAN name like http://hexabase:3000 is refused at registration,
before any consent screen. It must also be the origin you actually browse: the
callback re-proves your session, and a cookie set for one host is never sent to
another. Leave it empty to use BETTER_AUTH_URL.
The full redirect handed to providers is that base plus
/api/integrations/mcp/callback.
Providers that will not self-register. Most register this app automatically. Some — HubSpot among them — do not, and the connection fails with "Incompatible auth server: does not support dynamic client registration". Create an app with the provider (HubSpot: Development → MCP Auth Apps), then paste its client ID and secret into the connection and authorize again.
3. The approval policy
MCP carries no "this tool writes" flag, and a tool's description is written by the server — which is the last party a cautious client should take its safety assurances from. So the workspace decides, per connection:
| Policy | Behaviour |
|---|---|
| Ask before every call | Default. The assistant shows the tool, the server and the exact arguments, and waits. |
| Run without asking | No prompt. Only for servers whose tools you know are read-only. |
| Decide per tool | Rule tool by tool after connecting. Anything unruled is asked about. |
A gated call is approved for those exact arguments. Approving "delete issue 4" does not approve "delete issue 5", and an approval lasts for one assistant turn — it never carries into a later message.
4. When a server changes its tools
A server may serve a different definition for a name it already advertised. That is the MCP "rug pull": a description rewritten into an instruction, carrying whatever trust you granted when you approved the original.
Every conversation re-reads the tool list and compares it against what you approved. A tool whose definition changed — or one that appeared since — is withheld from the assistant entirely, and the connection shows what changed. Press Re-approve once you have read the new list.
Re-approval is always a human act. Nothing in a conversation can write the baseline.
5. What is refused
- stdio transport. It is not an option in the form and not expressible in the schema. It would spawn a process on the server from a value someone typed into a settings page.
- Private, loopback, link-local and CGNAT addresses, and any port other than
80/443. The same SSRF guard the assistant's
fetch_urluses, applied when the URL is saved and again on every connect, because DNS can move under a stored value. - Redirects. The transport is left at its
redirect: 'error'default, so a public URL cannot bounce to a private one after the check passed. - A non-https authorization server, or one the SSRF guard rejects. A server
names its own authorization server in its metadata, and that is checked before
the metadata is fetched, so it cannot point us at an internal address. A
different site is allowed and logged rather than refused:
mcp.stripe.comauthorizes againstaccess.stripe.com, and refusing that would break every provider that separates the two roles — which is the arrangement the spec is built around. - Retries. Disabled deliberately: nothing tells us which of a server's tools are idempotent, so a retry could send a second email or raise a second invoice.
6. Tool output is data
Anything a server returns is fenced and labelled untrusted before the model sees it, and the system prompt states that a tool result can never issue instructions — the same treatment fetched web pages get. A result telling the assistant to ignore its instructions is reported, not obeyed.
7. When it does not work
| Symptom | Cause |
|---|---|
| "not allowed: … private or reserved address" | The URL resolves somewhere the guard refuses. Public hosts only. |
| "has not been authorized yet" | OAuth connection with no grant. Press Authorize. |
| "N tool definition(s) changed since they were approved" | The server changed its tools. Read the list, then Re-approve. |
| Tools never appear | The connection is disabled, or Connect was never pressed — a connection with no approved baseline offers nothing. |
| "No encryption key is configured" | Set CREDENTIALS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) and restart. |
| "This authorization did not originate here" | The callback's state matches no connection — it was already used, or the flow did not start here. Start again from Settings. |
| "accept a redirect URI only over https…" | The redirect base is a plain-http LAN name. Set it to your https origin, or browse at http://localhost and set that. |
| "Incompatible auth server: does not support dynamic client registration" | The provider will not self-register. Create an app with it and paste the client ID and secret into the connection. |
| Authorization fails after you approve consent | The redirect base is a different origin from the one you browse, so your session cookie never reached the callback. Make them the same. |