目前主流 MCP Server 的开发语言多为 Python,但凭借 Java 成熟且强大的生态体系,在实际业务开发场景中,绝大多数业务的后端开发仍以 Java 为主。Spring AI 提供了对 MCP 的支持实现,这极大地方便了原有 Java 应用对 MCP 的接入。

本文直接上干货,来实践使用Java构建mcp server和mcp client。

目录

一、什么是MCP

二、Spring AI 支持 MCP 实现

三、 使用Spring AI构建mcp server

(1)基于stdio的方式构建

(2)基于sse方式构建

四、如何使用Spring AI构建的mcp client

(1)Claude Desktop使用以上构建的mcp服务

(2)基于Spring AI Alibaba以stdio方式集成mcp client

五、结语

一、什么是MCP

MCP(Model Context Protocol,模型上下文协议)是一种标准化的通信协议,旨在连接 AI 模型与工具链,提供统一的接口以支持动态工具调用、资源管理、对话状态同步等功能。它允许开发者构建灵活的 AI 应用程序,与不同的模型和工具进行交互,同时保持协议的可扩展性和跨语言兼容性。

*感兴趣的伙伴欢迎查看本专栏关于MCP的系列文章:

1.爆火的MCP背后,为何被称作 AI 模型的“万能适配器”?

2.详解Google A2A协议,谁才是Agent的未来标准?

3.基于FastMCP 2.0 开发MCP Serve

二、Spring AI 支持 MCP 实现

Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互,支持同步和异步通信模式。整体架构如下:

Spring AI MCP 采用模块化架构,包括以下组件:

•Spring AI 应用程序:使用 Spring AI 框架构建想要通过 MCP 访问数据的生成式 AI 应用程序

•Spring MCP 客户端:MCP 协议的 Spring AI 实现,与服务器保持 1:1 连接

通过 Spring AI MCP,可以快速搭建 MCP 客户端和服务端程序。

三、 使用Spring AI构建mcp server

Spring AI 提供了两种机制快速搭建 MCP Server,通过这两种方式开发者可以快速向 AI 应用开放自身的能力,这两种机制如下:

•基于 stdio 的进程间通信传输,以独立的进程运行在 AI 应用本地,适用于比较轻量级的工具。

•基于 SSE(Server-Sent Events) 进行远程服务访问,需要将服务单独部署,客户端通过服务端的 URL 进行远程访问,适用于比较重量级的工具。

环境要求

•JDK17+

•pring Boot 3.0.0+

(1)基于stdio的方式构建

添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

    </dependencies>

配置项目yaml

spring:
  main:
    web-application-type: none
    banner-mode: off
  ai:
    mcp:
      server:
        name: mcp-weather-server
        version: 0.0.1

实现mcp工具

