用 MCP 服务器处理 OAuth
连接受 OAuth 保护的 MCP 服务器(例如 Slack 或 Notion)时,用户需要先认证,Agent 才能访问他们的数据。本指南介绍如何实现 OAuth 流程,实现无缝授权。
工作原理
- 调用
addMcpServer(),传入服务器 URL - 如果需要 OAuth,会返回
authUrl,而不是立即建立连接 - 把
authUrl呈现给用户(重定向、弹窗或链接) - 用户在 provider 网站完成认证
- Provider 重定向回你 Agent 的 callback URL
- 你的 Agent 自动完成连接
MCP client 使用内置的 DurableObjectOAuthClientProvider 来安全地管理 OAuth 状态 — 存储 nonce 和 server ID、在 callback 时验证、使用后或过期后清理。
启动 OAuth
连接受 OAuth 保护的服务器时,检查是否返回了 authUrl。如果有,把用户重定向去完成授权:
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 });
}
}
Explain Code
TypeScript
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 });
}
}
Explain Code
其他方式
除了自动重定向,你也可以把 authUrl 通过以下方式呈现给用户:
- 弹窗:
window.open(authUrl, '_blank', 'width=600,height=700'),适合仪表盘风格的应用 - 可点击链接:作为按钮或链接显示,适合多步骤流程
- 深链接:在移动应用中使用自定义 URL scheme
配置 callback 行为
OAuth 完成后,provider 会把请求重定向回你 Agent 的 callback URL。默认情况下,认证成功会重定向到你应用的 origin,认证失败会显示一个带错误消息的 HTML 错误页。
重定向回你的应用
OAuth 完成后,把用户重定向回你的应用:
JavaScript
export class MyAgent extends Agent {
onStart() {
this.mcp.configureOAuthCallback({
successRedirect: "/dashboard",
errorRedirect: "/auth-error",
});
}
}
TypeScript
export class MyAgent extends Agent<Env> {
onStart() {
this.mcp.configureOAuthCallback({
successRedirect: "/dashboard",
errorRedirect: "/auth-error",
});
}
}
成功时用户回到 /dashboard,失败时回到 /auth-error?error=<message>。
关闭弹窗
如果你是在弹窗中打开 OAuth,完成后自动关闭它:
JavaScript
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" },
});
},
});
}
}
Explain Code
TypeScript
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" },
});
},
});
}
}
Explain Code
你的主应用可以检测到弹窗关闭并刷新连接状态。如果 OAuth 失败,连接状态会变成 "failed",错误消息存储在 server.error 中,可以在你的 UI 上显示。
监控连接状态
React 应用
通过 WebSocket 使用 useAgent hook 获取实时更新:
JavaScript
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>
);
}
Explain Code
TypeScript
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>
);
}
Explain Code
onMcpUpdate 回调在 MCP 状态变化时自动触发 — 不需要轮询。
其他框架
通过端点轮询连接状态:
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 });
}
}
Explain Code
TypeScript
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 });
}
}
Explain Code
连接状态流转:authenticating(需要 OAuth)→ connecting(完成配置中)→ ready(可用)
处理失败
OAuth 失败时,连接状态变为 "failed",错误消息存储在 server.error 字段。在你的 UI 中显示这个错误,并允许用户重试:
JavaScript
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>
);
}
Explain Code
TypeScript
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>
);
}
Explain Code
常见失败原因:
- 用户取消:在完成授权前关闭了 OAuth 窗口
- 凭证无效:Provider 凭证不正确
- 权限不足:用户缺少必需权限
- 会话过期:OAuth 会话超时
失败的连接会保留在状态中,直到用 removeMcpServer(serverId) 移除。错误消息会自动转义以防 XSS 攻击,所以可以直接安全地在 UI 中显示。
完整示例
下面的例子展示了与 Cloudflare Observability 的完整 OAuth 集成。用户连接、在弹窗中授权,然后连接就可用了。错误会自动存储在连接状态中,供 UI 显示。
JavaScript
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 })
);
},
};
Explain Code
TypeScript
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>;
Explain Code
相关内容
连接到 MCP 服务器 不带 OAuth 的入门。
MCP Client API MCP client 的完整 API 文档。