使用Spring AI 进行MCP开发
由于我的server都是发一个http请求所以创建一个BaseServcie。(2)在mcp客户端头部携带token请求时,将token塞到本地线程中。这是用来注册tool的,下面是我在实际开发的一个例子。(1)创建本地线程类。
·
快速构建一个基于Spring AI 的mcp Server
1.创建一个spring3..x.x程序


2. 引入相关依赖
<?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.ygl</groupId>
<artifactId>mcp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp</name>
<description>mcp</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</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>
<!-- spring-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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>
3.建立处理携带token的问题
(1)创建本地线程类
package com.echronos.mcp.model;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 本地线程
* @author
* @date 2025/4/15
*/
public class AppThreadLocal {
private AppThreadLocal(){
}
private static final ThreadLocal<HeaderModel> LOCAL = new TransmittableThreadLocal<>();
public static void set(HeaderModel header){
LOCAL.set(header);
}
public static HeaderModel get(){
return LOCAL.get();
}
/**
* 设置租户ID
*/
public static void setToken(String token) {
if(null!=get()) {
get().setToken(token);
}else{
HeaderModel header = new HeaderModel();
header.setToken(token);
set(header);
}
}
/**
* 获取租户ID
*/
public static String getToken() {
if(null!=get()) {
return get().getToken();
}
return null;
}
/**
* 清除信息
*/
public static void remove(){
if(null!=get()) {
LOCAL.remove();
}
}
}
(2)在mcp客户端头部携带token请求时,将token塞到本地线程中
package com.echronos.mcp.mcp.config;
import com.echronos.mcp.model.AppThreadLocal;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.*;
@Configuration
@Slf4j
public class ReMcpWebServerAutoConfig {
@Bean
public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectProvider<ObjectMapper> objectMapperProvider, McpServerProperties serverProperties) {
ObjectMapper objectMapper = (ObjectMapper)objectMapperProvider.getIfAvailable(ObjectMapper::new);
return new WebMvcSseServerTransportProvider(objectMapper, serverProperties.getBaseUrl(), serverProperties.getSseMessageEndpoint(), serverProperties.getSseEndpoint());
}
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction().filter(new HandlerFilterFunction<ServerResponse, ServerResponse>() {
@Override
public ServerResponse filter(ServerRequest request, HandlerFunction<ServerResponse> next) throws Exception {
if ("/sse".equals(request.path())){
String token = request.headers().firstHeader("Authorization");
if (token!=null){
// log.info("Thread in filter: {}", Thread.currentThread().getId());
AppThreadLocal.setToken(token);
System.out.println(token);
}
}
return next.handle(request);
}
});
}
}
上面是在建立链接时塞入token。
还可以使用拦截器塞入token
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.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("/mcp/message")){
String requestBody = IOUtils.toString(wrappedRequest.getInputStream(), wrappedRequest.getCharacterEncoding());
System.out.printf("requestBody:"+requestBody);
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(requestBody);
String method = rootNode.get("method").asText();
if (method.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);
}
}
}
// 打印请求基本信息
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;
}
}
}
4.BaseServcie
由于我的server都是发一个http请求所以创建一个BaseServcie
package com.echronos.mcp.service;
import com.echronos.mcp.model.AppThreadLocal;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
/**
* 服务基类
* @author zhuangjf
* @date 2025/4/15
*/
public class BaseService {
@Autowired
RestTemplate restTemplate;
@Value("${gateway.host}")
String host;
@Resource
private ObjectMapper objectMapper;
public String execute(String url, HttpMethod method, Object body,Map<String,Object> query) {
HttpEntity<String> entity = null;
HttpHeaders headers = buildHeader();
if(null!=body) {
try {
entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
} else {
entity = new HttpEntity<>(headers);
}
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(host+url);
if (!CollectionUtils.isEmpty(query)) {
query.forEach(builder::queryParam);
}
// 不自动编码参数
UriComponents uriComponents = builder.build();
String result = restTemplate.exchange(uriComponents.toUriString(), method,entity,String.class).getBody();
return result;
}
private HttpHeaders buildHeader(){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, AppThreadLocal.getToken());
return headers;
}
}
5.tool注册配置类
这是用来注册tool的,下面是我在实际开发的一个例子
package com.echronos.mcp.mcp.config;
import com.echronos.mcp.service.*;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MCP服务器配置类,负责注册MCP工具
*/
@Configuration
public class McpServerConfig {
/**
* 注册CRM工具
* @Author yangguanglei
* @Date 2025/6/17
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean
public ToolCallbackProvider crmTools(CrmService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
/**
* 注册Bidding工具
* @Author yangguanglei
* @Date 2025/6/17
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean
public ToolCallbackProvider BiddingTools(BiddingService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
/**
* 注册base工具
* @Author yangguanglei
* @Date 2025/6/17
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean("BaseTools")
public ToolCallbackProvider BaseTools(BaseToolService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
/**
* 注册System工具
* @Author yangguanglei
* @Date 2025/6/17
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean("SystemTools")
public ToolCallbackProvider SystemTools(SystemService systemService) {
return MethodToolCallbackProvider.builder()
.toolObjects(systemService)
.build();
}
/**
* 注册Measure工具
* @Author yangguanglei
* @Date 2025/6/17
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean("MeasureTools")
public ToolCallbackProvider MeasureTools(MeasureService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
/**
* 注册Mcs工具
* @Author yangguanglei
* @Date 2025/6/25
* @Param [service]
* @Return org.springframework.ai.tool.ToolCallbackProvider
*/
@Bean("McsTools")
public ToolCallbackProvider McsTools(McsService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
}
6.tool
package com.echronos.mcp.service;
import com.echronos.mcp.param.QueryBasicShopSkuParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import java.util.HashMap;
@Slf4j
@Service
public class McsService extends BaseService{
@Tool(name = "queryLoginCompanyBasicSkuPageList",
description = "获取当前用户公司下的商品列表")
public String queryLoginCompanyBasicSkuPageList() {
QueryBasicShopSkuParam param = new QueryBasicShopSkuParam();
log.info("获取当前用户公司下的商品列表");
return execute("/ech-mcs/v1/shop/sku/basic/by/login/company", HttpMethod.POST, param, new HashMap<>());
}
}
7.yml
spring:
application:
name: mcp-server
ai:
mcp:
server:
enabled: true
name: management-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/message
server:
port: 8090
gateway:
host: ${gateway_host:https://gate-test.myhuahua.com/}
8.客户端配置

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐

所有评论(0)