大数跨境
0
0

AGI|告别Python依赖!用Java也能玩转MCP Server

AGI|告别Python依赖!用Java也能玩转MCP Server AI实践工程院
2025-09-17
0
导读:Java也能玩转MCP!

Java也能玩转MCP!


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


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


Part1

什么是MCP 


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


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

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

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

3.基于FastMCP 2.0 开发MCP Server



Part2

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 客户端和服务端程序。 



Part3

使用Spring AI构建mcp server


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


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


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


环境要求


•JDK17+

•pring Boot 3.0.0+


基于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
        version0.0.1


实现mcp工具


@Service
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 -> "晴朗";
            case123 -> "多云";
            case4548 -> "雾";
            case515355 -> "毛毛雨";
            case5657 -> "冻雨";
            case616365 -> "雨";
            case6667 -> "冻雨";
            case717375 -> "雪";
            case77 -> "雪粒";
            case808182 -> "阵雨";
            case8586 -> "阵雪";
            case95 -> "雷暴";
            case9699 -> "雷暴伴有冰雹";
            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.9042116.4074));
        // 北京空气质量(模拟数据)
        System.out.println(client.getAirQuality(39.9042116.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客户端使用。


基于sse方式构建


添加依赖


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


配置项目yaml


server:
  port8080  # 服务器端口配置

spring:
  ai:
    mcp:
      server:
        name: my-weather-server # MCP服务器名称
        version0.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 启动。


Part4

如何使用Spring AI构建的mcp client


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


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工具


验证是否可以调用成功




基于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: ${DASH_SCOPE_API_KEY}
    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方法中,就可以自动的适配。



Part5

结语


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




参考:

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





作者

肖泉|AI开发工程师

bug不一定真是bug,也有可能是新的思路



【延伸探索】🔮

AI 不是黑箱魔法,而是可拆解的工程!


✨ 关注 神州数码云基地

✨ 星标公众号,解锁:


▸ 神州问学使用指南

▸ 企业级 AI 场景落地避坑指南

▸ AI 技术落地实战

▸ AI 行业前沿资讯



- END -


往期精选




基于React DnD + 虚拟化技术的文本分段可视化




分布式推理框架--Nvidia Dynamo




Open Deep Research智能生成全流程


了解云基地,就现在!

【声明】内容源于网络
0
0
AI实践工程院
我们致力于用数字技术重构企业价值,助力企业实现数字化转型升级。
内容 325
粉丝 0
AI实践工程院 我们致力于用数字技术重构企业价值,助力企业实现数字化转型升级。
总阅读2
粉丝0
内容325