Java来实现一个获取实时天气信息的MCP服务端

作为一名Java开发者,近期在啃AI大模型开发,先后学习了Spring AI以及LangChain4J,目前也算是入门了。在学习过程中了解到MCP这个技术,MCP也出了Java的SDK,顺便学习一下如果自己开发一个MCP服务端大概是什么流程,然后根据MCP官方文档来自己写了一个基于天气API的MCP服务端

准备工作

开始开发

创建SpringBoot工程,主要依赖如下:

<properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
        <spring-boot.version>3.4.3</spring-boot.version>
    </properties>
    <dependencies>
        <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>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.36</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

配置application.yml

其中最重要的就是logging.pattern.console必须是空,spring.main.banner-mode必须是off

weather:
  api:
    api-key: your-api-key
spring:
  application:
    name: mcp-server-weather
  ai:
    mcp:
      server:
        name: ${spring.application.name}
        version: 0.0.1

  main:
    banner-mode: off
    web-application-type: none

logging:
  pattern:
    console:
  file:
    name: D://${spring.application.name}.log
server:
  servlet:
    encoding:
      charset: UTF-8
      force: true
      enabled: true

天气配置Properties

@ConfigurationProperties(prefix = "weather.api")
@Data
@Component
public class WeatherApiProperties {
    private String apiKey;
}

编写WeatherServer.java


@Slf4j
@Service
public class WeatherService {

    @Resource
    private WeatherApiProperties weatherApiProperties;

    @Tool(description = "获取某个城市的实时天气")
    public WeatherFunctionResponse getWeather(WeatherFunctionRequest request) {
        String city = request.getCity();
        log.info("开始获取天气,城市:{}",city);
        // 先调用城市搜索接口,查询到该城市的locationId
        String citySearchApiUrl = "https://geoapi.qweather.com/v2/city/lookup";
        @Cleanup
        HttpResponse citySearchHttpResponse = HttpUtil.createGet(citySearchApiUrl)
                .header("Content-Type", "application/json")
                .header("X-QW-Api-Key", weatherApiProperties.getApiKey())
                .form("location", city)
                .execute();

        String cityResponseBody = citySearchHttpResponse.body();
        log.info("城市搜索接口返回结果:{}", cityResponseBody);
        String locationId = JSONUtil.getByPath(JSONUtil.parseObj(cityResponseBody), "$.location[0].id", null);

        log.info("城市的locationId为:{}", locationId);
        // 再调用天气接口,获取天气信息
        @Cleanup
        HttpResponse weatherHttpResponse = HttpUtil.createGet("https://devapi.qweather.com/v7/weather/now")
                .header("Content-Type", "application/json")
                .header("X-QW-Api-Key", weatherApiProperties.getApiKey())
                .form("location", locationId)
                .form("lang", "zh")
                .execute();
        String body = weatherHttpResponse.body();
        log.info("天气接口返回结果:{}", body);
        JSONObject jsonObject = JSONUtil.parseObj(body);
        return jsonObject.getJSONObject("now").toBean(WeatherFunctionResponse.class);
    }

配置工具

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

到这里,服务端开发工作就完成了,接下来将这个项目打包
mvn clean package -Dmaven.test.skip=true
最后会打包成一个jar包,稍后会使用到


客户端使用

这里我用LangChain4J作为客户端来使用,使用最新的版本为1.0.0-beta1

引入LangChain4J相关依赖

     <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
    </dependency>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai</artifactId>
    </dependency>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-mcp</artifactId>
    </dependency>

新增AiAssistant助手

public interface AiAssistant {

    String chat(String message);

}

新增LLMConfig配置类


