大家好,今天我们来深入探讨一个很有意思的话题——MCP(Model Context Protocol)。

MCP 是Anthropic发起的一种开放协议,旨在标准化应用程序向大型语言模型(LLM)提供上下文的方式。我们可以把 MCP 想象成 AI 应用领域的 USB-C 接口。正如 USB-C 为你的设备与各种外设和配件的连接提供了标准化方式一样,MCP 也为 AI 模型与不同数据源和工具的连接提供了标准化的方法。

然而,MCP官网只模糊地提到它是基于 JSON-RPC 2.0 的协议,并提供了包括 C# 在内的八种语言的SDK。但对于其底层的HTTP请求和响应格式,官方文档并未给出清晰的描述,这让许多想要深入了解或自行实现的开发者感到困惑。

本文将通过一个具体的 C# 实例,结合抓包数据,一步步揭开 MCP 协议在 HTTP 层面上的神秘面纱。

准备工作:示例代码

为了抓包和演示,我们首先需要一个客户端和一个服务端。这里我们使用的是 ModelContextProtocol 0.3.0-preview.3 版本的 NuGet 包。

客户端 (Client)

客户端代码负责发起连接、列出可用工具并调用它们。

// 需要安装NuGet包:ModelContextProtocol 0.3.0-preview.3

var clientTransport = new SseClientTransport(new SseClientTransportOptions()
{
Name = "MyServer",
Endpoint = new Uri("http://localhost:5000/"),
}); var client = await McpClientFactory.CreateAsync(clientTransport); // Print the list of tools available from the server.
(await client.ListToolsAsync()).Select(x => new { x.Name, Desc = JsonObject.Parse(x.JsonSchema.ToString()) }).Dump(); // Execute a tool (this would normally be driven by LLM tool invocations).
(await client.CallToolAsync(
"echo",
new Dictionary<string, object?>() { ["message"] = ".NET is awesome!" },
cancellationToken: CancellationToken.None)).Dump(); (await client.CallToolAsync(
"count",
new Dictionary<string, object?>() { ["n"] = 5 },
new Reporter(),
cancellationToken: CancellationToken.None)).Dump(); (await client.CallToolAsync("test_throw", cancellationToken: CancellationToken.None)).Dump(); (await client.CallToolAsync("not-existing-tool", cancellationToken: CancellationToken.None)).Dump(); public class Reporter : IProgress<ProgressNotificationValue>
{
public void Report(ProgressNotificationValue value)
{
value.Dump();
}
}

服务端 (Server)

服务端代码定义了几个可供客户端调用的工具(Tool),并处理 MCP 请求。

// 需要安装NuGet包:ModelContextProtocol.AspNetCore 0.3.0-preview.3
var builder = WebApplication.CreateBuilder(); builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddHttpContextAccessor()
.AddMcpServer()
.WithHttpTransport(c => c.Stateless = true) // 注意这里!
.WithTools<Tools>();
var app = builder.Build(); app.MapMcp();
await app.RunAsync(QueryCancelToken); [McpServerToolType]
public class Tools(IHttpContextAccessor http)
{
[McpServerTool, Description("Echoes the message back to the client.")]
public string Echo(string message) => $"hello {message}"; [McpServerTool, Description("Returns the IP address of the client.")]
public string EchoIP() => http.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; [McpServerTool, Description("Counts from 0 to n, reporting progress at each step.")]
public async Task<int> Count(int n, IProgress<ProgressNotificationValue> progress)
{
for (int i = 0; i < n; ++i)
{
progress.Report(new ProgressNotificationValue()
{
Progress = i,
Total = n,
Message = $"Step {i} of {n}",
});
await Task.Delay(100);
}
return n;
} [McpServerTool, Description("Throws an exception for testing purposes.")]
public string TestThrow()
{
throw new Exception("This is a test exception");
}
}

特别注意:在我的服务端示例中,我明确指定了 .WithHttpTransport(c => c.Stateless = true)。这代表我使用的是无状态的HTTP传输方式。MCP目前默认是有状态的,如果使用有状态模式,具体的请求和响应格式会略有不同。本文的分析全部基于此处的无状态模式。

第一部分:初始化握手

MCP的连接始于一个分为两步的初始化过程,我们可以称之为“协商”与“确认”。

