Skip to main content

Connectors SDK

gaia.connectors is GAIA’s external-integration layer. It manages two kinds of connectors:
  • OAuth (type oauth_pkce) — user-authorized flows (Google, etc.). Stores refresh tokens in the OS keyring.
  • MCP server (type mcp_server) — API-key-based connections to third-party MCP servers (GitHub, Brave Search, etc.). Stores keys in the OS keyring as $keyring references.
Three caller surfaces share the same keyring and grants file: SDK (direct Python import), CLI (gaia connectors …), and Agent UI (Settings → Connectors page).

Catalog

The catalog is populated at import time by gaia.connectors.catalog. Every connector is a ConnectorSpec:
Catalog entries cover 23 connectors across core, productivity, dev, and search tiers.

SDK use — OAuth

get_access_token raises AuthRequiredError on four failure modes: AGENT_NOT_GRANTED with agent_id=None is a distinct case: the call came from inside an agent turn that carries no identity, so no grant could be checked and none is assumed. Fix the identity, not the grants — see the warning below. It also raises ConfigurationError when an agent_id resolves but scopes is empty. An empty list satisfies the grant check vacuously, so it would hand back a full-capability token with the ledger never consulted; naming nothing is refused rather than treated as needing nothing. It raises ConnectionBusyError when another GAIA operation (in this process or another) is saving the connection at that moment. That is transient, not a disconnect: retry shortly, and don’t prompt the user to reconnect. Over HTTP it is a 503 with {"error": "connection_busy"} and a Retry-After header. Listing connections at that moment (list_connections(), get_connection(), or GET /v1/connections[/{provider}]) still returns the row, with "error": "busy" and empty scopes. Don’t base a scope decision on that row; ask again shortly.

Forwarded connections (no re-auth)

When a host app has already authenticated the user (its own OAuth flow), it can FORWARD that connection to GAIA instead of triggering a second consent screen. GAIA persists the forwarded grant and refreshes as the host app’s client — credential forwarding, not a GAIA-run OAuth flow. The host app forwards three things: the OAuth client it was issued under (client_id + client_secret) and the user’s long-lived refresh_token.

REST: POST /v1/connections/{provider}

The same ingestion is exposed over REST by the Agent UI server (which binds localhost by default and gates remote/tunnel access behind a token). Mutating routes require the X-Gaia-UI: 1 header.
After a successful POST the agent resolves the connection ambiently — the triage request carries no credentials; get_access_token reads the persisted grant and refreshes transparently.
Disconnecting a forwarded connection never revokes it with the provider. The stored refresh_token was minted under the host app’s OAuth client, not GAIA’s own — Google’s revoke endpoint takes no client auth and would kill the whole grant for whoever the token belongs to, signing the host app itself out of the user’s account. DELETE /v1/connections/{provider} (and gaia connectors disconnect) only ever clears GAIA’s local copy for a forwarded connection; to fully revoke access, disconnect it from the host app that forwarded it, or from the account’s connected-apps page.
The host app’s initial consent must request the UNION of every scope both it and GAIA need. GAIA refreshes with the forwarded refresh_token and cannot widen scope at refresh time — a refresh only ever returns the scopes the original consent granted. If the forwarded scopes don’t cover what the target agent needs (for the Email agent: gmail.modify, gmail.send, and a calendar scope), the import fails loudly with ScopeMismatchError (HTTP 403 + missing_scopes) rather than silently under-provisioning. Re-consent by the host app — with the wider scope set — is the only fix.
GAIA refuses to persist tokens to an insecure keyring backend (plaintext / weak file-backed stores). On such a backend the import fails loudly with an actionable error pointing at the system credential-store setup. Forwarded secrets are never written to ~/.gaia/ and never echoed in any response. If a forward fails partway through saving, GAIA puts back the client and connection that were there before, so a failed forward never costs the user the connection they already had.

SDK use — MCP server

MCP server connectors are configured once and then provide their API keys via the get_credential dispatcher:
The MCP bridge injects these credentials as environment variables when it launches the MCP server process.

CLI use

Agent-author guide

