Handle OAuth with MCP servers
When you build an Agent that connects to OAuth-protected MCP servers (like Slack or Notion), your end users will need to authenticate before the Agent can access their data. This guide shows you how to implement OAuth flows so your users can authorize access seamlessly.
When your Agent connects to an OAuth-protected MCP server, here's what happens:
- Your code calls
addMcpServer()with the server URL - If OAuth is required, it returns an
authUrlinstead of immediately connecting - Your application presents the
authUrlto your user - Your user authenticates on the provider's site (Slack, etc.)
- The provider redirects back to your Agent's callback URL with an authorization code
- Your Agent completes the connection automatically
When you connect to an OAuth-protected server (like Cloudflare Observability), check if authUrl is returned. If present, automatically redirect your user to complete authorization:
export class ObservabilityAgent extends Agent { async onRequest(request) { const url = new URL(request.url);
if ( url.pathname.endsWith("connect-observability") && request.method === "POST" ) { // Attempt to connect to Cloudflare Observability MCP server 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); }
// No OAuth needed - connection complete return new Response( JSON.stringify({ serverId: id, status: "connected" }), { headers: { "Content-Type": "application/json" } }, ); }
return new Response("Not found", { status: 404 }); }}export class ObservabilityAgent extends Agent<Env, never> { async onRequest(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname.endsWith("connect-observability") && request.method === "POST") { // Attempt to connect to Cloudflare Observability MCP server 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); }
// No OAuth needed - connection complete return new Response( JSON.stringify({ serverId: id, status: "connected" }), { headers: { "Content-Type": "application/json" } }, ); }
return new Response("Not found", { status: 404 }); }}Your user is automatically redirected to the provider's OAuth page to authorize access.
Instead of an automatic redirect, you can also 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 API documentation or multi-step flows)
- Deep link: Use custom URL schemes for mobile apps
After your user completes OAuth, the provider redirects back to your Agent's callback URL. Configure what happens next.
For the automatic redirect approach, 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, maintaining a smooth flow.
If you used window.open() to open OAuth in a popup:
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" } }, ); } }, }); }}The popup closes automatically, and your main application can detect this and refresh the connection status.
Use the useAgent hook for automatic state updates:
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 receives real-time updates via WebSocket. No polling needed!
If you're not using React, poll the connection status:
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, // "authenticating" | "connecting" | "ready" | "failed" isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return new Response(JSON.stringify(connections, null, 2), { headers: { "Content-Type": "application/json" }, }); }
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, // "authenticating" | "connecting" | "ready" | "failed" isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return new Response(JSON.stringify(connections, null, 2), { headers: { "Content-Type": "application/json" }, }); }
return new Response("Not found", { status: 404 }); }}Connection states: 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 or clean up:
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) => { setMcpState(mcpServers); }, });
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-observability`, { 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: (mcpServers: MCPServersState) => { setMcpState(mcpServers); }, });
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-observability`, { 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: Slack credentials were incorrect
- Permission denied: User lacks required permissions (e.g., not a workspace admin)
- Expired session: OAuth session timed out
Failed connections remain in state until you remove them with removeMcpServer(serverId).
This example demonstrates a complete Cloudflare Observability OAuth integration. Users connect to Cloudflare Observability, authorize in a popup window, and the connection becomes available. The Agent provides endpoints to connect, check status, and disconnect.
import { Agent, routeAgentRequest } from "agents";export class ObservabilityAgent extends Agent { onStart() { // Configure OAuth callback to close popup window 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);
// Endpoint: Connect to Cloudflare Observability MCP server if ( url.pathname.endsWith("connect-observability") && request.method === "POST" ) { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { return new Response( JSON.stringify({ serverId: id, authUrl: authUrl, message: "Please authorize Cloudflare Observability access", }), { headers: { "Content-Type": "application/json" } }, ); }
return new Response( JSON.stringify({ serverId: id, status: "connected" }), { headers: { "Content-Type": "application/json" } }, ); }
// Endpoint: 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, isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return new Response(JSON.stringify(connections, null, 2), { headers: { "Content-Type": "application/json" }, }); }
// Endpoint: Disconnect from Cloudflare Observability if (url.pathname.endsWith("disconnect") && request.method === "POST") { const { serverId } = await request.json(); await this.removeMcpServer(serverId);
return new Response( JSON.stringify({ message: "Disconnected from Cloudflare Observability", }), { headers: { "Content-Type": "application/json" } }, ); }
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 = { ObservabilityAgent: AgentNamespace<ObservabilityAgent>;};
export class ObservabilityAgent extends Agent<Env, never> { onStart() { // Configure OAuth callback to close popup window 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);
// Endpoint: Connect to Cloudflare Observability MCP server if (url.pathname.endsWith("connect-observability") && request.method === "POST") { const { id, authUrl } = await this.addMcpServer( "Cloudflare Observability", "https://observability.mcp.cloudflare.com/mcp", );
if (authUrl) { return new Response( JSON.stringify({ serverId: id, authUrl: authUrl, message: "Please authorize Cloudflare Observability access", }), { headers: { "Content-Type": "application/json" } }, ); }
return new Response( JSON.stringify({ serverId: id, status: "connected" }), { headers: { "Content-Type": "application/json" } }, ); }
// Endpoint: 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, isReady: server.state === "ready", needsAuth: server.state === "authenticating", authUrl: server.auth_url, }), );
return new Response(JSON.stringify(connections, null, 2), { headers: { "Content-Type": "application/json" }, }); }
// Endpoint: Disconnect from Cloudflare Observability if (url.pathname.endsWith("disconnect") && request.method === "POST") { const { serverId } = (await request.json()) as { serverId: string }; await this.removeMcpServer(serverId);
return new Response( JSON.stringify({ message: "Disconnected from Cloudflare Observability" }), { headers: { "Content-Type": "application/json" } }, ); }
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 tutorial (no OAuth)
- McpClient — API reference — Complete API documentation
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
-