找回密码
 立即注册
首页 业界区 安全 自定义MCP Server

自定义MCP Server

哈梨尔 3 天前
1. MCP Server
引入依赖
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     spring-boot-starter-webflux</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>org.springframework.ai</groupId>
  7.     spring-ai-starter-mcp-server-webflux</artifactId>
  8. </dependency>
复制代码
定义工具
  1. @Service
  2. public class DateService {
  3.     private static final String[] WEEKDAY_NAMES = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" };
  4.     /**
  5.      * 获取今天是星期几
  6.      *
  7.      * @return 星期描述
  8.      */
  9.     @Tool(description = "获取今天是星期几")
  10.     public String getTodayWeekday() {
  11.         System.out.println("getTodayWeekday 被调用");
  12.         DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
  13.         int index = dayOfWeek.getValue() - 1;
  14.         if (index < 0 || index >= WEEKDAY_NAMES.length) {
  15.             return "未知星期";
  16.         }
  17.         return String.format("今天是%s", WEEKDAY_NAMES[index]);
  18.     }
  19. }
  20. @Service
  21. public class NumberService {
  22.     /**
  23.      * 判断一个数字是奇数还是偶数
  24.      *
  25.      * @param number 要判断的数字
  26.      * @return 判断结果,返回"奇数"或"偶数"
  27.      */
  28.     @Tool(description = "判断一个数字是奇数还是偶数")
  29.     public String checkOddOrEven(@ToolParam(description = "要判断的数字") int number) {
  30.         System.out.println("checkOddOrEven 被调用");
  31.         if (number % 2 == 0) {
  32.             return String.format("数字 %d 是偶数", number);
  33.         } else {
  34.             return String.format("数字 %d 是奇数", number);
  35.         }
  36.     }
  37. }
  38. @Service
  39. public class MyToolService {
  40.     /**
  41.      * 打印 1-9 的九九乘法表
  42.      *
  43.      * @return 九九乘法表文本
  44.      */
  45.     @Tool(description = "打印标准九九乘法表")
  46.     public String printMultiplicationTable() {
  47.         System.out.println("printMultiplicationTable 被调用");
  48.         StringBuilder table = new StringBuilder();
  49.         for (int i = 1; i <= 9; i++) {
  50.             for (int j = 1; j <= i; j++) {
  51.                 table.append(String.format("%d×%d=%-2d", j, i, i * j));
  52.                 if (j < i) {
  53.                     table.append("  ");
  54.                 }
  55.             }
  56.             if (i < 9) {
  57.                 table.append('\n');
  58.             }
  59.         }
  60.         return table.toString();
  61.     }
  62. }
  63. @Service
  64. public class BankService {
  65.     private static final String BASE_URL = "https://www.abc.xyz.com/payBankInfo/getByBankNo";
  66.     private final RestClient restClient;
  67.     private final ObjectMapper objectMapper;
  68.     public BankService(RestClient.Builder restClientBuilder, ObjectMapper objectMapper) {
  69.         this.restClient = restClientBuilder
  70.                 .baseUrl(BASE_URL)
  71.                 .build();
  72.         this.objectMapper = objectMapper;
  73.     }
  74.     @Tool(description = "通过行号查询开户行信息")
  75.     public String queryBankInfo(@ToolParam(description = "开户行行号") String bankNo) {
  76.         System.out.println("queryBankInfo 被调用");
  77.         try {
  78.             // 参数验证
  79.             if (bankNo == null || bankNo.trim().isEmpty()) {
  80.                 return "行号不能为空";
  81.             }
  82.             String responseBody = restClient.post()
  83.                     .uri(uriBuilder -> uriBuilder.queryParam("bankNo", bankNo.trim()).build())
  84.                     .contentType(MediaType.APPLICATION_JSON)
  85.                     .accept(MediaType.APPLICATION_JSON)
  86.                     .retrieve()
  87.                     .body(String.class);
  88.             // 尝试格式化 JSON
  89.             try {
  90.                 Object json = objectMapper.readValue(responseBody, Object.class);
  91.                 return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
  92.             } catch (Exception e) {
  93.                 // 如果不是 JSON,直接返回原始响应
  94.                 return responseBody;
  95.             }
  96.         } catch (RestClientResponseException e) {
  97.             return String.format("请求失败: HTTP %d - %s", e.getStatusCode().value(), e.getStatusText());
  98.         } catch (RestClientException e) {
  99.             return String.format("请求异常: %s", e.getMessage());
  100.         } catch (Exception e) {
  101.             return String.format("处理异常: %s", e.getMessage());
  102.         }
  103.     }
  104. }
