<!-- title: 构建带OAuth2认证的远程MCP服务器 -->

## 概述

本教程聚焦 MCP（Model Context Protocol）的一个被多数入门文章忽略、却在生产环境里绕不开的维度：**远程 MCP 服务器的身份认证**。前面你已经掌握了用 stdio 跑本地 MCP、用 FastMCP/Go/TypeScript 暴露工具、甚至把真实 API 封装成工具；但当一台 MCP 服务器要部署到公网、被多个 Claude Code 客户端（或团队成员、外部 Agent）访问时，"谁能调用"就成了必须正面回答的问题。本教程从 Streamable HTTP 传输讲起，拆解 MCP 规范中基于 OAuth 2.1 的授权模型（双 metadata 发现、PKCE、动态客户端注册），带你用 FastMCP 搭建一台带认证的远程服务器，并演示 Claude Code 如何以 OAuth 客户端身份完成授权后接入。适合已经能独立写出本地 MCP 服务器、想把工具推向团队级/企业级共享的中高级开发者。

## 前置条件

- 已掌握至少一种语言的 MCP 服务器写法（Python/FastMCP、TypeScript SDK 或 Go），能跑通本地 stdio 版本
- 理解 HTTP 基础：请求方法、状态码、`Authorization` 头、SSE（Server-Sent Events）概念
- 对 OAuth 2.0 有基本认知（知道授权码流程、access token、refresh token 是什么）；不熟悉也无妨，本教程会带流程图补齐
- 本机已安装 Node.js 20+、Python 3.10+、以及 Claude Code CLI（`claude --version` 可正常输出版本号）
- 注册一个 GitHub OAuth App 或准备一个 IdP（Azure AD / Google / Okta）用于提供真实的身份源——本教程以 GitHub 为例，因为门槛最低

---

## 第一步：理解"远程"为什么必须谈认证

本地 stdio MCP 和远程 HTTP MCP，是两种完全不同的信任模型，这决定了认证是否必要。

**stdio 模式**：Claude Code 在你本机拉起一个子进程，通过标准输入输出通信。服务器跑在你自己的机器上，访问的是你本地的文件、本地命令，调用者就是你自己。此时 MCP 规范明确规定——**授权不在范围内**（authorization is out of scope for stdio）。没有认证不是疏忽，而是合理的：进程边界本身就是隔离。

**HTTP 模式**：服务器跑在另一台主机上（公司内网服务器、云函数、VPS），任何能访问到那个 URL 的客户端都可以发 JSON-RPC 请求。如果你的工具会读内部知识库、调付费 API、改数据库，"裸奔"等于把能力免费开放给整个网络。MCP 规范为此专门定义了 `Authorization` 章节，**仅适用于 HTTP 类传输**，并强制要求实现 OAuth 2.1。

所以这条经验法则要刻进脑子里：

> 一旦 MCP 服务器的传输从 stdio 切到 Streamable HTTP，认证就从"可选"变成"必须"。认证逻辑写在传输层（401 + 元数据发现），而不是你的工具函数里。

这条原则会贯穿后面所有步骤。

## 第二步：Streamable HTTP 传输——远程化的物理基础

远程 MCP 早期用 HTTP+SSE 双端点方案，2025 年 3 月的规范（2025-03-26）推出了 **Streamable HTTP** 作为新的标准传输，用一个端点搞定双向通信。它取代了旧方案，也是 OAuth 的承载层。

它的核心机制是"单端点、多动词"：

- 默认端点路径是 `/mcp`
- **POST**：客户端向服务器发 JSON-RPC 请求，服务器可以返回普通 JSON，也可以返回 `text/event-stream`（SSE）流式响应
- **GET**：客户端打开一条 SSE 长连接，用来接收服务器主动推送的通知
- **DELETE**：客户端通知服务器结束当前会话
- `Mcp-Session-Id` 响应头标识一次有状态会话，客户端需在后续请求中回带它

用 FastMCP 启动一台最简单的 Streamable HTTP 服务器：

```python
# server.py
from fastmcp import FastMCP

mcp = FastMCP("demo-remote")


@mcp.tool
def whoami() -> dict:
    """返回当前调用者的身份信息"""
    # 注意：此刻还没有认证，先占位
    return {"user": "anonymous", "source": "no-auth"}


if __name__ == "__main__":
    # transport=http 即 Streamable HTTP
    mcp.run(transport="http", host="0.0.0.0", port=8000)
```

