Streamable HTTP

之前在博客中使用Spring AI搭建了使用sse通信模式的mcp server ,然而sse通信模式在实际的开发应用中会遇到不少问题,由于工作繁忙一直没有去升级协议,这两天Spring AI发布快照1.1.0-SNAPSHOT,其中就有对新协议Streamable HTTP的支持,今天笔者来学习一下。

HTTP+SSE 原理及缺陷

在原有的 MCP 实现中,客户端和服务器通过两个主要通道通信:

  • HTTP 请求/响应:客户端通过标准 HTTP 请求发送消息到服务器
  • 服务器发送事件(SSE) :服务器通过专门的 /sse 端点向客户端推送消息

主要问题

这种设计虽然简单直观,但存在几个关键问题:

不支持断线重连/恢复

当 SSE 连接断开时,所有会话状态丢失,客户端必须重新建立连接并初始化整个会话。例如,正在执行的大型文档分析任务会因 WiFi 不稳定而完全中断,迫使用户重新开始整个过程。

服务器需维护长连接

服务器必须为每个客户端维护一个长时间的 SSE 连接,大量并发用户会导致资源消耗剧增。当服务器需要重启或扩容时,所有连接都会中断,影响用户体验和系统可靠性。

服务器消息只能通过 SSE 传递

即使是简单的请求-响应交互,服务器也必须通过 SSE 通道返回信息,造成不必要的复杂性和开销。对于某些环境(如云函数)不适合长时间保持 SSE 连接。

基础设施兼容性限制

许多现有的 Web 基础设施如 CDN、负载均衡器、API 网关等可能不能正确处理长时间的 SSE 连接,企业防火墙可能会强制关闭超时连接,导致服务不可靠。

Streamable HTTP:设计与原理

Streamable HTTP 的设计基于以下几个核心理念:

  • 最大化兼容性:与现有 HTTP 生态系统无缝集成
  • 灵活性:同时支持无状态和有状态模式
  • 资源效率:按需分配资源,避免不必要的长连接
  • 可靠性:支持断线重连和会话恢复

关键改进

相比原有机制,Streamable HTTP 引入了几项关键改进:

  1. 统一端点 :移除专门的 /sse 端点,所有通信通过单一端点(如 /message)进行
  2. 按需流式传输:服务器可灵活选择是返回普通 HTTP 响应还是升级为 SSE 流
  3. 会话标识:引入会话 ID 机制,支持状态管理和恢复
  4. 灵活初始化:客户端可通过空 GET 请求主动初始化 SSE 流

技术细节

Streamable HTTP 的工作流程如下:

  1. 会话初始化

    • 客户端发送初始化请求到 /message 端点
    • 服务器可选择生成会话 ID 返回给客户端
    • 会话 ID 用于后续请求中标识会话
  2. 客户端向服务器通信

    • 所有消息通过 HTTP POST 请求发送到 /message 端点
    • 如果有会话 ID,则包含在请求中
  3. 服务器响应方式

    • 普通响应:直接返回 HTTP 响应,适合简单交互
    • 流式响应:升级连接为 SSE,发送一系列事件后关闭
    • 长连接:维持 SSE 连接持续发送事件
  4. 主动建立 SSE 流

    • 客户端可发送 GET 请求到 /message 端点主动建立 SSE 流
    • 服务器可通过该流推送通知或请求
  5. 连接恢复

    • 连接中断时,客户端可使用之前的会话 ID 重新连接
    • 服务器可恢复会话状态继续之前的交互

Streamable 工作原理

Streamable HTTP 的工作流程如下:

1.会话初始化(非强制,适用于有状态实现场景):

客户端发送初始化请求到 /mcp 端点

服务器可选择生成会话 ID 返回给客户端

会话 ID 用于后续请求中标识会话

  1. 客户端向服务器通信:

所有消息通过 HTTP POST 请求发送到 /mcp 端点

如果有会话 ID,则包含在请求中

  1. 服务器响应方式:

普通响应: 直接返回 HTTP 响应,适合简单交互

流式响应: 升级连接为 SSE,发送一系列事件后关闭

长连接: 维持 SSE 连接持续发送事件

  1. 主动建立 SSE 流:

客户端可发送 GET 请求到 /mcp 端点主动建立 SSE 流

服务器可通过该流推送通知或请求

  1. 连接恢复:

连接中断时,客户端可使用之前的会话 ID 重新连接

服务器可恢复会话状态继续之前的交互

实际应用场景

无状态服务器模式

场景:简单工具 API 服务,如数学计算、文本处理等。

