Streamable HTTP

发布于:2025-09-04 ⋅ 阅读:(21) ⋅ 点赞:(0)

        之前在博客中使用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 用于后续请求中标识会话


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

        所有消息通过 HTTP POST 请求发送到  /mcp 端点
        如果有会话 ID,则包含在请求中


3. 服务器响应方式:

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


4. 主动建立 SSE 流:

        客户端可发送 GET 请求到  /mcp 端点主动建立 SSE 流
        服务器可通过该流推送通知或请求


5. 连接恢复:

        连接中断时,客户端可使用之前的会话 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度";
    }


网站公告

今日签到

点亮在社区的每一天
去签到