运行：

```bash
pip install fastmcp
python server.py
```

**预期结果**：终端输出类似 `Uvicorn running on http://0.0.0.0:8000`，MCP 端点实际为 `http://localhost:8000/mcp`。此时任何人都能调用 `whoami`——这就是我们要解决的问题。

**常见问题**：

- *端口被占用*：换个端口，或 `lsof -i :8000` 找到占用进程。
- *用 `claude mcp add` 接入报 "transport not supported"*：确认你写的是 `--transport http` 而非 `sse`；旧的 sse 选项在较新版本中已被废弃或合并。
- *云服务器上 `host=127.0.0.1` 导致外部连不上*：远程部署必须监听 `0.0.0.0` 或对应网卡地址，并在前置反向代理处处理 TLS。

## 第三步：拆解 MCP 的 OAuth 2.1 授权规范

这是整个教程的核心。MCP 没有重新发明一套认证，而是直接站在 OAuth 2.1 和三个 RFC 上：

| 能力 | 依赖的规范 | 端点 / 机制 |
|---|---|---|
| 受保护资源的元数据发现 | RFC 9728 | `GET /.well-known/oauth-protected-resource` |
| 授权服务器元数据发现 | RFC 8414 | `GET /.well-known/oauth-authorization-server` |
| 动态客户端注册 | RFC 7591 | `POST {registration_endpoint}` |
| 授权码 + PKCE | OAuth 2.1 / RFC 7636 | `authorization_endpoint` → `token_endpoint` |
| 令牌校验 | RFC 6750 | `Authorization: Bearer <token>` |

整个授权流程的关键设计是**两次发现 + 一次注册 + 一次授权**，分四个角色：Claude Code（OAuth 客户端）、MCP 服务器（资源服务器 RS）、授权服务器（AS，可与 RS 同体也可独立）、用户（资源所有者）。

**为什么需要两次 metadata 发现？** 因为 MCP 服务器（RS）和身份提供方（AS）可能是分离的。客户端先问 RS"你归谁管"，拿到 AS 的 URL；再去问 AS"我该怎么登录"，拿到所有端点地址。这种解耦让企业可以把 MCP 服务器接到已有的 Azure AD / Okta 后面，而不必在每个 MCP 服务器里重写登录逻辑。

**为什么强制 PKCE？** OAuth 2.1 把 PKCE（Proof Key for Code Exchange）从"推荐"升级为"强制"。流程是：客户端生成一个随机 `code_verifier`，算出 `code_challenge = BASE64URL(SHA256(code_verifier))`，请求授权时只发 `code_challenge`，换 token 时才发 `code_verifier` 自证。这样即使授权码在浏览器跳转过程中被截获，攻击者没有 `code_verifier` 也换不到 token。Claude Code 在背后自动完成全部 PKCE 计算，你不需要手写。

**为什么需要动态客户端注册（DCR）？** 传统 OAuth 需要提前在 AS 注册一个 `client_id`/`client_secret`。但 MCP 客户端种类繁多、用户分散，规范允许 AS 提供 `registration_endpoint`，让客户端运行时自助注册拿到 `client_id`。如果 AS 不支持 DCR，则需要手动配置静态 `client_id`——这是接企业 IdP 时最常见的卡点。

下面这张流程图把整条链路画清楚：

```mermaid
sequenceDiagram
    participant CC as Claude Code (客户端)
    participant RS as MCP 服务器 (资源服务器)
    participant AS as 授权服务器 (IdP)
    participant U as 用户浏览器

    Note over CC,RS: ① 首次调用，无 token
    CC->>RS: POST /mcp (无 Authorization 头)
    RS-->>CC: 401 + WWW-Authenticate (resource_metadata URL)

    Note over CC,AS: ② 第一次发现：受保护资源元数据
    CC->>RS: GET /.well-known/oauth-protected-resource
    RS-->>CC: { authorization_servers: ["https://idp..."] }

    Note over CC,AS: ③ 第二次发现：授权服务器元数据
    CC->>AS: GET /.well-known/oauth-authorization-server
    AS-->>CC: { authorization_endpoint, token_endpoint, registration_endpoint }

    Note over CC,AS: ④ 动态客户端注册（可选）
    CC->>AS: POST registration_endpoint
    AS-->>CC: { client_id }

    Note over CC,U: ⑤ 授权码 + PKCE
    CC->>CC: 生成 code_verifier / code_challenge(S256)
    CC->>U: 打开浏览器到 authorization_endpoint?code_challenge=...
    U->>AS: 登录并同意授权
    AS-->>CC: 重定向回，带 authorization_code

    Note over CC,AS: ⑥ 换取令牌
    CC->>AS: POST token_endpoint (code + code_verifier)
    AS-->>CC: { access_token, refresh_token }

    Note over CC,RS: ⑦ 携带令牌访问
    CC->>RS: POST /mcp  Authorization: Bearer access_token
    RS-->>CC: 200 + 工具调用结果
```

