Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

用 MCP 服务器处理 OAuth

连接受 OAuth 保护的 MCP 服务器(例如 Slack 或 Notion)时,用户需要先认证,Agent 才能访问他们的数据。本指南介绍如何实现 OAuth 流程,实现无缝授权。

工作原理

  1. 调用 addMcpServer(),传入服务器 URL
  2. 如果需要 OAuth,会返回 authUrl,而不是立即建立连接
  3. authUrl 呈现给用户(重定向、弹窗或链接)
  4. 用户在 provider 网站完成认证
  5. Provider 重定向回你 Agent 的 callback URL
  6. 你的 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 文档。