Solo CRM

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.

FieldNotes
NamePrefixes every tool the server contributes, so the assistant can tell two servers apart. Lowercase letters, digits and underscores.
Server URLThe MCP endpoint. https only, on port 443 — see What is refused below.
TransportStreamable HTTP (recommended) or SSE.
AuthenticationNone, a header, or OAuth.
Approval policyWhen 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:

PolicyBehaviour
Ask before every callDefault. The assistant shows the tool, the server and the exact arguments, and waits.
Run without askingNo prompt. Only for servers whose tools you know are read-only.
Decide per toolRule 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_url uses, 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.com authorizes against access.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

SymptomCause
"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 appearThe 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 consentThe redirect base is a different origin from the one you browse, so your session cookie never reached the callback. Make them the same.

On this page