哈梨尔 发表于 2025-11-7 18:55:00

自定义MCP Server

1. MCP Server
引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>定义工具
@Service
public class DateService {

    private static final String[] WEEKDAY_NAMES = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日" };

    /**
   * 获取今天是星期几
   *
   * @return 星期描述
   */
    @Tool(description = "获取今天是星期几")
    public String getTodayWeekday() {
      System.out.println("getTodayWeekday 被调用");
      DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
      int index = dayOfWeek.getValue() - 1;
      if (index < 0 || index >= WEEKDAY_NAMES.length) {
            return "未知星期";
      }
      return String.format("今天是%s", WEEKDAY_NAMES);
    }

}


@Service
public class NumberService {

    /**
   * 判断一个数字是奇数还是偶数
   *
   * @param number 要判断的数字
   * @return 判断结果,返回"奇数"或"偶数"
   */
    @Tool(description = "判断一个数字是奇数还是偶数")
    public String checkOddOrEven(@ToolParam(description = "要判断的数字") int number) {
      System.out.println("checkOddOrEven 被调用");
      if (number % 2 == 0) {
            return String.format("数字 %d 是偶数", number);
      } else {
            return String.format("数字 %d 是奇数", number);
      }
    }
}


@Service
public class MyToolService {

    /**
   * 打印 1-9 的九九乘法表
   *
   * @return 九九乘法表文本
   */
    @Tool(description = "打印标准九九乘法表")
    public String printMultiplicationTable() {
      System.out.println("printMultiplicationTable 被调用");
      StringBuilder table = new StringBuilder();
      for (int i = 1; i <= 9; i++) {
            for (int j = 1; j <= i; j++) {
                table.append(String.format("%d×%d=%-2d", j, i, i * j));
                if (j < i) {
                  table.append("");
                }
            }
            if (i < 9) {
                table.append('\n');
            }
      }
      return table.toString();
    }
}


@Service
public class BankService {

    private static final String BASE_URL = "https://www.abc.xyz.com/payBankInfo/getByBankNo";

    private final RestClient restClient;
    private final ObjectMapper objectMapper;

    public BankService(RestClient.Builder restClientBuilder, ObjectMapper objectMapper) {
      this.restClient = restClientBuilder
                .baseUrl(BASE_URL)
                .build();
      this.objectMapper = objectMapper;
    }

    @Tool(description = "通过行号查询开户行信息")
    public String queryBankInfo(@ToolParam(description = "开户行行号") String bankNo) {
      System.out.println("queryBankInfo 被调用");
      try {
            // 参数验证
            if (bankNo == null || bankNo.trim().isEmpty()) {
                return "行号不能为空";
            }

            String responseBody = restClient.post()
                  .uri(uriBuilder -> uriBuilder.queryParam("bankNo", bankNo.trim()).build())
                  .contentType(MediaType.APPLICATION_JSON)
                  .accept(MediaType.APPLICATION_JSON)
                  .retrieve()
                  .body(String.class);

            // 尝试格式化 JSON
            try {
                Object json = objectMapper.readValue(responseBody, Object.class);
                return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
            } catch (Exception e) {
                // 如果不是 JSON,直接返回原始响应
                return responseBody;
            }
      } catch (RestClientResponseException e) {
            return String.format("请求失败: HTTP %d - %s", e.getStatusCode().value(), e.getStatusText());
      } catch (RestClientException e) {
            return String.format("请求异常: %s", e.getMessage());
      } catch (Exception e) {
            return String.format("处理异常: %s", e.getMessage());
      }
    }
}启动MCP服务
package com.example.springaimcpserver;

import com.example.springaimcpserver.service.BankService;
import com.example.springaimcpserver.service.DateService;
import com.example.springaimcpserver.service.MyToolService;
import com.example.springaimcpserver.service.NumberService;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.time.LocalDateTime;

@SpringBootApplication
public class SpringAiMcpServerApplication {

        public static void main(String[] args) {
                SpringApplication.run(SpringAiMcpServerApplication.class, args);
        }


        /**
       * ToolCallback:单个工具,适合简单、独立的工具
       * ToolCallbackProvider:工具提供者,适合批量注册(通过扫描 @Tool 方法)
       * 两种方式都会被 Spring AI 识别并注册为 MCP 工具。
       */

