Catalog entries
How to declare one AI integration - the AiIntegrationSpec fields, the auth axis of none, api key, OAuth sign-in and MCP headers, and a worked Google Search Console entry.
A catalog entry is pure, non-secret metadata in packages/config/src/ai/integrations.ts: it renders the connect form, says how the backend reaches the service, and decides whether the model may call it. The storing, probing and tool-building are AI integrations.
Add an entry
aiIntegrations array and adapt it - it carries one worked example per auth style.oauth entry, register an OAuth client with the provider on the redirect URI <API_URL>/ai/integrations/oauth/<id>/callback, then set that client's id and secret in your server env under the names the entry declares.Google Search Console, as an api entry authenticated by sign-in:
// packages/config/src/ai/integrations.ts
export const aiIntegrations: readonly AiIntegrationSpec[] = [
{
id: "google-search-console",
name: "Google Search Console",
kind: "api",
description: "Sign in to read search performance for a verified site property.",
baseUrl: "https://searchconsole.googleapis.com",
credentials: [{ key: "siteUrl", label: "Site URL", type: "url", required: true }],
auth: {
method: "oauth",
oauth: {
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
scopes: ["https://www.googleapis.com/auth/webmasters.readonly"],
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET"
}
},
health: { path: "/webmasters/v3/sites", identityPath: "siteEntry.0.siteUrl" },
callable: true
}
];Fields
| Field | Purpose |
|---|---|
id | Stable catalog id stored connections reference. Letters, digits, _ or - only - it can become a tool name. |
name | Display name in the picker. |
kind | api | mcp - what the backend talks to, see the two kinds. |
auth | How it authenticates - see the auth axis. |
baseUrl | The fixed origin reached. Omit to make the entry generic: the user supplies the host in a baseUrl credential. |
description | One-line picker copy (optional). |
credentials | The connect-form schema: { key, label, type: "text" | "password" | "url", required } per field. |
health | api only: { path, method?, identityPath? } - the probe proving the credentials work and naming the account (optional). |
callable | Whether the model may call it as a tool. A non-callable entry is still connectable. |
mutating | Marks a write-capable tool (optional; omitted means read-only). |
allowedScopes | Where this entry may be connected: any of project, organization, user (see scope). Omitted offers every scope the deployment enables; listing fewer can only ever narrow. |
maxInstances | Cap on connected instances, counted per scope context - 1 allows one in each project, one shared with the organization, and one of the user's own (optional; omitted = unlimited). |
surfaces | ("web" | "desktop")[] - which app's picker offers it, and nothing else (optional; omitted means both). A presentation filter, not a permission: every kind executes server-side, so a connection made on one surface is a tool on all of them. |
An id can become a tool name, so name every one like an identifier. A provider-pinned entry gets the tool <id>_request, and mcp_ is reserved for the tools of connected MCP servers, so no id may start with it. Tool names leave your server: a user's coding CLI joins them into one comma-separated allowlist it reads as permission rules, so an id like acme,Bash - or a merely exotic one like acme.crm - breaks every terminal session that user opens. Every id is charset-checked where the catalog is read, so a bad one fails the app at boot.
The auth axis
Authentication is a separate axis from the kind, so a key-authenticated API and a sign-in service are the same kind with different auth.method.
kind | auth.method | What it does |
|---|---|---|
api | none | Sends the request unauthenticated. |
api | apikey | Renders the declared placements from the stored credentials. |
api | oauth | Runs the authorization-code + PKCE flow and sends the auto-refreshed bearer token. |
mcp | none | Dials with no auth. |
mcp | headers | Renders the declared header placements onto the dial. |
Placements
A placement says which stored credential rides where, so a new provider is an entry rather than new code. A blank credential's placement is skipped, which makes an optional credential declarable.
placements: [
{ carrier: "header", name: "Authorization", prefix: "Bearer ", field: "apiKey" },
{ carrier: "query", name: "key", field: "apiKey" }
]Sign-in entries
An oauth entry carries an AiIntegrationOAuthDescriptor of static, non-secret values. Client credentials are referenced by env var name - set the values in your server env, beside the other integration keys.
| 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 your OAuth client id and secret. |
A sign-in entry must declare baseUrl - the fixed origin its minted token is host-locked to. The catalog refuses one without it at boot.
AI integrations
The backend-owned catalog of third-party services a signed-in user connects once, so chat, automated runs and desktop agents can all act on them as tools.
Connection scope
Where an AI integration connection lives - the project, organization and user levels, the flags that offer each, how visibility unions, and the allowedScopes narrowing.