本教程聚焦 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 服务器、想把工具推向团队级/企业级共享的中高级开发者。
Authorization 头、SSE(Server-Sent Events)概念claude --version 可正常输出版本号)本地 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 + 元数据发现),而不是你的工具函数里。
这条原则会贯穿后面所有步骤。
远程 MCP 早期用 HTTP+SSE 双端点方案,2025 年 3 月的规范(2025-03-26)推出了 Streamable HTTP 作为新的标准传输,用一个端点搞定双向通信。它取代了旧方案,也是 OAuth 的承载层。
它的核心机制是"单端点、多动词":
/mcptext/event-stream(SSE)流式响应Mcp-Session-Id 响应头标识一次有状态会话,客户端需在后续请求中回带它用 FastMCP 启动一台最简单的 Streamable HTTP 服务器:
# 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)
运行:
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 和三个 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 时最常见的卡点。
下面这张流程图把整条链路画清楚:
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 + 工具调用结果
记住这张图,后面写代码时每一步都对得上。
理解了规范,落地就清晰了。MCP 授权有两种工程实现模式:
example-remote-server)就内置了一套。下面演示外部模式,用 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 流程都封装好了:
# 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 存储与会话续期都有详尽说明。
常见问题:
client_id 配置,或在 AS 侧启用 DCR(GitHub 不支持 DCR,但 FastMCP 等客户端库会自动回退到静态 client 模式)。base_url + /auth/callback 完全一致,连末尾斜杠都要对齐。http://localhost 是唯一豁免项。一旦部署到非 localhost 域名,没有 TLS 会被浏览器和规范同时拒绝。服务器就绪后,接入方在 Claude Code 侧——它扮演 OAuth 客户端,自动执行上面流程图里的全部步骤。
# 注意是 --transport http,URL 指向 /mcp 端点
claude mcp add --transport http kb-remote http://localhost:8000/mcp
接着在 Claude Code 里调用任意一个该服务器的工具,或者运行:
claude mcp list # 查看连接状态
预期结果:Claude Code 检测到服务器返回 401,自动触发授权流程——系统会弹出浏览器窗口跳转到 GitHub 登录页,你授权后浏览器显示"授权成功,可关闭此页面",与此同时 Claude Code 在后台完成 PKCE 校验、换取 token、把 token 安全存储到操作系统钥匙串(macOS Keychain / Windows Credential Manager / Linux secret service)。之后 claude mcp list 显示该服务器状态为 connected,工具可正常调用。
常见问题:
gnome-keyring 或 pass 并解锁钥匙环;无头服务器上认证较麻烦,建议在有桌面的机器上完成首次授权。refresh_token 机制会自动刷新;若 IdP 没签发 refresh token,需重新走一次授权。把认证服务器从 localhost 推到真实环境,有四个不能省的动作。
1. TLS 终结 + 反向代理。 用 Nginx / Caddy / Cloudflare 在前面做 HTTPS,把 /mcp、/.well-known/*、/auth/callback 都反代到 FastMCP 进程。Caddy 的好处是自动签发证书。示例 Caddyfile 片段:
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)则无此约束,但会牺牲部分服务器主动推送能力。
把上面所有步骤串成一个完整可运行的最小项目。目标:团队任意成员用 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 里写明):
# 1. 添加远程服务器(一次性)
claude mcp add --transport http company-kb https://mcp.yourcompany.com/mcp
# 2. 在 Claude Code 对话里直接问
# "用 company-kb 搜一下 PDU 会话建立的排障手册"
第一次调用会弹出 GitHub 登录,授权后 token 缓存到本地钥匙串,后续无需重复登录。管理员在 IdP 侧把离职成员移出允许的组织,其 token 立即失效——这就是远程 + 认证相比本地 stdio 在管理上的巨大优势:身份与权限集中可控。
下面这张架构图展示了部署后的完整拓扑:
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 和管身份",职责清晰,符合企业安全治理要求。
modelcontextprotocol.io/specification 的 Authorization 章节,是所有实现的唯一权威来源,尤其要读 2025-03-26 及之后版本的更新(OAuth 元数据发现机制在这一版定型)。modelcontextprotocol/example-remote-server,其 docs/oauth-implementation.md 是目前最完整的 OAuth 实现指南,覆盖内部/外部两种模式。[!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 等场景全覆盖。
