基于 Spring AI 的 MCP Server 的 api_key 鉴权教程

前言

最近在参考高德地图的 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,WebMvcSseServerTransportRouterFunction

第一个 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())) { // 判断 key 是否有效
// 有效,放行
return next.handle(request);
}
}
// 无效,返回403
return ServerResponse.status(HttpStatus.HTTP_FORBIDDEN).build();
}
return next.handle(request);
});
}
}