记住这张图，后面写代码时每一步都对得上。

## 第四步：给 FastMCP 服务器装上认证

理解了规范，落地就清晰了。MCP 授权有两种工程实现模式：

- **内部认证模式（Internal / 自带 AS）**：MCP 服务器自己同时扮演 RS 和 AS，适合开发调试和简单场景。规范参考服务器（`example-remote-server`）就内置了一套。
- **外部认证模式（External / 委托给 IdP）**：MCP 服务器只做 RS，把认证委托给 GitHub / Azure AD / Okta 等专业 IdP。这是生产推荐模式。

下面演示外部模式，用 GitHub 作为 IdP（你只需要去 GitHub → Settings → Developer settings → OAuth Apps → New OAuth App 注册一个，拿到 `client_id` 和 `client_secret`，回调 URL 填 `http://localhost:8000/auth/callback`）。

FastMCP 2.x 内置了常用 IdP Provider，把繁琐的 metadata 端点和 PKCE 流程都封装好了：

```python
# protected_server.py
from fastmcp import FastMCP
from fastmcp.server.auth import GitHubProvider

# 委托给 GitHub 做身份认证
auth = GitHubProvider(
    client_id="Ov23liXXXXXXXXXXXX",        # 来自 GitHub OAuth App
    client_secret="你的 client_secret",
    base_url="http://localhost:8000",        # 本机回调基地址
    # 可选：限定允许的 GitHub 用户/组织，实现最小权限
)

mcp = FastMCP("kb-remote", auth=auth)


@mcp.tool
def whoami() -> dict:
    """返回当前调用者的 GitHub 身份"""
    # FastMCP 会把已认证主体信息注入上下文
    return {"status": "authenticated"}


@mcp.tool
def search_kb(query: str, limit: int = 5) -> list:
    """在内部知识库中检索文档"""
    # 这里接你的向量库 / Elasticsearch
    return [{"title": f"匹配 {query} 的结果 {i}", "score": 0.9 - i * 0.1}
            for i in range(min(limit, 3))]


if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)
```

**预期结果**：启动后，服务器会在 `/.well-known/oauth-protected-resource` 和 `/.well-known/oauth-authorization-server` 暴露标准元数据，并在 `/mcp` 端点要求 Bearer token。没有 token 直接 `curl http://localhost:8000/mcp` 会收到 401。

**如果你想看清底层**，FastMCP 之外也可以用纯 Starlette/FastAPI 中间件手写，关键是实现三个点：返回 401 时带 `WWW-Authenticate` 头、暴露两个 `.well-known` 端点、在每次请求中间件里校验 Bearer token。规范参考仓库 `modelcontextprotocol/example-remote-server` 的 `oauth-implementation.md` 给出了完整的端到端实现，建议通读一遍——它对"内部模式"和"外部模式"的差异、token 存储与会话续期都有详尽说明。

**常见问题**：

- *报错 "registration_endpoint not supported"*：说明你的 IdP 不支持 DCR。改用静态 `client_id` 配置，或在 AS 侧启用 DCR（GitHub 不支持 DCR，但 FastMCP 等客户端库会自动回退到静态 client 模式）。
- *回调 URL 不匹配*：GitHub OAuth App 里填的 Authorization callback URL 必须和服务器 `base_url + /auth/callback` 完全一致，连末尾斜杠都要对齐。
- *生产环境必须 HTTPS*：OAuth 规范要求重定向走 HTTPS，本机 `http://localhost` 是唯一豁免项。一旦部署到非 localhost 域名，没有 TLS 会被浏览器和规范同时拒绝。

## 第五步：用 Claude Code 完成授权并接入