复制代码
启动MCP服务
  1. package com.example.springaimcpserver;
  2. import com.example.springaimcpserver.service.BankService;
  3. import com.example.springaimcpserver.service.DateService;
  4. import com.example.springaimcpserver.service.MyToolService;
  5. import com.example.springaimcpserver.service.NumberService;
  6. import org.springframework.ai.tool.ToolCallback;
  7. import org.springframework.ai.tool.ToolCallbackProvider;
  8. import org.springframework.ai.tool.function.FunctionToolCallback;
  9. import org.springframework.ai.tool.method.MethodToolCallbackProvider;
  10. import org.springframework.boot.SpringApplication;
  11. import org.springframework.boot.autoconfigure.SpringBootApplication;
  12. import org.springframework.context.annotation.Bean;
  13. import java.time.LocalDateTime;
  14. @SpringBootApplication
  15. public class SpringAiMcpServerApplication {
  16.         public static void main(String[] args) {
  17.                 SpringApplication.run(SpringAiMcpServerApplication.class, args);
  18.         }
  19.         /**
  20.          * ToolCallback:单个工具,适合简单、独立的工具
  21.          * ToolCallbackProvider:工具提供者,适合批量注册(通过扫描 @Tool 方法)
  22.          * 两种方式都会被 Spring AI 识别并注册为 MCP 工具。
  23.          */
  24.         @Bean
  25.         public ToolCallbackProvider customTools(MyToolService myToolService) {
  26.                 return MethodToolCallbackProvider.builder().toolObjects(myToolService).build();
  27.         }
  28.         @Bean
  29.         public ToolCallbackProvider customTools2(NumberService numberService, DateService dateService, BankService bankService) {
  30.                 return MethodToolCallbackProvider.builder().toolObjects(numberService, dateService, bankService).build();
  31.         }
  32.         public record TextInput(String input) {
  33.         }
  34.         public record CalculatorInput(double a, double b, String operation) {
  35.         }
  36.         /**
  37.          * 将输入字母转为大写
  38.          */
  39.         @Bean
  40.         public ToolCallback toUpperCase() {
  41.                 System.out.println("toUpperCase 被调用");
  42.                 return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase())
  43.                                 .inputType(TextInput.class)
  44.                                 .description("将字母转成大写")
  45.                                 .build();
  46.         }
  47.         /**
  48.          * 计算器工具 - 支持加减乘除运算
  49.          */
  50.         @Bean
  51.         public ToolCallback calculator() {
  52.                 System.out.println("calculator 被调用");
  53.                 return FunctionToolCallback.builder("calculator", (CalculatorInput input) -> {
  54.                                         double a = input.a();
  55.                                         double b = input.b();
  56.                                         String op = input.operation().toLowerCase().trim();
  57.                                         return switch (op) {
  58.                                                 case "add", "+" -> String.format("%.2f + %.2f = %.2f", a, b, a + b);
  59.                                                 case "subtract", "-" -> String.format("%.2f - %.2f = %.2f", a, b, a - b);
  60.                                                 case "multiply", "*", "×" -> String.format("%.2f × %.2f = %.2f", a, b, a * b);
  61.                                                 case "divide", "/", "÷" -> {
  62.                                                         if (b == 0) {
  63.                                                                 yield "错误:除数不能为零";
  64.                                                         }
  65.                                                         yield String.format("%.2f ÷ %.2f = %.2f", a, b, a / b);
  66.                                                 }
  67.                                                 default -> "错误:不支持的操作符。支持的操作:add/+, subtract/-, multiply/*, divide//";
  68.                                         };
  69.                                 })
  70.                                 .inputType(CalculatorInput.class)
  71.                                 .description("执行基本的数学运算(加法、减法、乘法、除法)。支持的操作:add/+, subtract/-, multiply/*, divide//")
  72.                                 .build();
  73.         }
  74.         /**
  75.          * 获取当前时间
  76.          */
  77.         @Bean
  78.         public ToolCallback getCurrentTime() {
  79.                 System.out.println("getCurrentTime 被调用");
  80.                 return FunctionToolCallback.builder("getCurrentTime",
  81.                                                 () -> LocalDateTime.now().toString())
  82.                                 .inputType(Void.class)
  83.                                 .description("获取当前时间")
  84.                                 .build();
  85.         }
  86. }