1. 协商 (Negotiation)

客户端首先向服务器发送一个initialize方法的JSON-RPC请求。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "LINQPad.ScriptHost",
"version": "1.0.0.0"
}
},
"id": 1,
"jsonrpc": "2.0"
}

这个请求告诉服务器:客户端期望使用2025-06-18版本的协议,并附上了自己的身份信息。

服务器收到后,会返回一个 Server-Sent Events (SSE) 响应。这个响应中包含一个关键的HTTP头 Mcp-Session-Id,以及对初始化请求的回复。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"LINQPad.ScriptHost","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"}

从响应中可以看到,服务器同意使用2025-06-18协议版本,并返回了自己的能力(capabilities)。最重要的是,它提供了一个唯一的会话ID Mcp-Session-Id,这个ID将用于后续的所有通信。

2. 确认 (Confirmation)

拿到会话ID后,客户端会发送第二个请求,这次是notifications/initialized通知,用于确认初始化。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "notifications/initialized",
"params": {},
"jsonrpc": "2.0"
}

这个请求在HTTP头中带上了上一步获取的Mcp-Session-Id和双方商定的MCP-Protocol-Version

服务器收到后,会再次返回一个SSE响应,内容与第一次类似,标志着握手完成,会话正式建立。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"LINQPad.ScriptHost","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"}

深度解析:为何需要两步初始化?

你可能会问,为什么设计如此复杂的两步初始化过程?

根本原因在于,这是一个健壮且灵活的协议设计模式,其核心思想是分离“协商”与“确认”。这确保了客户端和服务器在开始正式数据交换前,就所有关键参数(如协议版本、会话ID、双方能力等)达成完全一致。

  • 第一步 initialize (协商阶段):客户端发起提议,服务器响应提议、确定通信参数并创建会话,返回会话ID(Mcp-Session-Id)。此时,双方只是达成了“如何沟通”的共识。
  • 第二步 initialized (确认阶段):客户端使用会话ID和协议版本发起确认,告诉服务器:“我已经收到你的响应,并准备好按商定的规则开始通信了。”

这种设计的必要性体现在:

  1. 避免竞态条件 (Race Conditions):如果没有第二步确认,客户端可能在收到initialize响应后立即发送业务请求,但此时服务器可能还未完全准备好。第二步就像一个明确的同步信号。
  2. 保证状态一致性:类似TCP的三次握手,这种模式确保了通信双方对会话状态的认知完全一致,为后续的稳定通信奠定基础。
  3. 灵活性和扩展性:该设计允许在协商阶段加入更复杂的逻辑。例如,服务器可以要求客户端在确认前完成某些额外设置。

简单类比一下,这就像一个正式的电话会议:

  1. 第一步 (initialize): 你打电话:“你好,我是张三,能现在开会讨论项目A吗?” 对方回答:“可以,我是李四。我们就用中文讨论,会议号是12345。”
  2. 第二步 (initialized): 你说:“好的,收到,会议号12345,我们正式开始吧。”

没有第二步,对方就无法确定你是否已准备就绪。总之,MCP通过两步初始化,实现了一个可靠、同步且灵活的握手过程

第二部分:方法确认 (GET请求)

在初始化完成后,SDK可能会尝试发送一个GET请求来确认连接。

GET / HTTP/1.1
Host: localhost:5000
Accept: text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18

然而,在我使用的 ModelContextProtocol.AspNetCore 0.3.0-preview.3 版本中,服务端并未实现对GET请求的处理逻辑。因此,服务器返回了 HTTP 405 Method Not Allowed,并在 Allow 头中明确指出只支持 POST

HTTP/1.1 405 Method Not Allowed
Content-Length: 0
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Allow: POST

第三部分:正常通信

握手成功后,客户端和服务端就可以开始真正的数据交换了。所有业务请求都通过POST方法进行。

1. 列出可用工具

首先,我们发送一个 tools/list 请求来获取服务端提供的所有工具。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "tools/list",
"params": {},
"id": 2,
"jsonrpc": "2.0"
}

