跳转到内容
搜索文档

处理 MCP 服务器的 OAuth

最后更新 查看 MarkdownAgent 设置

连接到 OAuth 保护的 MCP 服务器(如 Slack 或 Notion)时,用户需先认证,Agent 才能访问其数据。本指南涵盖实现 OAuth 流程以实现无缝授权。

工作原理

  1. 使用服务器 URL 调用 addMcpServer()
  2. 若需要 OAuth,会返回 authUrl 而非立即连接
  3. 向用户展示 authUrl(重定向、弹窗或链接)
  4. 用户在 provider 站点完成认证
  5. Provider 重定向回 Agent 的回调 URL
  6. Agent 自动完成连接

MCP 客户端使用内置 DurableObjectOAuthClientProvider 安全管理 OAuth state——存储 nonce 与 server ID、在回调时验证、使用后或过期后清理。

发起 OAuth

连接到 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 });
	}
}
src/index.tsts
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",
		});
	}
}
src/index.tsts
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" },
				});
			},
		});
	}
}
src/index.tsts
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 显示。

监控连接状态

React 应用

使用 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>
	);
}
src/App.tsxtsx
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 });
	}
}
src/index.tsts
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>
	);
}
src/App.tsxtsx
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 })
		);
	},
};
src/index.tsts
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>;

相关

这篇文档对您有帮助吗?