    /**
     * 阿里云的模型
     *
     * @return
     */
    @Bean
    public ChatLanguageModel chatLanguageModel() {
        return OpenAiChatModel.builder()
                .apiKey(System.getenv("AI_DASHSCOPE_API_KEY"))
                .modelName("qwen-turbo")
                .logRequests(true)
                .logResponses(true)
                .baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
                .build();
    }
    /**
     * 初始化MCP Client
     */
    @Bean
    public McpClient mcpClientWeather() {
        return new DefaultMcpClient.Builder()
                .transport(new StdioMcpTransport.Builder()
                        .command(List.of(
                                "java",
                                "-Dspring.ai.mcp.server.stdio=true", 
                                "-jar", 
                                "mcp-server-weather-0.0.1-SNAPSHOT.jar", 
                                "--weather.api.api-key=%s".formatted(System.getenv("HEFENG_WEATHER_API_KEY"))))
                        .logEvents(true) // only if you want to see the traffic in the log
                        .build())
                .build();
    }

    /**
     * 使用LangChain4J的高级API来构建一个AI助手,注入MCP Client
     * @param mcpClientWeather
     * @return
     */
    @Bean
    public AiAssistant aiAssistant(McpClient mcpClientWeather) {
        ToolProvider toolProvider = McpToolProvider.builder()
                .mcpClients(List.of(mcpClientWeather))
                .build();
        return AiServices.builder(AiAssistant.class)
                .chatLanguageModel(chatLanguageModel())
                .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
                .toolProvider(toolProvider)
                .build();
    }

测试

   
    @Resource
    private AiAssistant aiAssistant;

    @Test
    public void testWeather1(){
        System.out.println(aiAssistant.chat("今天重庆的天气怎么样?"));
        /**
         * AI回复以下内容:
         * 
         *
         * 今天重庆的天气情况如下:
         - 天气状况:阴
         - 气温:18℃
         - 体感温度:16℃
         - 风向:东风
         - 风力:2级
         - 湿度:47%
         - 降水量:0.0mm
         - 空气压力:980hPa
         - 能见度:9km
         - 云量:91% 
         */
    }

理论上在Claude上也可以使用,我没有测试:

{
  "mcpServers": {
    "mcp-server-weather": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        "你的jar包路径",
        "--weather.api.api-key=YOUR API KEY"
      ]
    }
  }
}

服务端源码

GitHub源码地址

一个bug

我在测试过程中,发现如果客户端给服务端传递的参数是中文,则服务端接受到的参数是乱码,例如我的问题是请问重庆的天气怎么样?,服务端接受到的城市参数就会乱码。我翻看了一些资料也没有找到解决方法,如果有知道如何解决的佬友请不吝赐教。

我目前的解决方案是给参数设置了一个描述:城市名称,如果是中文汉字请先转换为汉语拼音,例如北京:beijing,这样服务端接受到的是一个拼音,天气API也能正常使用。

写在最后

本人也是在前期摸索阶段,如有错误,请各位佬提示,我会尽快修改。

112 个赞

太强了!

6 个赞

MCP是趋势啊,除了官方文档,佬友在哪看MCP的教程啊?

12 个赞

大佬太强了!

6 个赞

不错 可以的

7 个赞

目前是跟着MCP官网学习,第一次了解到是在学习SpringAI的时候,在SpringAI的文档上了解到的

6 个赞

实践动手能力太棒了

6 个赞

学习一下

5 个赞

佬友动手能力很强,支持!

4 个赞

字符集不是UTF-8吧

3 个赞

感谢大佬分享,学习一下

3 个赞

很好很强大

3 个赞

牛哇牛哇

2 个赞

mark一下,马上学习

1 个赞

感谢分享,找机会学习一下

1 个赞

666,学习一下

1 个赞

mark,学习一下

1 个赞

就是编码问题,就是不晓得在哪里设置,也翻了源码 :joy:

1 个赞

佬,我还是有点弄不明白,跟大模型聊天的时候怎么才能命中MCP调用服务,比如我问今天北京天气怎么样,它是怎么知道调相应的MCP天气服务呢。。还是说每次跟大模型交互都会调用一次服务,由大模型自行取舍

1 个赞

支持分享。在摸索中学习,加油

1 个赞