        @Bean
        public ToolCallbackProvider customTools(MyToolService myToolService) {
                return MethodToolCallbackProvider.builder().toolObjects(myToolService).build();
        }

        @Bean
        public ToolCallbackProvider customTools2(NumberService numberService, DateService dateService, BankService bankService) {
                return MethodToolCallbackProvider.builder().toolObjects(numberService, dateService, bankService).build();
        }


        public record TextInput(String input) {
        }

        public record CalculatorInput(double a, double b, String operation) {
        }

        /**
       * 将输入字母转为大写
       */
        @Bean
        public ToolCallback toUpperCase() {
                System.out.println("toUpperCase 被调用");
                return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase())
                                .inputType(TextInput.class)
                                .description("将字母转成大写")
                                .build();
        }

        /**
       * 计算器工具 - 支持加减乘除运算
       */
        @Bean
        public ToolCallback calculator() {
                System.out.println("calculator 被调用");
                return FunctionToolCallback.builder("calculator", (CalculatorInput input) -> {
                                        double a = input.a();
                                        double b = input.b();
                                        String op = input.operation().toLowerCase().trim();

                                        return switch (op) {
                                                case "add", "+" -> String.format("%.2f + %.2f = %.2f", a, b, a + b);
                                                case "subtract", "-" -> String.format("%.2f - %.2f = %.2f", a, b, a - b);
                                                case "multiply", "*", "×" -> String.format("%.2f × %.2f = %.2f", a, b, a * b);
                                                case "divide", "/", "÷" -> {
                                                        if (b == 0) {
                                                                yield "错误:除数不能为零";
                                                        }
                                                        yield String.format("%.2f ÷ %.2f = %.2f", a, b, a / b);
                                                }
                                                default -> "错误:不支持的操作符。支持的操作:add/+, subtract/-, multiply/*, divide//";
                                        };
                                })
                                .inputType(CalculatorInput.class)
                                .description("执行基本的数学运算(加法、减法、乘法、除法)。支持的操作:add/+, subtract/-, multiply/*, divide//")
                                .build();
        }

        /**
       * 获取当前时间
       */
        @Bean
        public ToolCallback getCurrentTime() {
                System.out.println("getCurrentTime 被调用");
                return FunctionToolCallback.builder("getCurrentTime",
                                                () -> LocalDateTime.now().toString())
                                .inputType(Void.class)
                                .description("获取当前时间")
                                .build();
        }
}2. MCP Client
pom.xml
spring:
application:
    name: spring-ai-mcp-server
main:
    banner-mode: off
