AI integrations
The shared integration catalog the web AI can act on, its entry anatomy, and how a connector attaches.
Integrations are the third-party services the AI can act on for a signed-in user - an HTTP API, an OAuth provider, or an MCP server. The catalog is one module, packages/config/src/ai/integrations.ts (@repo/config/ai/integrations), and it ships empty: add an entry to offer a connector. The module's JSDoc carries a copyable shape for each kind.
An entry
Each entry is an AiIntegrationSpec - pure, non-secret metadata (no credentials, no tokens). It drives the connect form and decides whether the model may call the connector.
| Field | Purpose |
|---|---|
id | Stable catalog id that stored connections reference. Letters, digits, _ or - only - it can become an AI tool name (see below). |
name | Display name in the picker. |
kind | api | oauth | mcp | cli - see below. |
description | One-line picker copy (optional). |
credentials | The connect-form schema: { key, label, type: "text" | "password" | "url", required } per field. |
oauth | The OAuth descriptor - required for oauth, absent otherwise. |
callable | Whether the model may call it as a tool. A non-callable entry is still connectable, just never advertised as a tool. |
mutating | Marks a write-capable tool (optional; omitted means read-only). |
maxInstances | Cap on connected instances (optional; 1 forces a single connection). |
surfaces | ("web" | "desktop")[] - which surfaces offer it (optional; omitted means both). |
An id can become a tool name, so name every one like an identifier. A single-connection oauth connector's tool is literally <id>_request. (An api connector's is api_request, and once a user connects several instances of one integration, both kinds are named from that user's label instead - store_main__api_request.) Wherever the id does surface, it leaves your server: when a user runs your app's AI through their own coding CLI, tool names are joined into one comma-separated allowlist the CLI reads as a list of permission rules. An id like acme,Bash would, absent the daemon's own guard, pre-approve unprompted shell execution on that user's machine; the daemon refuses such a session, so what it really does - like a merely exotic id (acme.crm, Google Search Console) - is silently break every terminal session. The catalog is charset-checked where it is read, and every declared id is checked, so a bad one fails the app at boot - loudly, in front of you, rather than quietly on a user's machine.
The four kinds
kind | What it connects | On the web |
|---|---|---|
api | An HTTP API reached by a base URL and an API key (sent as a Bearer header). | Callable. |
oauth | A sign-in provider authorized by an OAuth-2 authorization-code + PKCE flow. | Callable once the user signs in - the server mints and auto-refreshes the token. |
mcp | A Model Context Protocol server reached by command or URL. | Connectable, never callable - MCP execution is desktop-only. |
cli | A local command run as a tool. | Hidden - the web has no machine to run a command on. |
OAuth entries
An oauth entry carries an AiIntegrationOAuthDescriptor. It holds only static, non-secret values; the buyer's client credentials are referenced by env var name, never stored here.
| Field | Value |
|---|---|
authUrl / tokenUrl | The provider's authorization and token endpoints. |
scopes | The scopes the sign-in requests. |
clientIdEnv / clientSecretEnv | The env var names holding the buyer's OAuth client id and secret. |
apiBaseUrl | The fixed provider origin the bearer token is host-locked to - the callable tool refuses any request path that resolves off this origin. |
How the web attaches a connector
Metadata alone renders the connect form; the executable half lives in the API:
- Tool builders (
packages/api/src/ai/integration-tools.ts) turn the two server-executable kinds (api,oauth) into AI-SDK tools per connection;mcpandcliare skipped. A tool sends any HTTP method (GET by default) with an optional JSON body, so the model can use the whole API - reports that POST, records that PUT - not just reads. - The OAuth flow runs through the integrations router (
packages/api/src/routes/internal/ai/integrations-router.ts) and the flow helpers (packages/api/src/ai/oauth-flow.ts), which mint and auto-refresh the token per user. - Shared security helpers (
@repo/ai/integrations) enforce the two invariants every tool upholds: the model supplies a request path (plus method and body), never a full URL - the path is host-locked to the connection's fixed origin so a prompt-injected model can never retarget the request; and credentials are resolved fresh per call, fail-closed - a connection that no longer decrypts yields an error string, never a throw and never a secret.
Gating
Web integrations ride config.ai.enabled - the integrations routes sit behind the AI router's guard, so they are inert when AI is off.
How users connect one
End-users connect an integration on the Integrations tab in Settings (/settings/integrations): they pick a catalog entry and fill its credential fields (or sign in, for oauth), and the connection is stored server-side, never returned to the client. Once connected, its tools are advertised to chat and scheduled runs.