commit 7761645f0ea2759db4df9e975975b16f80ebe786 Author: bigtian <7990497@qq.com> Date: Thu Jun 26 14:44:47 2025 +0800 Add CRM configuration and service implementation for API interactions diff --git a/mcp-webflux-server-example/README.md b/mcp-webflux-server-example/README.md new file mode 100644 index 0000000..5c0f391 --- /dev/null +++ b/mcp-webflux-server-example/README.md @@ -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 diff --git a/mcp-webflux-server-example/pom.xml b/mcp-webflux-server-example/pom.xml new file mode 100644 index 0000000..2de09cf --- /dev/null +++ b/mcp-webflux-server-example/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + com.alibaba.cloud.ai + ${revision} + spring-ai-alibaba-examples + ../pom.xml + + + com.example + + mcp-webflux-server-example + + Spring AI MCP WEBFLUX server + Sample Spring Boot application demonstrating MCP client and server usage + + + + org.springframework.ai + spring-ai-starter-mcp-server-webflux + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.57 + + + org.springframework + spring-web + + + org.projectlombok + lombok + + + cn.hutool + hutool-all + 5.8.38 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + diff --git a/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/CrmMcpService.java b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/CrmMcpService.java new file mode 100644 index 0000000..8e00d3a --- /dev/null +++ b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/CrmMcpService.java @@ -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); + } + +} diff --git a/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/McpServerApplication.java b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/McpServerApplication.java new file mode 100644 index 0000000..beeea4e --- /dev/null +++ b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/McpServerApplication.java @@ -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(); + } +} diff --git a/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/config/CrmProperties.java b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/config/CrmProperties.java new file mode 100644 index 0000000..88ccec0 --- /dev/null +++ b/mcp-webflux-server-example/src/main/java/net/rzdata/mcp/server/crm/config/CrmProperties.java @@ -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; + } + + + +} diff --git a/mcp-webflux-server-example/src/main/resources/application-dev.yml b/mcp-webflux-server-example/src/main/resources/application-dev.yml new file mode 100644 index 0000000..48a19d9 --- /dev/null +++ b/mcp-webflux-server-example/src/main/resources/application-dev.yml @@ -0,0 +1,4 @@ +crm: + url: https://www.rzdata.net/bcrm-statis-jwt/ + tenantId: BIO + token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJJUXdrczhuUWc4NnJPQkpuenJDUThtbjl1N3FEQ2lWZyJ9.91GO7OMhVbc0tLBRZ7HYiFDEA92PXE7H1gHduwxWu2I \ No newline at end of file diff --git a/mcp-webflux-server-example/src/main/resources/application.yml b/mcp-webflux-server-example/src/main/resources/application.yml new file mode 100644 index 0000000..c47bb6d --- /dev/null +++ b/mcp-webflux-server-example/src/main/resources/application.yml @@ -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= diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..429d32c --- /dev/null +++ b/pom.xml @@ -0,0 +1,162 @@ + + + + + + 4.0.0 + com.alibaba.cloud.ai + spring-ai-alibaba-examples + ${revision} + pom + + Spring AI Alibaba Examples + Spring AI Alibaba Examples + https://github.com/springaialibaba/spring-ai-alibaba-examples + + + + 1.0.0 + + UTF-8 + UTF-8 + 17 + 17 + 17 + + + 1.0.0 + + + 1.0.0.2 + + + 3.4.0 + + + 3.1.1 + 1.3.0 + 3.8.1 + + + + mcp-webflux-server-example + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + com.alibaba.cloud.ai + spring-ai-alibaba-bom + ${spring-ai-alibaba.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + + -parameters + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + sonatype + OSS Sonatype + https://oss.sonatype.org/content/groups/public/ + + true + + + true + + + + aliyunmaven + aliyun + https://maven.aliyun.com/repository/public + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + false + + + +