Handle OAuth with MCP servers
When connecting to OAuth-protected MCP servers (like Slack or Notion), your users need to authenticate before your Agent can access their data. This guide covers implementing OAuth flows for seamless authorization.
- Call
addMcpServer()with the server URL - If OAuth is required, an
authUrlis returned instead of connecting immediately - Present the
authUrlto your user (redirect, popup, or link) - User authenticates on the provider's site
- Provider redirects back to your Agent's callback URL
- Your Agent completes the connection automatically
The MCP client uses a built-in DurableObjectOAuthClientProvider to manage OAuth state securely — storing a nonce and server ID, validating on callback, and cleaning up after use or expiration.
When connecting to an OAuth-protected server, check if authUrl is returned. If present, redirect your user to complete authorization:
export class MyAgent extends Agent { async onRequest(request) { const url = new URL(request.url);
if (url.pathname.endsWith("/connect") && request.method === "POST") { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { // OAuth required - redirect user to authorize return Response.redirect(authUrl, 302); }
// Already authenticated - connection complete return Response.json({ serverId: id, status: "connected" }); }
return new Response("Not found", { status: 404 }); }}export class MyAgent extends Agent<Env, never> { async onRequest(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname.endsWith("/connect") && request.method === "POST") { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { // OAuth required - redirect user to authorize return Response.redirect(authUrl, 302); }
// Already authenticated - connection complete return Response.json({ serverId: id, status: "connected" }); }
return new Response("Not found", { status: 404 }); }}Instead of an automatic redirect, you can present the authUrl to your user as a:
- Popup window:
window.open(authUrl, '_blank', 'width=600,height=700')for dashboard-style apps - Clickable link: Display as a button or link for multi-step flows
- Deep link: Use custom URL schemes for mobile apps
After OAuth completes, the provider redirects back to your Agent's callback URL. Configure what happens next.
Redirect users back to your application after OAuth completes:
export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", errorRedirect: "/auth-error", }); }}export class MyAgent extends Agent<Env, never> { onStart() { this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", errorRedirect: "/auth-error", }); }}Users return to /dashboard on success or /auth-error?error=<message> on failure.
If you opened OAuth in a popup, close it automatically when complete:
import { Agent } from "agents";export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: (result) => { if (result.authSuccess) { // Success - close the popup return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); } else { // Error - show message, then close return new Response( `<script>alert('Authorization failed: ${result.authError}'); window.close();</script>`, { headers: { "content-type": "text/html" } }, ); } }, }); }}import { Agent } from "agents";import type { MCPClientOAuthResult } from "agents/mcp";
export class MyAgent extends Agent<Env, never> { onStart() { this.mcp.configureOAuthCallback({ customHandler: (result: MCPClientOAuthResult) => { if (result.authSuccess) { // Success - close the popup return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); } else { // Error - show message, then close return new Response( `<script>alert('Authorization failed: ${result.authError}'); window.close();</script>`, { headers: { "content-type": "text/html" } }, ); } }, }); }}Your main application can detect the popup closing and refresh the connection status.
Use the useAgent hook for real-time updates via WebSocket:
import { useAgent } from "agents/react";function App() { const [mcpState, setMcpState] = useState({ prompts: [], resources: [], servers: {}, tools: [], });
const agent = useAgent({ agent: "my-agent", name: "session-id", onMcpUpdate: (mcpServers) => { // Automatically called when MCP state changes! setMcpState(mcpServers); }, });
return ( <div> {Object.entries(mcpState.servers).map(([id, server]) => ( <div key={id}> <strong>{server.name}</strong>: {server.state} {server.state === "authenticating" && server.auth_url && ( <button onClick={() => window.open(server.auth_url, "_blank")}> Authorize </button> )} </div> ))} </div> );}import { useAgent } from "agents/react";import type { MCPServersState } from "agents";
function App() { const [mcpState, setMcpState] = useState<MCPServersState>({ prompts: [], resources: [], servers: {}, tools: [], });
const agent = useAgent({ agent: "my-agent", name: "session-id", onMcpUpdate: (mcpServers: MCPServersState) => { // Automatically called when MCP state changes! setMcpState(mcpServers); }, });
return ( <div> {Object.entries(mcpState.servers).map(([id, server]) => ( <div key={id}> <strong>{server.name}</strong>: {server.state} {server.state === "authenticating" && server.auth_url && ( <button onClick={() => window.open(server.auth_url, "_blank")}> Authorize </button> )} </div> ))} </div> );}The onMcpUpdate callback fires automatically when MCP state changes — no polling needed.
Poll the connection status via an endpoint:
export class MyAgent extends Agent { async onRequest(request) { const url = new URL(request.url);
if ( url.pathname.endsWith("connection-status") && request.method === "GET" ) { const mcpState = this.getMcpServers();
const connections = Object.entries(mcpState.servers).map( ([id, server]) => ({ serverId: id, name: server.name, state: server.state, isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return Response.json(connections); }
return new Response("Not found", { status: 404 }); }}export class MyAgent extends Agent<Env, never> { async onRequest(request: Request): Promise<Response> { const url = new URL(request.url);
if ( url.pathname.endsWith("connection-status") && request.method === "GET" ) { const mcpState = this.getMcpServers();
const connections = Object.entries(mcpState.servers).map( ([id, server]) => ({ serverId: id, name: server.name, state: server.state, isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return Response.json(connections); }
return new Response("Not found", { status: 404 }); }}Connection states flow: authenticating (needs OAuth) → connecting (completing setup) → ready (available for use)
When OAuth fails, the connection state becomes "failed". Detect this in your UI and allow users to retry:
import { useAgent } from "agents/react";function App() { const [mcpState, setMcpState] = useState({ prompts: [], resources: [], servers: {}, tools: [], });
const agent = useAgent({ agent: "my-agent", name: "session-id", onMcpUpdate: setMcpState, });
const handleRetry = async (serverId, serverUrl, name) => { // Remove failed connection await fetch(`/agents/my-agent/session-id/disconnect`, { method: "POST", body: JSON.stringify({ serverId }), });
// Retry connection const response = await fetch(`/agents/my-agent/session-id/connect`, { method: "POST", body: JSON.stringify({ serverUrl, name }), }); const { authUrl } = await response.json(); if (authUrl) window.open(authUrl, "_blank"); };
return ( <div> {Object.entries(mcpState.servers).map(([id, server]) => ( <div key={id}> <strong>{server.name}</strong>: {server.state} {server.state === "failed" && ( <div> <p>Connection failed. Please try again.</p> <button onClick={() => handleRetry(id, server.server_url, server.name)} > Retry Connection </button> </div> )} </div> ))} </div> );}import { useAgent } from "agents/react";import type { MCPServersState } from "agents";
function App() { const [mcpState, setMcpState] = useState<MCPServersState>({ prompts: [], resources: [], servers: {}, tools: [], });
const agent = useAgent({ agent: "my-agent", name: "session-id", onMcpUpdate: setMcpState, });
const handleRetry = async (serverId: string, serverUrl: string, name: string) => { // Remove failed connection await fetch(`/agents/my-agent/session-id/disconnect`, { method: "POST", body: JSON.stringify({ serverId }), });
// Retry connection const response = await fetch(`/agents/my-agent/session-id/connect`, { method: "POST", body: JSON.stringify({ serverUrl, name }), }); const { authUrl } = await response.json(); if (authUrl) window.open(authUrl, "_blank"); };
return ( <div> {Object.entries(mcpState.servers).map(([id, server]) => ( <div key={id}> <strong>{server.name}</strong>: {server.state}
{server.state === "failed" && ( <div> <p>Connection failed. Please try again.</p> <button onClick={() => handleRetry(id, server.server_url, server.name)}> Retry Connection </button> </div> )} </div> ))} </div> );}Common failure reasons:
- User canceled: Closed OAuth window before completing authorization
- Invalid credentials: Provider credentials were incorrect
- Permission denied: User lacks required permissions
- Expired session: OAuth session timed out
Failed connections remain in state until removed with removeMcpServer(serverId).
This example demonstrates a complete OAuth integration with Cloudflare Observability. Users connect, authorize in a popup window, and the connection becomes available.
import { Agent, routeAgentRequest } from "agents";export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: (result) => { if (result.authSuccess) { return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); } else { return new Response( `<script>alert('Authorization failed: ${result.authError}'); window.close();</script>`, { headers: { "content-type": "text/html" } }, ); } }, }); }
async onRequest(request) { const url = new URL(request.url);
// Connect to MCP server if (url.pathname.endsWith("/connect") && request.method === "POST") { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { return Response.json({ serverId: id, authUrl: authUrl, message: "Please authorize access", }); }
return Response.json({ serverId: id, status: "connected" }); }
// Check connection status if (url.pathname.endsWith("/status") && request.method === "GET") { const mcpState = this.getMcpServers(); const connections = Object.entries(mcpState.servers).map( ([id, server]) => ({ serverId: id, name: server.name, state: server.state, authUrl: server.auth_url, }), ); return Response.json(connections); }
// Disconnect if (url.pathname.endsWith("/disconnect") && request.method === "POST") { const { serverId } = await request.json(); await this.removeMcpServer(serverId); return Response.json({ message: "Disconnected" }); }
return new Response("Not found", { status: 404 }); }}
export default { async fetch(request, env) { return ( (await routeAgentRequest(request, env, { cors: true })) || new Response("Not found", { status: 404 }) ); },};import { Agent, type AgentNamespace, routeAgentRequest } from "agents";import type { MCPClientOAuthResult } from "agents/mcp";
type Env = { MyAgent: AgentNamespace<MyAgent>;};
export class MyAgent extends Agent<Env, never> { onStart() { this.mcp.configureOAuthCallback({ customHandler: (result: MCPClientOAuthResult) => { if (result.authSuccess) { return new Response("<script>window.close();</script>", { headers: { "content-type": "text/html" }, }); } else { return new Response( `<script>alert('Authorization failed: ${result.authError}'); window.close();</script>`, { headers: { "content-type": "text/html" } }, ); } }, }); }
async onRequest(request: Request): Promise<Response> { const url = new URL(request.url);
// Connect to MCP server if (url.pathname.endsWith("/connect") && request.method === "POST") { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { return Response.json({ serverId: id, authUrl: authUrl, message: "Please authorize access", }); }
return Response.json({ serverId: id, status: "connected" }); }
// Check connection status if (url.pathname.endsWith("/status") && request.method === "GET") { const mcpState = this.getMcpServers(); const connections = Object.entries(mcpState.servers).map( ([id, server]) => ({ serverId: id, name: server.name, state: server.state, authUrl: server.auth_url, }), ); return Response.json(connections); }
// Disconnect if (url.pathname.endsWith("/disconnect") && request.method === "POST") { const { serverId } = (await request.json()) as { serverId: string }; await this.removeMcpServer(serverId); return Response.json({ message: "Disconnected" }); }
return new Response("Not found", { status: 404 }); }}
export default { async fetch(request: Request, env: Env) { return ( (await routeAgentRequest(request, env, { cors: true })) || new Response("Not found", { status: 404 }) ); },};- Connect to an MCP server — Get started (no OAuth)
- MCP Client API reference
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-