@srvice
publicclass OpenMeteoService {

    // OpenMeteo免费天气API基础URL
    privatestaticfinal String BASE_URL = "https://api.open-meteo.com/v1";

    privatefinal RestClient restClient;

    public OpenMeteoService() {
        this.restClient = RestClient.builder()
        .baseUrl(BASE_URL)
        .defaultHeader("Accept", "application/json")
        .defaultHeader("User-Agent", "OpenMeteoClient/1.0")
        .build();
    }

    // OpenMeteo天气数据模型
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record WeatherData(
        @JsonProperty("latitude") Double latitude,
        @JsonProperty("longitude") Double longitude,
        @JsonProperty("timezone") String timezone,
        @JsonProperty("current") CurrentWeather current,
        @JsonProperty("daily") DailyForecast daily,
        @JsonProperty("current_units") CurrentUnits currentUnits) {

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentWeather(
            @JsonProperty("time") String time,
            @JsonProperty("temperature_2m") Double temperature,
            @JsonProperty("apparent_temperature") Double feelsLike,
            @JsonProperty("relative_humidity_2m") Integer humidity,
            @JsonProperty("precipitation") Double precipitation,
            @JsonProperty("weather_code") Integer weatherCode,
            @JsonProperty("wind_speed_10m") Double windSpeed,
            @JsonProperty("wind_direction_10m") Integer windDirection) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record CurrentUnits(
            @JsonProperty("time") String timeUnit,
            @JsonProperty("temperature_2m") String temperatureUnit,
            @JsonProperty("relative_humidity_2m") String humidityUnit,
            @JsonProperty("wind_speed_10m") String windSpeedUnit) {
        }

        @JsonIgnoreProperties(ignoreUnknown = true)
        public record DailyForecast(
            @JsonProperty("time") List<String> time,
            @JsonProperty("temperature_2m_max") List<Double> tempMax,
            @JsonProperty("temperature_2m_min") List<Double> tempMin,
            @JsonProperty("precipitation_sum") List<Double> precipitationSum,
            @JsonProperty("weather_code") List<Integer> weatherCode,
            @JsonProperty("wind_speed_10m_max") List<Double> windSpeedMax,
            @JsonProperty("wind_direction_10m_dominant") List<Integer> windDirection) {
        }
    }

    /**
     * 获取天气代码对应的描述
     */
    private String getWeatherDescription(int code) {
        returnswitch (code) {
            case0 -> "晴朗";
            case1, 2, 3 -> "多云";
            case45, 48 -> "雾";
            case51, 53, 55 -> "毛毛雨";
            case56, 57 -> "冻雨";
            case61, 63, 65 -> "雨";
            case66, 67 -> "冻雨";
            case71, 73, 75 -> "雪";
            case77 -> "雪粒";
            case80, 81, 82 -> "阵雨";
            case85, 86 -> "阵雪";
            case95 -> "雷暴";
            case96, 99 -> "雷暴伴有冰雹";
            default -> "未知天气";
        };
    }

    /**
     * 获取风向描述
     */
    private String getWindDirection(int degrees) {
        if (degrees >= 337.5 || degrees < 22.5)
            return"北风";
        if (degrees >= 22.5 && degrees < 67.5)
            return"东北风";
        if (degrees >= 67.5 && degrees < 112.5)
            return"东风";
        if (degrees >= 112.5 && degrees < 157.5)
            return"东南风";
        if (degrees >= 157.5 && degrees < 202.5)
            return"南风";
        if (degrees >= 202.5 && degrees < 247.5)
            return"西南风";
        if (degrees >= 247.5 && degrees < 292.5)
            return"西风";
        return"西北风";
    }

    /**
     * 获取指定经纬度的天气预报
     *
     * @param latitude 纬度
     * @param longitude 经度
     * @return 指定位置的天气预报
     * @throws RestClientException 如果请求失败
     */
    @Tool(description = "获取指定经纬度的天气预报")
    public String getWeatherForecastByLocation(double latitude, double longitude) {
        // 获取天气数据(当前和未来7天)
        var weatherData = restClient.get()
                .uri("/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m,wind_direction_10m&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code,wind_speed_10m_max,wind_direction_10m_dominant&timezone=auto&forecast_days=7",
                        latitude, longitude)
                .retrieve()
                .body(WeatherData.class);

        // 拼接天气信息
        StringBuilder weatherInfo = new StringBuilder();

        // 添加当前天气信息
        WeatherData.CurrentWeather current = weatherData.current();
        String temperatureUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().temperatureUnit()
                : "°C";
        String windSpeedUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().windSpeedUnit() : "km/h";
        String humidityUnit = weatherData.currentUnits() != null ? weatherData.currentUnits().humidityUnit() : "%";

        weatherInfo.append(String.format("""
                当前天气:
                温度: %.1f%s (体感温度: %.1f%s)
                天气: %s
                风向: %s (%.1f %s)
                湿度: %d%s
                降水量: %.1f 毫米

                """,
                current.temperature(),
                temperatureUnit,
                current.feelsLike(),
                temperatureUnit,
                getWeatherDescription(current.weatherCode()),
                getWindDirection(current.windDirection()),
                current.windSpeed(),
                windSpeedUnit,
                current.humidity(),
                humidityUnit,
                current.precipitation()));

        // 添加未来天气预报
        weatherInfo.append("未来天气预报:\n");
        WeatherData.DailyForecast daily = weatherData.daily();

        for (int i = 0; i < daily.time().size(); i++) {
            String date = daily.time().get(i);
            double tempMin = daily.tempMin().get(i);
            double tempMax = daily.tempMax().get(i);
            int weatherCode = daily.weatherCode().get(i);
            double windSpeed = daily.windSpeedMax().get(i);
            int windDir = daily.windDirection().get(i);
            double precip = daily.precipitationSum().get(i);

            // 格式化日期
            LocalDate localDate = LocalDate.parse(date);
            String formattedDate = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd (EEE)"));

            weatherInfo.append(String.format("""
                    %s:
                    温度: %.1f%s ~ %.1f%s
                    天气: %s
                    风向: %s (%.1f %s)
                    降水量: %.1f 毫米

                    """,
                    formattedDate,
                    tempMin, temperatureUnit,
                    tempMax, temperatureUnit,
                    getWeatherDescription(weatherCode),
                    getWindDirection(windDir),
                    windSpeed, windSpeedUnit,
                    precip));
        }

        return weatherInfo.toString();
    }

    /**
     * 获取指定位置的空气质量信息 (使用备用模拟数据)
     * 注意:由于OpenMeteo的空气质量API可能需要额外配置或不可用,这里提供备用数据
     *
     * @param latitude 纬度
     * @param longitude 经度
     * @return 空气质量信息
     */
    @Tool(description = "获取指定位置的空气质量信息(模拟数据)")
    public String getAirQuality(@ToolParam(description = "纬度") double latitude,
                                @ToolParam(description = "经度") double longitude) {

        try {
            // 从天气数据中获取基本信息
            var weatherData = restClient.get()
                    .uri("/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m&timezone=auto",
                            latitude, longitude)
                    .retrieve()
                    .body(WeatherData.class);

            // 模拟空气质量数据 - 实际情况下应该从真实API获取
            // 根据经纬度生成一些随机但相对合理的数据
            int europeanAqi = (int) (Math.random() * 100) + 1;
            int usAqi = (int) (europeanAqi * 1.5);
            double pm10 = Math.random() * 50 + 5;
            double pm25 = Math.random() * 25 + 2;
            double co = Math.random() * 500 + 100;
            double no2 = Math.random() * 40 + 5;
            double so2 = Math.random() * 20 + 1;
            double o3 = Math.random() * 80 + 20;

            // 根据AQI评估空气质量等级
            String europeanAqiLevel = getAqiLevel(europeanAqi);
            String usAqiLevel = getUsAqiLevel(usAqi);

            return String.format("""
                    空气质量信息(模拟数据):

                    位置: 纬度 %.4f, 经度 %.4f
                    欧洲空气质量指数: %d (%s)
                    美国空气质量指数: %d (%s)
                    PM10: %.1f μg/m³
                    PM2.5: %.1f μg/m³
                    一氧化碳(CO): %.1f μg/m³
                    二氧化氮(NO2): %.1f μg/m³
                    二氧化硫(SO2): %.1f μg/m³
                    臭氧(O3): %.1f μg/m³

                    数据更新时间: %s

                    注意: 由于OpenMeteo空气质量API限制,此处显示模拟数据,仅供参考。
                    """,
                    latitude, longitude,
                    europeanAqi, europeanAqiLevel,
                    usAqi, usAqiLevel,
                    pm10,
                    pm25,
                    co,
                    no2,
                    so2,
                    o3,
                    weatherData.current().time());
        } catch (Exception e) {
            // 如果获取基本天气数据失败,返回完全模拟的数据
            return String.format("""
                    空气质量信息(完全模拟数据):

                    位置: 纬度 %.4f, 经度 %.4f
                    欧洲空气质量指数: %d (%s)
                    美国空气质量指数: %d (%s)
                    PM10: %.1f μg/m³
                    PM2.5: %.1f μg/m³
                    一氧化碳(CO): %.1f μg/m³
                    二氧化氮(NO2): %.1f μg/m³
                    二氧化硫(SO2): %.1f μg/m³
                    臭氧(O3): %.1f μg/m³

                    注意: 由于API限制,此处显示完全模拟数据,仅供参考。
                    """,
                    latitude, longitude,
                    50, getAqiLevel(50),
                    75, getUsAqiLevel(75),
                    25.0,
                    15.0,
                    300.0,
                    20.0,
                    5.0,
                    40.0);
        }
    }

    /**
     * 获取欧洲空气质量指数等级
     */
    private String getAqiLevel(Integer aqi) {
        if (aqi == null)
            return"未知";

        if (aqi <= 20)
            return"优";
        elseif (aqi <= 40)
            return"良";
        elseif (aqi <= 60)
            return"中等";
        elseif (aqi <= 80)
            return"较差";
        elseif (aqi <= 100)
            return"差";
        else
            return"极差";
    }

    /**
     * 获取美国空气质量指数等级
     */
    private String getUsAqiLevel(Integer aqi) {
        if (aqi == null)
            return"未知";

        if (aqi <= 50)
            return"优";
        elseif (aqi <= 100)
            return"中等";
        elseif (aqi <= 150)
            return"对敏感人群不健康";
        elseif (aqi <= 200)
            return"不健康";
        elseif (aqi <= 300)
            return"非常不健康";
        else
            return"危险";
    }

    publicstatic void main(String[] args) {
        OpenMeteoService client = new OpenMeteoService();
        // 北京坐标
        System.out.println(client.getWeatherForecastByLocation(39.9042, 116.4074));
        // 北京空气质量(模拟数据)
        System.out.println(client.getAirQuality(39.9042, 116.4074));
    }
}

