Connect to OAuth-protected MCP servers
Connect to OAuth-protected MCP servers
Hosted MCP servers expose their tools over OAuth 2.1. A Mastra agent can use them directly: MCPClient pulls the tools, and @mastra/mcp’s MCPOAuthClientProvider handles dynamic client registration (RFC 7591) and PKCE for you.
The one part that trips people up on a deployed agent is the redirect URI. After a user approves access, the OAuth provider sends their browser to that URL with an authorization code, so it has to resolve back to your agent. Locally that’s http://localhost, but in production it needs to be your agent’s public hostname. Astro injects exactly that as ASTRO_EXTERNAL_AGENT_URL, so you can forge the redirect at runtime and complete the flow hands-free.
This guide shows the full setup end to end.
Examples use @mastra/mcp. The same principles apply to any MCP client, only the provider API differs.
1. 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.
2. Forge 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.
3. 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 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.
4. 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 outsidel, OAuth redirects, webhook callbacks, share links. It’s absent in local dev and on messaging-only agents, so always provide a localhost fallback.