Prerequisites
- A
url-type MCP instance that already exists. OAuth connect resolves the remote URL from the instance’s parent spec and returns 400 if there is none — create the instance first with Connect a remote MCP server. - The provider’s MCP endpoint must publish protected-resource metadata (RFC 9728) so the authorization server can be discovered.
- Redis reachable from the API. The in-flight flow state lives there between the two requests.
- A browser. The authorization step requires a human at a consent screen.
Steps
Ask what the provider needs
status decides the next step:Pass
server_id instead of instance_id to ask about a catalog spec before
any connection exists, or catalog_item_id to ask about a catalog API
connection; connected is then always false. The same endpoints serve both
kinds of connection.Every outcome is a 200 — the endpoint answers a question about capability
rather than failing. It runs live discovery against the MCP URL on each call,
so expect a round trip to the provider.Start the flow
authorize_url in a browser
yourself.Behind that single call AgentArea discovers the authorization server from the MCP
URL, picks the OAuth client — the operator’s app when one serves the provider,
else a client registered via Dynamic Client Registration — generates a PKCE S256
pair, persists the client credentials on an auth config, and stores the flow state
in Redis keyed by state. The state holds a reference to those credentials, never
a copy of the client secret.The offered scopes come from the resource’s own metadata (RFC 9728) where it
publishes them, since that is the only place a provider states what the MCP
endpoint itself accepts. A sign-in asks for all of them unless one of these
narrows it, first match wins:scopesin the request body: the person’s pick, which must be a non-empty subset of the offered ones (anything else is a 422). The connect panel shows the offered scopes as checkboxes under “Access”.- On a reconnect, the scopes the connection’s current credential asked for, minus any the server no longer offers.
metadata["agentarea:oauth_scopes"]on the catalog spec. Gmail’s server offers twenty scopes, permanent deletion among them, while its tools need one, so its entry declares"agentarea:oauth_scopes": ["https://www.googleapis.com/auth/gmail.modify"]. A declared scope the server does not offer is refused with a 502 rather than sent to a consent screen that would reject it.
GET /oauth/preflight returns both lists: scopes (offered) and
default_scopes (what a sign-in without scopes asks for).offline_access is added only when the authorization
server advertises it — asking a provider for a scope it never claimed risks
invalid_scope on the consent screen, which costs the whole authorization
rather than just its refresh token.A provider that needs more than OAuth 2.1 declares it on the server’s catalog
spec, under metadata["agentarea:oauth_authorize_params"], and those parameters
are added to the authorize URL. Google ignores offline_access and issues a
refresh token only for access_type=offline on a fresh consent, so its catalog
entries declare:client_id, redirect_uri, state, scope, resource, PKCE); such a spec
is refused with a 502.return_to is where the browser lands afterwards. It is validated against an
allowed base, so an arbitrary URL is rejected.Complete consent in the browser
Open the URL, approve the scopes. The provider redirects to
GET /v1/oauth/callback with code and state.The callback is public — it must be, because the provider redirects a browser to
it — and it is protected by the state token rather than your API key. It does
not redeem the code itself: whoever opens a provider URL lands there, not only
the person the authorize call returned it to. It parks the code for ten minutes
and sends the browser to the web app’s /oauth/finish page.That page calls POST /v1/oauth/finish in the signed-in session, and the API
goes on only if that person started the flow; anyone else gets a 403 and nothing
is attached. It then exchanges the code for tokens, updates the auth config
created in the previous step with the access token, refresh token, scope, and
expiry, links it to the instance via auth_config_id, and redirects the browser
to {return_to}/connections/{instance_id}?oauth=success. Sign in to the web app
as the user whose token started the flow before you open the URL.Tool discovery is kicked off in the background at that point, so the tool list
may be a moment behind the redirect.Connect a provider without dynamic registration
When preflight returned The credentials belong to the connection, not to the deployment: each workspace
authorizes through its own app, and the client secret is stored encrypted under
the connection’s auth config.Either credential can instead reference an existing workspace secret, which is
what to use when you rotate the app without re-authorizing every connection:Exactly one source per credential. Sending both a value and a secret id for the
same credential, or neither, is rejected with 422 rather than one silently
winning. Referenced secrets must be user-owned workspace secrets — a secret
already owned by another entity is refused.
oauth_app_required — or you would rather sign in
through your own app than the operator’s — register an OAuth app with the
provider yourself, using the redirect_uri preflight returned
($AGENTAREA_URL/v1/oauth/callback, the one callback for every connection).
Then pass its credentials with the authorize call:Verify
Confirm the instance now carries an auth config and that a call through the governed proxy succeeds with an injected token.Authorization header is stripped before the request goes upstream, so this
only succeeds if the OAuth credential was stored correctly.
Troubleshooting
400 "Instance has no remote URL configured"
400 "Instance has no remote URL configured"
The instance is
docker or command , not url . OAuth connect only
applies to remote servers; a managed workload takes its credential from the
environment instead.502 "OAuth discovery failed"
502 "OAuth discovery failed"
The MCP endpoint does not publish protected-resource metadata, or the
authorization server metadata document is unreachable. Confirm the provider
documents OAuth for MCP; if it uses a plain API key, use
Pass secrets to an MCP server .
422 with code "oauth_app_required"
422 with code "oauth_app_required"
The authorization server has no registration endpoint, or registration
failed, so there is no client for AgentArea to authorize as. The response
names the issuer in
detail.issuer . Register an OAuth app with that
provider and retry with credential_mode: custom .A verified connection still cannot call tools
A verified connection still cannot call tools
A remote server that lists its tools without a token verifies as reachable
and stores its tool list while nobody is authorized; the tool calls are what
401. Check
auth_config_id on the instance — null means no token is
stored, regardless of verification.status .The callback reports an invalid or expired state
The callback reports an invalid or expired state
Flow state is held in Redis with a bounded lifetime and is consumed on first
use. A stale browser tab, a second attempt at the same
authorize_url , or
a Redis restart between the two steps all produce this. Start again from
/authorize .return_to is ignored or rejected
return_to is ignored or rejected
It is validated against an allowed frontend base to stop the callback
becoming an open redirect. Use your deployment’s configured frontend origin.
Calls start failing with 401 days later
Calls start failing with 401 days later
The stored refresh token is used to mint new access tokens, and some
providers rotate refresh tokens on each use. If a refresh fails the
connection needs re-authorizing — re-run
/authorize for the same instance.A tool call fails with "needs access it was not granted"
A tool call fails with "needs access it was not granted"
The server answered 403 with
error="insufficient_scope" (RFC 6750): the
token is valid but was granted too little, usually because a scope was
unticked at sign-in. The message names the missing scopes, and the
verification records them under detail.required_scopes. Reconnect and
tick them; refreshing the token cannot add a scope.Two auth configs for one instance
Two auth configs for one instance
Each successful flow creates a new auth config named
mcp-oauth-{first 8 chars of instance id} and repoints auth_config_id .
Older configs are left behind; the instance uses only the one it points at.Related
Connect a remote MCP server
Point AgentArea at an MCP server somebody else operates, test the endpoint
before saving it
Pass secrets to an MCP server
Declare which inputs are credentials with env_schema, supply their values on
the instance
MCP
What the Model Context Protocol gives an agent, and how AgentArea hosts MCP
servers