MCP 鉴权:OAuth 两侧分工

看懂文档那句话:"This page is the server side. A client that discovers your authorization server and fetches the token — see OAuth clients."
目的为将来把你的 MCP server 部署成公网 HTTP 服务做准备——那时"谁能调我的工具"就成了必答题,而文档把答案拆在了 server / client 两页里,得先看懂分工。
学完标准看完你应该能回答:
  1. 完整流程 11 步,每一步是谁在干活?哪些步是"本页(server 侧)"教的,哪些步是"《OAuth clients》页"教的?→ 视图 ①
  2. 你现在的 stdio server 为什么不需要这一套?什么时候才需要?→ 视图 ②
  3. 为什么是 token 而不是密码?为什么客户端能"自动发现"授权服务器?→ 视图 ③
深度预算4 个视图。这是"将来时"的知识——你当前的 stdio 模式用不到,目标是建立地图,不是背细节。

一次"从被拒到放行"的完整旅程

四个角色:用户(在浏览器里点同意的人)、MCP 客户端(如 Claude Code)、你的 MCP server、授权服务器(发 token 的,通常是 Auth0 / Keycloak 这类现成产品)。每一步的徽章告诉你:这是文档哪一页教的活。点「下一步」。

👤 用户浏览器
🖥 MCP 客户端Claude Code 等
🛠 你的 MCP server受保护资源
🔐 授权服务器Auth0 / Keycloak…
客户端的活(OAuth clients 页) server 侧的活(本页) 授权服务器的活(现成产品) 用户亲自操作
按「下一步」开始

客户端第一次调用你的 server,还没有任何凭证。

📜 线上真实往来(累积)

# HTTP 报文摘要会随步骤出现在这里
💡 数一数徽章就看懂了那句文档:紫色步骤(声明自己受保护、指路、验 token)= "This page is the server side";青色步骤(发现授权服务器、走流程拿 token、带 token 重试)= "A client that discovers your authorization server and fetches the token" → 《OAuth clients》页。琥珀色的授权服务器你通常不用自己写——用现成产品,你只负责和它对接。

你现在的 stdio server,为什么一行鉴权都不用写?

鉴权要回答的问题是:"这个请求是谁发来的?他有资格吗?"——两种传输方式下,这个问题的处境完全不同。

stdio(你现在的模式)

  • server 是客户端亲手启动的子进程——能启动它,说明你已经登录了这台电脑
  • 通信走 stdin/stdout 管道,只有父进程摸得到,外人物理上无法插话
  • 信任由操作系统的进程边界保证,天然只有一个调用者
  • 结论:不需要 OAuth。"谁在调用"这个问题在进程创建那一刻已经回答完了。

Streamable HTTP(公网部署)

  • server 挂在一个 URL 上,全世界任何人都能对它发请求
  • 请求可能来自你、你同事、爬虫、攻击者——网络包不自带身份
  • 工具可能碰真实数据(数据库、内部 API),放错人进来就是事故
  • 结论:必须鉴权。每个请求都要出示凭证,server 每次都要验。
📌 什么时候需要回来读这两页文档?触发条件就一个:你把 server 从 stdio 改成 HTTP 对外提供服务的那天。在那之前,OAuth 这整套对你是"知道存在即可"。这也是文档设计的信号:鉴权章节只出现在 HTTP 传输的章节树下,stdio 教程里提都不提。

为什么必须长这样

和协程页一样的玩法:左边是客观约束,右边是 OAuth/MCP 的设计选择。取消勾选某个约束,只靠它支撑的设计会变灰——检验哪些是"必然",哪些只是"惯例"。

约束(试着关掉)

被逼出来的设计

凭 token 而不是凭密码

每个请求都要证明身份(C1),但密码不能到处传(C2)→ 用户只在授权服务器登录一次,换出一张范围受限、会过期、可吊销的 token 给客户端用。泄露了也只是废掉一张票,不是丢掉账户。
C1C2
约束被关掉了——如果请求天然可信或密码可以随便给,直接传密码更简单,token 失去理由。

发现机制:401 指路 + /.well-known 元数据

客户端不认识你(C3),所以协议规定了"问路"的标准动作:被拒时 server 在 WWW-Authenticate 头里给出元数据地址,元数据里写着授权服务器在哪、端点是什么。任何客户端对任何 server 都能自动走通,无需人工配置。
C3
约束被关掉了——如果客户端和 server 是一家人、配置写死即可,就不需要发现机制。

PKCE:一次性的"暗号对"

桌面客户端藏不住固定密钥(C4)→ 每次授权前现场生成随机 verifier,把它的哈希(challenge)带去授权;换 token 时出示原文。中途截获授权码的人没有 verifier,换不走 token。用一次性随机数替代了藏不住的长期秘密。
C4
约束被关掉了——如果客户端能安全保管密钥(如后端服务器),经典的 client_secret 就够了。

三角分工:server 只验票,不发票

发 token 涉及登录页、用户库、密钥轮换——安全上极难做对。所以架构上把"发票"外包给专业的授权服务器(C1 的风险集中到专业选手),你的 server 只做无状态验票。这正是那句文档的由来:两页文档,两侧分工。
C1C3
约束被关掉了——没有公网风险时,server 自己随便发个 API key 也未尝不可。

预测,然后验证

老规矩:先在心里给出答案,再点揭晓。

题 1:token 是谁签发的?

客户端最终拿到的 access token,是下面哪个角色签发的?

答案:授权服务器。这是三角分工的核心:发票的和验票的不是同一个人。你的 server 拿到 token 后,用授权服务器公开的签名公钥验证它(签名对不对、过没过期、audience 是不是自己),全程不需要问授权服务器一句话。如果你选了 A——那是 API key 模式的直觉,OAuth 恰恰为了把"发票"这个高危业务从你的 server 里挪走。

题 2:你把现在的 server.py 部署到公司内网 HTTP,谁的代码要改?

你的 Demo server 从 stdio 改为 HTTP 并要求鉴权。add 和 greeting 这些工具函数本身需要改动吗?

答案:不用改。又是那个反复出现的主题——分层。验 token 发生在请求进门的传输层,过了门,工具函数收到的就是干净的参数。协议和传输分层、类型和传输分层、鉴权和业务分层——MCP 的整个 SDK 都在重复同一个设计动作。(需要知道"是谁在调用"的工具,可以从请求上下文里读身份,但那是读信息,不是做验证。)

没看懂?把问题带回给 Claude

勾选没看懂的部分,或用自己的话复述流程,点复制后粘回 Claude Code 的对话。

已复制到剪贴板 ✓