实现

复制代码
客户端                                 服务器
   |                                    |
   |-- POST /message (计算请求) -------->|
   |                                    |-- 执行计算
   |<------- HTTP 200 (计算结果) -------|
   |                                    |

优势:极简部署,无需状态管理,适合无服务器架构和微服务。

流式进度反馈模式

场景:长时间运行的任务,如大文件处理、复杂 AI 生成等。

实现

复制代码
客户端                                 服务器
   |                                    |
   |-- POST /message (处理请求) -------->|
   |                                    |-- 启动处理任务
   |<------- HTTP 200 (SSE开始) --------|
   |                                    |
   |<------- SSE: 进度10% ---------------|
   |<------- SSE: 进度30% ---------------|
   |<------- SSE: 进度70% ---------------|
   |<------- SSE: 完成 + 结果 ------------|
   |                                    |

优势:提供实时反馈,但不需要永久保持连接状态。

复杂 AI 会话模式

场景:多轮对话 AI 助手,需要维护上下文。

实现

复制代码
客户端                                 服务器
   |                                    |
   |-- POST /message (初始化) ---------->|
   |<-- HTTP 200 (会话ID: abc123) ------|
   |                                    |
   |-- GET /message (会话ID: abc123) --->|
   |<------- SSE流建立 -----------------|
   |                                    |
   |-- POST /message (问题1, abc123) --->|
   |<------- SSE: 思考中... -------------|
   |<------- SSE: 回答1 ----------------|
   |                                    |
   |-- POST /message (问题2, abc123) --->|
   |<------- SSE: 思考中... -------------|
   |<------- SSE: 回答2 ----------------|

优势:维护会话上下文,支持复杂交互,同时允许水平扩展。

断线恢复模式

场景:不稳定网络环境下的 AI 应用使用。

实现

复制代码
客户端                                 服务器
   |                                    |
   |-- POST /message (初始化) ---------->|
   |<-- HTTP 200 (会话ID: xyz789) ------|
   |                                    |
   |-- GET /message (会话ID: xyz789) --->|
   |<------- SSE流建立 -----------------|
   |                                    |
   |-- POST /message (长任务, xyz789) -->|
   |<------- SSE: 进度30% ---------------|
   |                                    |
   |     [网络中断]                      |
   |                                    |
   |-- GET /message (会话ID: xyz789) --->|
   |<------- SSE流重新建立 --------------|
   |<------- SSE: 进度60% ---------------|
   |<------- SSE: 完成 ------------------|

优势:提高弱网环境下的可靠性,改善用户体验。

Streamable HTTP 的主要优势

技术优势

  1. 简化实现:可以在普通 HTTP 服务器上实现,无需特殊支持
  2. 资源效率:按需分配资源,不需要为每个客户端维护长连接
  3. 基础设施兼容性:与现有 Web 基础设施(CDN、负载均衡器、API 网关)良好配合
  4. 水平扩展:支持通过消息总线路由请求到不同服务器节点
  5. 渐进式采用:服务提供者可根据需求选择实现复杂度
  6. 断线重连:支持会话恢复,提高可靠性

业务优势

  1. 降低运维成本:减少服务器资源消耗,简化部署架构
  2. 提升用户体验:通过实时反馈和可靠连接改善体验
  3. 广泛适用性:从简单工具到复杂 AI 交互,都有合适的实现方式
  4. 扩展能力:支持更多样化的 AI 应用场景
  5. 开发友好:降低实现 MCP 的技术门槛

实现参考

服务器端实现要点

  1. 端点设计

    • 实现单一的 /message 端点处理所有请求
    • 支持 POST 和 GET 两种 HTTP 方法
  2. 状态管理

    • 设计会话 ID 生成和验证机制
    • 实现会话状态存储(内存、Redis 等)
  3. 请求处理

    • 解析请求中的会话 ID
    • 确定响应类型(普通 HTTP 或 SSE)
    • 处理流式响应的内容类型和格式
  4. 连接管理

    • 实现 SSE 流初始化和维护
    • 处理连接断开和重连逻辑

客户端实现要点

  1. 请求构造

    • 构建符合协议的消息格式
    • 正确包含会话 ID(如有)
  2. 响应处理

    • 检测响应是普通 HTTP 还是 SSE
    • 解析和处理 SSE 事件
  3. 会话管理

    • 存储和管理会话 ID
    • 实现断线重连逻辑
  4. 错误处理

    • 处理网络错误和超时
    • 实现指数退避重试策略

结论

