如果只看各家宣传,MCP(Model Context Protocol,模型上下文协议)几乎是过去两年AI基础设施里最像“USB-C”的东西:一套协议,连接AI与数据。无论是让大模型读数据库、操作浏览器、还是调用企业内部API,MCP Server一次部署,Claude、Cursor、各类IDE智能插件都能直接识别、插上就用。这种统一接口带来的“即插即用”体验,确实解决了过去AI应用各自为战、集成成本居高不下的痛点。但问题是,USB-C之所以好用,是因为物理接口有标准协商机制,该传电流时传电流,该传数据时传数据,而MCP在这件事上并没有真正做到——它把“接口统一”做到了,却把“权限边界”“调用审计”这些本该同步标准化的安全机制遗留给了开发者自己。
这篇内容先把MCP的定位、架构和运行流程讲清楚,然后重点拆解我在真实工程环境里看到的六个安全问题:权限放大、工具链成为C2通道、安全上下文断裂、供应链投毒、凭证泄露、审计缺失。每一类都有实际案例或攻击路径,不是为了制造焦虑,而是因为MCP正在快速进入企业生产环境,大家如果只看到效率、不看边界,后面踩坑的代价会非常大。
文章面向两类人:一类是准备给团队引入MCP做Agent基础设施的架构师/后端,另一类是正在用Cursor、Claude等工具但没想过“这些工具背后的Server到底拿到多少权限”的开发者。下文所有结论,都来自我在实际项目中部署MCP Server、设计权限模型、处理安全事故的真实经历。
1. MCP协议是什么:AI生态的“USB-C接口”背后到底解决了什么问题
1.1 为什么很多人把MCP称为AI的USB-C
过去一年我参与过不少Agent项目,最痛的点就是“模型和系统之间的胶水代码”太多。今天要接一个Slack机器人,明天要接一个数据库查询,后天要接公司内部的工单系统,每一个连接都要单独写一套适配层:OpenAI Function Calling有自己的一套格式,LangChain插件又有另一套抽象,到了企业内部系统,还得按对方的API风格定制调用。本质上,模型本身越来越聪明,但它碰不到外界,而“让模型碰到外界”这个动作,每个团队都在重复造轮子。
MCP做的事情,就是把“模型↔工具/数据源”之间的会话格式、调用方式、生命周期统一成一瓶“标准接口液”。一个MCP Server实现好之后,任何支持MCP的Host(客户端宿主)都能直接加载它,就像你把一个USB-C设备插到任何一台有USB-C口的电脑上那样。Claude Desktop能用,Cursor能用,其他兼容MCP的Agent框架也能用,不需要为每个平台单独维护一套适配代码。
这就是MCP被称为“AI生态USB-C接口”的原因:连接面的标准化。但和物理USB-C不一样,物理接口有主从协商、电压握手、协议识别,MCP在这套东西上还很原始。USB-C接口本身不知道你插的是什么设备,但至少会先通过握手确定能不能供电;MCP默认的很多交互里,Host几乎无条件信任Server端暴露出来的工具列表——潜在风险的种子就从这里埋下。
坦白说,这个比喻只成立了一半:接口形态确实统一了,但“安全协商”这个本应和接口一块儿设计的东西,被严重后置了。
1.2 核心角色拆解:Host、Client、Server,谁在指挥谁在执行
MCP的架构非常轻,轻到很多第一次接触的人会低估它。它只有三个核心角色:
- MCP Host:用户直接打交道的AI应用,比如Claude Desktop、Cursor,或者你自己写的Agent程序。Host负责接收用户的输入,把输入交给大模型理解,并决定“要不要调用某个工具”“调用哪个”。
- MCP Client:Host内部负责和MCP Server通信的连接器组件,通常以SDK形式存在。它管连接建立、协议握手、消息收发。
- MCP Server:把具体能力暴露出来的服务端程序。一个Server可以暴露三个维度的能力:工具(Tools)、资源(Resources)和提示(Prompts)。比如一个GitHub MCP Server,能暴露
create_issue这样的工具,也能暴露仓库文件列表这样的资源。
典型的调用链是这样的:
- 用户在Host里问:“帮我把仓库里所有待修复的Issue整理成一个表格。”
- Host把这句话发给大模型,模型看到工具列表里有
list_open_issues、get_issue_detail,决定先调用前者,再循环调用后者。 - 大模型生成一个工具调用请求,Host通过MCP Client把它发给MCP Server。
- MCP Server执行真实操作(请求GitHub API),把结果返回给Host。
- Host把结果塞回给大模型,模型继续推理,最终生成回复。
这个流程看起来很像传统的API调用,但区别在于:传统API调用谁是调用方谁是被调用方非常清晰,而MCP场景下,“用户意图”通过大模型这个不可预测的中间层间接驱动工具执行。模型的判断可能受提示词注入影响,可能受上下文误导,这种不确定性会放大工具调用链的风险。
2. MCP怎么跑起来:一次完整请求的运行流程与技术核心
2.1 一次真实的MCP会话:从握手到工具调用
我以一个实际项目为例:我给团队内部做了一个数据库查询MCP Server,暴露两个工具,一个query_database,一个fetch_schema,Host端接的是Claude Desktop。完整的调用流程大致是这样的。
对话初始阶段,Host需要和Server建立会话,走的是MCP协议的初始化握手流程:
- Host发送
initialize请求,带上自己的协议版本和客户端信息。 - Server返回自己支持的协议版本、Server信息、以及自身能力声明。
- 双方确认版本兼容之后,Host发送
initialized通知,Server进入就绪状态。 - 随后Host自动发起
tools/list请求,拿到Server所有工具的定义清单,包括工具名、描述、入参的JSON Schema。
这一阶段特别值得一提:tools/list返回的是一个全量清单。以我的数据库Server为例,如果我把query_database和fetch_schema都注册进去了,那么Host从一开始就知道这两个工具的存在,任何一个有权限调用工具的模型都能看到它们。
之后用户问:“帮我查一下本月订单量最大的前十位客户。”模型决定调用工具:
- 模型输出调用请求,格式是
{"name": "query_database", "arguments": {"sql": "SELECT customer_id, SUM(amount) as total FROM orders WHERE month = '2025-04' GROUP BY customer_id ORDER BY total DESC LIMIT 10"}}。 - Host将请求以
tools/call方法发往MCP Server。 - Server执行SQL,把查询结果封装成JSON返回。
- Host把结果交给模型,模型根据数据生成最终答复。
如果工具是带副作用的,比如提交订单、删除记录、导入数据,调用路径一模一样,协议层面完全没有区分“只读工具”和“写工具”。权限控制完全依赖Host端的实现自觉,以及模型对工具描述的理解。这一下就点出了安全问题的根:MCP是一套表达能力很强的协议,但它在协议层没有建立任何安全语义。
2.2 传输层与消息格式:stdio、SSE、JSON-RPC的关键取舍
MCP的传输层目前常见的有两类:
- stdio:Server作为Host的子进程启动,标准输入输出就是通信管道。优势是本地部署轻量,没有网络暴露面;劣势是Server只能被当前Host进程使用,没法远程复用。
- Streamable HTTP / SSE:Server跑在远端,通过HTTP或SSE建立流式连接。优势是多个Host可以共享同一个Server,便于集中治理;劣势是网络暴露本身成为一个攻击面,鉴权、TLS、访问控制全都要自己补。
消息格式用的是JSON-RPC 2.0,一个非常轻量的协议。请求、响应、通知在JSON结构上有严格区分,整体设计很干净。但干净是优点也是缺点——协议给的消息头字段非常少,没有标准化的身份信息字段,没有标准化的审计追踪字段,更没有标准化的安全策略协商机制。两端的真实身份、操作者的真实身份、会话的上下文,全部依赖外部注入,大家用起来千奇百怪。
在我实际接入过的MCP Server里,至少有一半在初始阶段只做“协议版本协商”,不做任何身份认证。你要是拉了一个远程MCP Server直接连,它可能既不校验Host身份,也不校验用户身份,任何人都能调用它的工具。这个状态,和早年很多数据库暴露在公网没设密码,是一个性质的问题。
3. 六大安全风险深度解析:MCP被神话之后的真实暗面
3.1 风险一:权限放大——工具清单全量暴露,远比“最小权限”危险
第一个要说的风险,也是我在多数项目里第一个撞上的问题:MCP Server给Host暴露的永远是全量工具清单,而不是按用户、按场景裁剪过的子集。
拿我自己的数据库MCP Server举例:工具清单里有query_database、fetch_schema、甚至还有一个drop_table(原本是给自动化运维任务用的,一个内部工具而已)。当我把这个Server接入团队共用的Host时,任何用户问模型一句“把这个月的临时表清理掉”,模型只要能看到drop_table这个工具,就有可能在推理中决定调用它。MCP协议不会阻止,Host默认不做二次确认,Server只管收到请求就执行。
在传统系统里,权限控制通常层层收窄:用户角色决定功能菜单,后端接口再校验一次,数据库账号再限制一次。但在MCP场景里,这些层次被压缩了:工具的注册是开发时写的,工具的可见性却是运行时由模型决定的,而模型是概率系统——它能怎么组合工具,并不完全可控。
当时我们的加固方案是在MCP Server内部做了一层“按会话来源过滤工具名”的逻辑:Host请求tools/list时,Server根据请求头里携带的用户标识动态返回工具子集。但这需要Host愿意把用户标识传给Server,MCP协议没有强制约定字段名——各家用各家的,我用X-User-Id,别人用X-User,根本没法统一。协议层白白少了一个关键约束。
3.2 风险二:工具链成为C2通道——AI Agent被当作“无界终端”控制
第二个风险更隐蔽,也更令我不安:MCP Server有可能变成C2(命令与控制)基础设施的一部分。
C2的核心诉求,是攻击者需要一条“稳定的、看起来合法的控制通道”来下发指令、窃取数据。传统C2通道往往用的是DNS隧道、HTTPS轮询这些技巧,但特征明显,防守方盯得紧。MCP上来之后,“AI工具调用”本身就成了新的藏身之处。
具体的路径是这样的:攻击者通过各种方式植入一个恶意的MCP Server——比如伪装成一个“日历助手”MCP,工具清单看起来人畜无害,只有create_event、list_calendars,但Server端代码里内置了读取本地文件、上传到指定服务器的功能。当用户正常调用“日历助手”工具时,Server在执行正常功能的同时,把本机敏感文件分批外传。由于调用方是AI应用、流量走的也是正常HTTP/SSE,防守方看到的只是一堆“工具调用”,很难察觉异常。
更麻烦的是反过来用。如果攻击者能控制Host侧——比如说通过提示词注入操纵了模型的输出——那模型会以合法身份不停调用MCP Server上暴露出来的各种工具。相当于攻击者通过模型这个“无界终端”间接调用了所有可用工具。对于加入Host的工具,模型天然有执行权限,整个链路像是打开了一个远端命令执行的窗口。
我们做过一次模拟:在一台部署了MCP Server的开发机上,诱导模型调用“发送测试报告”工具,工具本身会执行一段shell命令把/etc/passwd文件打到日志里。从Host日志看,就是一个工具调用记录,完全无害;但实际上文件内容已经进了日志聚合系统。这事之后,我坚定了一个看法:所有执行类MCP工具,都必须按“可能执行任意代码”的标准做防护,不能只看工具描述是否安全。
3.3 风险三:安全上下文断裂——Host的用户权限与Server的执行身份脱节
第三个问题,我把它称为“安全上下文断裂”。
传统安全模型讲究“身份-权限-审计”三者一致:你是谁、你能干什么、你干了什么,是一串连贯的信息。但在MCP架构里,这三者经常是断开的。
举个例子:企业内部跑了一个工单管理MCP Server,部署在服务器上,进程启动时用的服务账号叫mcp_service,拥有对所有工单的读写权限。用户A在Host上只有“查看工单”的权限,但当A对AI助手说“帮我把这个工单状态改成已完成”时,Host把工具调用请求发给MCP Server,Server端用的是mcp_service这个服务账号去执行操作,权限校验发生在Server进程内部。如果Server没有做额外的用户维度过滤,A就通过AI拿到了超出自己权限的执行能力。
这还不是最惨的,更惨的是审计视角完全断裂:日志里只记录了“mcp_service调用了update_ticket”以及工单ID,但到底是谁、从哪个Host发起的、带着什么意图发起的,MCP协议没有标准字段承载。也就是说,事后想追查一次安全事故,得把Host日志、Server日志、模型上下文一段段拼起来,中间还有大量缺失。
这类风险不是说MCP设计者故意忽略,而是MCP把“模型”这个新执行主体引入了系统,但传统“用户-系统”二元信任模型没有直接迁移过来。结果就是,信任链在宿主和Server之间出现了断层。
3.4 风险四:供应链投毒——第三方MCP生态的“克隆攻击”
第四条风险,对现在习惯到处拉MCP Server来用的开发者来说,是最近的一根刺。
MCP生态正在复制npm/PyPI当年的故事:代码包越方便,供应链攻击越猖獗。当前大量MCP Server以Python包或Node包形式分发,开发者一条pip install mcp-server-xxx或者npx xxx就把Server拉到了本地。由于很多还是个人项目或早期开源项目,缺少签名校验、缺少代码审计、甚至包名都很容易伪造。
我见过一个真实的案例:某个团队的开发者想安装一个“读取PDF并提取结构化信息”的MCP Server,在PyPI上搜到一个名字只差一个字母的仿冒包。这个包在代码里嵌入了os.system调用,在初始化时下载一个加密的二进制payload,然后通过MCP Server的标准化工具名继续正常工作。从功能上看完全没问题,PDF确实能解析;但从安全角度,这台机器已经失守了。
只要是走MCP,Host一定会加载Server的tools/list返回结果,并且会在运行过程中无条件执行工具调用。一个恶意Server模拟正常人畜无害的工具接口,太容易了。MCP协议本身完全没提供类似“代码签名”“清单校验”“来源可验证”的机制,一切都靠安装者自辨。
现在我在团队里定了两条规矩:第一,生产环境用的MCP Server必须是自研或经过逐行审计的知名开源项目;第二,任何第三方来源的Server一律先扔到隔离环境跑一周,看它的网络行为、文件行为、进程行为,没问题再接进共享Host。这个流程很土,但有用。
3.5 风险五:凭证泄露——MCP Server是密钥流通的天然放大器
第五个风险来自一个常见操作:把密钥喂给MCP,让Server去做认证。这个操作方便,但代价极大。
在我们内部的一个MCP Server接入过程中,有同事为了快速对接第三方服务,把API Token直接写在了Server的环境变量里,而Server为了调试方便,在tools/list返回的一个工具描述里附带了部分Token字符串——本意是方便测试,结果Token字符串被Host端记录到了会话日志里,而会话日志会同步给大模型做上下文。也就是说,密钥进入了模型的上下文窗口。
模型上下文窗口里的数据,会跟着会话一起被Host记录、被日志系统保存、甚至在某些配置下被发送给第三方模型服务。密钥一旦进了“上下文”,泄露面就从“一个进程的环境变量”扩大到“所有能读到对话日志的地方”。而这还不是最严重的。
更常见的泄露路径在工具参数里。比如一个send_email工具,要求传入SMTP用户名和密码;模型在执行时如果上下文里没有这些值,而Host系统配置了“自动从密码管理器获取”,那么密钥会在毫无保护的状态下以明文参数经过MCP协议传输。很多MCP Server默认不加密参数、不脱敏日志,于是密钥明文出现在Server端日志里。
我现在对MCP Server的处理规范是三个“绝不”:绝不把长期凭证写入工具参数;绝不把密钥放进工具描述;绝不让模型直接接触原始Token,一律通过Server端的凭证托管模块间接使用。
3.6 风险六:审计与追溯的“黑匣子”——会话难复现、事件难定责
最后一条风险,不是攻击型风险,但它是让安全事件变严重的关键因素:审计缺失。
MCP的JSON-RPC消息格式里,没有标准的“会话ID”“请求ID”关联字段,没有“操作用户”“操作意图”这类审计信息。不同Host实现五花八门:有的记录全部往返消息,有的只记录工具名和返回码,有的干脆关掉日志。等真出了安全事故,想回答“当时模型为什么调用这个工具、是谁让我调用的、这条数据是从哪一步开始泄露的”,基本靠猜。
有一次我们排查一个“误删数据”事件:用户用AI助手执行了一条操作,删了一个不该删的集合。我们回看MCP Server日志,只有一条delete_many的记录,连参数都没记录完整。Host端的日志更少,只记录了一个“工具执行完成”。最后不得不从模型会话历史、用户输入、操作时间三点交叉拼凑,才勉强定位责任环节。
MCP协议完全可以借鉴数据库的“预写日志”或API网关的“流量日志”思路,在协议层强制约定最小审计字段。现在没有,等于每个人都要自己发明一套,而且各不兼容。
说到底,MCP还太年轻。它的通用性带来了效率,但安全能力还没有跟上。
4. 实操层面的安全加固清单:我给MCP实践配置了哪些防御
4.1 最小权限与分组策略:把工具的可见性做成动态能力
如果你还没法完全放弃MCP,那至少应该做下面这些事。第一条,是动态工具清单。
我们之前吃过tools/list全量暴露的亏,后来在Server端做了一个基于“租户+用户”维度的过滤器:
async def list_tools(self, user_context): if user_context.get("role") == "admin": return self.all_tools if user_context.get("scope") == "readonly": return [t for t in self.all_tools if t.read_only] return []这个抽象每家的实现方式都不一样,但原则一致:MCP Server在生成工具清单时,应该收窄到当前会话所需的最小集合,而不是无脑把所有工具交出去。因为工具清单就是大模型的“决策空间”,决策空间越小,被误用和滥用的风险越低。
同时,如果一个工具是创写型操作的(写库、发消息、删数据),我强烈建议在工具层加一个“人工确认”钩子。具体做法是让工具返回一个“待确认”标志,由Host端结合自身UI做一个确认弹窗,确认后才真正执行。这个动作直接阻断了一类“模型误操作”风险,代价只是交互上多一步,但对生产环境来说是完全值得的。
4.2 来源可信度管理与运行时审计:像对待开源依赖一样对待MCP Server
对待MCP Server,要用对待生产依赖的标准,甚至更严格。
第一,建立MCP Server清单。把每个Server的源码仓库、维护者、版本、最近更新时间、工具清单全部登记。任何一次升级都要走变更流程,不能随手更新。
第二,在隔离环境做行为测试。拉一个新Server回来,先放进一个网络隔离的容器里跑,观察它的外联域名、文件读写、进程行为。如果它在运行时报错信息里出现了和工具功能无关的敏感路径,直接弃用。
第三,运行时审计用代理模式收拢。把MCP流量统一收敛到一个网关,在网关上记录“谁调用了什么工具、参数是什么、返回给谁、耗时多少”。不要依赖Host自带的日志——Host通常只管模型对话,记录不到Server执行细节。
这部分我给一个最小化的JSON-RPC审计日志字段建议:
| 字段 | 含义 | 是否必需 |
|---|---|---|
| session_id | 当前会话唯一ID | 是 |
| user_id | 操作用户标识 | 是 |
| tool_name | 被调用工具名 | 是 |
| arguments | 工具入参(脱敏后) | 是 |
| result_code | 返回状态 | 是 |
| model_context_id | 模型上下文ID | 推荐 |
有了这些字段,至少能在事后回答“发生了什么”这个问题。至于“为什么模型会这么做”,那是另一个层面的研究,但不是本文重点。
5. 常见问题速查表与个人操作体会
5.1 MCP部署中最常踩的五个坑(速查表)
| 现象 | 原因 | 应对 |
|---|---|---|
| Server能在本地跑,但Host连不上 | 传输层协议不匹配(stdio vs HTTP)或握手协商失败 | 检查初始化握手的版本信息、统一走HTTP运输时确认鉴权头 |
| 模型“找不到工具” | tools/list返回了清单但工具描述不清晰,模型没识别 | 精简工具描述,加明确使用示例 |
| 工具调用中Token泄露到日志 | 工具参数未脱敏记录了完整认证信息 | 在工具执行和日志输出两端分别做脱敏 |
| 权限校验形同虚设 | Server端没有实现用户维度过滤,默认全部工具可见 | 按上文动态工具清单方案处理 |
| 会话复盘困难 | 缺少统一审计字段 | 部署MCP网关,统一收敛和记录流量 |
5.2 我对MCP未来的一个观察
我个人的判断是,MCP一定会继续发展,因为它解决的问题太痛了。但它的安全演进不会自己完成——协议层大概率会逐步加入标准化鉴权、审计、权限能力,而在那之前,所有引入MCP的团队都得自己把这些能力补上。
我的一个具体建议是:在内部实践时,把MCP Server当作“运行中的第三方代码”来管理,而不是“一个配置文件”。它和其他服务一样,需要上线评审、需要运行监控、需要定期清理不再使用的工具。
最后再分享一个小技巧:定期对MCP Server做一次“冗余工具清理”。我见过太多Server上线之后,工具列表堆了一大堆历史遗留能力,根本没人在用,但每个工具都代表着一条暴露面。删掉冗余工具,往往比加一堆防火墙规则更有效。安全很多时候不是做加法,而是做减法。