复制代码
2. MCP Client
pom.xml
  1. spring:
  2.   application:
  3.     name: spring-ai-mcp-server
  4.   main:
  5.     banner-mode: off
  6.   ai:
  7.     mcp:
  8.       server:
  9.         name: my-spring-ai-mcp-server
  10.         version: 1.0.0
  11.         type: async
  12.         sse-endpoint: /sse
  13.         sse-message-endpoint: /mcp/message
  14.         capabilities:
  15.           tool: true
  16.           resource: true
  17.           prompt: true
  18.           completion: true
复制代码
application.yml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.         <modelVersion>4.0.0</modelVersion>
  5.         <parent>
  6.                 <groupId>org.springframework.boot</groupId>
  7.                 spring-boot-starter-parent</artifactId>
  8.                 <version>3.5.7</version>
  9.                 <relativePath/>
  10.         </parent>
  11.         <groupId>com.example</groupId>
  12.         spring-ai-mcp-client</artifactId>
  13.         <version>0.0.1-SNAPSHOT</version>
  14.         <name>spring-ai-mcp-client</name>
  15.         <description>spring-ai-mcp-client</description>
  16.         <properties>
  17.                 <java.version>17</java.version>
  18.                 <spring-ai.version>1.0.3</spring-ai.version>
  19.                 <spring-ai-alibaba.version>1.0.0.4</spring-ai-alibaba.version>
  20.         </properties>
  21.         <dependencies>
  22.                 <dependency>
  23.                         <groupId>org.springframework.ai</groupId>
  24.                         spring-ai-starter-mcp-client-webflux</artifactId>
  25.                 </dependency>
  26.                 <dependency>
  27.                         <groupId>com.alibaba.cloud.ai</groupId>
  28.                         spring-ai-alibaba-starter-dashscope</artifactId>
  29.                 </dependency>
  30.         </dependencies>
  31.         <dependencyManagement>
  32.                 <dependencies>
  33.                         <dependency>
  34.                                 <groupId>org.springframework.ai</groupId>
  35.                                 spring-ai-bom</artifactId>
  36.                                 <version>${spring-ai.version}</version>
  37.                                 <type>pom</type>
  38.                                 <scope>import</scope>
  39.                         </dependency>
  40.                         <dependency>
  41.                                 <groupId>com.alibaba.cloud.ai</groupId>
  42.                                 spring-ai-alibaba-bom</artifactId>
  43.                                 <version>${spring-ai-alibaba.version}</version>
  44.                                 <type>pom</type>
  45.                                 <scope>import</scope>
  46.                         </dependency>
  47.                 </dependencies>
  48.         </dependencyManagement>
  49.         <build>
  50.                 <plugins>
  51.                         <plugin>
  52.                                 <groupId>org.springframework.boot</groupId>
  53.                                 spring-boot-maven-plugin</artifactId>
  54.                         </plugin>
  55.                 </plugins>
  56.         </build>
  57. </project>
复制代码
测试工具调用
  1. server:
  2.   port: 8081
  3. spring:
  4.   application:
  5.     name: my-spring-ai-mcp-client
  6.   main:
  7.     web-application-type: none
  8.   ai:
  9.     dashscope:
  10.       api-key: sk-66217287a102487c4c52
  11.       chat:
  12.         enabled: true
  13.         options:
  14.           model: qwen3-max
  15.           temperature: 0
  16.     mcp:
  17.       client:
  18.         toolcallback:
  19.           enabled: true
  20.         sse:
  21.           connections:
  22.             server1:
  23.               url: http://localhost:8080
  24.               sse-endpoint: /sse
  25.             mcp-chinese-fortune:
  26.               url: https://mcp.api-inference.modelscope.net
  27.               sse-endpoint: /d103803834594b/sse
