Extend

OAuth sign-in for the MCP server and REST API

TL;DR

Agent Planners' MCP server and REST API accept OAuth 2.1 sign-in, so Claude, ChatGPT, Codex or your own app can connect without anyone copying an API key. A person signs in, picks the workspace, approves what the app may do, and can disconnect it any time on the API & MCP page.

OAuth sign-in or an API key?

Both reach the same MCP server (https://www.agentplanners.com/api/mcp) and REST API (https://www.agentplanners.com/api/v1). The difference is who holds the credential and how it got there.

  • Use OAuth when a person connects an AI host: Claude, ChatGPT, Codex and other MCP hosts that implement MCP authorization find the sign-in from the server URL alone — nothing to copy into a config file.
  • Use OAuth to build Agent Planners into your own product or internal tool: register your app on API & MCP, and the people who use it sign in and approve it.
  • Use an API key for scripts, CI jobs and servers with no person present to sign in.
  • Either way the result is the same kind of connection: an approved app is listed on API & MCP under Connected apps with the same scopes, platform choice, write-approval setting and audit trail as an API key, and it can be disconnected there.

Connect from Claude (claude.ai, Claude Desktop and mobile)

  1. 1In Claude, open Customize → Connectors and choose Add custom connector. On a Team or Enterprise plan an Owner adds it under Organization settings → Connectors, then each member connects with their own account.
  2. 2Enter the server URL https://www.agentplanners.com/api/mcp and leave the OAuth client fields empty — Claude identifies itself.
  3. 3Select Connect. Claude opens the Agent Planners consent page: sign in, choose the workspace, review what Claude may do and select Allow access.
  4. 4In a chat, turn the connector on from the + menu → Connectors and ask, for example, “List my connected ad accounts.”
Claude Desktop and the Claude mobile apps use the connectors of the claude.ai account they are signed in to. The Claude Free plan allows one custom connector.

Connect from Claude Code

Add the server once, then sign in from a session: run /mcp and follow the sign-in for agent-planners (or run claude mcp login agent-planners in your shell). Your browser opens the consent page; Claude Code stores and refreshes the tokens.

Claude Code returns to a port on your own computer, so the consent page warns that the app runs on this computer — continue only if you started the sign-in yourself.

shell
claude mcp add --transport http agent-planners https://www.agentplanners.com/api/mcp

Connect from ChatGPT

  1. 1Turn on developer mode: Settings → Security and login → Developer mode (availability depends on your plan and workspace policy).
  2. 2Open ChatGPT's plugins page (chatgpt.com/plugins), select +, give the connection a name such as Agent Planners and enter the MCP server URL https://www.agentplanners.com/api/mcp.
  3. 3ChatGPT reads the server's sign-in metadata and opens the Agent Planners consent page: sign in, choose the workspace and select Allow access.
  4. 4Start a new conversation and add the Agent Planners connection from the tools menu.
ChatGPT renames these menus from time to time. If yours differ, look for developer mode and for adding an app or connector by its MCP URL.

Connect from Codex

Codex does not start the sign-in on its own: after adding the server, codex mcp login opens the consent page in your browser and stores the tokens. codex mcp list shows the server; in ~/.codex/config.toml it is [mcp_servers.agentplanners] with url = "https://www.agentplanners.com/api/mcp".

shell
codex mcp add agentplanners --url https://www.agentplanners.com/api/mcp
codex mcp login agentplanners

What the person sees on the consent page

  • The app's name and where it will send them back. Claude, Claude Code, ChatGPT and Visual Studio Code are recognised by their exact callback address or client metadata document and shown as “Sends you back to Claude” (and so on). Any other app is marked Unverified app, with the name it gave itself in quotes — and, when it identifies itself with a metadata document, the host that serves that document — plus a warning to continue only if they started connecting it themselves.
  • The account they are signed in as, the workspace, and what the app connects to: the MCP server, the REST API or both. An app connects to one workspace; the workspace can be switched right there.
  • What the app will be able to do, one checkbox per permission, in plain words. Raw platform API access is never ticked in advance. A permission their role cannot grant is struck through with the reason.
  • Platforms, when the app asks for platform tools: every platform connected to the workspace (including ones connected later), or only the ones they tick.
  • Changes on your platforms, when the app asks to propose changes: Wait for approval is the default — each change the app proposes waits on API & MCP → Write approvals until an admin or editor approves it, and the person who connected it gets an email. A workspace admin can choose Apply ordinary changes at once instead; deletes and other changes a platform marks as a person's decision still wait for a person, and the workspace's guardrails still apply.
  • Allow access sends a one-time code back to the app; Cancel sends it an access_denied error. Nothing is granted before Allow access.
The consent page stays valid for 10 minutes. If the person switches account or workspace after it opens, they connect again from the app.

Permissions (scopes)

An app asks for scopes in the scope parameter, separated by spaces. An app that names none is asked for tasks:read tasks:write accounts:read tools:read tools:write. Words this server does not grant (openid, profile and the like) are ignored, never an error, and offline_access is accepted but changes nothing: every connection gets a refresh token.

ScopeWhat it allowsWho can grant it
tasks:readSee the workspace's tasks, their plans, results and reports.A workspace admin or editor
tasks:writeStart and follow up tasks; they run on the workspace's credits under its approval rules.A workspace admin or editor
accounts:readList the ad, analytics and store accounts connected to the workspace.A workspace admin or editor
tools:readCall the platforms' read tools — reports, settings, lists.A workspace admin, on a paid plan
tools:writeCall write tools. With the default setting each change waits for a person in the workspace to approve it.A workspace admin, on a paid plan
tools:rawCall the platforms' own APIs with the workspace's connections; raw writes are queued for approval by default.A workspace admin, on a paid plan
A member who is neither admin nor editor cannot connect an app. The platform-tool scopes follow the same rule as a tools-mode API key: a workspace admin, on a paid plan.

Build it into your own app: register it

Claude, ChatGPT and Codex need nothing here — they register themselves. To connect your own product or internal tool, a workspace admin registers it once:

  1. 1Open API & MCP → Your own OAuth apps and select Register an app.
  2. 2Give it a name and 1–10 redirect URIs, one per line.
  3. 3Choose Server app (a client secret, shown once — store it in your server's secret store; only a hash is kept) or Public app (no secret: a browser, desktop or mobile app that proves itself with PKCE alone).
  4. 4Copy the client_id (and the secret) into your app, then run the flow below.
Only members of the workspace that registered the app can authorize it, and each of them still approves on the consent page. Removing the app ends every connection made with it. A workspace can register up to 20 apps.

Redirect URIs

  • Matched exactly, as a whole string — never as a prefix or a wildcard.
  • https on any host.
  • http only on localhost, 127.0.0.1 or [::1]. A loopback redirect matches whatever port your app opened (RFC 8252); the path and query must still match.
  • A private-use scheme in reverse-domain form, such as com.example.app:/callback, for a native app.
  • No fragment, no user name or password, and never an address on Agent Planners' own site.

Step 1 — send the person to /oauth/authorize

Open the authorization URL in the person's browser. Generate a fresh code_verifier for every sign-in — 43 to 128 characters of A–Z, a–z, 0–9 and - . _ ~ — and send its challenge, BASE64URL(SHA-256(code_verifier)), with code_challenge_method=S256; plain is refused. The example uses RFC 7636's own pair, whose code_verifier is dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.

redirect_uri may be left out only when the app registered exactly one. resource is optional: leave it out (or send https://www.agentplanners.com/api/mcp) for the MCP server, send https://www.agentplanners.com/api/v1 for the REST API, or the origin https://www.agentplanners.com for a token valid on both. state comes back unchanged (up to 2,048 characters; a longer one is refused). A parameter sent twice is refused.

An unknown client_id or a redirect_uri the app did not register is answered on our page and never redirected. A later error (a missing PKCE challenge, a wrong resource …) goes to the redirect URI with error, state and iss — at once for a workspace's own app and for the recognised products; for any other app the person sees the error first, with a Return link to the app.

http
GET https://www.agentplanners.com/oauth/authorize?response_type=code
    &client_id=apc_0123456789abcdef01234567
    &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256
    &scope=tasks%3Aread%20tasks%3Awrite%20accounts%3Aread
    &state=f3a9c1d2
    &resource=https%3A%2F%2Fwww.agentplanners.com%2Fapi%2Fmcp

(one URL, split over lines for reading)

Step 2 — read the answer on your redirect URI

After the person decides, the browser is sent to your redirect URI with a 303. Check that state is the one you sent and that iss is https://www.agentplanners.com (RFC 9207) before you use the code.

The code is good for one exchange within 10 minutes. Exchanging the same code twice is refused, and the second attempt also ends the connection the first one created.

http
HTTP/1.1 303 See Other
Location: https://app.example.com/oauth/callback?code=apo_ac_...&state=f3a9c1d2&iss=https%3A%2F%2Fwww.agentplanners.com

If the person selects Cancel:
Location: https://app.example.com/oauth/callback?error=access_denied&error_description=The+person+declined+to+connect+this+app.&state=f3a9c1d2&iss=https%3A%2F%2Fwww.agentplanners.com

Step 3 — exchange the code at /oauth/token

This is a public app's request. A server app also authenticates: either an Authorization: Basic header carrying client_id:client_secret (each part form-encoded first), or client_secret in the body next to client_id — one of the two, never both. A public app sends client_id and no secret.

If step 1 sent redirect_uri, send the same value here — it is then required. If you send resource, it must name the same surface as step 1. The body is application/x-www-form-urlencoded as RFC 6749 specifies; JSON is accepted too.

http
POST https://www.agentplanners.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=apo_ac_...
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=apc_0123456789abcdef01234567
&resource=https%3A%2F%2Fwww.agentplanners.com%2Fapi%2Fmcp

The token response

scope lists what the person actually approved — they can untick permissions, and anything their role cannot grant is left out — so read it rather than assume you received what you asked for. Every grant includes a refresh token. Keep both tokens on your server or in the operating system's secure storage, never in a URL or a log.

http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "apo_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "apo_rt_...",
  "scope": "tasks:read tasks:write accounts:read"
}

Refresh: each refresh token works once

  • Each refresh answers with a new access token and a new refresh token; the refresh token you sent is spent. Save the new one before anything else.
  • A refresh token expires after 30 days without use, and every refresh starts a new 30 days — a connection that refreshes at least once every 30 days does not expire.
  • A spent refresh token presented again within 30 seconds, while the refresh token issued in its place is still unused, counts as a retry of a lost answer: that pair is withdrawn and a fresh pair is issued, so only one stays live. Any other reuse counts as theft: the whole connection ends and the answer is invalid_grant — the person has to connect again.
  • A refused refresh (a bad scope, a server error) does not spend the refresh token you sent; retry with it.
  • scope on a refresh may keep or narrow the new access token's scopes, never widen them (invalid_scope). The refresh token always keeps the scopes the person approved.
  • At each refresh, the person who approved the connection must still belong to the workspace with the role it needs — an admin for platform tools, an admin or editor for the rest. If not, the connection ends with invalid_grant. If the role cannot be confirmed right now and was last confirmed more than 24 hours ago, the answer is temporarily_unavailable (503): try again later — the connection is not ended.
  • A connection ends 365 days after it was approved, however often it refreshes; the person connects again.
http
POST https://www.agentplanners.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=apo_rt_...
&client_id=apc_0123456789abcdef01234567

Revoke a token

Token revocation (RFC 7009), authenticated the same way as the token endpoint. A refresh token ends the whole connection — every token of it stops working and it shows as disconnected on API & MCP; an access token ends only itself. The answer is 200 with {} whether or not the token existed.

People can also disconnect any app themselves on API & MCP → Connected apps.

http
POST https://www.agentplanners.com/oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=apo_rt_...
&client_id=apc_0123456789abcdef01234567

Call the MCP server or the REST API

  • Send the access token as Authorization: Bearer on https://www.agentplanners.com/api/mcp (MCP over Streamable HTTP) or https://www.agentplanners.com/api/v1 (REST). The tools the MCP server lists follow the connection's scopes, exactly as they do for an API key.
  • A token works only on the surface it was issued for: the MCP server (the default), the REST API when step 1 named /api/v1, or both when step 1 named the origin. On the other surface it is refused like an unknown token.
  • REST scopes: GET /api/v1/accounts needs accounts:read; GET /api/v1/tasks and GET /api/v1/tasks/{taskId} need tasks:read; POST /api/v1/tasks needs tasks:write.
  • A missing, expired or revoked token gets 401 with a WWW-Authenticate header naming the metadata below. On the REST API, a token without the needed scope gets 403 with error="insufficient_scope" and the scope to ask for.
  • Requests are rate-limited per connection, like an API key's; a 429 carries Retry-After.
shell
curl https://www.agentplanners.com/api/v1/accounts -H "Authorization: Bearer apo_at_..."

Discovery: the metadata documents

  • https://www.agentplanners.com/.well-known/oauth-protected-resource/api/mcp — the MCP server's protected resource metadata (RFC 9728). The bare /.well-known/oauth-protected-resource answers the same.
  • https://www.agentplanners.com/.well-known/oauth-protected-resource/api/v1 — the REST API's.
  • https://www.agentplanners.com/.well-known/oauth-authorization-server — the authorization server metadata (RFC 8414): the endpoints, the scopes, code_challenge_methods_supported ["S256"], client_id_metadata_document_supported and authorization_response_iss_parameter_supported.
  • The issuer is https://www.agentplanners.com. An unauthenticated call to the MCP server is answered like this (the REST API names its own document):
http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://www.agentplanners.com/.well-known/oauth-protected-resource/api/mcp", scope="tasks:read tasks:write accounts:read tools:read tools:write"

How a client identifies itself

  • A registered client_id (apc_…): an app a workspace admin registered on API & MCP, or one registered through Dynamic Client Registration.
  • Dynamic Client Registration (RFC 7591): POST JSON to /oauth/register with 1–10 redirect_uris and optionally client_name and token_endpoint_auth_method — none (the default: a public client), client_secret_basic or client_secret_post. Registering grants nothing; a person still approves. A confidential registration's client_secret appears in the answer once, and a registration no token was ever issued for is forgotten after 3 days.
  • Client ID Metadata Documents: a client_id that is an https URL with a path (on the default port) is fetched when a person opens the consent page — never by the token endpoint — with no redirects followed, at most 64 KB, within 5 seconds. It must be a JSON document whose client_id equals that URL, with redirect_uris and no secret (token_endpoint_auth_method none). It is cached as its Cache-Control says, between 5 minutes and 24 hours, and forgotten after 30 days without a token issued for it.
http
POST https://www.agentplanners.com/oauth/register
Content-Type: application/json

{"client_name": "Example MCP host", "redirect_uris": ["http://127.0.0.1:33418/callback"], "token_endpoint_auth_method": "none"}

HTTP/1.1 201 Created

{"client_id": "apc_...", "client_id_issued_at": 1791158400, "client_name": "Example MCP host", "redirect_uris": ["http://127.0.0.1:33418/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"]}

Lifetimes

WhatLifetime
Authorization code10 minutes, one exchange
Access token1 hour (expires_in 3600)
Refresh token30 days without use; replaced at every refresh
Connection365 days from approval, however often it refreshes
Retry window for a spent refresh token30 seconds
Consent page10 minutes from opening
Self-registered client no token was issued for3 days
Client ID Metadata Document cache5 minutes to 24 hours, per Cache-Control; forgotten after 30 days without a token

Errors

errorStatusWhen
invalid_request400A parameter is missing, malformed or sent twice; PKCE is missing; client credentials were sent both as Basic and in the body.
invalid_client401Unknown client_id, a wrong secret, or a secret sent by a public client.
invalid_grant400The code is invalid, expired, already used or issued to another client; redirect_uri is missing (step 1 sent one) or does not match; the PKCE code_verifier does not match; the refresh token is expired, revoked or already used (the connection is ended); the approver can no longer grant the connection; the connection is a year old.
invalid_scope400A refresh tried to widen the scopes.
invalid_target400resource is not the MCP server, the REST API or the origin, or does not match the authorization.
unsupported_grant_type400grant_type is not authorization_code or refresh_token.
unsupported_response_typeto the redirect URIresponse_type is not code.
access_deniedto the redirect URIThe person selected Cancel, or approved no permission.
invalid_redirect_uri400A registration's redirect_uris are missing, more than 10, or break the redirect rules above.
invalid_client_metadata400A registration is not a JSON object, or names an unsupported token_endpoint_auth_method, grant type or response type.
temporarily_unavailable429 or 503Too many requests from one address, or registration is paused for the day (429, with Retry-After); or a refresh could not re-check the approver's role right now (503) — try again shortly.
server_error500 or 503Something failed on our side; try again.

Security notes

  • Authorization code with PKCE (S256) is the only flow, for public and server apps alike — no implicit flow, no password grant, no client credentials grant.
  • Tokens, codes and client secrets are stored only as SHA-256 hashes; a client secret is shown once.
  • Redirect URIs are matched exactly, and a redirect the app did not register is never followed.
  • Every authorization response carries iss (RFC 9207), and every token is bound to the resource it was issued for (RFC 8707).
  • The consent page cannot be embedded in another site's frame, Allow access becomes clickable only after the page has been on screen for a moment (so a click aimed at another window cannot approve it), and an approval must be posted from Agent Planners itself, once.
  • When an app is connected, the workspace's admins get an email naming the app, the person who approved it and what it may do, with a link to disconnect it.
  • Ad-platform credentials never leave Agent Planners: a connected app receives results, never a Google, Meta or store token.
  • Approving, declining and ending a connection are written to the workspace's audit log, and the credits a connected app uses are charged to the workspace it connected to.

Frequently asked questions

Do I still need an API key?
Not to connect Claude, ChatGPT, Codex or another MCP host that supports MCP authorization — they sign in with OAuth. API keys remain for scripts, CI jobs and servers where no person is present to sign in.
Which plan do I need?
Any plan can connect an app for tasks and connected accounts. Direct platform tools (tools:read, tools:write, tools:raw) need a paid plan and a workspace admin's approval — the same rule as a tools-mode API key.
Why did a connection end on its own?
A refresh token was used twice outside its 30-second retry window, the authorization code was used twice, the person who approved it left the workspace or lost the role it needs, the app was removed from the workspace, the app disconnected itself, or the connection reached 365 days — API & MCP → Connected apps shows which. A connection whose refresh token goes 30 days unused also stops working. Either way, connect again from the app.
Can the app change my campaigns without asking?
Not with the default setting: each change it proposes waits on API & MCP → Write approvals for an admin or editor. A workspace admin can choose to apply ordinary changes at once; deletes and other changes a platform marks as a person's decision still wait for a person, inside the workspace's guardrails.
Does the app see my Google, Meta or Shopify credentials?
No. Platform credentials stay in Agent Planners; the app holds only its own Agent Planners tokens and receives results.

Related