服务器就绪后，接入方在 Claude Code 侧——它扮演 OAuth 客户端，自动执行上面流程图里的全部步骤。

```bash
# 注意是 --transport http，URL 指向 /mcp 端点
claude mcp add --transport http kb-remote http://localhost:8000/mcp
```

接着在 Claude Code 里调用任意一个该服务器的工具，或者运行：

```bash
claude mcp list            # 查看连接状态
```

**预期结果**：Claude Code 检测到服务器返回 401，自动触发授权流程——系统会**弹出浏览器窗口**跳转到 GitHub 登录页，你授权后浏览器显示"授权成功，可关闭此页面"，与此同时 Claude Code 在后台完成 PKCE 校验、换取 token、把 token 安全存储到操作系统钥匙串（macOS Keychain / Windows Credential Manager / Linux secret service）。之后 `claude mcp list` 显示该服务器状态为 connected，工具可正常调用。

**常见问题**：

- *浏览器没自动弹出*：终端会打印一个授权 URL，手动复制到浏览器打开即可；常见于 SSH 远程会话场景，需要端口转发或本地浏览器。
- *"Token 存储失败 / cannot access keyring"*：Linux 上需安装 `gnome-keyring` 或 `pass` 并解锁钥匙环；无头服务器上认证较麻烦，建议在有桌面的机器上完成首次授权。
- *token 过期后工具调用报 401*：规范的 `refresh_token` 机制会自动刷新；若 IdP 没签发 refresh token，需重新走一次授权。
- *多用户共享同一台客户端机器*：token 按用户隔离存储，切换系统用户即切换身份。

## 第六步：生产部署的四个要点

把认证服务器从 localhost 推到真实环境，有四个不能省的动作。

**1. TLS 终结 + 反向代理。** 用 Nginx / Caddy / Cloudflare 在前面做 HTTPS，把 `/mcp`、`/.well-known/*`、`/auth/callback` 都反代到 FastMCP 进程。Caddy 的好处是自动签发证书。示例 Caddyfile 片段：

```caddy
mcp.yourcompany.com {
    reverse_proxy /mcp              127.0.0.1:8000
    reverse_proxy /.well-known/*    127.0.0.1:8000
    reverse_proxy /auth/*           127.0.0.1:8000
}
```

**2. 作用域最小化（Scope）。** 不要让一个 token 能调所有工具。把工具按敏感度分组，在 AS 侧定义 scope（如 `kb:read`、`db:write`），在 MCP 服务器的请求中间件里校验 token 的 scope 是否覆盖被调工具。这是从"知道你是谁"升级到"知道你能干什么"。

**3. 限流与审计。** 远程服务器暴露在网络上，必须对 `/mcp` 做速率限制（按 token、按 IP），并记录每次工具调用的 `调用者、工具名、参数摘要、时间、结果状态` 到审计日志。OWASP 的 MCP Security Cheat Sheet 把这列为高优先级项。

**4. 容器化与会话亲和。** Streamable HTTP 的有状态会话依赖 `Mcp-Session-Id`。如果横向扩展多实例，前置负载均衡需做会话亲和（sticky session），或在应用层把会话状态外置到 Redis。无状态模式（stateless）则无此约束，但会牺牲部分服务器主动推送能力。

## 实战案例：带认证的企业内部知识库 MCP

把上面所有步骤串成一个完整可运行的最小项目。目标：团队任意成员用 Claude Code 接入，登录 GitHub 后即可在对话里检索公司内部文档；非授权人员无法调用。

目录结构：

```
kb-mcp/
├── server.py          # FastMCP + GitHub OAuth
├── requirements.txt   # fastmcp
├── Caddyfile          # 生产 TLS + 反代
└── README.md          # 接入说明（给团队成员）
```

`server.py` 已在第四步给出完整代码。`requirements.txt`：

```
fastmcp>=2.3.0
```

团队成员接入只需两条命令（README 里写明）：

```bash
# 1. 添加远程服务器（一次性）
claude mcp add --transport http company-kb https://mcp.yourcompany.com/mcp

# 2. 在 Claude Code 对话里直接问
# "用 company-kb 搜一下 PDU 会话建立的排障手册"
```

第一次调用会弹出 GitHub 登录，授权后 token 缓存到本地钥匙串，后续无需重复登录。管理员在 IdP 侧把离职成员移出允许的组织，其 token 立即失效——这就是远程 + 认证相比本地 stdio 在管理上的巨大优势：**身份与权限集中可控**。

