How Agent use MCP
How an Agent use MCP tools?
9月的时候有幸参加了字节安开实习岗的面试,感觉学到了很多的东西
AI确实越来越成为就业~~和比赛(你知道的,我不打传统CTF很久了)~~的重点考察对象了
平时一直在用AI,也没关注过到底Agent是怎么调用那些MCP工具,怎么自己读取skills?skills又是什么结构的呢
后面大概会亿点点补上来吧,也算是重新找到了新的动力和方向
How mcp work?
MCP和Agent的标准传输机制还是有几种种类的
从官方文档可以发现,一开始主流传输流的类型有stdio和HTTP with Server-Sent Events(SSE)
也支持自定义协议,只要协议能保证全双工通信就可以
规定MCP使用JSON-RPC对消息进行编码,且JSON-RPC消息必须是被UTF-8编码过的
JSON-RPC又是什么呢?
是一种使用特定格式JSON进行client-server通信的协议
请求的JSON通常包括method,params,id三个字段,分别表示需要远程调用的方法,输入参数,消息的序号
而响应的JSON包括Result,Error,响应id,结果最为重要,error仅在调用时出现问题会进行返回(发送错误的代码和报错消息),响应id记录传输过来的消息的序号并继承
like this:

传输通过request,notifications(progress/message),response三种消息类型

stdio使用标准输入和标准输出进行通信,并且客户端应尽可能支持stdio这种传输机制
后来SSE被Streamable HTTP代替,为什么呢?等谈到MCP协议的迭代史时就知道了
MCP组成
在一开始,MCP的机制还是比较简单的
支持三种消息类型:request,notifications(单向通信、不包含id),response
除了notification以外的消息都是包含id的
主要支持stdio和SSE,在传输层由基础协议、生命周期管理、服务器功能、客户端功能、工具五部分组成
最核心的是基本协议(即JSON-RPC)和生命周期管理(连接的初始化、能力协商和会话控制)
一开始,鉴权并没有被作为MCP核心规范的一部分
在2025年3月的更新中引入了OAuth的授权,但是不支持STDIO的传输
一次mcp传输的生命周期
用直观一点的方式:

初始化阶段
在这个阶段客户端和服务端建立协议版本兼容性;交换和协商功能;分享实现细节
客户端先向服务端发起初始化请求(包含protocolVersion capabilities 以及clientInfo)
{
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"sampling": { },
"elicitation": {
"form": { },
"url": { }
}
},
"clientInfo": {
"name": "pi-mcp-yakit",
"version": "1.0.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
同时服务端接收到信息后也返回自己对应的信息
如果服务端支持请求的协议版本,则必须返回相同的版本。否则,服务端必须返回其支持的另一个协议版本。该版本应该是服务端支持的最新版本。
如果客户端不支持服务器响应中的版本,则应该断开连接。
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"prompts": { "listChanged": true },
"resources": { "listChanged": true },
"tools": { "listChanged": true }
},
"serverInfo": { "name": "Yaklang MCP Server", "version": "0.0.2" }
}
}
最初规定client和server可以有的功能有这些:
| Category | Capability | Description |
|---|---|---|
| Client | roots |
Ability to provide filesystem roots |
| Client | sampling |
Support for LLM sampling requests |
| Client | experimental |
Describes support for non-standard experimental features |
| Server | prompts |
Offers prompt templates |
| Server | resources |
Provides readable resources |
| Server | tools |
Exposes callable tools |
| Server | logging |
Emits structured log messages |
| Server | experimental |
Describes support for non-standard experimental features |
并且协商后只能使用协商好的协议版本和功能
初始化成功后,客户端必须发送一条notifications的已初始化消息来表明准备好开始来调用mcp server的方法了

并且在客户端响应initialize请求前只能发送PING请求而不能发送其他request
而服务端也必须以空的result来响应

