MCP Streamable HTTP 协议入门与 100 行代码实现
最近在开发 MCP 服务,服务上线后,想到不使用 SDK 实现 MCP 服务是否困难,因此有了这篇文章,这篇文章会介绍 MCP 协议中 Streamable HTTP 传输机制的一些具体内容,然后手动实现一个简易版的 MCP Streamable HTTP 服务,只需要100行代码。
MCP 简介
MCP(Model Context Protocol)模型上下文协议是一个基于 JSON-RPC 的开放协议,它提供了一种标准化的方式,让 LLM 与所需服务交互,实现 LLM 应用与外部数据源及工具之间的无缝集成。无论是在构建 AI 驱动的 IDE、AI 聊天界面,还是创建自定义 AI 工作流程、构建 AI 应用,都极大地降低了对接复杂度。
MCP 兼顾了标准化与灵活性。如对基础交互的消息类型、生命周期的初始化、能力协商、会话管理、优雅关闭,服务功能的暴露通知方式都有明确的定义。
说简单点就是:为 LLM 应用与服务之间提供了一个使用 JSON 格式交互的统一规范。大家都遵循这个规范,LLM 应用对接外部服务就变得简单。
协议基本规范
MCP 协议定义了 STDIO 和 Streamable HTTP 两种标准传输机制。STDIO 运行在本地,Streamable HTTP 可以是远程的 HTTP 服务,这篇文章围绕 Streamable HTTP 方式进行介绍。
按照 MCP Streamable HTTP 的生命周期要求,执行一个 Tools 调用的完整最小链路为:
- 连接初始化:initialize
- 连接建立完成通知:notifications/initialized
- 获取工具列表:tools/list
- 调用一个工具:tools/call。
这其中 tools/list 非强制选项,如果已经提前缓存了 tools/list ,可以直接发起调用,但在动态环境中建议先使用 tools/list 以确保调用合法性。
需要注意的是:MCP Streamable HTTP 可以通过 Session ID 维护状态,但是这是可选项,本文后续默认使用无状态 Streamable HTTP 进行介绍。
MCP Streamable HTTP API
MCP 协议使用 JSON-RPC 对消息进行编码。同时采用 UTF-8 编码。MCP 服务必须提供 HTTP API,同时支持 POST 和 GET 请求(但仅 POST 用于 RPC)。例如:https://example.com/mcp 这个 URL。客户端会使用 POST 向 MCP API 发起请求。
Client Request
方法与格式:客户端必须使用 HTTP POST 向 MCP 端点发送消息,内容必须是单一的 JSON-RPC 请求、通知或响应。
请求体 Body 的基本格式如下:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params":{
//...
}
}
其中 jsonrpc 指定 RPC 版本,id 为本次会话中唯一的消息 id,可以是字符串也可以是数字。method 值为请求的操作,如 tools/list、tools/call 、ping 等。params 是可选的,如果是 method=tools/call,则 params 传入工具调用参数。
另外请求必须在 Accept 标头中列出 application/json 和 text/event-stream。
请求头示例:
Accept: application/json, text/event-stream
Server Response
MCP Streamable HTTP 服务端处理通知请求时,如客户端发送连接建立完成通知,服务端则接受后返回 202 Accepted,无正文;若不接受则返回 HTTP 错误码(如 400)。
若客户端发送的是其他请求,如工具调用。服务器根据操作耗时决定响应模式,快速操作(Quick operations)通常直接返回 JSON 响应,而耗时操作(Long-running tasks)或需要服务器主动推送消息时,会切换为 SSE 流(text/event-stream)
tools/call 响应示例:
{
"id": 3,
"jsonrpc": "2.0",
"result": {
//...
}
}
为了兼容性,客户端需能够同时支持处理上述两种响应格式,也就是普通 JSON 响应和 SSE 流。
MCP Streamable HTTP 生命周期
已知在 MCP Streamable HTTP 机制下,执行一个 Tools 调用的最小链路为:initialize -> notifications/initialized -> tools/list -> tools/call。下面会重点介绍一些这几个生命周期阶段的具体协议内容。
初始化 initialize
在交互之前,客户端必须先发送 initialize与服务器建立连接,在初始化阶段可以协商协议版本并交换各自支持的能力(Capabilities),比如确认服务器是否支持工具(Tools)功能。
比如下面的示例 POST Body,protocolVersion 字段表明了客户端支持的协议版本。
{
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "Cherry Studio",
"version": "1.5.9"
}
},
"jsonrpc": "2.0",
"id": 0
}
服务端示例 Response:
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"completions": {},
"prompts": {
"listChanged": false
},
"resources": {
"subscribe": false,
"listChanged": false
},
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "mcp-server",
"version": "1.0.0"
}
}
}
这个示例中,服务端也说明了自身支持的协议版本为 2025-06-18,同时从 capabilities->tools 中可以确定服务端支持 tools。
通知 notifications/initialized
在客户端发起 initialize 请求并收到服务端响应后,向服务端发送 notifications/initialized 用于通知服务器初始化已完成,可开始后续通信。
通知行为可以没有 id 字段。
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
服务端收到后响应无正文的 202 Accepted。
ping
作用:用于检测连接是否依然可用,如客户端向服务端发起检测,反之亦然。
示例 POST Body :
{
"method": "ping",
"jsonrpc": "2.0",
"id": 1
}
服务端响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
tools/list
用于工具发现。在初始化完成后,客户端通过调用此方法获取服务器提供的所有工具及其详细定义,包括工具名称、描述以及遵循 JSON Schema 的输入参数模式(inputSchema)。
用途:用于获取支持的工具信息,以便于后续的 LLM 选择调用。
注意:若客户端已缓存工具信息且服务器未声明 listChanged,可跳过。
示例 POST Body :
{
"method": "tools/list",
"jsonrpc": "2.0",
"id": 2
}
响应
{
"id": 2,
"jsonrpc": "2.0",
"result": {
"tools": [
{
"description": "获取城市天气",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"additionalProperties": false
},
"name": "getWeather"
}
]
}
}
tools/call
这是工具调用的核心方法。客户端通过此方法发起工具调用,其中包含要调用的工具名称和具体参数(arguments),服务器执行对应操作后返回结果。
用途:客户端调用具体的服务端工具。
示例 POST Body :
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "getWeather",
"arguments": { "city": "杭州" }
}
}
服务端响应:
{
"id": 3,
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "杭州今日晴转多云"
}
],
"isError": false
}
}
若工具内部出错,服务器应在响应中设置 isError: true,而非返回 JSON-RPC 错误,以便 LLM 能感知并自我修正 。
手动实现 MCP Streamable HTTP 服务
基于上述内容,实现一个基本的 MCP Streamable HTTP 服务,只需要对 MCP 生命周期各个阶段的 RPC 消息,进行解析处理并给出相应的响应即可。
为了简化实现,只需要支持 MCP 协议的关键生命周期方法:
initialize:协商协议版本并返回服务能力(声明支持tools)。notifications/initialized:接收客户端初始化完成通知,返回202 Accepted。ping:用于连接健康检查。tools/list:返回可用工具列表及其 JSON Schema 输入规范。tools/call:执行具体工具调用(如getWeather)。
手动实现
一个基于 Spring Boot + Fastjson2 的 简易 MCP(Model Context Protocol)服务,提供 城市天气查询功能的实现如下。
-
引入 Spring Boot WebMVC 和 FastJSON。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.53</version> </dependency> -
定义接口
/mcp,实现 MCP 生命周期的基本 RPC 消息的解析处理。@RestController @RequestMapping("/mcp") public class McpWeatherController { private static final Logger log = LoggerFactory.getLogger(McpWeatherController.class); // 1. 静态化工具定义,使 tools/list 极其简洁 private static final List<Tool> AVAILABLE_TOOLS = List.of( new Tool("getWeather", "获取指定城市的天气预报", JSONObject.parseObject(""" { "type": "object", "properties": { "city": { "type": "string", "description": "城市名" } }, "required": ["city"], "additionalProperties": false } """)) ); @PostMapping(consumes = "application/json", produces = "application/json") public ResponseEntity<?> handleMcpRequest(@RequestBody JsonRpcRequest request) { Object id = request.id(); var response = switch (request.method()) { case "initialize" -> ok(id, new InitializeResult()); case "notifications/initialized" -> accepted(); case "ping" -> ok(id, Map.of()); case "tools/list" -> ok(id, Map.of("tools", AVAILABLE_TOOLS)); case "tools/call" -> handleToolCall(id, request.params()); default -> ResponseEntity.notFound().build(); }; log.info("\nrequest: {}\nresponse:{}", JSON.toJSONString(request, Feature.PrettyFormat), JSON.toJSONString(response.getBody(),Feature.PrettyFormat)); return response; } /** * 优雅处理工具调用:直接通过 JSONObject 转换,无需 String 二次中转 */ private ResponseEntity<?> handleToolCall(Object id, JSONObject params) { if (params == null) return badRequest(); var callParams = params.toJavaObject(ToolCallParams.class); // 使用 switch 处理多工具扩展性更好 return switch (callParams.name()) { case "getWeather" -> { String city = String.valueOf(callParams.arguments().getOrDefault("city", "未知城市")); yield ok(id, new ToolCallResult(city + "今日雷暴雨,建议居家")); } default -> badRequest(); }; } // --- 辅助方法 --- private static ResponseEntity<JsonRpcResponse> ok(Object id, Object result) { return ResponseEntity.ok(new JsonRpcResponse(id, result)); } private static ResponseEntity<Void> accepted() { return ResponseEntity.status(202).build(); } private static ResponseEntity<Void> badRequest() { return ResponseEntity.badRequest().build(); } // --- MCP 协议 Records (Java 21) --- // 将 params 定义为 JSONObject,方便后续 toJavaObject 转换 public record JsonRpcRequest(String jsonrpc, Object id, String method, JSONObject params) {} public record JsonRpcResponse(String jsonrpc, Object id, Object result) { public JsonRpcResponse(Object id, Object result) { this("2.0", id, result); } } // 初始化结果模型 public record InitializeResult(String protocolVersion, Capabilities capabilities, ServerInfo serverInfo) { public InitializeResult() { this("2025-06-18", new Capabilities(new Tools(false)), new ServerInfo("mcp-weather-server", "1.0.0")); } } public record ServerInfo(String name, String version) {} public record Capabilities(Tools tools) {} public record Tools(boolean listChanged) {} // 工具定义模型 public record Tool(String name, String description, Object inputSchema) {} // 工具调用参数模型 public record ToolCallParams(String name, Map<String, Object> arguments) {} // 响应内容模型 public record Content(String type, String text) { public Content(String text) { this("text", text); } } public record ToolCallResult(List<Content> content, boolean isError) { public ToolCallResult(String text) { this(List.of(new Content(text)), false); } } }
这就是完整代码了,启动后是一个最小化实现的 MCP Streamable HTTP 服务,它遵循 MCP Streamable HTTP 协议规范,利用 Java 21 的现代语言特性(如 record、switch 表达式、文本块等)来提升代码的简洁性与可读性。
使用测试
我使用 LLM 客户端 Cherry Studio 添加这个 MCP 服务,然后询问北京天气,从日志中可以看到服务端在初始化->通知->tools/list 之后,进行了 tools/call 调用,并响应了“北京今日雷暴雨,建议居家”。
request: {"id":0,"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"Cherry Studio","version":"1.5.9"}}}
response: {"id":0,"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":false}},"protocolVersion":"2025-06-18","serverInfo":{"name":"mcp-weather-server","version":"1.0.0"}}}
-------------------
request: {"jsonrpc":"2.0","method":"notifications/initialized"}
response: null
-------------------
request: {"id":1,"jsonrpc":"2.0","method":"tools/list"}
response: {"id":1,"jsonrpc":"2.0","result":{"tools":[{"description":"获取指定城市的天气预报","inputSchema":{"type":"object","properties":{"city":{"type":"string","description":"城市名"}},"required":["city"],"additionalProperties":false},"name":"getWeather"}]}}
-------------------
request: {"id":2,"jsonrpc":"2.0","method":"ping"}
response: {"id":2,"jsonrpc":"2.0","result":{}}
-------------------
request: {"id":3,"jsonrpc":"2.0","method":"prompts/list"}
response: null
-------------------
request: {"id":4,"jsonrpc":"2.0","method":"ping"}
response: {"id":4,"jsonrpc":"2.0","result":{}}
-------------------
request: {"id":5,"jsonrpc":"2.0","method":"resources/list"}
response: null
-------------------
request: {"id":6,"jsonrpc":"2.0","method":"ping"}
response: {"id":6,"jsonrpc":"2.0","result":{}}
-------------------
request: {"id":7,"jsonrpc":"2.0","method":"tools/call","params":{"name":"getWeather","arguments":{"city":"北京"},"_meta":{"progressToken":9}}}
response: {"id":7,"jsonrpc":"2.0","result":{"content":[{"text":"北京今日雷暴雨,建议居家","type":"text"}],"isError":false}}
文中代码已经上传到 Github.com/niumoo/JavaNotes 仓库。
参考
https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
https://modelcontextprotocol.info/specification/draft/server/tools/
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
Member discussion