注册mcp工具

@SpringBootApplication
public class McpWeatherServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpWeatherServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
        return MethodToolCallbackProvider.builder().toolObjects(openMeteoService).build();
    }

}

打包

mvn clean package -DskipTests

将项目打包后可以供mcp客户端使用。

(2)基于sse方式构建

添加依赖

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
</dependency>

配置项目yaml

server:
  port: 8080  # 服务器端口配置

spring:
  ai:
    mcp:
      server:
        name: my-weather-server # MCP服务器名称
        version: 0.0.1            # 服务器版本号

实现mcp工具

@Service
publicclass OpenMeteoService {

    private final WebClient webClient;

    public OpenMeteoService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
                .baseUrl("https://api.open-meteo.com/v1")
                .build();
    }

    @Tool(description = "根据经纬度获取天气预报")
    publicString getWeatherForecastByLocation(
            @ToolParameter(description = "纬度,例如:39.9042") String latitude,
            @ToolParameter(description = "经度,例如:116.4074") String longitude) {

        try {
            String response = webClient.get()
                    .uri(uriBuilder -> uriBuilder
                            .path("/forecast")
                            .queryParam("latitude", latitude)
                            .queryParam("longitude", longitude)
                            .queryParam("current", "temperature_2m,wind_speed_10m")
                            .queryParam("timezone", "auto")
                            .build())
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            // 解析响应并返回格式化的天气信息
            return"当前位置(纬度:" + latitude + ",经度:" + longitude + ")的天气信息:\n" + response;
        } catch (Exception e) {
            return"获取天气信息失败:" + e.getMessage();
        }
    }

    @Tool(description = "根据经纬度获取空气质量信息")
    publicString getAirQuality(
            @ToolParameter(description = "纬度,例如:39.9042") String latitude,
            @ToolParameter(description = "经度,例如:116.4074") String longitude) {

        // 模拟数据,实际应用中应调用真实API
        return"当前位置(纬度:" + latitude + ",经度:" + longitude + ")的空气质量:\n" +
                "- PM2.5: 15 μg/m³ (优)\n" +
                "- PM10: 28 μg/m³ (良)\n" +
                "- 空气质量指数(AQI): 42 (优)\n" +
                "- 主要污染物: 无";
    }
}

