快速构建一个基于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.客户端配置

Logo

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

更多推荐