作为一名Java开发者,近期在啃AI大模型开发,先后学习了Spring AI以及LangChain4J,目前也算是入门了。在学习过程中了解到
MCP
这个技术,MCP也出了Java的SDK,顺便学习一下如果自己开发一个MCP服务端大概是什么流程,然后根据MCP官方文档来自己写了一个基于天气API的MCP服务端
准备工作
- JDK17+
- 一个和风天气的API-KEY,申请地址https://id.qweather.com/#/login
开始开发
创建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"
]
}
}
}
服务端源码
一个bug
我在测试过程中,发现如果客户端给服务端传递的参数是中文,则服务端接受到的参数是乱码,例如我的问题是
请问重庆的天气怎么样?
,服务端接受到的城市参数就会乱码。我翻看了一些资料也没有找到解决方法,如果有知道如何解决的佬友请不吝赐教。
我目前的解决方案是给参数设置了一个描述:
城市名称,如果是中文汉字请先转换为汉语拼音,例如北京:beijing
,这样服务端接受到的是一个拼音,天气API也能正常使用。
写在最后
本人也是在前期摸索阶段,如有错误,请各位佬提示,我会尽快修改。