通信阶段
就是字面意思了,通过发送message进行交互,只能使用协商好的协议版本和功能
终止阶段
在终止阶段有一方会先断开连接,并不会发送特定的消息,而是从底层上发送讯号通知另一方连接中断
MCP的工具项
mcp自带多种消息类型进行一些常规的操作,例如Cancellation对正在进行的操作进行取消,Ping对另一方进行探活
- Cancellation
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": "123",//需要取消的requestID
"reason": "User requested cancellation"//可供记录的取消原因
}
}
客户端不得取消初始化请求。
收到取消通知的接收者应该:停止处理已取消的请求;释放相关资源;不要对已取消的请求发送回复。
如果出现以下情况,接收方可以忽略取消通知:所引用的请求未知;处理已完成;请求无法取消。
- PING
request
{
"jsonrpc": "2.0",
"id": "123",
"method": "ping"
}
recevier
{
"jsonrpc": "2.0",
"id": "123",
"result": {}
}
- progress
MCP是支持通过使用progress来长期跟踪查看正在进行操作的进度
当一方想要接收请求的进度更新时,它会在请求元数据中包含一个 progressToken。
{
"jsonrpc": "2.0",
"id": 1,
"method": "some_method",
"params": {
"_meta": {
"progressToken": "abc123"
}
}
}
此时接收方会返回对应操作的进度
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",//原来操作的progressToken
"progress": 50,
"total": 100
}
}
进度通知必须仅引用在存活请求中提供的token与正在进行的操作关联的token。
进度请求的接收者可以选择不发送任何进度通知/以他们认为合适的任何频率发送通知/如果总值未知,则省略总值。
Client与Server的Capability
Client
- Roots
规定mcp服务器有权操作的工作区路径
当然这个也可以与从版本控制系统或项目文件中自动检测工作区相结合
然而,实现方案可以自由地通过任何符合自身需求的接口模式来暴露根目录——协议本身并未强制规定任何特定的用户交互模型。
若要支持这个功能,在初始化阶段,client就要在capabilities字段声明roots
{
"capabilities": {
"roots": {
"listChanged": true
}
}
}
listChanged 指示当根列表发生变化时,客户端是否会发出通知。
接着就是它在message中的应用
如果server要检索根目录,就要发送 roots/list 请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "roots/list"
}
此时client会返回类似这样的请求
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"roots": [
{
"uri": "file:///home/user/projects/myproject",
"name": "My Project"
}
]
}
}
当client的工作目录发生变化时,支持listChanged的client必须要发送一个notification给Server来通知目录已经发生了变化
{
"jsonrpc": "2.0",
"method": "notifications/roots/list_changed"
}
- Sampling
即MCP server反向调用client的LLM进行交互
若要支持这个功能,在初始化阶段,client就要在capabilities字段声明sampling
{
"capabilities": {
"sampling": {}
}
}
要请求LLM时Server就会发出sampling/createMessage请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What is the capital of France?"
}
}
],
"modelPreferences": {
"hints": [
{
"name": "claude-3-sonnet"
}
],
"intelligencePriority": 0.8,
"speedPriority": 0.5
},
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "The capital of France is Paris."
},
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}
Sampling的信息允许有文本/图片内容
图片的传输
{
“type”: “image”,
“data”: “base64-encoded-image-data”,
“mimeType”: “image/jpeg”
}
同时为了应付出现的client端使用的AI供应商和Server端请求的不同的情况,sampling可以使用preference来结合抽象的能力优先级与可选的模型提示
可以用以下字段来表示server需求的优先级
costPriority:降低成本有多重要?数值越高,选择价格更低的模型
speedPriority:低延迟有多重要?数值越高,选择速度更快的模型
intelligencePriority:高级功能有多重要?数值越高,选择功能更强大的模型
用hint表示对模型名称的模糊匹配(一些子字符串)
存在多个hint时会按顺序进行评估
client也可以将hint映射到同一模型的不同供应商上
但最后选择的决定由client决定
综合上述的字段,就有了下面这个例子
{
"hints": [
{ "name": "claude-3-sonnet" }, // Prefer Sonnet-class models
{ "name": "claude" } // Fall back to any Claude model
],
"costPriority": 0.3, // Cost is less important
"speedPriority": 0.8, // Speed is very important
"intelligencePriority": 0.5 // Moderate capability needs
}
客户端会处理这些偏好设置,从可用选项中选择合适的模型。例如,如果客户端没有 Claude 模型,但有 Gemini 模型,它可能会根据相似的功能将hint映射到 gemini-1.5-pro。
Server
三种功能在发生改变时都要向另一方发送notifications通知server->client
{
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed"//或者resources/tools
}
resource/list、resource/templates/list、prompts/list、tools/list都支持进行在数据量过大时进行分页(通过server返回的nextCursor字段运行,client可以通过在params中增加cursor参数来请求下一页内容)
- Prompts
prompts旨在让用户能够明确地选择使用server上的功能
通常来说用户可以通过/+命令来发起调用
和Client的功能同理,Server也要在初始化阶段就声明
{
"capabilities": {
"prompts": {
"listChanged": true
}
}
}
在通信阶段,client就能通过这样的消息来查询Server上存在的Prompts
{
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/list",
"params": {
"cursor": "optional-cursor-value"
}
}
响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"prompts": [
{
"name": "code_review",
"description": "Asks the LLM to analyze code quality and suggest improvements",
"arguments": [
{
"name": "code",
"description": "The code to review",
"required": true
}
]
}
],
"nextCursor": "next-page-cursor"
}
}
然后通过prompts/get来调用想要调用的prompts
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello():\n print('world')"
}
}
}
response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"description": "Code review prompt",
"messages": [
{
"role": "user",
"content": {
"type": "text",//同样也支持image
"text": "Please review this Python code:\ndef hello():\n print('world')"
}
}
]
}
}
除了text和image,还支持可嵌入资源,资源可以包含文本数据或二进制(blob)数据,但URI必须有效,mimetype需要恰当,blob内容和图片一样也需要进行Base64的编码
可以直接在Prompts中嵌入资源能够无缝地将服务器管理的内容(如文档、代码示例或其他参考资料)直接整合到对话流程中。
{
"type": "resource",
"resource": {
"uri": "resource://example",
"mimeType": "text/plain",
"text": "Resource content"
}
}
但文档也给出了安全提示
实现过程中必须仔细验证所有提示输入和输出,以防止注入攻击或未经授权的资源访问。
- Resources
这个依赖于应用层是否有传入的接口,感觉比较像是网页版AI的文件上传接口之类的实现(只是从我们带着资源请求LLM变成了LLM来请求我们给的资源)
URI的协议支持https file git
一样需要先声明
{
"capabilities": {
"resources": {
"subscribe": true,//客户是否可以订阅以接收有关单个资源变更的通知。
"listChanged": true
}
}
}
两个属性都是可选的
通信阶段:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {
"cursor": "optional-cursor-value"
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"description": "Primary application entry point",
"mimeType": "text/x-rust"
}
],
"nextCursor": "next-page-cursor"
}
}
如果需要查看资源的话就发送read请求
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "file:///project/src/main.rs"
}
}
Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {\n println!(\"Hello world!\");\n}"
}
]
}
}
还支持resource的模板和订阅,client订阅某资源后就会在资源被更新时收到server的notification
{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/templates/list",
"params": {
"cursor": "optional-cursor-value"
}
}
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resourceTemplates": [
{
"uriTemplate": "file:///{path}",
"name": "Project Files",
"description": "Access files in the project directory",
"mimeType": "application/octet-stream"
}
],
"nextCursor": "next-page-cursor"
}
}
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/subscribe",
"params": {
"uri": "file:///project/src/main.rs"
}
}
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "file:///project/src/main.rs"
}
}
- Tools
就是公开供Client端调用的工具,这些工具使模型能够与外部系统交互
LLM会根据其上下文理解和用户的提示自动发现和调用工具。
但是,实现可以自由地通过任何适合自身需求的接口模式来公开工具——协议本身并没有规定任何特定的用户交互模型。
所以一般为了保障信任和安全问题,默认必须始终有人工参与,并有权知道调用了什么工具,接收或拒绝工具调用。
一开始tool的定义只需要name description inputSchema
后来在2025-03-26增加了annotations,本意是以便更好地描述工具行为,所以除非工具注释来自受信任的服务器,否则client必须将其视为不可信信息。
同样的,也是要在初始化阶段进行声明
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
通信阶段:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": "optional-cursor-value"
}
}
Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or zip code"
}
},
"required": ["location"]
}
}
],
"nextCursor": "next-page-cursor"
}
}
如果想要调用tools
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "New York"
}
}
}
Response
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
}
],
"isError": false
}
}
返回结果这里同样也是支持嵌入式资源
-
Utilities
- completion
说是和IDE的代码补全机制差不多
在2025-3的更新后需要先声明了
{ "capabilities": { "completions": {} } }client需要补全建议时
{ "jsonrpc": "2.0", "id": 1, "method": "completion/complete", "params": { "ref": { "type": "ref/prompt", "name": "code_review" }, "argument": { "name": "language", "value": "py" } } } { "jsonrpc": "2.0", "id": 1, "result": { "completion": { "values": ["python", "pytorch", "pyside"],//最多100个 "total": 10,//可选匹配数 "hasMore": true } } }completion支持两种类型,
ref/prompt和ref/resourceType Description Example ref/promptReferences a prompt by name {"type": "ref/prompt", "name": "code_review"}ref/resourceReferences a resource URI {"type": "ref/resource", "uri": "file:///{path}"}- logging
即server向client发送日志的能力
server声明
{ "capabilities": { "logging": {} } }client在通信阶段能够通过发送以下消息来设置log等级
{ "jsonrpc": "2.0", "id": 1, "method": "logging/setLevel", "params": { "level": "info" } }设置后server就会以
notification的形式返回日志消息{ "jsonrpc": "2.0", "method": "notifications/message", "params": { "level": "error", "logger": "database", "data": { "error": "Connection failed", "details": { "host": "localhost", "port": 5432 } } } }而这样的日志中容易发生敏感信息泄露,这是server需要考虑并进行隐藏的
- Pagination
分页机制,已经提过了,是对可用resource/prompts过多时的应对措施
MCP支持的通信机制
stdio
在这个过程中,Agent担任Client的角色而MCP server担任Server的角色

Client通过将请求的JSON写入Server标准流的形式输入
like this:
{"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"sampling":{},"elicitation":{"form":{},"url":{}}},"clientInfo":{"name":"pi-mcp-yakit","version":"1.0.0"}},"jsonrpc":"2.0","id":0}
并且要求客户端不得向服务器的标准输入写入任何非有效的 MCP 消息,服务端不得向服务器的标准输出写入任何非有效的 MCP 消息。
SSE
而在SSE传输中,MCP Server作为一个可以处理多个Client连接的进程

需要维持一个/sse的GET流用于建立连接和持续向客户端传输消息,并提供一个endpoint来给客户端通信
/message或者/messages用于处理Client以POST方式发送的消息,对于客户端来说消息也必须用POST的方式来传输给endpoint
传输形式还是我们的JSON
比较的麻烦,并且如果 SSE 连接断开,整个会话状态通常会丢失(endpoint的sessionId失效)
而这个SessionId是进行POST传输必要的字段

那么就有可能发生泄露问题
文档中也明确标出了安全问题:
1.服务器必须验证所有传入连接的 Origin 标头,以防止 DNS 重绑定攻击
2.在本地运行时,服务器应该只绑定到本地主机(127.0.0.1),而不是所有网络接口(0.0.0.0)
3.服务器应该对所有连接实施适当的身份验证
如果没有这些保护措施,攻击者可以利用 DNS 重绑定从远程网站与本地 MCP 服务器进行交互。
Streamable HTTP
在2025-03-26的更新中替换掉了原本的SSE但是安全警告没变,SSE流变成了StreamHTTP中可选的一种通信方式
这种机制中,MCP Server也作为一个可以处理多个Client连接的进程,将GET/POST的请求集成到同一个endpoint
一般来说就像这样http://example.com/mcp
相较SSE,StreamHTTP中的Server可以批量处理多个请求(随后在6月18日的更新中被移除了,MCP不再支持批处理),Client的请求也要求携带Accept头,将 application/json 和 text/event-stream 列为支持的内容类型
Request:
[
{
"jsonrpc": "2.0",
"method": "getUser",
"params": {"id": 1},
"id": 1
},
{
"jsonrpc": "2.0",
"method": "getOrders",
"params": {"userId": 1},
"id": 2
}
]
此时也相对应的返回结果数组
[
{
"jsonrpc": "2.0",
"result": {
"id": 1,
"name": "Alice",
"email": "alice@example.com"
},
"id": 1
},
{
"jsonrpc": "2.0",
"result": [
{"orderId": 101, "amount": 299.99},
{"orderId": 102, "amount": 149.50}
],
"id": 2
}
]
如果服务器无法接受输入,则必须返回 HTTP 错误状态码(例如,400 Bad Request)。HTTP 响应正文可以包含一个没有 ID 的 JSON-RPC 错误响应
服务器的响应必须要同时支持SSE流的启动和正常对JSON的返回
如果启动的是SSE流,那么服务器就不能在收到响应后,发送响应前关闭流,除非会话过期
如果意外断连,那么为了避免message丢失,服务器要有使流可回复的能力
1.客户端可以向 MCP 端点发出 HTTP GET 请求。这可以用来打开一个 SSE 流,从而允许服务器与客户端通信,而无需客户端先通过 HTTP POST 发送数据。
2.客户端必须包含 Accept 标头,并将 text/event-stream 列为受支持的内容类型。
3.服务器必须返回 Content-Type: text/event-stream 以响应此 HTTP GET 请求,否则返回 HTTP 405 Method Not Allowed,表明服务器在此端点不提供 SSE 流。
4.如果服务器发起 SSE 流:服务器可以向该流发送 JSON-RPC 请求和通知。这些请求和通知可以批量处理。这些消息应该与客户端同时运行的任何 JSON-RPC 请求无关。除非恢复与先前客户端请求关联的流,否则服务器不得向该流发送 JSON-RPC 响应。服务器可以随时关闭 SSE 流。客户端也可以随时关闭 SSE 流。
那么如何保证断开的连接可恢复呢?
服务器可以按照 SSE 标准中的描述,在其 SSE 事件中附加一个 id 字段:如果存在,则 ID 在该会话的所有流中必须是全局唯一的——或者,如果未使用会话管理,则在该特定客户端的所有流中必须是全局唯一的。
如果客户端希望在连接断开后恢复连接,则应向 MCP 端点发出 HTTP GET 请求,并包含 Last-Event-ID 标头以指示其收到的最后一个事件 ID:服务器可以使用此标头重放断开连接的流上最后一个事件 ID 之后发送的消息,并从该点恢复流;服务器不得重放原本应该通过其他流发送的消息。
换句话说,这些事件 ID 应该由服务器按流进行分配,以便在该特定流中充当游标。
Stateful
一开始采用有状态的流
Stateless
在2026-09-03的更新中流为了无状态的
The message between Agent and MCP server
如果是SSE时代
最基本的搭建通信是通过一个持续的GET流来做,所以要抓包的话要自己一个代理做转发,当然也可以tcpdump直接抓流量读
然后才是通过POST+session id做message的传输
如果要调试mcp的使用,主要是通过MCP Inspector来做
在HTTP流时代因为都通过一个路由(比如/mcp)在做传输,直接抓包就可以呢
后面的内容由于时间缘故暂时没来得及看完(MCP的官方文档真的很长…),后面会慢慢更新上来呢🥲
Attack towards MCP?
了解完了工作原理,就应该看看有什么安全漏洞了
先不看现有有什么吧,看完了实现总应该有些自己的感觉之类的
首先从实现上呢,由于他是一个server-client的通信方式,其实更像是什么呢?一个第三方的资源库
那么就避免不了一个问题,rogue server
只要实现一个恶意的MCP server并且欺骗用户进行连接,那么就可以通过sampling/tools等方式恶意引导LLM进行预期外的危险操作,比如执行命令或者删除修改数据。同时还可以盗取OAuth的令牌
再讲讲它的认证方式上的缺陷,最早期的SSE的模式,其实很明显看的出来,sessionId是明文写在包里面的,没有任何加密
那么如果出现会话被监听的情况,用户的sessionId就会发生泄露,产生进一步的敏感信息泄露问题
用户的会话也有可能被污染,导致出现恶意调用(并且协议归协议,实现还是要看各厂商的源码本身有没有对协议的内容做严格遵循),这就是另一个攻击面,上下文的污染和间接的提示注入
最后一个就是设计层面的问题了
我们知道MCP Server是允许携带resource的,那么如果是一个公网上的的MCP Server,在resource的访问路径的sanitization上有问题,就会导致路径穿越了,从而泄露凭证和关键文件
那么具体实际中有哪些分类,感觉众说纷纭,可以看看这篇文章呢
Exploring MCP Security Risks | X1r0z Blog
感觉讲的会比我的系统很多(