GenerateSaaS

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

Copy a shape from the module's JSDoc into the aiIntegrations array and adapt it - it carries one worked example per auth style.
For an 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.
Restart. A malformed entry fails the app at boot; a valid one appears in the picker on every surface it targets.

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

FieldPurpose
idStable catalog id stored connections reference. Letters, digits, _ or - only - it can become a tool name.
nameDisplay name in the picker.
kindapi | mcp - what the backend talks to, see the two kinds.
authHow it authenticates - see the auth axis.
baseUrlThe fixed origin reached. Omit to make the entry generic: the user supplies the host in a baseUrl credential.
descriptionOne-line picker copy (optional).
credentialsThe connect-form schema: { key, label, type: "text" | "password" | "url", required } per field.
healthapi only: { path, method?, identityPath? } - the probe proving the credentials work and naming the account (optional).
callableWhether the model may call it as a tool. A non-callable entry is still connectable.
mutatingMarks a write-capable tool (optional; omitted means read-only).
allowedScopesWhere 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.
maxInstancesCap 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.

kindauth.methodWhat it does
apinoneSends the request unauthenticated.
apiapikeyRenders the declared placements from the stored credentials.
apioauthRuns the authorization-code + PKCE flow and sends the auto-refreshed bearer token.
mcpnoneDials with no auth.
mcpheadersRenders 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.

FieldValue
authUrl / tokenUrlThe provider's authorization and token endpoints.
scopesThe scopes the sign-in requests.
clientIdEnv / clientSecretEnvThe 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.

On this page