服务器返回一个SSE消息,data字段中包含了工具列表的JSON数组,每个工具都有名称、描述和输入参数的Schema。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA event: message
data: {"result":{"tools":[{"name":"echo","description":"Echoes the message back to the client.","inputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},{"name":"test_throw","description":"Throws an exception for testing purposes.","inputSchema":{"type":"object","properties":{}}},{"name":"count","description":"Counts from 0 to n, reporting progress at each step.","inputSchema":{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}},{"name":"echo_ip","description":"Returns the IP address of the client.","inputSchema":{"type":"object","properties":{}}}]},"id":2,"jsonrpc":"2.0"}

2. 调用简单工具 (echo)

接下来,我们调用 echo 工具。请求的 methodtools/callparams 中指定了工具名称和参数。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {
"message": ".NET is awesome!"
}
},
"id": 3,
"jsonrpc": "2.0"
}

服务器返回结果,result.content 字段包含了工具的输出。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 02:28:19 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA event: message
data: {"result":{"content":[{"type":"text","text":"hello .NET is awesome!"}]},"id":3,"jsonrpc":"2.0"}

3. 调用带进度报告的工具 (count)

MCP的一个强大功能是支持进度报告。我们通过调用 count 工具来演示。注意,请求的params中增加了一个 _meta 字段,其中包含一个客户端生成的 progressToken

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "tools/call",
"params": {
"name": "count",
"arguments": {
"n": 5
},
"_meta": {
"progressToken": "9021fd27304a48e8ada90e35a66bc1dd"
}
},
"id": 4,
"jsonrpc": "2.0"
}

这次,服务器的SSE响应是一个事件流。它会陆续发送多个 event: message,其中包含了进度更新。这些进度通知的methodnotifications/progress,并通过 progressToken 与原始请求关联。当任务完成后,最后一条消息才包含最终的result

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 02:28:19 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":0,"total":5,"message":"Step 0 of 5"},"jsonrpc":"2.0"} event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":1,"total":5,"message":"Step 1 of 5"},"jsonrpc":"2.0"} event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":2,"total":5,"message":"Step 2 of 5"},"jsonrpc":"2.0"} event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":3,"total":5,"message":"Step 3 of 5"},"jsonrpc":"2.0"} event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":4,"total":5,"message":"Step 4 of 5"},"jsonrpc":"2.0"} event: message
data: {"result":{"content":[{"type":"text","text":"5"}]},"id":4,"jsonrpc":"2.0"}

第四部分:异常与错误处理

一个健壮的协议必须能优雅地处理各种意外情况。MCP协议通过两种不同的方式来报告错误,我们通过调用 test_throw(在服务端会主动抛出异常)和调用一个不存在的工具 not-existing-tool 来观察这两种机制。

4. 工具执行时抛出异常 (test_throw)

现在,我们调用那个被设计为一定会失败的 test_throw 工具。

请求 (Request)

请求本身与调用普通工具无异,它遵循标准的 tools/call 格式。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "tools/call",
"params": {
"name": "test_throw"
},
"id": 5,
"jsonrpc": "2.0"
}

响应 (Response)

这是有趣的地方。服务器返回的HTTP状态码依然是 200 OK,表示HTTP通信本身是成功的。然而,响应体内的JSON-RPC报文揭示了真实情况。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 03:17:20 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA event: message
data: {"result":{"content":[{"type":"text","text":"An error occurred invoking 'test_throw'."}],"isError":true},"id":5,"jsonrpc":"2.0"}

深度解析:

请注意,JSON-RPC报文返回的不是一个顶级的 error 对象,而是一个 result 对象。这说明从JSON-RPC协议的层面来看,这次调用是“成功”的。但是,result 对象内部增加了一个关键字段:"isError": true

这是一种精巧的设计:它区分了协议层面的错误业务逻辑层面的错误

  • 协议层面:客户端的请求格式正确,服务器也找到了名为 test_throw 的工具并成功尝试执行它。因此,JSON-RPC的交互流程是完整的。
  • 业务逻辑层面:工具在执行期间内部发生了未捕获的异常。MCP服务端捕获了这个异常,并将其封装成一个“错误结果”返回。isError: true 就是一个明确的信号,告诉客户端:“我尝试执行了,但工具自己出错了”。

这种方式让客户端可以统一处理所有 tools/call 的响应,然后通过检查 isError 标志来判断工具的执行是否真正成功。