Streamable HTTP 传输层代表了 MCP 协议的重要进化,它通过结合 HTTP 和 SSE 的优点,同时克服二者的局限,为 AI 应用的通信提供了更灵活、更可靠的解决方案。它不仅解决了原有传输机制的问题,还为未来更复杂的 AI 交互模式奠定了基础。

这个协议的设计充分体现了实用性原则,既满足了技术先进性要求,又保持了与现有 Web 基础设施的兼容性。它的灵活性使得开发者可以根据自身需求选择最合适的实现方式,从简单的无状态 API 到复杂的交互式 AI 应用,都能找到合适的解决方案。

随着这个 PR 的合并,MCP 社区的技术生态将更加丰富多样,也为更多开发者采用 MCP 提供了便利。相信在不久的将来,我们将看到基于 Streamable HTTP 的 MCP 实现在各种 AI 应用中的广泛应用。

Spring AI 实现基于Streamable HTTP 的mcp server

接下来的内容我会基于我之前文章的内容来写。

使用Spring AI 进行MCP开发_spring ai mcp开发-CSDN博客

解决spring ai 在底层调度mcp工具时线程复用导致token同时复用的问题_Spring AI多线程调度优化-CSDN博客

1.首先修改引入的spring-ai-bom版本

(由于是快照版本,spring并没有上传到中央仓库,想要下载jar包需要修改pom.xml和maven的配置文件)

官方文档参考:Getting Started :: Spring AI Reference

修改maven的配置文件

复制代码
<mirrors>
    <mirror>
        <id>maven-public</id>
        <name>maven-public</name>
        <!-- 关键:排除两个快照仓库,让它们直接请求官方地址 -->
        <mirrorOf>*,!spring-snapshots,!central-portal-snapshots</mirrorOf>
        <url>xxxxxxxxxxxxxxxxxxxxxxxxx</url>
    </mirror>
</mirrors>

修改pom.xml

修改内容:

复制代码
 <!-- 版本管理,不会实际引入依赖 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.1.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

 <repositories>
        <!-- 1. Spring 官方快照仓库:存储 Spring AI 快照版本 -->
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled> <!-- 关闭正式版下载,只拉快照 -->
            </releases>
        </repository>
        <!-- 2. Sonatype 中央快照仓库:补充快照依赖 -->
        <repository>
            <name>Central Portal Snapshots</name>
            <id>central-portal-snapshots</id>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled> <!-- 开启快照版下载 -->
            </snapshots>
        </repository>
        <!-- 3. 保留 Maven 中央仓库:拉取非快照依赖(如 Spring Boot 基础包) -->
        <repository>
            <id>central</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
    </repositories>

整体内容:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ech</groupId>
    <artifactId>mcp-server</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>17</java.version>
        <snapshot.version>-SNAPSHOT</snapshot.version>
        <release.version>.RELEASE</release.version>
    </properties>
    <repositories>
        <!-- 1. Spring 官方快照仓库:存储 Spring AI 快照版本 -->
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled> <!-- 关闭正式版下载,只拉快照 -->
            </releases>
        </repository>
        <!-- 2. Sonatype 中央快照仓库:补充快照依赖 -->
        <repository>
            <name>Central Portal Snapshots</name>
            <id>central-portal-snapshots</id>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled> <!-- 开启快照版下载 -->
            </snapshots>
        </repository>
        <!-- 3. 保留 Maven 中央仓库:拉取非快照依赖(如 Spring Boot 基础包) -->
        <repository>
            <id>central</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
    </repositories>
    <!-- 版本管理,不会实际引入依赖 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.1.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>3.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
            <version>2.18.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.11.5</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.36</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.57</version>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>com.ech</groupId>-->
<!--            <artifactId>iform-api</artifactId>-->
<!--            <version>2.0.0${snapshot.version}</version>-->
<!--            <exclusions>-->
<!--                <exclusion>-->
<!--                    <groupId>io.springfox</groupId>-->
<!--                    <artifactId>*</artifactId>-->
<!--                </exclusion>-->
<!--                <exclusion>-->
<!--                    <groupId>org.springframework.cloud</groupId>-->
<!--                    <artifactId>*</artifactId>-->
<!--                </exclusion>-->
<!--            </exclusions>-->
<!--        </dependency>-->
        <!-- spring-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.5.0</version> <!-- 兼容 Maven 3.5.2 的版本 -->
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.2.0</version> <!-- 兼容 Maven 3.5.x -->
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.echronos.mcp.McpServerApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <release>17</release>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

2.修改yml配置使用Streamable HTTP协议