下面这张架构图展示了部署后的完整拓扑：

```mermaid
flowchart LR
    subgraph 团队成员
        A1[开发者A<br/>Claude Code]
        A2[开发者B<br/>Claude Code]
    end

    A1 & A2 -->|HTTPS + Bearer Token| LB[Cloudflare / Caddy<br/>TLS 终结 + 限流]

    LB -->|反向代理| MCP1[FastMCP RS<br/>:8000]
    LB --> MCP2[FastMCP RS<br/>:8000 副本]

    MCP1 & MCP2 <-->|校验 token| GH[GitHub OAuth<br/>授权服务器 AS]
    MCP1 & MCP2 -->|检索| KB[(内部知识库<br/>向量库/ES)]

    style GH fill:#fff4e6,stroke:#d97706
    style LB fill:#e6f7ff,stroke:#096dd9
```

注意图中 RS（FastMCP）与 AS（GitHub）是分离的——这正是第三步强调的"外部认证模式"。RS 只负责"认 token"，AS 负责"发 token 和管身份"，职责清晰，符合企业安全治理要求。

## 延伸阅读

- **MCP 授权规范原文**：`modelcontextprotocol.io/specification` 的 Authorization 章节，是所有实现的唯一权威来源，尤其要读 2025-03-26 及之后版本的更新（OAuth 元数据发现机制在这一版定型）。
- **官方参考实现**：GitHub 仓库 `modelcontextprotocol/example-remote-server`，其 `docs/oauth-implementation.md` 是目前最完整的 OAuth 实现指南，覆盖内部/外部两种模式。
- **三个 RFC**：RFC 9728（受保护资源元数据）、RFC 8414（授权服务器元数据）、RFC 7591（动态客户端注册），理解它们就能读懂任何 MCP 认证实现。
- **OWASP MCP Security Cheat Sheet**：把 MCP 部署的安全清单整理成了可勾选条目，部署前过一遍。
- **云厂商部署指南**：AWS "Deploying MCP servers on AWS"、Google Cloud Run "Build and deploy a remote MCP server"——直接给出容器化 + 托管 + 认证的落地参考架构。

## 训练营适用性评估

- **适合人群**：中高级后端/AI 工程师，已能独立写出本地 MCP 服务器，需把工具推向团队共享或企业生产。不适合纯零基础——建议先完成"TypeScript/FastMCP 本地服务器"的前置课程。
- **训练营类型**：适合"AI Agent 工程化"或"企业 AI 基础设施"方向的进阶营；也可作为安全合规主题营的实操环节。
- **预计教学时长**：8 课时。其中规范与流程图讲解 2 课时、FastMCP 认证服务器搭建 2 课时、Claude Code 接入调试 2 课时、生产部署（TLS+限流+审计）2 课时。
- **实训产出要求**：学员需独立交付一台可远程访问、带 OAuth 登录、部署在 HTTPS 域名后的 MCP 服务器，并附团队接入文档。
- **评估重点**：能否说清"何时该用认证"（信任模型）、能否画出 OAuth 全链路时序图、能否处理 DCR 不支持 / token 过期 / 无头机授权等真实排障场景。

## 参考来源

1. [Authorization — Model Context Protocol Specification (2025-03-26)](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) — MCP 授权规范的权威原文，定义 OAuth 2.1 授权码流程与元数据发现
2. [OAuth Implementation Guide — modelcontextprotocol/example-remote-server](https://github.com/modelcontextprotocol/example-remote-server/blob/main/docs/oauth-implementation.md) — 官方参考服务器的完整 OAuth 2.0 + PKCE 实现指南，含内部/外部两种认证模式
3. [Bringing streamable HTTP transport and Python language support to MCP servers — Cloudflare Blog](https://blog.cloudflare.com/streamable-http-mcp-servers-python/) — Streamable HTTP 传输机制的设计动机与工程实践
4. [Complete Guide to MCP (Model Context Protocol) in 2026 — DEV Community](https://dev.to/x4nent/complete-guide-to-mcp-model-context-protocol-in-2026-architecture-implementation-and-4a11) — 含 OAuth 2.1 企业安全章节与架构总览
5. [MCP Security Cheat Sheet — OWASP](https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html) — MCP 部署的安全检查清单，涵盖限流、审计、作用域最小化

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
