Skip to content
Cloudflare Docs

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.

How it works

  1. Call addMcpServer() with the server URL
  2. If OAuth is required, an authUrl is returned instead of connecting immediately
  3. Present the authUrl to your user (redirect, popup, or link)
  4. User authenticates on the provider's site
  5. Provider redirects back to your Agent's callback URL
  6. 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.

Initiate OAuth

When connecting to an OAuth-protected server, check if authUrl is returned. If present, redirect your user to complete authorization:

JavaScript
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 });
}
}

Alternative approaches

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

Configure callback behavior

After OAuth completes, the provider redirects back to your Agent's callback URL. Configure what happens next.

Redirect to your application

Redirect users back to your application after OAuth completes:

JavaScript
export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
successRedirect: "/dashboard",
errorRedirect: "/auth-error",
});
}
}

Users return to /dashboard on success or /auth-error?error=<message> on failure.

Close popup window

If you opened OAuth in a popup, close it automatically when complete:

JavaScript
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" } },
);
}
},
});
}
}

Your main application can detect the popup closing and refresh the connection status.

Monitor connection status

React applications

Use the useAgent hook for real-time updates via WebSocket:

JavaScript
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>
);
}

The onMcpUpdate callback fires automatically when MCP state changes — no polling needed.

Other frameworks

Poll the connection status via an endpoint:

JavaScript
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 });
}
}

Connection states flow: authenticating (needs OAuth) → connecting (completing setup) → ready (available for use)

Handle failures

When OAuth fails, the connection state becomes "failed". Detect this in your UI and allow users to retry:

JavaScript
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>
);
}

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).

Complete example

This example demonstrates a complete OAuth integration with Cloudflare Observability. Users connect, authorize in a popup window, and the connection becomes available.

JavaScript
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 })
);
},
};