注册mcp工具

@SpringBootApplication
publicclass McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(openMeteoService)
                .build();
    }

    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

部署服务

服务端将在 http://localhost:8080 启动。

四、如何使用Spring AI构建的mcp client

在上面我们构建好了mcp server,我们如何在客户端使用,或者如何构建mcp client。下面我们来实践应用一下。

(1)Claude Desktop使用以上构建的mcp服务

这里我们演示stdio方式

添加配置到Claude的配置文件

{
  "mcpServers": {
    "fetch": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "mcp/fetch"]
    },
    "mcp-weather-server": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-Dspring.main.web-application-type=none",
        "-Dlogging.pattern.console=",
        "-jar",
        "/Users/username/Desktop/mcp-server/mcp-weather-server-0.0.1-SNAPSHOT.jar"
      ],
      "env": {}
    }

  }
}

Claude Desktop窗口出现mcp工具

验证是否可以调用成功

(2)基于Spring AI Alibaba以stdio方式集成mcp client

添加依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>
<!-- 添加Spring AI MCP starter依赖 -->
<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>

配置yaml文件

spring:
  ai:
    dashscope:
      # 配置通义千问API密钥
      api-key:xxxxxxx
    mcp:
      client:
        stdio:
          # 指定MCP服务器配置文件路径(推荐)
          servers-configuration: classpath:/mcp-servers-config.json
          # 直接配置示例,和上边的配制二选一
          # connections:
          # server1:
          # command: java
          # args:
          # - -jar
          # - /path/to/your/mcp-server.jar