5. 调用不存在的工具

接下来,我们尝试调用一个从未在服务端定义过的工具:not-existing-tool

请求 (Request)

请求结构依然是标准的 tools/call

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8 {
"method": "tools/call",
"params": {
"name": "not-existing-tool"
},
"id": 6,
"jsonrpc": "2.0"
}

响应 (Response)

这次的响应与上一个场景截然不同。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 03:17:20 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA event: message
data: {"error":{"code":-32602,"message":"Unknown tool: 'not-existing-tool'"},"id":6,"jsonrpc":"2.0"}

深度解析:

看到区别了吗?这次的响应体直接包含了一个顶级的 error 对象,完全符合JSON-RPC 2.0的错误响应规范。

  • "code": -32602:这是JSON-RPC的一个标准错误码,代表 “Invalid params” (无效参数)。在这里,服务端认为tools/call方法中的name参数值"not-existing-tool"是无效的,因为找不到对应的工具。
  • "message": "Unknown tool: 'not-existing-tool'":提供了人类可读的错误描述。

这被视为一个协议层面的错误,因为客户端请求执行一个从服务器视角看根本不存在的方法(或资源)。服务器甚至都无法开始“执行工具”这个业务逻辑,因为它在第一步——查找工具时——就失败了。因此,它直接返回一个标准的JSON-RPC错误,终止了这次调用。

总结

