自定义MCP Server

1. MCP Server

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>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[index]);
    }

}


@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());
        }
    }
}

注册工具

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();
	}
}

启动MCP服务

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: true

2. MCP Client

pom.xml

<?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>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.7</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>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>
			<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>com.alibaba.cloud.ai</groupId>
			<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
		</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>
			<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>
			</plugin>
		</plugins>
	</build>

</project>

application.yml

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[i].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 被调用

客户端控制台输出:

========== 所有可用的工具 ==========
工具总数: 8

【工具 1】
  名称: spring_ai_mcp_client_server1_calculator
  描述: 执行基本的数学运算(加法、减法、乘法、除法)。支持的操作:add/+, subtract/-, multiply/*, divide//

【工具 2】
  名称: spring_ai_mcp_client_server1_toUpperCase
  描述: 将字母转成大写

【工具 3】
  名称: spring_ai_mcp_client_server1_getCurrentTime
  描述: 获取当前时间

【工具 4】
  名称: spring_ai_mcp_client_server1_queryBankInfo
  描述: 通过行号查询开户行信息

【工具 5】
  名称: spring_ai_mcp_client_server1_checkOddOrEven
  描述: 判断一个数字是奇数还是偶数

【工具 6】
  名称: spring_ai_mcp_client_server1_printMultiplicationTable
  描述: 打印标准九九乘法表

【工具 7】
  名称: spring_ai_mcp_client_server1_getTodayWeekday
  描述: 获取今天是星期几

【工具 8】
  名称: spring_ai_mcp_client_mcp_chinese_fortune_fortune_teller
  描述: 根据出生年月日小时分析生辰八字和命理信息来进行算命
=====================================

2025-11-07T18:15:26.146+08:00  INFO 57852 --- [my-spring-ai-mcp-client] [           main] c.e.s.SpringAiMcpClientApplication       : Started SpringAiMcpClientApplication in 3.926 seconds (process running for 4.366)

>>> QUESTION: 当前时间是多少
>>> ASSISTANT: 当前时间是:2025年11月7日 18:15:27。

>>> QUESTION: 将abcd转成大写
>>> ASSISTANT: ABCD

>>> QUESTION: 计算 123 加 456 等于多少
>>> ASSISTANT: 123 加 456 等于 579。

>>> QUESTION: 今天星期几
>>> ASSISTANT: 今天是星期五。

>>> QUESTION: 打印九九乘法表
>>> ASSISTANT: 以下是标准的九九乘法表:

```
1×1=1 
1×2=2   2×2=4 
1×3=3   2×3=6   3×3=9 
1×4=4   2×4=8   3×4=12  4×4=16
1×5=5   2×5=10  3×5=15  4×5=20  5×5=25
1×6=6   2×6=12  3×6=18  4×6=24  5×6=30  6×6=36
1×7=7   2×7=14  3×7=21  4×7=28  5×7=35  6×7=42  7×7=49
1×8=8   2×8=16  3×8=24  4×8=32  5×8=40  6×8=48  7×8=56  8×8=64
1×9=9   2×9=18  3×9=27  4×9=36  5×9=45  6×9=54  7×9=63  8×9=72  9×9=81
```

>>> QUESTION: 查询行号为303100000629的开户行信息
>>> ASSISTANT: 根据查询结果,行号为 **303100000629** 的开户行信息如下:

- **银行名称**: 中国光大银行股份有限公司北京通州支行
- **联系电话**: 80883251
- **生效日期**: 2014-08-06
- **失效日期**: 2999-12-31

如需其他帮助,请随时告知!

>>> QUESTION: 数字16是奇数还是偶数
>>> ASSISTANT: 数字 16 是偶数。

>>> QUESTION: 帮我算下命,出生时间 2015年 10月19日8点
>>> ASSISTANT: 根据您提供的出生时间(2015年10月19日8点),以下是命理分析结果:

### 基本信息
- **公历生日**:2015年10月19日
- **农历生日**:乙未年九月初七
- **生肖**:羊
- **星座**:天秤座
- **八字**:乙未 丙戌 戊辰 丙辰
- **纳音五行**:金

### 八字解析
- **日主**:戊土(代表自己)
- **年柱**:乙未(比肩)
- **月柱**:丙戌(伤官)
- **日柱**:戊辰(日主坐财)
- **时柱**:丙辰(伤官)

### 命理特点
1. **性格特征**:
   - 日主为戊土,为人诚实稳重,有责任感。
   - 月柱和时柱均为丙火伤官,表示聪明伶俐、思维活跃,具有创造力和表达能力。
   - 伤官透出,说明个性独立,不喜欢受约束,有时可能显得有些叛逆。

2. **五行分析**:
   - 八字中土旺(戊土日主,辰戌未三土),火生土(丙火伤官),整体偏燥。
   - 需要注意调和五行,适当补充金水元素以平衡命局。

3. **运势特点**:
   - 伤官生财,说明有较强的赚钱能力和商业头脑。
   - 财星在日支,表示财运较好,但需注意理财规划。
   - 比肩在年柱,代表早年家庭环境较为稳定,但竞争意识较强。

4. **注意事项**:
   - 伤官过旺,需要注意情绪管理和人际关系,避免过于直言不讳。
   - 土旺之人容易固执,建议多听取他人意见,保持开放心态。

### 建议
- **发展方向**:适合从事创意、艺术、技术或商业相关的工作。
- **健康方面**:注意脾胃和消化系统的保养,避免过度劳累。
- **人际关系**:培养耐心和包容心,有助于建立良好的人际网络。

以上是基于传统命理学的分析,仅供参考。命运掌握在自己手中,积极努力才是成功的关键!

抓包截图

企业微信截图_20251107173532

SSE方式通信流程:

74d131390da2d6c83d90b43d00f352e2_compress

参考文档:

https://docs.spring.io/spring-ai/reference/getting-started.html

https://github.com/spring-ai-community/awesome-spring-ai

 

posted @ 2025-11-07 18:39  废物大师兄  阅读(4)  评论(0)    收藏  举报