连接到 OAuth 保护的 MCP 服务器(如 Slack 或 Notion)时,用户需先认证,Agent 才能访问其数据。本指南涵盖实现 OAuth 流程以实现无缝授权。
- 使用服务器 URL 调用
addMcpServer() - 若需要 OAuth,会返回
authUrl而非立即连接 - 向用户展示
authUrl(重定向、弹窗或链接) - 用户在 provider 站点完成认证
- Provider 重定向回 Agent 的回调 URL
- Agent 自动完成连接
MCP 客户端使用内置 DurableObjectOAuthClientProvider 安全管理 OAuth state——存储 nonce 与 server ID、在回调时验证、使用后或过期后清理。
连接到 OAuth 保护的服务器时,检查是否返回 authUrl。若存在,重定向用户完成授权:
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> {
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 });
}
}除自动重定向外,可将 authUrl 以以下方式呈现给用户:
- 弹窗:
window.open(authUrl, '_blank', 'width=600,height=700'),适用于仪表板类应用 - 可点击链接:在多步流程中显示为按钮或链接
- Deep link:移动应用使用自定义 URL scheme
OAuth 完成后,provider 重定向回 Agent 回调 URL。默认情况下,成功认证重定向到应用 origin,失败则显示带错误信息的 HTML 错误页。
OAuth 完成后将用户重定向回应用:
export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
successRedirect: "/dashboard",
errorRedirect: "/auth-error",
});
}
}export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureOAuthCallback({
successRedirect: "/dashboard",
errorRedirect: "/auth-error",
});
}
}成功时用户返回 /dashboard,失败时返回 /auth-error?error=<message>。
若在弹窗中打开 OAuth,完成时自动关闭:
import { Agent } from "agents";
export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: () => {
// Close the popup after OAuth completes
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
}import { Agent } from "agents";
export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: () => {
// Close the popup after OAuth completes
return new Response("<script>window.close();</script>", {
headers: { "content-type": "text/html" },
});
},
});
}
}主应用可检测弹窗关闭并刷新连接状态。若 OAuth 失败,连接 state 变为 "failed",错误信息存储在 server.error 中供 UI 显示。
使用 useAgent hook 通过 WebSocket 获取实时更新:
import { useAgent } from "agents/react";
import { useState } from "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>
)}
{server.state === "failed" && server.error && (
<p className="error">{server.error}</p>
)}
</div>
))}
</div>
);
}import { useAgent } from "agents/react";
import { useState } from "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>
)}
{server.state === "failed" && server.error && (
<p className="error">{server.error}</p>
)}
</div>
))}
</div>
);
}MCP state 变化时 onMcpUpdate 回调自动触发——无需轮询。
通过端点轮询连接状态:
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> {
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 });
}
}连接 state 流程:authenticating(需要 OAuth)→ connecting(完成设置)→ ready(可用)
OAuth 失败时,连接 state 变为 "failed",错误信息存储在 server.error 字段。在 UI 中显示错误并允许用户重试:
import { useAgent } from "agents/react";
import { useState } from "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>
{server.error && <p className="error">{server.error}</p>}
<button
onClick={() => handleRetry(id, server.server_url, server.name)}
>
Retry Connection
</button>
</div>
)}
</div>
))}
</div>
);
}import { useAgent } from "agents/react";
import { useState } from "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>
{server.error && <p className="error">{server.error}</p>}
<button
onClick={() => handleRetry(id, server.server_url, server.name)}
>
Retry Connection
</button>
</div>
)}
</div>
))}
</div>
);
}常见失败原因:
- 用户取消:在完成授权前关闭 OAuth 窗口
- 无效凭据:Provider 凭据不正确
- 权限被拒绝:用户缺少所需权限
- Session 过期:OAuth session 超时
失败连接保留在 state 中,直到用 removeMcpServer(serverId) 移除。错误信息自动转义以防 XSS,可安全直接在 UI 中显示。
本示例演示与 Cloudflare Observability 的完整 OAuth 集成。用户连接、在弹窗中授权,连接即可用。错误自动存储在连接 state 中供 UI 显示。
import { Agent, routeAgentRequest } from "agents";
export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: () => {
// Close popup after OAuth completes (success or failure)
return new Response("<script>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, routeAgentRequest } from "agents";
type Env = {
MyAgent: DurableObjectNamespace<MyAgent>;
};
export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureOAuthCallback({
customHandler: () => {
// Close popup after OAuth completes (success or failure)
return new Response("<script>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 })
);
},
} satisfies ExportedHandler<Env>;