复制代码
spring:
  application:
    name: mcp-server
  ai:
    mcp:
      server:
        enabled: true
        name: management-server
        version: 1.0.0
        type: SYNC
#        sse-message-endpoint: /mcp/message
        request-timeout: 120s
        protocol: STREAMABLE
        streamable-http:
          mcp-endpoint: /api/mcp
          keep-alive-interval: 30s
  mvc:
    async:
      request-timeout: 180000
server:
  port: 8090
  tomcat:
    connection-timeout: 180000
复制代码
mcp-endpoint: 定义请求的端点
复制代码
keep-alive-interval: 连接保持活动间隔(定义这个服务端会定时ping客户端保持sse链接)

之前在文章中使用了两种塞入token的方式

1.在建立sse链接时塞入token。

2.使用拦截器参入token。

使用这两种方式时都会有问题线程复用的问题,原因是使用工具实际执行的线程并不是Tomcat的线程池而是Reactor包中定义的线程池,这就导致了线程服用时获取不到上下文了,而现在使用spring AI 的Streamable HTTP协议时工具执行使用的就是Tomcat的线程池的线程池中的线程并不存在上下文的问题,所以并不需要做ReactorConfig的适配工作了,可以将ReactorConfig注释掉。

注:按照Streamable HTTP协议的原理来看还是会有sse执行工具的情况,但我根据spring AI的描述和自己调试的情况spring AI的工具执行都相当于几个Http请求进组合而形成的,如果有错误后面会进行更新。

3.修改传递token的拦截器(建立sse链接时塞入token已经弃用)

复制代码
package com.echronos.mcp.filter;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import com.echronos.mcp.model.AppThreadLocal;
import com.echronos.mcp.model.HeaderModel;
import com.echronos.mcp.model.RepeatableReadRequestWrapper;
import com.echronos.mcp.model.SharedContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.websocket.server.ServerEndpointConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.tomcat.websocket.server.UpgradeUtil;
import org.apache.tomcat.websocket.server.WsServerContainer;
import org.slf4j.MDC;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

@Slf4j
public class WsFilter extends GenericFilter {
    private static final long serialVersionUID = 1L;
    private transient WsServerContainer sc;
    // ✅ 手动创建静态 ObjectMapper 实例
    private static final ObjectMapper objectMapper = new ObjectMapper();
    public WsFilter() {
    }

