11 min read

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 协议定义了 STDIOStreamable HTTP 两种标准传输机制。STDIO 运行在本地,Streamable HTTP 可以是远程的 HTTP 服务,这篇文章围绕 Streamable HTTP 方式进行介绍。

按照 MCP Streamable HTTP 的生命周期要求,执行一个 Tools 调用的完整最小链路为:

  1. 连接初始化:initialize
  2. 连接建立完成通知:notifications/initialized
  3. 获取工具列表:tools/list
  4. 调用一个工具: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/listtools/callping 等。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)服务,提供 城市天气查询功能的实现如下。

  1. 引入 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>
    
  2. 定义接口 /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 的现代语言特性(如 recordswitch 表达式、文本块等)来提升代码的简洁性与可读性。

使用测试

我使用 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