通过以上的抓包分析,我们可以清晰地总结出(无状态)MCP协议的核心通信模式:

  1. 协议基础:MCP构建在 JSON-RPC 2.0 之上,通过 HTTP POST 请求进行交互。
  2. 会话管理:通过一个健壮的两步握手initializeinitialized)来建立会话,并使用 Mcp-Session-Id HTTP头来标识和维持该会话。
  3. 响应机制:服务端使用 Server-Sent Events (SSE) (Content-Type: text/event-stream) 来响应客户端。这种方式天然支持流式数据,非常适合长任务的进度报告。
  4. 数据格式:无论是请求的body还是SSE返回的data部分,都遵循JSON-RPC 2.0的报文结构 ({"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...}{"jsonrpc": "2.0", "result": ..., "id": ...})。
  5. 错误处理:MCP协议区分了两种错误。协议层面的错误(如调用不存在的工具)会返回标准的JSON-RPC error对象。而工具执行期间的业务逻辑错误则通过在成功的 result 对象中附加 isError: true 标志来表示,实现了协议与业务的分离。

希望本文能帮助你彻底搞懂MCP的底层通信原理。掌握了这些,你不仅能更好地使用官方SDK,甚至可以在不支持的语言或环境中实现自己的MCP客户端或服务端。


感谢您的阅读,如果您有任何问题或想法,欢迎在评论区留言讨论。

也欢迎加入我们的 .NET骚操作 QQ群一起探讨:495782587

官方文档没告诉你的:通过抓包,深入揭秘MCP协议底层通信的更多相关文章

  1. 《SpringCloudDubbo开发日记》(一)Nacos连官方文档都没写好

    背景 现在的微服务框架一般分dubbo和springcloud两套服务治理体系,dubbo是基于zookeeper为注册中心,springcloud是基于eureka作为注册中心. 但是现在eurek ...

  2. MySQL 标识符到底区分大小写么——官方文档告诉你

    最近在阿里云服务器上部署一个自己写的小 demo 时遇到一点问题,查看 Tomcat 日志后定位到问题出现在与数据库服务器交互的地方,执行 SQL 语句时会返回 指定列.指定名 不存在的错误.多方查证 ...

  3. Spring 4 官方文档学习(十二)View技术

    关键词:view technology.template.template engine.markup.内容较多,按需查用即可. 介绍 Thymeleaf Groovy Markup Template ...

  4. Swift -- 官方文档Swift-Guides的学习笔记

    在经历的一段时间的郁闷之后,我发现感情都是虚伪的,只有代码是真实的(呸) 因为看了swift语法之后依然不会用swift,然后我非常作死的跑去看官方文档,就是xcode里自带的help>docu ...

  5. 从官方文档去学习之FreeMarker

    一.前言 上一篇 <从现在开始,试着学会用官方文档去学习一个技术框架>提倡大家多去从官方文档学习技术,没有讲到具体的实践,本篇就拿一个案例具体的说一说,就是FreeMarker,选择这个框 ...

  6. Spring Data Commons 官方文档学习

    Spring Data Commons 官方文档学习   -by LarryZeal Version 1.12.6.Release, 2017-07-27 为知笔记版本在这里,带格式. Table o ...

  7. Vue2.0 官方文档学习笔记

    VUE2.0官方文档 基础部分: 1.VUE简介 Vue是一个基于MVVM的框架,其中M代表数据处理层,V代表视图层即我们在Vue组件中的html部分,VM即M和V的结合层,处理M层相应的逻辑数据,在 ...

  8. 【苦读官方文档】2.Android应用程序基本原理概述

    官方文档原文地址 应用程序原理 Android应用程序是通过Java编程语言来写.Android软件开发工具把你的代码和其它数据.资源文件一起编译.打包成一个APK文件,这个文档以.apk为后缀,保存 ...

  9. Spring 通读官方文档

    Spring 通读官方文档 这部分参考文档涵盖了Spring Framework绝对不可或缺的所有技术. 其中最重要的是Spring Framework的控制反转(IoC)容器.Spring框架的Io ...

  10. 【AutoMapper官方文档】DTO与Domin Model相互转换(上)

    写在前面 AutoMapper目录: [AutoMapper官方文档]DTO与Domin Model相互转换(上) [AutoMapper官方文档]DTO与Domin Model相互转换(中) [Au ...

随机推荐

  1. 导入别人的android studio项目

    在导入别人的android studio项目(假设为项目A)时,会遇到gradle不一致的情况,以下简短介绍解决方法: 1. 打开要导入的项目的目录,删除下图红框中的文件. 2. 找到自己以前在自己的 ...

  2. Linux C 获取本机IPV4和IPV6地址列表

    有时候设备网卡上有多个IPv6,其中只有一个是可用的,另外一个是内网地址,无法使用,如果程序需要绑定一个V6地址的时候,需要获取网卡上的V6地址,并且要求是可用的. 通过ifconfig可用看到,et ...

  3. TVM VLOG打印

    TVM 提供了详细日志记录功能,允许提交跟踪级别的调试消息,而不会影响生产中 TVM 的二进制大小或运行时.你可以在你的代码中使用 VLOG 如下: void Foo(const std::strin ...

  4. RPC实战与核心原理之安全体系

    安全体系:如何建立可靠的安全体系? 回顾 异步化".调用方利用异步化机制实现并行调用多个服务,以缩短整个调用时间:而服务提供方则可以利用异步化把业务逻辑放到自定义线程池里面去执行,以提升单机 ...

  5. UML类图-UML Class Diagram

    .wj_nav { display: inline-block; width: 100%; margin-top: 0; margin-bottom: 0.375rem } .wj_nav_1 { p ...

  6. 自己做的linux动态壁纸软件

    自己做的linux动态壁纸软件 https://github.com/dependon/fantascene-dynamic-wallpaper

  7. 使用Streamlit构建批量二维码生成器

    Streamlit是一个优秀的Python库,让数据科学家和开发者能够快速创建交互式Web应用.今天,我将展示如何使用Streamlit和qrcode库构建一个简单而实用的批量二维码生成器. 技术栈 ...

  8. Mac Catalina关闭系统更新提示

    catalina每隔一段时间就会提示更新,系统更新图标上会显示红色的更新提示,有没有觉得很烦? 如果有那就如下操作: 1.打开系统设置->软件更新 2.点击右下角高级 3.取消所有的勾选(这一步 ...

  9. JuiceFS v1.3-Beta2:集成 Apache Ranger,实现更精细化的权限控制

    在大数据场景中,文件系统和应用组件的权限管理至关重要.在最新发布的 JuiceFS 社区版 v1.3-Beta 2 中,JuiceFS 引入了与 Apache Ranger 的集成,提供了更为灵活和细 ...

  10. Git使用随记

    前言 记录Git软件使用相关的流程.命令. 注:这不是一份专业的教程. Git是什么? Git 是一个用于管理源代码的分布式版本控制系统. 版本控制系统会在您修改文件时记录并保存更改,使用户可以随时恢 ...