    public void init() throws ServletException {
        this.sc = (WsServerContainer)this.getServletContext().getAttribute("jakarta.websocket.server.ServerContainer");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 包装原始请求(支持多次读取body)
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        RepeatableReadRequestWrapper wrappedRequest = new RepeatableReadRequestWrapper(httpRequest);
        if (wrappedRequest.getRequestURI().equals("/api/mcp")){

            String requestBody = IOUtils.toString(wrappedRequest.getInputStream(), wrappedRequest.getCharacterEncoding());
            log.info("requestBody:"+requestBody);
            ObjectMapper mapper = new ObjectMapper();
            JsonNode rootNode = mapper.readTree(requestBody);

            JsonNode methodNode = rootNode.get("method");
            if (methodNode != null){
                if (methodNode.asText().equals("tools/call")){
//                String traceId = rootNode.get("params").get("arguments").get("toolContext").get("X-B3-Traceid").asText();
//                if (StringUtils.isNotBlank(traceId)){
//                    AppThreadLocal.setTraceId(traceId);
//                    MDC.put("X-B3-TraceId", traceId);
//                    MDC.put("X-B3-TraceId", traceId);
//                }
//                String token = rootNode.get("params").get("arguments").get("toolContext").get("Authorization").asText();
//                if (StringUtils.isNotBlank(token)){
//                    AppThreadLocal.setToken(token);
//                    log.info("token: " + token);
//                }
                    wrappedRequest.getHeader("Authorization");
                    AppThreadLocal.setToken(wrappedRequest.getHeader("Authorization"));
                }
            }
        }
        // 打印请求基本信息
        logRequestInfo(wrappedRequest);

        // 使用反射调用 areEndpointsRegistered 方法
        Method method = null;
        boolean endpointsRegistered;
        try {
            method = WsServerContainer.class.getDeclaredMethod("areEndpointsRegistered");
            method.setAccessible(true);
            endpointsRegistered = (boolean) method.invoke(this.sc);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (endpointsRegistered && UpgradeUtil.isWebSocketUpgradeRequest(wrappedRequest, response)) {
            HttpServletRequest req = wrappedRequest;
            HttpServletResponse resp = (HttpServletResponse)response;
            String pathInfo = req.getPathInfo();
            String path;
            if (pathInfo == null) {
                path = req.getServletPath();
            } else {
                String var10000 = req.getServletPath();
                path = var10000 + pathInfo;
            }

            Object mappingResult = this.sc.findMapping(path);
            if (mappingResult == null) {
                chain.doFilter(wrappedRequest, response);
            } else {
                 WsMappingResult newMappingResult = (WsMappingResult)mappingResult;
                UpgradeUtil.doUpgrade(this.sc, req, resp, newMappingResult.getConfig(), newMappingResult.getPathParams());
            }
        } else {
            chain.doFilter(wrappedRequest, response);
        }
    }

    /**
     * 打印请求参数和请求体
     */
    private void logRequestInfo(RepeatableReadRequestWrapper  request) {
        try {
            // 1. 记录基础信息
            log.info("请求 {} {}", request.getMethod(), request.getRequestURI());
            log.info("request:{}", request);
            if (request.getRequestURI().equals("/mcp/message")) {
                String requestBody = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
                //处理请求体中的敏感信息
                String sanitizedBody = sanitizeRequestBody(requestBody);
                log.info("请求体: {}", sanitizedBody);
            }
            // 2. 记录URL查询参数 (如 ?name=abc&age=20)
            Map<String, String[]> urlParams = request.getParameterMap();
            if (!urlParams.isEmpty()) {
                log.info("URL参数: {}", formatParams(urlParams));
            }
        } catch (Exception e) {
            log.error("记录请求信息失败", e);
        }
    }
    private String sanitizeRequestBody(String json) {
        try {
            JsonNode rootNode = objectMapper.readTree(json);

            if (rootNode.isObject()) {
                ObjectNode objectNode = (ObjectNode) rootNode;

                // 脱敏 Authorization 字段
                if (objectNode.has("params") && objectNode.get("params").isObject()) {
                    JsonNode paramsNode = objectNode.get("params");

                    if (paramsNode.has("toolContext") && paramsNode.get("toolContext").isObject()) {
                        JsonNode toolContextNode = paramsNode.get("toolContext");
                        if (toolContextNode.has("Authorization")) {
                            ((ObjectNode) toolContextNode).put("Authorization", "bearer <hidden>");
                        }
                    }
                }

                return objectMapper.writeValueAsString(objectNode);
            }

            return json; // 如果不是对象,返回原内容
        } catch (Exception e) {
            return json; // 出错时返回原始内容
        }
    }
    // 格式化参数输出
    private String formatParams(Map<String, String[]> params) {
        StringBuilder sb = new StringBuilder();
        params.forEach((key, values) -> {
            sb.append(key).append("=");
            sb.append(values.length == 1 ? values[0] : Arrays.toString(values));
            sb.append(", ");
        });
        return sb.length() > 0 ? sb.substring(0, sb.length() - 2) : "";
    }

    class WsMappingResult {
        private final ServerEndpointConfig config;
        private final Map<String, String> pathParams;

        WsMappingResult(ServerEndpointConfig config, Map<String, String> pathParams) {
            this.config = config;
            this.pathParams = pathParams;
        }

        ServerEndpointConfig getConfig() {
            return this.config;
        }

        Map<String, String> getPathParams() {
            return this.pathParams;
        }
    }
}

现在可以找客户端试一试

复制代码
    @Tool(name = "getWeater",description = "获取天气信息")
    public String getWeather(){
        log.info("获取天气信息");
        String token = AppThreadLocal.getToken();
        log.info("token:"+token);
        return "当地天气400度";
    }
相关推荐
m0_651593915 分钟前
Netty网络架构与Reactor模式深度解析
网络·架构
大面积秃头44 分钟前
Http基础协议和解析
网络·网络协议·http
我也要当昏君2 小时前
6.3 文件传输协议 (答案见原书 P277)
网络
Greedy Alg3 小时前
Socket编程学习记录
网络·websocket·学习
刘逸潇20054 小时前
FastAPI(二)——请求与响应
网络·python·fastapi
软件技术员4 小时前
使用ACME自动签发SSL 证书
服务器·网络协议·ssl
我也要当昏君4 小时前
6.4 电子邮件 (答案见原书 P284)
网络协议
Mongnewer4 小时前
通过虚拟串口和网络UDP进行数据收发的Delphi7, Lazarus, VB6和VisualFreeBasic实践
网络
我也要当昏君5 小时前
6.5 万维网(答案见原书P294)
网络
嶔某5 小时前
网络:传输层协议UDP和TCP
网络·tcp/ip·udp