MCP(Model Context Protocol,模型上下文协议)是一套让 Agent 获取外部上下文、调用外部工具的开放协议。它解决的不是“大语言模型怎么推理”,而是“大语言模型所在的应用,怎么用统一方式发现工具、理解工具参数、调用工具并拿到结果”。
如果没有 MCP,每个 Agent 平台都可能有自己的工具接入方式:接口描述格式不同、参数结构不同、鉴权方式不同、结果解析方式也不同。工具开发者要为不同平台适配多套接入逻辑,Agent 开发者也要关心“怎么选工具、怎么拼参数、怎么调接口、怎么把结果喂回模型”。
MCP 把这条链路标准化了。
一个工具只要按 MCP Server 的规范暴露出去,支持 MCP 的 Agent 平台就能通过 MCP Client 与它通信。平台负责把工具描述转换成模型可理解的 Function Calling(函数调用)能力,工具服务只需要稳定地提供“有哪些工具”和“如何调用工具”。
MCP 的三个核心角色
MCP 通信里有三个角色:MCP Host、MCP Client、MCP Server。
flowchart LR
U[用户] --> H[MCP Host<br/>Agent 应用或 AI 客户端]
H --> C[MCP Client<br/>协议客户端]
C <--> S[MCP Server<br/>工具与上下文服务]
S --> D[(业务系统 / 数据库 / 外部 API)]
D --> S
S --> C
C --> H
H --> U
| 角色 | 作用 | 常见形态 |
|---|---|---|
| MCP Host | 执行 Agent 逻辑,接收用户问题,决定是否调用工具 | Agent 平台、AI IDE、桌面 AI 客户端 |
| MCP Client | 按 MCP 协议连接 MCP Server,发送请求并接收响应 | 通常内置在 MCP Host 中 |
| MCP Server | 暴露工具、资源或上下文数据,处理调用请求 | 独立服务、本地进程、业务系统适配层 |
MCP Host 往往不会把 MCP Client 单独暴露给使用者。使用 Agent 平台时,看到的是“添加 MCP Server”“连接工具服务”“调用工具”等入口,真正的协议请求由平台内部的 MCP Client 完成。
MCP Server 才是工具开发者主要实现的部分。它需要告诉客户端:
- 支持哪个 MCP 协议版本;
- 服务器有哪些能力;
- 有哪些工具可以调用;
- 每个工具需要什么参数;
- 调用工具后返回什么结果。
MCP 解耦的到底是什么
MCP 的价值可以类比成 Type-C 接口,但要注意这个类比的边界。Type-C 统一的是设备连接方式,MCP 统一的是 Agent 与工具之间的通信方式。
在 Agent 场景里,模型并不是直接去请求业务接口。典型流程是:
flowchart TD
A[用户提出问题] --> B[Agent 判断是否需要工具]
B --> C[模型选择工具并生成参数]
C --> D[MCP Host 通过 MCP Client 调用 MCP Server]
D --> E[MCP Server 执行业务逻辑]
E --> F[返回结构化结果]
F --> G[Agent 把结果交给模型继续生成回答]
没有 MCP 时,工具接入经常会变成平台绑定开发:
| 对比项 | 自定义工具接入 | MCP 工具接入 |
|---|---|---|
| 工具描述 | 每个平台一套格式 | 统一通过 tools/list 暴露 |
| 工具调用 | 每个平台一套调用协议 | 统一通过 tools/call 调用 |
| 参数结构 | 由平台自定义 | 使用 JSON Schema 描述 |
| 复用成本 | 平台越多,适配越多 | 支持 MCP 的平台可复用 |
| Agent 开发关注点 | 要关心接口调用细节 | 更关注任务规划与结果使用 |
| 工具开发关注点 | 要适配不同 Agent 平台 | 按 MCP Server 规范发布能力 |
所以,MCP 并不是替代大模型的 Function Calling,而是把外部工具以统一协议暴露给 MCP Host。MCP Host 再根据模型厂商的差异,把这些工具描述转换成模型可用的函数调用格式。
基于 HTTP + SSE 的 MCP 通信模型
MCP 支持不同传输方式。这里实现的是基于 HTTP + SSE 的 MCP Server,也是理解 MCP Server 最直观的一种形态。
在这种传输方式里,MCP Client 和 MCP Server 之间有两条通道:
flowchart LR
C[MCP Client] -- "GET /sse<br/>建立 SSE 长连接" --> S[MCP Server]
S -- "event: endpoint<br/>event: message" --> C
C -- "POST /message/{sessionId}<br/>发送 JSON-RPC 请求" --> S
两条通道分工很明确:
| 通道 | 方向 | 协议 | 用途 |
|---|---|---|---|
| SSE 通道 | Server → Client | HTTP GET + text/event-stream | 服务端推送 endpoint、响应结果、通知、心跳 |
| POST 通道 | Client → Server | HTTP POST + JSON | 客户端发送 initialize、tools/list、tools/call 等请求 |
也就是说,MCP Client 不会把 JSON-RPC 请求直接发到 /sse。它会先连接 /sse,拿到服务端通过 SSE 推送的 POST 地址,然后把后续请求发到这个 POST 地址。服务端处理完请求后,再通过 SSE 通道把结果推回客户端。
SSE:负责服务端如何把消息推回客户端
SSE(Server-Sent Events,服务器发送事件)是一种基于 HTTP 的单向事件流机制。它看起来像服务端主动推送,本质上仍然是客户端先发起一个 HTTP 请求,服务端一直不结束响应,而是不断往响应体里追加事件数据。
SSE 响应头通常长这样:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
响应体由一条条事件组成,每条事件用两个换行分隔:
event: message
data: {"hello":"world"}
event: ping
data: {"type":"ping"}
SSE 常用字段只有几个:
| 字段 | 含义 |
|---|---|
data | 事件负载,可以是普通文本,也可以是 JSON 字符串 |
event | 事件名称,客户端可按名称监听不同事件 |
id | 事件编号,可用于断线续传 |
retry | 客户端断线后重连间隔,单位是毫秒 |
在 MCP 的 SSE 传输里,常见事件有两类。
第一类是 endpoint,用于告诉客户端后续 POST 请求应该发到哪里:
event: endpoint
data: http://localhost:8080/message/client-1710000000000
第二类是 message,用于推送 JSON-RPC 响应:
event: message
data: {"jsonrpc":"2.0","id":"1","result":{"tools":[{"name":"get_time","description":"Returns current server time"}]}}
SSE 只负责“怎么推”。它不规定 MCP 的方法名、参数、工具格式,这些内容由 JSON-RPC 2.0 和 MCP 协议层定义。
JSON-RPC 2.0:负责消息长什么样
JSON-RPC 2.0 是一种轻量级远程过程调用协议。MCP 使用它来包装 Client 与 Server 之间的请求、响应和通知。
JSON-RPC 请求一般包含这些字段:
| 字段 | 是否必需 | 含义 |
|---|---|---|
jsonrpc | 是 | 固定为 "2.0" |
method | 是 | 要调用的方法,例如 initialize、tools/list |
params | 否 | 方法参数 |
id | 请求必需,通知不需要 | 请求编号,响应必须带回同一个 id |
一次普通请求长这样:
{
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": "1"
}
成功响应长这样:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"tools": []
}
}
失败响应长这样:
{
"jsonrpc": "2.0",
"id": "1",
"error": {
"code": -32601,
"message": "Method not found"
}
}
通知和请求很像,但没有 id,也不需要服务端响应。例如初始化完成通知:
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}
常见 JSON-RPC 错误码可以按这个规则处理:
| 错误码 | 含义 | 典型场景 |
|---|---|---|
-32700 | Parse error | JSON 解析失败 |
-32600 | Invalid Request | 请求结构不符合 JSON-RPC |
-32601 | Method not found | 不支持的方法 |
-32602 | Invalid params | 参数错误 |
-32603 | Internal error | 服务端内部异常 |
MCP 的完整调用生命周期
基于 SSE Transport 的 MCP 通信可以拆成五个动作:连接、取端点、初始化、使用工具、断开连接。
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
Client->>Server: GET /sse
Server-->>Client: event: endpoint /message/{sessionId}
Client->>Server: POST /message/{sessionId}<br/>initialize
Server-->>Client: event: message<br/>initialize result
Client->>Server: POST /message/{sessionId}<br/>notifications/initialized
Note over Server: 记录客户端初始化完成<br/>通知不返回响应
Client->>Server: POST /message/{sessionId}<br/>tools/list
Server-->>Client: event: message<br/>工具列表
Client->>Server: POST /message/{sessionId}<br/>tools/call
Server-->>Client: event: message<br/>工具调用结果
Client--xServer: 关闭 SSE 连接
初始化请求用于协商协议版本和能力:
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"sampling": {},
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "mcp-inspector",
"version": "0.9.0"
}
},
"id": "0"
}
服务端返回自身能力:
{
"jsonrpc": "2.0",
"id": "0",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "SpringBoot MCP Server",
"version": "1.0.0"
}
}
}
初始化完成后,客户端才会进入工具发现和工具调用阶段。
用 Spring Boot WebFlux 实现一个 MCP Server
一个最小可用的 MCP Server 至少需要三部分:
- SSE 端点:接收客户端连接,并向客户端推送事件;
- POST 端点:接收客户端发来的 JSON-RPC 请求;
- 协议处理器:处理
initialize、tools/list、tools/call等方法。
项目结构可以这样组织:
springboot-mcp-server/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/example/mcp/
│ ├── McpApplication.java
│ ├── protocol/
│ │ ├── McpRequest.java
│ │ └── McpResponse.java
│ ├── server/
│ │ └── McpServerHandler.java
│ ├── tools/
│ │ └── McpToolRegistry.java
│ └── transport/
│ └── SseTransport.java
└── resources/
└── application.properties
1. 准备依赖
使用 Spring Boot WebFlux 实现 SSE 会比较自然,因为它可以直接返回 Flux<ServerSentEvent<?>>。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
配置端口:
server.port=8080
启动类:
package com.example.mcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpApplication {
public static void main(String[] args) {
SpringApplication.run(McpApplication.class, args);
}
}
2. 定义 JSON-RPC 请求和响应
McpRequest 用来接收客户端请求:
package com.example.mcp.protocol;
import lombok.Data;
import java.util.Map;
@Data
public class McpRequest {
private String jsonrpc;
private String id;
private String method;
private Map<String, Object> params;
}
McpResponse 用来返回成功结果或错误结果。响应里 result 和 error 只能出现一个。
package com.example.mcp.protocol;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class McpResponse {
private String jsonrpc = "2.0";
private String id;
private Object result;
private McpError error;
public static McpResponse success(String id, Object result) {
McpResponse response = new McpResponse();
response.setId(id);
response.setResult(result);
return response;
}
public static McpResponse error(String id, int code, String message) {
McpResponse response = new McpResponse();
response.setId(id);
response.setError(new McpError(code, message));
return response;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class McpError {
private int code;
private String message;
private Object data;
public McpError(int code, String message) {
this.code = code;
this.message = message;
}
}
}
3. 实现工具注册表
MCP Server 需要能回答两个问题:
- 有哪些工具?
- 某个工具被调用时怎么执行?
这里实现三个简单工具:
| 工具名 | 作用 | 参数 |
|---|---|---|
hello_world | 返回问候语 | name,可选 |
get_time | 返回当前服务器时间 | 无 |
echo | 原样返回输入消息 | message |
package com.example.mcp.tools;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Component
public class McpToolRegistry {
public List<Map<String, Object>> listTools() {
return List.of(
Map.of(
"name", "hello_world",
"description", "Returns a Hello World message",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of(
"name", Map.of(
"type", "string",
"description", "Name to greet"
)
)
)
),
Map.of(
"name", "get_time",
"description", "Returns current server time",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of()
)
),
Map.of(
"name", "echo",
"description", "Echoes back the provided message",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of(
"message", Map.of(
"type", "string",
"description", "Message to echo back"
)
)
)
)
);
}
@SuppressWarnings("unchecked")
public Map<String, Object> callTool(String name, Map<String, Object> arguments) {
String text = switch (name) {
case "hello_world" -> {
String target = String.valueOf(arguments.getOrDefault("name", "World"));
yield "Hello, " + target + "!";
}
case "get_time" -> "Current server time: " + LocalDateTime.now();
case "echo" -> String.valueOf(arguments.getOrDefault("message", ""));
default -> throw new IllegalArgumentException("Unknown tool: " + name);
};
return Map.of(
"content", List.of(
Map.of(
"type", "text",
"text", text
)
)
);
}
}
工具列表里的 inputSchema 很关键。MCP Host 会根据它理解工具参数,进而让模型生成符合结构的调用参数。
4. 实现 MCP 协议处理器
McpServerHandler 负责处理 JSON-RPC 方法。最小实现需要覆盖:
initializenotifications/initializedtools/listtools/call
package com.example.mcp.server;
import com.example.mcp.protocol.McpRequest;
import com.example.mcp.protocol.McpResponse;
import com.example.mcp.tools.McpToolRegistry;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class McpServerHandler {
private final McpToolRegistry toolRegistry;
private volatile boolean initialized = false;
@SuppressWarnings("unchecked")
public McpResponse handleRequest(McpRequest request) {
if (request.getMethod() == null || request.getMethod().isBlank()) {
return McpResponse.error(request.getId(), -32600, "Invalid request: method is required");
}
try {
return switch (request.getMethod()) {
case "initialize" -> handleInitialize(request.getId());
case "notifications/initialized" -> {
initialized = true;
yield null;
}
case "tools/list" -> handleListTools(request.getId());
case "tools/call" -> {
Map<String, Object> params = request.getParams() == null
? Map.of()
: request.getParams();
String toolName = String.valueOf(params.get("name"));
Map<String, Object> arguments = params.get("arguments") instanceof Map<?, ?> map
? (Map<String, Object>) map
: Map.of();
yield handleCallTool(request.getId(), toolName, arguments);
}
default -> McpResponse.error(
request.getId(),
-32601,
"Method not found: " + request.getMethod()
);
};
} catch (IllegalArgumentException e) {
return McpResponse.error(request.getId(), -32602, e.getMessage());
} catch (Exception e) {
return McpResponse.error(request.getId(), -32603, "Internal error: " + e.getMessage());
}
}
private McpResponse handleInitialize(String id) {
return McpResponse.success(
id,
Map.of(
"protocolVersion", "2024-11-05",
"capabilities", Map.of(
"tools", Map.of(
"listChanged", true
)
),
"serverInfo", Map.of(
"name", "SpringBoot MCP Server",
"version", "1.0.0"
)
)
);
}
private McpResponse handleListTools(String id) {
return McpResponse.success(
id,
Map.of("tools", toolRegistry.listTools())
);
}
private McpResponse handleCallTool(
String id,
String toolName,
Map<String, Object> arguments
) {
if (!initialized) {
return McpResponse.error(id, -32600, "Client has not been initialized");
}
Object result = toolRegistry.callTool(toolName, arguments);
return McpResponse.success(id, result);
}
}
notifications/initialized 不返回响应,这是 JSON-RPC 通知的基本语义。客户端发通知只是告诉服务端“初始化流程已经完成”,服务端记录状态即可。
5. 实现 SSE 和 POST 两个端点
SseTransport 是整个 MCP Server 最关键的部分。它需要维护每个客户端的 SSE 连接,并把 POST 请求的处理结果推回对应客户端。
package com.example.mcp.transport;
import com.example.mcp.protocol.McpRequest;
import com.example.mcp.protocol.McpResponse;
import com.example.mcp.server.McpServerHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequiredArgsConstructor
public class SseTransport {
private final ObjectMapper objectMapper;
private final McpServerHandler serverHandler;
private final Map<String, Sinks.Many<ServerSentEvent<String>>> clientSinks =
new ConcurrentHashMap<>();
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> sseEndpoint(
@RequestParam(required = false) String clientId,
ServerWebExchange exchange
) {
String origin = exchange.getRequest().getHeaders().getFirst("Origin");
if (origin != null && !isValidOrigin(origin)) {
return Flux.error(new SecurityException("Invalid origin: " + origin));
}
String sessionId = clientId != null ? clientId : "client-" + UUID.randomUUID();
Sinks.Many<ServerSentEvent<String>> sink =
Sinks.many().multicast().onBackpressureBuffer();
clientSinks.put(sessionId, sink);
String endpointUri = getBaseUrl(exchange) + "/message/" + sessionId;
ServerSentEvent<String> endpointEvent = ServerSentEvent.<String>builder()
.event("endpoint")
.data(endpointUri)
.build();
Flux<ServerSentEvent<String>> heartbeat = Flux.interval(Duration.ofSeconds(30))
.map(tick -> ServerSentEvent.<String>builder()
.event("ping")
.data("{\"type\":\"ping\"}")
.build()
);
return Flux.concat(
Flux.just(endpointEvent),
Flux.merge(sink.asFlux(), heartbeat)
)
.doFinally(signalType -> {
clientSinks.remove(sessionId);
sink.tryEmitComplete();
});
}
@PostMapping(
value = "/message/{sessionId}",
consumes = MediaType.APPLICATION_JSON_VALUE
)
public Mono<ResponseEntity<Void>> handleMessage(
@PathVariable String sessionId,
@RequestBody String messageJson,
ServerWebExchange exchange
) {
return Mono.fromRunnable(() -> {
String origin = exchange.getRequest().getHeaders().getFirst("Origin");
if (origin != null && !isValidOrigin(origin)) {
sendMessageToClient(
sessionId,
McpResponse.error(null, -32600, "Invalid origin: " + origin)
);
return;
}
try {
McpRequest request = objectMapper.readValue(messageJson, McpRequest.class);
McpResponse response = serverHandler.handleRequest(request);
if (response != null) {
sendMessageToClient(sessionId, response);
}
} catch (Exception e) {
sendMessageToClient(
sessionId,
McpResponse.error(null, -32700, "Parse error: " + e.getMessage())
);
}
}).thenReturn(ResponseEntity.accepted().build());
}
private void sendMessageToClient(String sessionId, Object message) {
Sinks.Many<ServerSentEvent<String>> sink = clientSinks.get(sessionId);
if (sink == null) {
return;
}
try {
String json = objectMapper.writeValueAsString(message);
ServerSentEvent<String> event = ServerSentEvent.<String>builder()
.event("message")
.data(json)
.build();
sink.tryEmitNext(event);
} catch (Exception e) {
sink.tryEmitNext(ServerSentEvent.<String>builder()
.event("message")
.data("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Serialization error\"}}")
.build()
);
}
}
private String getBaseUrl(ServerWebExchange exchange) {
String scheme = exchange.getRequest().getURI().getScheme();
String host = exchange.getRequest().getHeaders().getHost().toString();
return scheme + "://" + host;
}
private boolean isValidOrigin(String origin) {
return origin.startsWith("http://localhost")
|| origin.startsWith("http://127.0.0.1");
}
}
这段代码里有几个关键点:
| 代码位置 | 作用 |
|---|---|
GET /sse | 建立 SSE 长连接 |
clientSinks | 按 sessionId 保存客户端连接 |
event: endpoint | 告诉客户端 POST 请求地址 |
POST /message/{sessionId} | 接收客户端 JSON-RPC 请求 |
sendMessageToClient | 把 JSON-RPC 响应通过 SSE 推回客户端 |
ping 事件 | 防止长连接长时间无数据被中间代理断开 |
Origin 校验 | 降低 DNS Rebinding 等攻击风险 |
POST 接口返回的是 HTTP 层面的接收状态,例如 202 Accepted。真正的 MCP 响应不从 POST 返回体返回,而是通过 SSE 的 event: message 推给客户端。
用 MCP Inspector 测试服务
启动服务:
mvn spring-boot:run
安装并启动 MCP Inspector:
npx "@modelcontextprotocol/inspector@0.9"
在 Inspector 中选择 SSE Transport,连接地址填:
http://localhost:8080/sse
连接建立后,服务端会先推送 endpoint:
event: endpoint
data: http://localhost:8080/message/client-xxxx
再按 MCP 流程执行:
InitializeList ToolsCall Tools
也可以直接构造工具调用请求。调用 hello_world 的 JSON-RPC 请求格式如下:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "hello_world",
"arguments": {
"name": "MCP"
}
},
"id": "2"
}
服务端通过 SSE 返回:
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"content": [
{
"type": "text",
"text": "Hello, MCP!"
}
]
}
}
调用 tools/list 时,返回结果会包含三个工具的描述和参数结构:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"tools": [
{
"name": "hello_world",
"description": "Returns a Hello World message",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name to greet"
}
}
}
}
]
}
}
实现 MCP Server 时容易踩的坑
1. SSE 通道和 POST 通道不要混在一起
/sse 只负责服务端推送事件,客户端请求应该发到 endpoint 事件给出的 POST 地址。把 initialize 直接 POST 到 /sse,通常不会得到预期结果。
2. JSON-RPC 响应必须带回相同的 id
客户端用 id 匹配请求和响应。服务端响应里的 id 如果丢了或变了,客户端就不知道这条结果对应哪次请求。
{
"jsonrpc": "2.0",
"id": "request-id-from-client",
"result": {}
}
3. 通知不需要响应
notifications/initialized 属于通知,不应该返回 JSON-RPC 响应。返回响应可能会让部分客户端状态机出现异常。
4. 工具参数要用 JSON Schema 描述清楚
inputSchema 不是装饰字段。Agent 平台会依赖它告诉模型应该生成什么参数。字段名、类型、必填项、描述不准确,模型就更容易生成错误参数。
可以给必填参数加上 required:
{
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Message to echo back"
}
},
"required": ["message"]
}
5. 长连接要处理心跳和断开清理
SSE 连接可能因为浏览器、代理、网关、负载均衡超时而断开。服务端最好定期发送心跳,并在连接断开时清理 sessionId 对应的资源。
6. 生产环境要认真处理安全边界
本地 Demo 只做了简单的 Origin 校验。生产环境还需要考虑:
| 风险 | 处理方式 |
|---|---|
| 未授权调用工具 | 加鉴权,例如 Token、OAuth、网关认证 |
| DNS Rebinding | 校验 Origin、Host,限制内网访问 |
| 参数注入 | 对工具参数做白名单校验 |
| 工具越权 | 每个工具单独做权限判断 |
| 敏感数据泄露 | 控制工具返回内容,不把内部异常栈直接返回给客户端 |
掌握 MCP Server 的关键判断标准
一个 MCP Server 能稳定工作,核心不在代码量,而在协议流程是否完整:
flowchart TD
A[暴露 GET /sse] --> B[返回 event: endpoint]
B --> C[接收 POST JSON-RPC]
C --> D{method}
D -->|initialize| E[返回协议版本和能力]
D -->|notifications/initialized| F[记录初始化完成]
D -->|tools/list| G[返回工具列表和参数 Schema]
D -->|tools/call| H[执行工具并返回 content]
D -->|未知方法| I[返回 JSON-RPC error]
E --> J[通过 SSE event: message 推回客户端]
G --> J
H --> J
I --> J
只要理解这条链路,MCP 就不再是一个抽象概念。它的底层并不神秘:SSE 解决“服务端怎么推消息”,JSON-RPC 2.0 解决“消息格式怎么统一”,MCP 协议解决“连接、握手、工具发现、工具调用的生命周期怎么固定”。
把这三层合在一起,一个 Agent 就能用统一方式连接外部工具,一个工具服务也能被多个支持 MCP 的平台复用。