前面我们已经介绍了怎么开发、部署一个MCP服务,并在客户端进行配置,但是会引入另外的问题。如果没办法分辨那些事合理的请求,会导致资源的滥用,甚至被不可预期的用户暴力使用,所以这个时候,我们需要开始考虑鉴权,在MCP的调用阶段进行用户的级别,确保是合法用户在使用我们的MCP服务。
目前常用的鉴权方式有 「OAuth」「Bearer」。
开始之前:鉴权到底拦的是哪一层?
想象 MCP 像一扇门后面的工具箱。Cursor 等宿主通过协议来「开门拿工具」。
- 鉴权(authentication / authorization 在实践里常混称「鉴权」):回答「来的人有没有资格开门、能拿哪些工具」。
- Tool 业务参数:回答「工具怎么用」,例如
template_name=sirna-predict。
常见误区:把长期密钥做成 Tool 参数,例如 submit_template(api_token=...)。模型可能把 token 写进对话、日志,也和协议推荐的做法不一致。正确位置是:
- 本地子进程:环境变量、本机密钥文件
- 远程 HTTP:请求头里的凭证(后面会反复看到
Authorization: Bearer ...)
段末注释:本文说的 MCP(Model Context Protocol,模型上下文协议)指工具协议本身;Host 指 Cursor、Claude Desktop 等承载模型的应用。
场景一:只有我自己在本机 Cursor 里用
你的需求
- 在自己电脑上调试 / 日常使用
- 不打算给同事一个网址去连
这时候通常怎么连
Host 用 command 在本机 拉起一个 Python 进程,双方通过进程的标准输入输出说话。这种传输叫 stdio。
1 | Cursor ──(stdin/stdout 管道)──▶ 本机 python server.py |
鉴权上你要改什么
应用层往往什么都不用加。 安全边界已经变成:
- 谁能登录这台电脑
- 谁能改
.cursor/mcp.json并执行你的脚本 - 脚本里用的本机凭据(例如
~/.kube/config、本机argo)属于谁。
这种模式一般不需要进行额外的鉴权,毕竟已经有权限操作服务端的电脑了,加了鉴权操作,电脑也能改
示例:最小 Server + Host 配置
1 | from mcp.server.fastmcp import FastMCP |
1 | { |
场景二:想让同事用 URL 连上你的 MCP(第一次上远程)
你的需求
- 服务跑在一台内网机器或容器里
- 同事的 Cursor 不再执行你的
server.py,而是填写一个url
为此我们需要做如下的步骤:
- Server:
transport="streamable-http",绑定合适的 host/port。 - 运行方式:用 systemd / Docker / 手动进程保持服务常驻;不再依赖 Cursor 拉起。
- 同事配置:
url指向你的 MCP 端点(常见形如http://host:8000/mcp)。 - 马上要面对的问题:端口一旦对网络可达,不认识的人也能打过来。所以从下一小节开始加「门口查票」。
首先切换MCP的启动模式(stdio -> streamable-http)
本地 stdio 时,Cursor 负责启动进程(进程是在本地启动的)。
远程时,你必须先 自己把 MCP 做成常驻 HTTP 服务(类似FastAPI),Cursor 只负责连接。现行推荐传输是 Streamable HTTP(可流式的 HTTP 传输)。
1 | 【本地】Cursor --command启动--> python(stdio) |
只把 mcp.run(transport="stdio") 改成 streamable-http,却仍用 command 去配 Cursor——会连不上。
启动切换后,对应的客户端配置也需要调整,进程已经在按「HTTP」听端口。远程场景下,同事的 mcp.json 应变成 url(可选再加鉴权头)。
示例:启动服务后,可以通过url访问
1 | from mcp.server.fastmcp import FastMCP |
同事侧:
1 | { |
我们可以先通过行面的示例,了解基于url模式的服务启动和调用模式。
场景三:内网共享,先共用一把「门口通行证」
我们之前已经开始用 http服务监听MCP的端口了,这个时候服务是在公网的,任何人都可能访问,为了安全,我们需要确保只有特定的人群可以访问我们部署到公网的服务,这个时候,我们需要开始进行鉴权。
先弄懂:Bearer 是什么
HTTP 里一种极常见的写法是:
1 | Authorization: Bearer <一段字符串> |
Bearer(持有者令牌)的字面意思是:谁「持有」这段字符串,谁就能用。
后面无论是「管理员发的固定口令」「JWT」还是「OAuth 换来的 access_token」,请求头长得往往一样;差别在于这段字符串 怎么发出来、服务端怎么验。
段末注释:Bearer 的用法约定见 RFC 6750。MCP 在 HTTP 资源请求里也使用该请求头形态。
鉴权上你要改什么
| 位置 | 调整 |
|---|---|
| 服务端 | 增加「读 Authorization → 和配置的密钥比对 → 失败返回 401」的逻辑(中间件或网关) |
| 服务端配置 | 环境变量如 MCP_API_TOKEN(不要写进仓库) |
同事 mcp.json |
在 headers 里带同一把 Bearer |
| 运维 | 内网 HTTPS 更佳;至少限制来源网段 |
依赖
- 已使用 Streamable HTTP
- ASGI 中间件(Starlette / FastAPI)或网关验头
- 比对建议用
hmac.compare_digest,降低时序攻击风险
示例:单密钥校验(理解用)
1 | from __future__ import annotations |
同事配置:
1 | { |
这个阶段的代价(心里有数即可)
- 一把密钥大家共用:泄露 = 全员沦陷
- 日志里看不出是谁调用的
- 换密钥要通知所有人改环境变量
只适合小范围的使用,公司内部的服务功能、有限范围等不记名授权。
场景四:要分清是谁在调用,并能单独作废某人
有时候,我们可能需要知道谁在调用 MCP,并能单独废除某人的权限,记录每个人的使用情况等。这个时候,我们仍然可以在后端中间件中进行功能等扩展,引入一个用户表(记录每个人的token、和我们需要记录的其他信息)。
发钥匙时可以给 Alice、Bob 各发一串随机 token;服务端 只存哈希,明文只在发放时展示一次。吊销 = 从表删掉对应行。
1 | 请求: Authorization: Bearer tok_alice_xxx |
鉴权上你要改什么
| 位置 | 调整 |
|---|---|
| 服务端 | 密钥表(文件 / SQLite / Redis);中间件解析 Bearer → 查表 → 注入「当前用户」 |
| 业务 Tool | 需要时读取当前用户,做 scope 检查或写入审计字段 |
| 同事配置 | 每人 headers 里用 自己的 token,不再共用一把 |
| 管理流程 | 由于个人token的引入,对应增加「发钥 / 吊销」操作(哪怕先是运维手工改表) |
示例:token → 用户
1 | from __future__ import annotations |
这已经能支撑「区分用户 + 吊销」。若你还希望 钥匙自动过期、票面自带角色信息,而不想维护一大张长期明文口令表,看场景五。
场景五:希望通行证会过期,并在票面上带上用户信息
希望 token 里直接带上用户 id、过期时间,MCP 本地就能验,减少长期的静态口令管理。
先弄懂:JWT 是一种「自带说明书的票」
JWT(JSON Web Token,JSON Web 令牌)通常长这样:头部.载荷.签名(三段 Base64)。
载荷里可以放 sub(主体,常是用户 id)、exp(过期时间)、aud(这张票打算给谁用)等字段。
- 签发方用密钥给票签名
- MCP用同一套密钥(HMAC)或公钥(RSA/EC)验签名,并检查是否过期、受众是否为自己
请求头仍然是:Authorization: Bearer eyJhbG...(看起来像乱码的 JWT)。
段末注释:载荷里的字段常叫 claim(声明)。
aud(audience,受众)用来防止「给 A 服务发的票被拿到 B 服务滥用」。
鉴权上你要改什么
| 位置 | 调整 |
|---|---|
| 新增「签发小服务」或登录接口 | 用户证明身份后,签发短时 JWT(例如 1 小时) |
| MCP | 中间件改为 jwt.decode(...),不再做字符串全表扫描(或与表结合做黑名单) |
| 配置 | 共享 JWT_SECRET(HS256)或 MCP 只持有公钥(RS256) |
| 客户端 | 仍配 Bearer;只是值改成 JWT,且过期后要重新获取 |
注意:JWT 只解决票的格式与校验;「用户怎么登录才拿到第一张 JWT」若做成完整浏览器授权流,就会自然走到场景六的 OAuth。
示例:签发与校验(HS256)
1 | from __future__ import annotations |
场景六:同事用公司账号登录,不想私下发一堆密钥
随着业务应用场景扩大,用户增加,自己管理密钥会是一个成本极高的事情,而我们之前的应用过程中,经常看到一类模式(用微信登陆、google 登陆、github登陆)这类的方式,密钥的轮转,管理等都交给统一的账号管理体系,服务端不在进行复杂的账号管理工作。在我们的MCP鉴权中也可以使用这种方案。
先弄懂:OAuth 并不换一种请求头,它换的是「票怎么发到用户手里」,调 MCP 时,Host 带的往往 还是:
1 | Authorization: Bearer <access_token> |
OAuth 2.1(在 OAuth 2.0 上收紧安全实践的授权框架)规定的是:如何安全地让用户在浏览器里同意授权,并让 Host 拿到这张 access_token。
参与方可以记成三角关系:

段末注释:IdP(Identity Provider,身份提供商)负责「你是谁」;在 OAuth 用语里它常承担 Authorization Server(授权服务器)。MCP 此时是 Resource Server(资源服务器):不负责登录页,只验票。
授权码是管理员配好的固定值吗?
不是。 每次用户点「允许」,IdP 现场生成一次性短码(authorization code)。Host 再用这串码去换真正的 access_token。
管理员提前配置的是:应用的 Client ID(以及可选的 Client Secret)、允许的回调 URL 等——那是「登记 Cursor 这家应用」,不是给每个用户发固定业务口令。
那 IdP 要先向 MCP 领取一把「有效 MCP token」吗?
不要。 没有「第二套 MCP 预置万能密钥」需要交给 IdP。
IdP 用 自己的私钥签发 access_token;MCP 事先被配置为 信任这家 IdP(记住 issuer、JWKS 公钥地址、期望的 audience)。验签通过,就认为票有效。
1 | 管理员预置的是「信任关系」: |
鉴权上你要改什么
| 位置 | 调整 |
|---|---|
| IdP | 注册应用;配置 Cursor 要求的回调地址;规划 scope |
| MCP | 实现/挂载「按 JWKS 验 JWT」或 introspection;按规范暴露受保护资源元数据,方便客户端发现该找哪家 IdP |
| MCP 配置 | issuer、JWKS URL、audience(绑到你的 MCP 资源标识) |
| 同事 Cursor | 使用 url;通过产品 OAuth 流程登录,或配置 auth(Client ID 等)。通常不再人手粘贴长期静态 Bearer |
| 运维 | HTTPS 必备;回调与发现端点网络可达 |
依赖
| 层级 | 常见选择 |
|---|---|
| IdP | Keycloak / Auth0 / Okta / 云厂商 IAM 等 |
| 验票 | PyJWT + PyJWKClient,或网关 JWT 插件 |
| 协议细节 | OAuth 2.1、PKCE、受保护资源元数据(RFC 9728)等,见文末链接 |
段末注释:PKCE(Proof Key for Code Exchange)用于降低授权码被截获盗用的风险,公有客户端场景下尤为重要;JWKS(JSON Web Key Set)是 IdP 发布公钥的标准方式。
示例:MCP 用 IdP 公钥验票
1 | from __future__ import annotations |
示例:受保护资源元数据(示意)
客户端需要知道「这个 MCP 认哪家授权服务器」。一种形态是提供 JSON 元数据(路径与发现顺序以规范为准):
1 | { |
示例:Cursor 侧结构(示意)
1 | { |
具体字段以 Cursor MCP 文档 与你所用 IdP 为准;桌面与 Web 的 OAuth 回调地址往往需要都登记到 IdP。
示例:不透明 token 时向 IdP「问询」
有的 IdP 发出的 access_token 不是 JWT,MCP 本地无法自验,可调用 introspection 接口:
1 | import os |
场景七:要上公网,或公司已经有统一 API 网关
你的需求
- 域名 + HTTPS、限流、证书,不想全塞进 Python 进程
- 或已有 Nginx / APISIX / 云 API 网关团队规范
先弄懂:反代不是一种「新鉴权协议」
反向代理(reverse proxy)站在公网与 MCP 之间:终止 TLS、按路径转发、调超时、有时顺带验 JWT。
它解决的是「怎么安全地暴露服务」;票仍然可以是场景三~六里的任意一种。
1 | Internet ──HTTPS──▶ 反代/网关 ──内网 HTTP──▶ MCP:8000 |
本机 127.0.0.1 调试:反代不是必须。
公网:强烈建议有反代(或等价的云负载均衡 + 证书)。
鉴权上你要改什么
| 位置 | 调整 |
|---|---|
| DNS / 证书 | 为 mcp.example.com 准备 HTTPS |
| 反代 | location 转到各域 MCP 端口;流式场景注意关闭错误缓冲;长任务调大 proxy_read_timeout |
| 鉴权策略二选一或组合 | 网关验票后转发;或网关只做 TLS,验票仍在 MCP 中间件 |
| 危险点 | 若 MCP「信任」X-User-Id 这类头,必须保证 只有网关能注入,外网不能直打 MCP 端口伪造该头 |
示例:Nginx 转发示意
1 | server { |
把场景串起来:你会反复看到的同一张「票面」
从场景三到场景六,同事调 MCP 时,HTTP 层几乎都是:
1 | Authorization: Bearer <这一段> |
| 你走到的场景 | <这一段> 常常是 |
服务端怎么认 |
|---|---|---|
| 三、共用通行证 | 管理员发的固定字符串 | 和配置相等 |
| 四、分人通行证 | 每人一把随机串 | hash 查用户表 |
| 五、会过期的票 | 自签 JWT | 共享密钥/公钥验签 |
| 六、公司登录 | IdP 签发的 access_token(常为 JWT) | JWKS / introspection |
所以学鉴权时可以记一句:门上的锁孔形状往往一样(Bearer);换场景,主要是换「谁有权印票、印完怎么验」。
对照复习:需求 → 你改哪些地方
| 我的情况 | 优先场景 | Server 侧主要改动 | 客户端主要改动 |
|---|---|---|---|
| 仅本人本机 | 一 | stdio,可不做 HTTP 鉴权 |
command + args |
| 第一次给同事 URL | 二 | 改为 Streamable HTTP 常驻 | 改为 url |
| 内网、先挡住陌生人 | 三 | Bearer 单密钥中间件 | headers.Authorization |
| 要用户身份与吊销 | 四 | 多密钥表 + 注入用户 | 每人不同 Bearer |
| 要过期与自描述票 | 五 | JWT 验签;另需签发方 | Bearer 改为短时 JWT |
| 公司账号统一登录 | 六 | 信任 IdP、验 JWKS;元数据 | OAuth / auth 配置 |
| 公网或已有网关 | 七 | 置于网关后;防伪头 | https:// 的 url |
动手时的安全习惯(简短清单)
- 密钥、Client Secret 不进 git
- Token 放请求头,不放 URL 查询串
- 生产用 HTTPS;本地调试绑定
127.0.0.1更稳妥 - HTTP MCP 注意 Origin 校验(防 DNS rebinding,见协议传输安全章节)
- 静态 token 优先存哈希,并保留吊销办法
- JWT / OAuth 务必校验过期、发行方、受众
- 按最小权限拆 scope(只读 vs 可 submit)
延伸阅读
- MCP Authorization 规范
- MCP Authorization 教程
- OAuth 2.1 Draft
- RFC 6750 Bearer · RFC 7519 JWT · RFC 9728 Protected Resource Metadata
- Cursor MCP 配置
- 本系列:03 部署方式与调用配置 · 09 配置化 Pipeline 实战(工程内
auth.yaml多 Bearer)