复制代码
测试结果:
服务器端控制台输出:
  1. package com.example.springaimcpclient;
  2. import org.springframework.ai.chat.client.ChatClient;
  3. import org.springframework.ai.tool.ToolCallbackProvider;
  4. import org.springframework.boot.CommandLineRunner;
  5. import org.springframework.boot.SpringApplication;
  6. import org.springframework.boot.autoconfigure.SpringBootApplication;
  7. import org.springframework.context.ConfigurableApplicationContext;
  8. import org.springframework.context.annotation.Bean;
  9. @SpringBootApplication
  10. public class SpringAiMcpClientApplication {
  11.         public static void main(String[] args) {
  12.                 SpringApplication.run(SpringAiMcpClientApplication.class, args);
  13.         }
  14.         @Bean
  15.         public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,
  16.                                                                                                  ConfigurableApplicationContext context) {
  17.                 // 打印所有可用的工具
  18.                 System.out.println("\n========== 所有可用的工具 ==========");
  19.                 var toolCallbacks = tools.getToolCallbacks();
  20.                 System.out.println("工具总数: " + toolCallbacks.length);
  21.                 for (int i = 0; i < toolCallbacks.length; i++) {
  22.                         var toolDef = toolCallbacks[i].getToolDefinition();
  23.                         System.out.println("\n【工具 " + (i + 1) + "】");
  24.                         System.out.println("  名称: " + toolDef.name());
  25.                         System.out.println("  描述: " + toolDef.description());
  26.                 }
  27.                 System.out.println("=====================================\n");
  28.         return args -> {
  29.                 String systemPrompt = """
  30.                                         你是一个智能助手,拥有多个可用的工具。
  31.                                         当用户提问时,你必须优先考虑使用可用的工具来回答问题。
  32.                                         只有在没有合适的工具时,才使用你自己的知识来回答。
  33.                                        
  34.                                         重要规则:
  35.                                         1. 如果有工具可以获取实时信息(如时间、日期),必须使用工具
  36.                                         2. 如果有工具可以执行操作(如字符串转换),必须使用工具
  37.                                         3. 使用工具后,将工具返回的结果展示给用户
  38.                                         """;
  39.                 var chatClient = chatClientBuilder
  40.                                 .defaultSystem(systemPrompt)
  41.                                 .defaultToolCallbacks(tools)
  42.                                 .build();
  43.                 // 测试问题1:当前时间(应该调用getCurrentTime工具)
  44.                 System.out.println("\n>>> QUESTION: 当前时间是多少");
  45.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("当前时间是多少").call().content());
  46.                 // 测试问题2:字符串转大写(应该调用toUpperCase工具)
  47.                 System.out.println("\n>>> QUESTION: 将abcd转成大写");
  48.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("将abcd转成大写").call().content());
  49.                 // 测试问题3:计算(应该调用calculator工具)
  50.                 System.out.println("\n>>> QUESTION: 计算 123 加 456 等于多少");
  51.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("计算 123 加 456 等于多少").call().content());
  52.                 // 测试问题4:今天星期几(应该调用getTodayWeekday工具)
  53.                 System.out.println("\n>>> QUESTION: 今天星期几");
  54.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("今天星期几").call().content());
  55.                 // 测试问题5:打印九九乘法表(应该调用printMultiplicationTable工具)
  56.                 System.out.println("\n>>> QUESTION: 打印九九乘法表");
  57.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("打印九九乘法表").call().content());
  58.                 // 测试问题6:查询开户行信息(应该调用queryBankInfo工具)
  59.                 System.out.println("\n>>> QUESTION: 查询行号为303100000629的开户行信息");
  60.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("查询行号为303100000629的开户行信息").call().content());
  61.                 // 测试问题7:判断奇偶数(应该调用checkOddOrEven工具)
  62.                 System.out.println("\n>>> QUESTION: 数字16是奇数还是偶数");
  63.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("数字16是奇数还是偶数").call().content());
  64.                 // 测试问题8:帮我算下命,出生时间 2015年 10月19日8点
  65.                 System.out.println("\n>>> QUESTION: 帮我算下命,出生时间 2015年 10月19日8点");
  66.                 System.out.println(">>> ASSISTANT: " + chatClient.prompt("帮我算下命,出生时间 2015年 10月19日8点").call().content());
  67.                
  68.                 context.close();
  69.         };
  70.         }
  71. }
复制代码
客户端控制台输出:
  1. getTodayWeekday 被调用
  2. printMultiplicationTable 被调用
  3. queryBankInfo 被调用
  4. checkOddOrEven 被调用
复制代码
抓包截图
1.png

SSE方式通信流程:
2.jpeg

参考文档:
https://docs.spring.io/spring-ai/reference/getting-started.html
https://github.com/spring-ai-community/awesome-spring-ai
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册