Connect to OAuth-protected MCP servers
Connect to OAuth-protected MCP servers
Hosted MCP servers expose their tools over OAuth 2.1. An agent deployed on Astro AI can connect to one and complete the OAuth flow at runtime: the platform injects the agent’s public URL as ASTRO_EXTERNAL_AGENT_URL, which is exactly what the OAuth redirect URI must point at.
The redirect URI is the part to get right on a deployed agent. After a user approves access, the OAuth provider sends their browser to that URL with an authorization code, so it must resolve back to your agent. Locally that’s http://localhost; in production it must be your agent’s public hostname. Build the redirect from ASTRO_EXTERNAL_AGENT_URL at runtime and the flow completes without a manual step.
This guide uses a Mastra agent as the worked example, with @mastra/mcp for the OAuth client: MCPClient pulls the tools, and MCPOAuthClientProvider handles dynamic client registration (RFC 7591) and PKCE. The same approach applies to any MCP client; only the provider API differs.
Before you start
- A Mastra agent you can run and deploy.
- A deployable Astropods blueprint (
astropods.yml) for the agent. - The
astCLI installed and authenticated.
Connect the MCP client
Point MCPClient at the hosted server’s URL and give it an OAuth provider:
listTools() skips servers that aren’t authorized yet (it logs and moves on), so your agent still boots before any OAuth has happened.
Build the redirect from your agent's public URL
The redirect URI must be a URL the browser can reach that lands back on your agent. Resolve it in priority order, explicit override, then the platform-injected public URL, then localhost for dev:
ASTRO_EXTERNAL_AGENT_URL is injected only when your agent exposes a public endpoint. To get one, and to actually receive the redirect, declare a frontend interface and serve the callback path:
Declaring frontend: true provisions a public HTTPS host, injects ASTRO_EXTERNAL_AGENT_URL, sets PORT=80, and routes the public host to your container on port 80. See Serve a frontend from your agent for the full topology.
Serve the callback on PORT (80 in production) — the same process that runs serve(agent) can host a tiny HTTP listener:
Binding :80 requires root. In your Dockerfile, EXPOSE 80 and do not switch to a non-root USER, a non-root process can’t bind privileged ports and the bind will fail with EACCES.
With this in place, the OAuth redirect lands on https://<your-host>/oauth/callback, the ingress routes it to your container, and the flow completes without a manual step.
On a messaging-only agent (no exposed endpoint), ASTRO_EXTERNAL_AGENT_URL is unset and the redirect falls back to localhost. The browser still shows the ?code=... in its address bar, so you can complete the flow by pasting that code back to the agent — handy as a fallback, but the frontend setup above is what makes it automatic.
Persist tokens in a writable store
The production container filesystem is read-only, so a file-based token store is wiped on every restart. Persist OAuth tokens (and the dynamic client registration) in a writable store — a redis knowledge store entry is the simplest:
Implement @mastra/mcp’s OAuthStorage interface (get / set / delete) against Redis when REDIS_URL is present, and fall back to a local file in dev. Tokens then survive restarts, and the MCP SDK refreshes expired access tokens from the stored refresh token automatically.
Drive consent from the conversation
Because a deployed agent is headless, let it hand the user the authorization URL in chat. Expose a Mastra tool that starts the flow and returns the URL:
The user opens the URL and approves; with the frontend callback from step 2 the connection completes on its own, and the agent re-lists that server’s tools so they’re immediately usable.
Gotchas with hosted MCP servers
- Resource indicator mismatch (
invalid_target). Per RFC 8707 the client sends aresourceindicator. Some authorization servers reject the path-form resource their own Protected Resource Metadata advertises (e.g.https://host/mcp) and only accept the origin (https://host). If you hitinvalid_target, pin the origin via the provider’svalidateResourceURLhook. - Stale client registration (
Invalid redirect_uri). A client registered under oneredirect_uri(say, localhost during dev) is rejected if reused under a different one (your public URL after you expose a frontend). Clear stored OAuth data before a fresh connect so a new registration uses the currentredirect_uri. - Instance restarts mid-flow. If consent state lives only in memory, a restart between issuing the URL and receiving the code breaks completion. Rebuild it from the persisted PKCE verifier + client registration so the paste/redirect still works.
The ASTRO_EXTERNAL_AGENT_URL variable
Use ASTRO_EXTERNAL_AGENT_URL whenever you need an absolute URL that points back at your agent from the outside: OAuth redirects, webhook callbacks, share links. It’s absent in local dev and on messaging-only agents, so always provide a localhost fallback.
Next steps
- Knowledge stores: connect the Redis store used for the token store
- Managing your agents: inspect and control the deployed agent