# MCP OAuth

Kads exposes protected-resource metadata at `/.well-known/oauth-protected-resource/api/mcp`. Customers can connect an OAuth-capable MCP client to `/api/mcp`, or continue using workspace API keys. The local Supabase Auth OAuth server implements dynamic client registration, authorization-code exchange with PKCE and refresh tokens.

Enable `ADS_MCP_OAUTH_ENABLED=true` only after enabling the Supabase OAuth server, setting its authorization URL to `/oauth/consent`, allowing dynamic client registration and configuring the custom access-token hook `pg-functions://postgres/private/advertising_oauth_claims`. The local configuration and database migration already do this. Apply the same configuration to a hosted Supabase project before enabling it there. Set the Supabase Site URL to the Kads origin.

Consent displays the registered client and callback, lets the user select an accessible workspace, and defaults to read-only access. Write access requires a member role or higher. A consent grant expires after 90 days. Tokens carry the exact MCP resource audience, client ID and Kads grant ID. A new consent replaces the previous client/user grant. Every request checks current membership, grant expiry and revocation; workflow execution rechecks access between native writes.

OAuth tokens use the `ads_mcp` role. This deliberately is not a PostgREST database role: an MCP authorization must not inherit browser-session database or storage access. Kads verifies the JWT and applies the grant's workspace/account scope through its shared advertising service. OAuth cannot administer API keys. The token hook leaves ordinary browser sessions unchanged and rejects OAuth refresh after grant revocation.

Manage grants on `/mcp` or through `GET /api/v1/ads/connected-agents` and `DELETE /api/v1/ads/connected-agents/{grantId}`. Browser revocation also revokes the Supabase client grant. Kads revocation takes effect immediately even if the identity provider is unavailable.

Local verification: `NODE_OPTIONS=--conditions=react-server npx tsx scripts/verify-advertising-oauth.ts`. This creates an isolated local user/client, verifies registration, S256 PKCE, consent, audience/role isolation, actual REST and MCP access, read-only enforcement, refresh, single-use authorization codes and immediate access/refresh revocation, then cleans up. It permits HTTP only to the local app and local Supabase. Supabase's local root RFC 8414 discovery path currently returns 404; its Auth OIDC discovery endpoint supplies the working authorization/token/registration endpoints.

Provider app approvals and live advertising tokens are not involved in this test.

## Campaign preparation and execution

The Streamable HTTP endpoint negotiates MCP versions `2025-11-25`, `2025-06-18` and `2025-03-26`. Send both `application/json` and `text/event-stream` in `Accept`, plus the negotiated `MCP-Protocol-Version` on subsequent requests.

Use `account` to select an accessible account, then `search_tools` to discover `post_validate_campaign` and `post_create`. Both use the same campaign input as REST. Reuse `idempotencyKey` when retrying preparation of the same change; the native `prepare` and `set_status` tools also accept that key. Unknown arguments are rejected rather than silently ignored.

After reviewing the prepared operation, call `execute` with its ID and `confirmed: true`. If the receipt returns `status: prepared` and `nextAction: execute`, continue the same operation; when `retryAfterMs` is present, wait that long before continuing while the provider processes media. The SDK handles that delay. A transport error, `unknown` result, or missing continuation instruction is not permission to repeat a write: inspect `operation` and the native account first. Completed campaign creation leaves serving entities paused; discover `post_activate` to prepare a separate activation.