这个配置文件设置了 MCP 客户端的基本配置,包括 API 密钥和服务器配置文件的位置。你也可以选择直接在配置文件中定义服务器配置,但是还是建议使用json文件管理 mcp 配置。在resources目录下创建mcp-servers-config.json配置文件:

{
    "mcpServers": {
        // 定义名为"weather"的MCP服务器
        "weather": {
            // 指定启动命令为java
            "command": "java",
            // 定义启动参数
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dspring.main.web-application-type=none",
                "-jar",
                "<修改为stdio编译之后的jar包全路径>"
            ],
            // 环境变量配置(可选)
            "env": {}
        }
    }
}

这个 JSON 配置文件定义了 MCP 服务器的详细配置,包括如何启动服务器进程、需要传递的参数以及环境变量设置,还是要注意引用的 jar 包必须是全路径的。

编写启动类测试

@SpringBootApplication
publicclassApplication {

    public static void main(String[] args) {
        // 启动Spring Boot应用
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public CommandLineRunner predefinedQuestions(
            ChatClient.Builder chatClientBuilder,
            ToolCallbackProvider tools,
            ConfigurableApplicationContext context) {
        return args -> {
            // 构建ChatClient并注入MCP工具
            var chatClient = chatClientBuilder
                    .defaultTools(tools)
                    .build();

            // 定义用户输入
            String userInput = "北京的天气如何?";
            // 打印问题
            System.out.println("\n>>> QUESTION: " + userInput);
            // 调用LLM并打印响应
            System.out.println("\n>>> ASSISTANT: " +
                chatClient.prompt(userInput).call().content());

            // 关闭应用上下文
            context.close();
        };
    }
}

这段代码展示了如何在 Spring Boot 应用中使用 MCP 客户端。它创建了一个命令行运行器,构建了 ChatClient 并注入了 MCP 工具,然后使用这个客户端发送查询并获取响应。在 Spring AI Alibaba 中使用 Mcp 工具非常简单,只需要把ToolCallbackProvider放到chatClientBuilder的defaultTools方法中,就可以自动的适配。

五、结语

使用Spring AI 能够很方便的接入MCP,能够在我们的Java应用中很方便的引入MCP服务的工具,同时也能够将我们的现有的API包装成MCP的工具提供服务供外部调用。这样对Java强大的生态非常友好,大家可以多做一些尝试。

参考:

https://github.com/springaialibaba/spring-ai-alibaba-examples/tree/main/spring-ai-alibaba-mcp-example

作者:肖泉|AI开发工程师

版权声明:本文由神州数码云基地团队整理撰写,若转载请注明出处。

公众号搜索神州数码云基地,回复【AI】进入AI社群讨论。

Logo

更多推荐