ai:
    mcp:
      server:
      name: my-spring-ai-mcp-server
      version: 1.0.0
      type: async
      sse-endpoint: /sse
      sse-message-endpoint: /mcp/message
      capabilities:
          tool: true
          resource: true
          prompt: true
          completion: trueapplication.yml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <groupId>org.springframework.boot</groupId>
                spring-boot-starter-parent</artifactId>
                <version>3.5.7</version>
                <relativePath/>
        </parent>
        <groupId>com.example</groupId>
        spring-ai-mcp-client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>spring-ai-mcp-client</name>
        <description>spring-ai-mcp-client</description>

        <properties>
                <java.version>17</java.version>
                <spring-ai.version>1.0.3</spring-ai.version>
                <spring-ai-alibaba.version>1.0.0.4</spring-ai-alibaba.version>
        </properties>

        <dependencies>
                <dependency>
                        <groupId>org.springframework.ai</groupId>
                        spring-ai-starter-mcp-client-webflux</artifactId>
                </dependency>

                <dependency>
                        <groupId>com.alibaba.cloud.ai</groupId>
                        spring-ai-alibaba-starter-dashscope</artifactId>
                </dependency>
        </dependencies>

        <dependencyManagement>
                <dependencies>
                        <dependency>
                                <groupId>org.springframework.ai</groupId>
                                spring-ai-bom</artifactId>
                                <version>${spring-ai.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                        </dependency>
                        <dependency>
                                <groupId>com.alibaba.cloud.ai</groupId>
                                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>
                                spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

</project>测试工具调用
server:
port: 8081

spring:
application:
    name: my-spring-ai-mcp-client
main:
    web-application-type: none
ai:
    dashscope:
      api-key: sk-66217287a102487c4c52
      chat:
      enabled: true
      options:
          model: qwen3-max
          temperature: 0
    mcp:
      client:
      toolcallback:
          enabled: true
      sse:
          connections:
            server1:
            url: http://localhost:8080
            sse-endpoint: /sse
            mcp-chinese-fortune:
            url: https://mcp.api-inference.modelscope.net
            sse-endpoint: /d103803834594b/sse测试结果:
服务器端控制台输出:
package com.example.springaimcpclient;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringAiMcpClientApplication {

        public static void main(String[] args) {
                SpringApplication.run(SpringAiMcpClientApplication.class, args);
        }


        @Bean
        public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,
                                                                                               ConfigurableApplicationContext context) {

                // 打印所有可用的工具
                System.out.println("\n========== 所有可用的工具 ==========");
                var toolCallbacks = tools.getToolCallbacks();
                System.out.println("工具总数: " + toolCallbacks.length);
                for (int i = 0; i < toolCallbacks.length; i++) {
                        var toolDef = toolCallbacks.getToolDefinition();
                        System.out.println("\n【工具 " + (i + 1) + "】");
                        System.out.println("名称: " + toolDef.name());
                        System.out.println("描述: " + toolDef.description());
                }
                System.out.println("=====================================\n");


        return args -> {

                String systemPrompt = """
                                        你是一个智能助手,拥有多个可用的工具。
                                        当用户提问时,你必须优先考虑使用可用的工具来回答问题。
                                        只有在没有合适的工具时,才使用你自己的知识来回答。
                                       
                                        重要规则:
                                        1. 如果有工具可以获取实时信息(如时间、日期),必须使用工具
                                        2. 如果有工具可以执行操作(如字符串转换),必须使用工具
                                        3. 使用工具后,将工具返回的结果展示给用户
                                        """;
                var chatClient = chatClientBuilder
                                .defaultSystem(systemPrompt)
                                .defaultToolCallbacks(tools)
                                .build();

                // 测试问题1:当前时间(应该调用getCurrentTime工具)
                System.out.println("\n>>> QUESTION: 当前时间是多少");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("当前时间是多少").call().content());

                // 测试问题2:字符串转大写(应该调用toUpperCase工具)
                System.out.println("\n>>> QUESTION: 将abcd转成大写");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("将abcd转成大写").call().content());

                // 测试问题3:计算(应该调用calculator工具)
                System.out.println("\n>>> QUESTION: 计算 123 加 456 等于多少");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("计算 123 加 456 等于多少").call().content());

                // 测试问题4:今天星期几(应该调用getTodayWeekday工具)
                System.out.println("\n>>> QUESTION: 今天星期几");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("今天星期几").call().content());

                // 测试问题5:打印九九乘法表(应该调用printMultiplicationTable工具)
                System.out.println("\n>>> QUESTION: 打印九九乘法表");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("打印九九乘法表").call().content());

                // 测试问题6:查询开户行信息(应该调用queryBankInfo工具)
                System.out.println("\n>>> QUESTION: 查询行号为303100000629的开户行信息");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("查询行号为303100000629的开户行信息").call().content());

                // 测试问题7:判断奇偶数(应该调用checkOddOrEven工具)
                System.out.println("\n>>> QUESTION: 数字16是奇数还是偶数");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("数字16是奇数还是偶数").call().content());

                // 测试问题8:帮我算下命,出生时间 2015年 10月19日8点
                System.out.println("\n>>> QUESTION: 帮我算下命,出生时间 2015年 10月19日8点");
                System.out.println(">>> ASSISTANT: " + chatClient.prompt("帮我算下命,出生时间 2015年 10月19日8点").call().content());
               
                context.close();
        };
        }
}客户端控制台输出:
getTodayWeekday 被调用
printMultiplicationTable 被调用
queryBankInfo 被调用
checkOddOrEven 被调用抓包截图

SSE方式通信流程:

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

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

腥狩频 发表于 昨天 01:33

东西不错很实用谢谢分享

请蒂 发表于 昨天 14:00

很好很强大我过来先占个楼 待编辑
页: [1]
查看完整版本: 自定义MCP Server