mcp 协议理解

  1. Mcp 协议分析

整个 Mcp ,无论是 Stdio 、 Sse 、Streamable ,只要遵守对应的 JSONRPC 请求和响应格式,即使你实际上只是一个空壳,大模型依然会相信你是个 Mcp 。

  1. Stdio

Stdio 实际上是本地执行命令。

  • 举个例子,假设你本地有一个 exe 文件,路径是:C://User/test.exe (路径我乱编的,格式可能有一点点错误,补药太在意哈),那么你的 Stdio Mcp 的 Command 选项就可以填 C://User/test.exe ,至于 Args 这个看你这个 exe 要不要,其实就是 -h 那些,而 Env 这个就是环境变量,配 C 语言环境的那个环境。
  • 为什么 Cherry Stdio 使用不需要我自己配环境?我没有深究过,但是基本可以确定 Cherry Stdio 提供了一个会话,这个会话中的环境变量被 Cherry Stdio 配过,所以只影响当前会话,而不会永久影响你的电脑环境配置。
  • 整个过程
    • 大模型通知 Cherry Stdio 我要用这个 Mcp ,包括对应的参数什么的
    • Cherry Stdio 收到消息,执行 exe 并在与 exe 的标准输入中输入 JSONRPC 消息
    • Exe 从标准输入流中获取信息并处理后返回一个 JSONRPC 消息,输出到标准输出里
    • Cherry Stdio 拿到数据,喂给大模型
    • 大模型根据新消息再吐出最终结果

参考的 JSONRPC 请求和响应数据

1
2
3
req: {"jsonrpc":"2.0","method":"initialize","id":0,"params":{"capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"mcp-inspector","version":"0.14.0"},"protocolVersion":"2025-03-26"}}

res: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"markitdown","version":"1.8.1"}}}
  1. Sse

Sse 骚操作有点多,他输入和输出不是同一个接口,也就是说他由两个接口组成一个完整的输入输出,类似下面

img

  • sse 端点:
    • 用于向客户端发消息,这个端点接口会持续存在,符合 sse 协议,持续吐消息
  • messages
    • 用于接收客户端的消息,一次请求为一次接收,消息处理的结果会从 sse 端点那里吐出
  • 整个过程
    • 请求 sse 端点, sse 端点会返回一个 endpoint 消息,里面包含了 messages 端点路径和分配的 sessionid ,至于前面的域名什么的,约定与 sse 端点的域名保持一致(http://127.0.0.1:8094)
    • img
    • 向 messages 端点发送初始化消息,messages 端点只会响应一个 accepted
    • img
    • img
    • 此时 sse 端点会得到 server 返回的信息
    • img
    • 包括 tool call 也是一样的
    • img
    • img
  1. Streamable

比起 sse 好一点,没那么精神分裂,非要搞两个接口,一个接口就完事了,实际上我个人感觉跟普通的 http 接口没啥区别,也就多了一些约定

约定

  • 请求
    • Header
      • Mcp-Session-Id sessionid 存在这里,用于告诉服务端听复用之前的信息
      • Accept 默认 application/json, text/event-stream
  • 响应
    • Header
      • Mcp-Session-Id 把 服务端分配的 sessionid 交给客户端

其他跟正常 http 请求没区别,只是请求响应 json 格式有固定要求而已

1
2
3
req: {"jsonrpc":"2.0","method":"initialize","id":0,"params":{"capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"mcp-inspector","version":"0.14.0"},"protocolVersion":"2025-03-26"}}

res: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"markitdown","version":"1.8.1"}}}
  • 过程展示
    • 这里就不展示了,就是一次请求一次响应信息,首次请求会在 header 中返回 sessionid ,之后的每次请求都带上这个 sessionid 以此保持会话状态。
    • 下面这个是我的 streamable 接口处理的一小部分代码,已经可以说明上面的约定了

img

img

  1. Mcp 请求响应分析

众所周知, Mcp 的请求响应都遵守了一个约定,字段名什么的都约定好了。

  1. 请求

这里可以看出来,以下约定:

  • 必须携带 jsonrpc 字段,一般是 2.0
  • method :根据实际想干嘛来提供对应的 method
  • id :每次请求叠加 1 ,初始为 0
  • params :根据 method 的不同也不同,对应 go 中的 interface{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 初始化
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 0,
"params": {
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "mcp-inspector",
"version": "0.14.0"
},
"protocolVersion": "2025-03-26"
}
}


// tool call
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"_meta": {
"progressToken": 3
},
"arguments": {
"uri": "https://aitoken-public.qnaigc.com/test/transformer.pdf"
},
"name": "convert_to_markdown"
}
}

// tool list
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 4,
"params": {
"_meta": {
"progressToken": 4
}
}
}
  1. 响应

约定

  • 必须携带 jsonrpc 字段,一般是 2.0
  • id :请求给啥返回啥
  • result: 客户端真正想要的数据,一样是 interface{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 初始化
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"serverInfo": {
"name": "markitdown",
"version": "1.8.1"
}
}
}


