Add CRM configuration and service implementation for API interactions

This commit is contained in:
bigtian 2025-06-26 14:44:47 +08:00
commit 7761645f0e
8 changed files with 746 additions and 0 deletions

View File

@ -0,0 +1,278 @@
# Spring AI MCP WebFlux 服务器示例
## 项目简介
本项目是一个基于 Spring AI 框架的 Model Context Protocol (MCP) WebFlux 服务器示例。它展示了如何构建一个支持 WebFlux 和 STDIO 两种通信方式的 MCP 服务器,提供天气查询和空气质量信息等工具服务。
## 主要功能
- 支持 WebFlux 和 STDIO 两种通信方式
- 提供天气预报查询服务(基于 OpenMeteo API
- 提供空气质量信息查询服务(模拟数据)
- 支持响应式编程模型
- 支持工具函数的动态注册和调用
## 技术栈
- Java 17+
- Spring Boot 3.x
- Spring WebFlux
- Spring AI MCP Server
- OpenMeteo API
- Maven
## 项目结构
```
starter-webflux-server/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── org/springframework/ai/mcp/sample/server/
│ │ │ ├── McpServerApplication.java # 应用程序入口
│ │ │ └── OpenMeteoService.java # 天气服务实现
│ │ └── resources/
│ │ └── application.properties # 应用配置
│ └── test/
│ └── java/
│ └── org/springframework/ai/mcp/sample/client/
│ ├── ClientStdio.java # STDIO 客户端测试
│ ├── ClientSse.java # SSE 客户端测试
│ └── SampleClient.java # 通用客户端测试
└── pom.xml # Maven 配置
```
## 核心组件
### McpServerApplication
应用程序的主入口类,负责:
- 配置 Spring Boot 应用
- 注册天气工具服务
- 初始化 MCP 服务器
### OpenMeteoService
提供两个主要工具服务:
1. `getWeatherForecastByLocation`: 获取指定位置的天气预报
- 支持当前天气和未来 7 天预报
- 包含温度、湿度、风向、降水量等信息
- 使用 OpenMeteo 免费 API
2. `getAirQuality`: 获取指定位置的空气质量信息
- 提供欧洲 AQI 和美国 AQI 两种标准
- 包含 PM10、PM2.5、CO、NO2、SO2、O3 等污染物数据
- 目前使用模拟数据(可扩展为真实 API
## 配置说明
### 服务器配置
`application.properties` 中:
```properties
# 服务器配置
spring.ai.mcp.server.name=my-weather-server
spring.ai.mcp.server.version=0.0.1
# 使用 STDIO 传输时的配置
spring.main.banner-mode=off
# logging.pattern.console=
```
### WebFlux 客户端配置
在客户端的 `application.properties` 中:
```properties
# 基本配置
server.port=8888
spring.application.name=mcp
spring.main.web-application-type=none
# API 密钥配置
spring.ai.dashscope.api-key=${AI_DASHSCOPE_API_KEY}
# SSE 连接配置
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
# 调试日志
logging.level.io.modelcontextprotocol.client=DEBUG
logging.level.io.modelcontextprotocol.spec=DEBUG
# 编码配置
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
spring.mandatory-file-encoding=UTF-8
# 用户输入配置
ai.user.input=北京的天气如何?
```
## 使用方法
### 1. 编译项目
```bash
mvn clean package -DskipTests
```
### 2. 启动服务器
#### 作为 Web 服务器启动
```bash
mvn spring-boot:run
```
服务器将在 http://localhost:8080 启动。
#### 作为 STDIO 服务器启动
```bash
java -Dspring.ai.mcp.server.stdio=true \
-Dspring.main.web-application-type=none \
-Dlogging.pattern.console= \
-jar target/mcp-starter-webflux-server-0.0.1-SNAPSHOT.jar
```
### 3. 客户端示例
#### WebFlux 客户端
##### 代码示例
代码文件为 org.springframework.ai.mcp.sample.client.ClientSse可以直接运行测试。
```java
// 配置 WebFlux 客户端
var transport = new WebFluxSseClientTransport(
WebClient.builder().baseUrl("http://localhost:8080")
);
// 创建聊天客户端
var chatClient = chatClientBuilder
.defaultTools(tools)
.build();
// 发送问题并获取回答
System.out.println("\n>>> QUESTION: " + userInput);
System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content());
```
##### mcp setting 配置示例
在 cursor、cline 等 MCP 客户端中,本示例可以使用如下配置:
```json
{
"mcpServers": {
"weather-local": {
"url": "http://localhost:8080/sse"
}
}
}
```
#### STDIO 客户端
##### 代码示例
代码文件为 org.springframework.ai.mcp.sample.client.ClientStdio可以直接运行测试。
```java
var stdioParams = ServerParameters.builder("java")
.args("-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"target/mcp-starter-webflux-server-0.0.1-SNAPSHOT.jar")
.build();
var transport = new StdioClientTransport(stdioParams);
new SampleClient(transport).run();
```
##### mcp setting 配置示例
在 cursor、cline 等 MCP 客户端中,本示例可以使用如下配置:
```json
{
"mcpServers": {
"weather-local": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"target/mcp-starter-webflux-server-0.0.1-SNAPSHOT.jar"
]
}
}
}
```
## 工具调用示例
### 获取天气预报
```java
CallToolResult weatherResult = client.callTool(
new CallToolRequest("getWeatherForecastByLocation",
Map.of("latitude", "39.9042", "longitude", "116.4074"))
);
```
### 获取空气质量信息
```java
CallToolResult airQualityResult = client.callTool(
new CallToolRequest("getAirQuality",
Map.of("latitude", "39.9042", "longitude", "116.4074"))
);
```
## 注意事项
1. OpenMeteo API 是免费的,无需 API 密钥
2. 空气质量数据目前使用模拟数据,实际应用中应替换为真实 API
3. 使用 STDIO 传输时,必须禁用控制台日志和 banner
4. 默认作为 WebFlux 服务器运行,支持 HTTP 通信
5. 客户端需要正确配置 SSE 连接 URL
6. 确保环境变量 `DASH_SCOPE_API_KEY` 已正确设置
## 扩展开发
如需添加新的工具服务:
1. 创建新的服务类
2. 使用 `@Tool` 注解标记方法
3. 在 `McpServerApplication` 中注册服务
示例:
```java
@Service
public class MyNewService {
@Tool(description = "新工具描述")
public String myNewTool(String input) {
// 实现工具逻辑
return "处理结果: " + input;
}
}
// 在 McpServerApplication 中注册
@Bean
public ToolCallbackProvider myTools(MyNewService myNewService) {
return MethodToolCallbackProvider.builder()
.toolObjects(myNewService)
.build();
}
```
## 许可证
Apache License 2.0

View File

@ -0,0 +1,61 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba.cloud.ai</groupId>
<version>${revision}</version>
<artifactId>spring-ai-alibaba-examples</artifactId>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.example</groupId>
<artifactId>mcp-webflux-server-example</artifactId>
<name>Spring AI MCP WEBFLUX server</name>
<description>Sample Spring Boot application demonstrating MCP client and server usage</description>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.57</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,106 @@
package net.rzdata.mcp.server.crm;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson2.JSONObject;
import net.rzdata.mcp.server.crm.config.CrmProperties;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CrmMcpService {
@Autowired
private CrmProperties crmProperties;
/**
* 公共的HTTP请求方法
*/
private String executeRequest(String apiPath, JSONObject requestBody) {
return HttpRequest.post(crmProperties.getApiUrl(apiPath))
.header("tenantid", crmProperties.getTenantId())
.header("Authorization", crmProperties.getToken())
.body(requestBody.toString())
.execute()
.body();
}
@Tool(description = "根据客户名称或客户编码查询客户负责人")
public String getClientResponsible(
@ToolParam(description = "客户编码",required = false) String clientCode,
@ToolParam(description = "客户名称",required = false) String clientName
) {
Assert.isTrue(StrUtil.isNotEmpty(clientCode) || StrUtil.isNotEmpty(clientName),
"客户编码和客户名称不能同时为空");
JSONObject requestBody = new JSONObject();
requestBody.put("clientCode", clientCode);
requestBody.put("clientName", clientName);
return executeRequest("ai/query/getClientResponsible", requestBody);
}
@Tool(description = "根据订单编码(即订单号)查询订单负责人")
public String getOrderSaller(
@ToolParam(description = "订单编码") String orderCode
) {
Assert.isTrue(StrUtil.isNotEmpty(orderCode), "订单编码不能为空");
JSONObject requestBody = new JSONObject();
requestBody.put("orderCode", orderCode);
return executeRequest("ai/query/getOrderSaller", requestBody);
}
@Tool(description = "根据客户名称或客户编码查询某个时间段内的订单销售额")
public String sumOrderTotalForClientInTime(
@ToolParam(description = "客户编码", required = false) String clientCode,
@ToolParam(description = "客户名称", required = false) String clientName,
@ToolParam(description = "开始时间", required = false) String startTime,
@ToolParam(description = "结束时间", required = false) String endTime
) {
JSONObject requestBody = new JSONObject();
requestBody.put("clientCode", clientCode);
requestBody.put("clientName", clientName);
requestBody.put("startTime", startTime);
requestBody.put("endTime", endTime);
return executeRequest("ai/query/sumOrderTotalForClientInTime", requestBody);
}
@Tool(description = "根据客户名称或客户编码查询某个时间段内购买的某个产品货号的数量")
public String sumProductNumForClientInTime(
@ToolParam(description = "客户编码", required = false) String clientCode,
@ToolParam(description = "客户名称", required = false) String clientName,
@ToolParam(description = "产品货号", required = false) String productnum,
@ToolParam(description = "开始时间", required = false) String startTime,
@ToolParam(description = "结束时间", required = false) String endTime
) {
JSONObject requestBody = new JSONObject();
requestBody.put("clientCode", clientCode);
requestBody.put("clientName", clientName);
requestBody.put("productnum", productnum);
requestBody.put("startTime", startTime);
requestBody.put("endTime", endTime);
return executeRequest("ai/query/sumProductNumForClientInTime", requestBody);
}
@Tool(description = "根据订单编码(即订单号)查询订单购买的产品明细")
public String detailOrderProduct(
@ToolParam(description = "订单编码") String orderCode
) {
Assert.isTrue(StrUtil.isNotEmpty(orderCode), "订单编码不能为空");
JSONObject requestBody = new JSONObject();
requestBody.put("orderCode", orderCode);
return executeRequest("ai/query/detailOrderProduct", requestBody);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2025-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author brianxiadong
*/
package net.rzdata.mcp.server.crm;
import net.rzdata.mcp.server.crm.config.CrmProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableConfigurationProperties(CrmProperties.class)
public class McpServerApplication {
@Autowired
private CrmProperties crmProperties;
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider crmTools(CrmMcpService crmMcpService) {
return MethodToolCallbackProvider.builder().toolObjects(crmMcpService).build();
}
}

View File

@ -0,0 +1,48 @@
package net.rzdata.mcp.server.crm.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* CRM 系统配置属性类
* 映射 application-dev.yml 中的 crm 配置项
*
* @author generated
*/
@Component
@ConfigurationProperties(prefix = "crm")
@Data
public class CrmProperties {
/**
* CRM 系统的基础 URL
*/
private String url;
/**
* 租户 ID
*/
private String tenantId;
/**
* 认证令牌
*/
private String token;
// 便利方法获取完整的 API URL
public String getApiUrl(String endpoint) {
if (url == null) {
throw new IllegalStateException("CRM URL is not configured");
}
String baseUrl = url.endsWith("/") ? url : url + "/";
String cleanEndpoint = endpoint.startsWith("/") ? endpoint.substring(1) : endpoint;
return baseUrl + cleanEndpoint;
}
}

View File

@ -0,0 +1,4 @@
crm:
url: https://www.rzdata.net/bcrm-statis-jwt/
tenantId: BIO
token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJUXdrczhuUWc4NnJPQkpuenJDUThtbjl1N3FEQ2lWZyJ9.91GO7OMhVbc0tLBRZ7HYiFDEA92PXE7H1gHduwxWu2I

View File

@ -0,0 +1,43 @@
#
# Copyright 2025-2026 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# spring.main.web-application-type=none
# NOTE: You must disable the banner and the console logging
# to allow the STDIO transport to work !!!
# Config reference: https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html#_webflux_server_configuration
spring:
main:
banner-mode: off
profiles:
active: dev
ai:
mcp:
server:
name: crm-mcp-server
version: 0.0.1
type: ASYNC # Recommended for reactive applications
# 配置 sse 的根路径,默认值为 /sse
# 下面的最终路径为 ip:port/sse/mcp
sse-endpoint: /sse
sse-message-endpoint: /mcp
capabilities:
tool: true
resource: true
prompt: true
completion: true
# logging.pattern.console=

162
pom.xml Normal file
View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2024-2025 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-examples</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<description>Spring AI Alibaba Examples</description>
<name>Spring AI Alibaba Examples</name>
<url>https://github.com/springaialibaba/spring-ai-alibaba-examples</url>
<properties>
<!-- Project revision -->
<revision>1.0.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<!-- Spring AI -->
<spring-ai.version>1.0.0</spring-ai.version>
<!-- Spring AI Alibaba -->
<spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
<!-- Spring Boot -->
<spring-boot.version>3.4.0</spring-boot.version>
<!-- maven plugin -->
<maven-deploy-plugin.version>3.1.1</maven-deploy-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
</properties>
<modules>
<module>mcp-webflux-server-example</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>${maven-deploy-plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
<compilerArgs>
<compilerArg>-parameters</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>sonatype</id>
<name>OSS Sonatype</name>
<url>https://oss.sonatype.org/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>aliyunmaven</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>