前言
最近在参考高德地图的 MCP Server 开发自己的 MCP Server

这个 MCP Server 是如何对 key 进行拦截的呢?
找了全网的教程和视频,似乎都没有找到合理的解决方案,遂翻源码
原理
在 Spring AI 自动装配的这个包下有一个叫 MpcWebMvcServerAutoConfiguration
的类, 用于自动配置 MCP Server 的 SSE 通信支持
(作者开发的 MCP Server 是基于 Web MVC 的)

@ConditionalOnClass({WebMvcSseServerTransport.class})
表明,只有当 WebMvcSseServerTransport
这个类在类路径(classpath)中存在时,当前配置类 MpcWebMvcServerAutoConfiguration
才会生效

项目引入了 spring-ai-mcp-server-webmvc-spring-boot-starter
,所以这个配置类会生效。没有引入 Web Flux
组件,所以 MpcWebFluxServerAutoConfiguration
不会生效
@ConditionalOnMissingBean({ServerMcpTransport.class})
表明,只有当 Spring 容器中没有 ServerMcpTransport
的 Bean 时才生效,防止重复定义,这为我们后续编写 McpConfig
配置类打下了基础
类中创建了两个 Bean,WebMvcSseServerTransport
和 RouterFunction
第一个 Bean 是用来建立连接和传输消息的,负责将 LLM 的响应推送到客户端
第二个 Bean 是将 /sse
暴露为 HTTP 路由的,可以在路由中添加一个过滤器进行鉴权
我们只要写一个配置类,覆盖原有的 RouterFunction
,鉴权通过后放行即可
但是配置类中并不能只返回一个 RouterFunction
,不然会导致有两个 RouterFunction
这个 Bean,所以我们要连带着 ServerMcpTransport
一起重写,这样才能完全覆盖原配置类
实践
在 config
软件包下创建 McpConfig
配置类,重写原配置类中的两个 Bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import cn.hutool.http.HttpStatus; import com.fasterxml.jackson.databind.ObjectMapper; import ink.linyang.dlut_eda_mcp.util.ApiKeyUtil; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport; import jakarta.annotation.Resource; import org.springframework.ai.autoconfigure.mcp.server.McpServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.function.*;
import java.util.Optional;
@Configuration public class McpConfig { @Bean public WebMvcSseServerTransport webMvcSseServerTransport(ObjectMapper objectMapper, McpServerProperties serverProperties) { return new WebMvcSseServerTransport(objectMapper, serverProperties.getSseMessageEndpoint()); }
@Bean public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransport transport) { return transport.getRouterFunction().filter((request, next) -> { String path = request.path(); if ("/sse".equals(path)) { Optional<String> key = request.param("key"); if (key.isPresent()) { if (isValidApiKey(key.get())) { return next.handle(request); } } return ServerResponse.status(HttpStatus.HTTP_FORBIDDEN).build(); } return next.handle(request); }); } }
|