OAuth sign-in for the MCP server and REST API
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)
- 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.
- 2Enter the server URL https://www.agentplanners.com/api/mcp and leave the OAuth client fields empty — Claude identifies itself.
- 3Select Connect. Claude opens the Agent Planners consent page: sign in, choose the workspace, review what Claude may do and select Allow access.
- 4In a chat, turn the connector on from the + menu → Connectors and ask, for example, “List my connected ad accounts.”
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.
claude mcp add --transport http agent-planners https://www.agentplanners.com/api/mcpConnect from ChatGPT
- 1Turn on developer mode: Settings → Security and login → Developer mode (availability depends on your plan and workspace policy).
- 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.
- 3ChatGPT reads the server's sign-in metadata and opens the Agent Planners consent page: sign in, choose the workspace and select Allow access.
- 4Start a new conversation and add the Agent Planners connection from the tools menu.
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".
codex mcp add agentplanners --url https://www.agentplanners.com/api/mcp
codex mcp login agentplannersWhat 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.
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.
| Scope | What it allows | Who can grant it |
|---|---|---|
| tasks:read | See the workspace's tasks, their plans, results and reports. | A workspace admin or editor |
| tasks:write | Start and follow up tasks; they run on the workspace's credits under its approval rules. | A workspace admin or editor |
| accounts:read | List the ad, analytics and store accounts connected to the workspace. | A workspace admin or editor |
| tools:read | Call the platforms' read tools — reports, settings, lists. | A workspace admin, on a paid plan |
| tools:write | Call 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:raw | Call the platforms' own APIs with the workspace's connections; raw writes are queued for approval by default. | 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:
- 1Open API & MCP → Your own OAuth apps and select Register an app.
- 2Give it a name and 1–10 redirect URIs, one per line.
- 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).
- 4Copy the client_id (and the secret) into your app, then run the flow below.
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.
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/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.comStep 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.
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%2FmcpThe 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/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.
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_0123456789abcdef01234567Revoke 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.
POST https://www.agentplanners.com/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=apo_rt_...
&client_id=apc_0123456789abcdef01234567Call 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.
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/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.
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
| What | Lifetime |
|---|---|
| Authorization code | 10 minutes, one exchange |
| Access token | 1 hour (expires_in 3600) |
| Refresh token | 30 days without use; replaced at every refresh |
| Connection | 365 days from approval, however often it refreshes |
| Retry window for a spent refresh token | 30 seconds |
| Consent page | 10 minutes from opening |
| Self-registered client no token was issued for | 3 days |
| Client ID Metadata Document cache | 5 minutes to 24 hours, per Cache-Control; forgotten after 30 days without a token |
Errors
| error | Status | When |
|---|---|---|
| invalid_request | 400 | A parameter is missing, malformed or sent twice; PKCE is missing; client credentials were sent both as Basic and in the body. |
| invalid_client | 401 | Unknown client_id, a wrong secret, or a secret sent by a public client. |
| invalid_grant | 400 | The 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_scope | 400 | A refresh tried to widen the scopes. |
| invalid_target | 400 | resource is not the MCP server, the REST API or the origin, or does not match the authorization. |
| unsupported_grant_type | 400 | grant_type is not authorization_code or refresh_token. |
| unsupported_response_type | to the redirect URI | response_type is not code. |
| access_denied | to the redirect URI | The person selected Cancel, or approved no permission. |
| invalid_redirect_uri | 400 | A registration's redirect_uris are missing, more than 10, or break the redirect rules above. |
| invalid_client_metadata | 400 | A registration is not a JSON object, or names an unsupported token_endpoint_auth_method, grant type or response type. |
| temporarily_unavailable | 429 or 503 | Too 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_error | 500 or 503 | Something 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.