Declare the connectors your agent needs as REQUIRED_CONNECTORS:
The Agent UI consent dialog renders reason in plain language. After the user grants the scopes, subsequent get_access_token_sync calls return fresh tokens transparently.
Credential access fails closed inside an agent turn. The tool above passes no agent_id, and that is correct: the agent runtime binds your agent’s identity to the current context and the credential call reads it from there. What changed is what happens when it cannot be read while a turn is running — the call is now refused with AuthRequiredError instead of falling through ungated.That matters when your tool hands work to something that does not inherit the context — a thread or process you start yourself, a background task queued for later, a callback fired from another runtime. Capture the id on the way in and pass it explicitly:
Outside an agent turn — the CLI, a script, your own SDK embedding — there is no identity to resolve and no grant to check, which is the documented escape hatch. Pass agent_id= explicitly there if you want the grant enforced anyway.
If instead your agent loads MCP servers dynamically at runtime (from ~/.gaia/mcp_servers.json) rather than declaring specific connectors, set CONSUMES_MCP_SERVERS = True so the Settings → Connectors “Active for” panel lists it for every mcp_server connector:

Per-agent activation (MCP servers only)

Activations are the second axis of the per-agent authorization model (issue #1005) and apply to mcp_server connectors only:
  • Grant (grants.json) — agent X is allowed to use connector Y’s credentials with scopes S. Gates credential access (get_access_token). Applies to every connector type.
  • Activation (activations.json) — agent X currently has MCP connector Y’s tools surfaced in its toolset. Gates MCP tool visibility (MCPClientManager.tools_for_agent).
A tool from an MCP connector is visible to an agent if and only if:
Activations default to inactive when absent — least-privilege opt-in. The ledger stores explicit true/false entries; an unknown pair returns is_agent_active == False.
OAuth connectors do not accept activations. Agents reach OAuth providers (Google, etc.) through native Python @tool functions that call get_credential_sync directly — there is no MCP tool surface to gate. Per-agent access for OAuth connectors is controlled entirely by the per-scope grant toggles above.Calling activate() / deactivate() for an OAuth connector raises ConfigurationError; the HTTP PUT / DELETE returns 400 Bad Request; the CLI exits with code 3. The Agent UI hides the “Active for” section for OAuth tiles for the same reason.
One-click activation: activating an agent with no prior grant auto-creates a grant using the agent’s declared REQUIRED_CONNECTORS scopes. This is the path Settings → Connectors → “Active for” takes when the user ticks an agent that has never been granted access:

Which agents appear in the “Active for” panel

For an mcp_server connector the panel lists agents from two sources:
  1. Agents that declare the connector in REQUIRED_CONNECTORS (static).
  2. Agents that set CONSUMES_MCP_SERVERS = True — they load MCP servers dynamically at runtime (from ~/.gaia/mcp_servers.json) and gate the tools through this same activation ledger, so they can use any MCP connector once activated. builtin:chat is the canonical example.
Category (2) agents declare no scopes of their own, so one-click activation auto-grants the canonical use scope. The registry surfaces the flag on each agent as consumes_mcp_servers. OAuth connectors have no activatable agents — the panel is hidden for them entirely. The HTTP router exposes:
Both mutating routes reject non-mcp_server connectors with 400 Bad Request (after the standard X-Gaia-UI CSRF check). Every activation change emits the SSE event connector.activation.changed ({connector_id, agent_id, active}) so an open Agent UI Settings tab refreshes the “Active for” state without a manual reload. The emit is centralized in api.activate / api.deactivate, so HTTP, SDK, and CLI callers all notify through one path. Because the CLI runs in a separate process from the UI server, the server also runs a background watcher on activations.json that emits the same event (within ~1 s) when an out-of-process writer changes the ledger. Disconnecting a connector wipes both grants and activations for that connector — re-adding the same connector_id must not silently inherit the previous user’s tool-visibility decisions.

Where things live

Adding a new OAuth provider

  1. Create src/gaia/connectors/providers/<name>.py satisfying the OAuthProvider protocol (auth/token URLs, client env vars, etc.).
  2. Register it in src/gaia/connectors/providers/__init__.py:get.
  3. Add a ConnectorSpec for it in a module under src/gaia/connectors/catalog/ (see google.py, microsoft.py) and import that module in src/gaia/connectors/catalog/__init__.py.
  4. Add unit tests under tests/unit/connectors/test_providers.py.

Adding a new MCP server connector

Add a ConnectorSpec to _ALL_SPECS in src/gaia/connectors/catalog/mcp_servers.py. Pin the package version:
The MCP bridge will inject MY_SERVICE_API_KEY from the keyring as an environment variable when launching the server process.