// tool call
{
"jsonrpc":"2.0",
"id":3,
"result":{
"content":[
{
"type":"text",
"text":"。。。。"
}
]
}
}

// tool list
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"tools": [
{
"annotations": {},
"description": "Convert a resource described by an http:, https:, file: or data: URI to markdown",
"inputSchema": {
"properties": {
"uri": {
"title": "Uri",
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"name": "convert_to_markdown"
}
]
}
}
  1. Method

这个好像很全:https://modelcontextprotocol.info/zh-cn/

每个 method 都有对应的确认的 params 格式和 result 格式,可以看我下面提供的例子里看他的 params 和 result 长啥样。

目前我知道的 method 分为了以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const (
Initialize = "initialize"
Ping = "ping"
ResourcesList = "resources/list"
ResourcesTemplatesList = "resources/templates/list"
ResourcesRead = "resources/read"
ResourcesSubscribe = "resources/subscribe"
ResourcesUnsubscribe = "resources/unsubscribe"
PromptsList = "prompts/list"
PromptsGet = "prompts/get"
ToolsList = "tools/list"
ToolsCall = "tools/call"
LoggingSetLevel = "logging/setLevel"
CompletionComplete = "completion/complete"
NotificationsInitialized = "notifications/initialized"
)
  1. initialize

在客户端正式启动之前,需要一个激活操作,也就是客户端往服务端发送 initialize 消息,告知服务端,客户端的身份,服务端会分配一个 sessionid 给客户端,之后的聊天就依据 sessionid 知道你是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
req: {
"jsonrpc": "2.0",
"method": "initialize",
"id": 0,
"params": {
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "mcp-inspector",
"version": "0.14.0"
},
"protocolVersion": "2025-03-26"
}
}

res: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"serverInfo": {
"name": "markitdown",
"version": "1.8.1"
}
}
}
  1. ping

就是 ping 一下看你还活不活着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
req: {
"jsonrpc": "2.0",
"method": "ping",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
  1. resources/list

不知道能干啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "resources/list",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": []
}
}
  1. resources/templates/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "resources/templates/list",
"id": 2,
"params": {
"_meta": {
"progressToken": 2
}
}
}

res: {
"jsonrpc": "2.0",
"id": 2,
"result": {
"resourceTemplates": []
}
}
  1. resources/read

没用过

  1. resources/subscribe

没用过

  1. resources/unsubscribe

没用过

  1. prompts/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "prompts/list",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"prompts": []
}
}
  1. prompts/get

没用过

  1. tools/list

列出工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
req:{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 4,
"params": {
"_meta": {
"progressToken": 4
}
}
}





res:{
"jsonrpc": "2.0",
"id": 4,
"result": {
"tools": [
{
"annotations": {},
"description": "Convert a resource described by an http:, https:, file: or data: URI to markdown",
"inputSchema": {
"properties": {
"uri": {
"title": "Uri",
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"name": "convert_to_markdown"
}
]
}
}
  1. tools/call

执行工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
req:{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"_meta": {
"progressToken": 3
},
"arguments": {
"uri": "https://aitoken-public.qnaigc.com/test/transformer.pdf"
},
"name": "convert_to_markdown"
}
}


res:{
"jsonrpc":"2.0",
"id":3,
"result":{
"content":[
{
"type":"text",
"text":"。。。。"
}
]
}
}
  1. logging/setLevel

没用过

  1. completion/complete

没用过

  1. notifications/initialized

这个有点东西,动不动发一个的,但是你不用回复

  1. Mcp 与 Eino

Eino

是字节研发的一款大模型二开框架

Eino 是怎么用 Mcp 的(参考 react agent)

  • 对于 Eino 而言, Mcp 被视为了一个 Tool ,所以通过 Eino 让大模型调用 Mcp 就变成了:
    • Eino 告诉大模型有这些 Tool
    • 大模型通过 tool_calls 通知 Eino 调用对应的 Mcp
    • Eino 将 Mcp 调用结果重新“投喂”给大模型
    • 大模型根据上下文再说出最终的结果

如何将 Eino 隐藏起来的对话消耗 Token 拿到手

从上面描述可以得知,Eino 最终吐出的消息在背地里是经过了 n 次对话产生的

  1. 学习资料

  • mcp (server or client) go 版:

https://github.com/mark3labs/mcp-go

  • mcp 流量转发:

https://github.com/kkb-1/mcprouter

  • mcp 调试工具( F12 看他的网络请求,了解 mcp 实际协议干了啥):

https://mcp-docs.cn/docs/tools/inspector

  • mcp proxy(协议转换,例如 stdio 转 sse ,这个会用就行):

https://github.com/sparfenyuk/mcp-proxy

  • eino (字节的 cloudwego 系列,大模型二开框架):

https://www.cloudwego.io/zh/docs/eino/ecosystem_integration/tool/tool_mcp/