📖 前言

在之前的教程中,我们已经学习了 Spring AI 的基础概念、聊天模型、函数调用等核心功能。今天,我们将深入探索 Spring AI 生态系统中一个非常重要的协议:MCP(Model Context Protocol)

MCP 是一个开放协议,用于标准化应用程序与 AI 模型之间的上下文交互。通过 MCP,我们可以将工具(Tools)、资源(Resources)和提示模板(Prompts)以统一的方式暴露给 AI 模型,使 AI 应用更加模块化和可扩展。

在本篇教程中,我们将通过完整的代码示例,学习如何:

  • ✅ 构建 MCP Server,提供天气查询、系统信息和问候模板服务
  • ✅ 构建 MCP Client,连接并消费 MCP Server 的能力
  • ✅ 将 Resources 和 Prompts 转换为 Tools,让 LLM 动态调用
  • ✅ 实现 Tools、Resources、Prompts 的综合应用场景

🎯 什么是 MCP?

MCP(Model Context Protocol) 是由 Anthropic 提出的开放协议,旨在标准化 AI 应用与外部数据源、工具和服务的交互方式。

MCP 的三大核心能力

  1. Tools(工具):允许 AI 模型执行操作,如查询天气、计算数据等
  2. Resources(资源):提供数据访问接口,如读取文件、查询系统信息等
  3. Prompts(提示模板):预定义的提示模板,支持参数化生成个性化内容

Spring AI MCP 架构

┌─────────────────────────────────────────┐
│         MCP Client (Spring AI)          │
│  ┌──────────┬──────────┬──────────────┐ │
│  │  Tools   │Resources │   Prompts    │ │
│  └──────────┴──────────┴──────────────┘ │
└──────────────┬──────────────────────────┘
               │ Streamable-HTTP / SSE
┌──────────────┴──────────────────────────┐
│         MCP Server (Spring AI)          │
│  ┌──────────┬──────────┬──────────────┐ │
│  │@McpTool  │@McpRes.  │  @McpPrompt  │ │
│  └──────────┴──────────┴──────────────┘ │
└─────────────────────────────────────────┘

🏗️ 项目结构

本教程包含两个独立的 Spring Boot 应用:

spring-ai-demo/
├── pom.xml                    # 父 POM(依赖管理)
├── spring-ai-mcp-server/      # MCP 服务器(端口 8100)
│   ├── pom.xml
│   └── src/main/java/cn/dianyu/ai/mcp/server/
│       ├── McpServerDemoApplication.java
│       ├── McpWeatherTools.java   # 天气查询工具
│       ├── SystemInfoResource.java # 系统信息资源
│       └── GreetingPrompt.java    # 问候提示模板
│
└── spring-ai-mcp-client/      # MCP 客户端(端口 8200)
    ├── pom.xml
    └── src/main/java/cn/dianyu/ai/mcp/client/
        ├── McpClientDemoApplication.java
        ├── ChatController.java           # 聊天控制器
        ├── McpResourceToolConfig.java    # Resource 转 Tool 配置
        └── McpPromptToolConfig.java      # Prompt 转 Tool 配置

父 POM 说明

项目的根目录 pom.xml 作为父 POM,负责统一的依赖管理和版本控制:

<project>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.14</version>
    </parent>
    
    <groupId>cn.dianyu.ai</groupId>
    <artifactId>spring-ai-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    
    <modules>
        <module>spring-ai-mcp-server</module>
        <module>spring-ai-mcp-client</module>
    </modules>
    
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.6</spring-ai.version>
    </properties>
    
    <!-- 公共依赖(所有子模块共享) -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- DeepSeek 模型支持 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-deepseek</artifactId>
        </dependency>
        
        <!-- 智谱 AI 模型支持 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-zhipuai</artifactId>
        </dependency>
    </dependencies>
    
    <!-- 依赖管理:统一 Spring AI 版本 -->
    <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>
        </dependencies>
    </dependencyManagement>
</project>

父 POM 的关键作用:

  1. 统一版本管理:通过 <properties> 定义 Spring AI 版本(1.1.6),所有子模块使用相同版本
  2. 依赖管理:通过 <dependencyManagement> 导入 Spring AI BOM,确保依赖版本一致性
  3. 公共依赖:将 Web、DeepSeek、智谱 AI 等通用依赖放在父 POM,子模块无需重复声明
  4. 模块化构建:通过 <modules> 声明子模块,支持一次性构建整个项目

子模块 POM 简化:

由于父 POM 已经管理了大部分依赖,子模块的 pom.xml 非常简洁:

<!-- spring-ai-mcp-server/pom.xml -->
<project>
    <parent>
        <groupId>cn.dianyu.ai</groupId>
        <artifactId>spring-ai-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    
    <artifactId>spring-ai-mcp-server</artifactId>
    
    <dependencies>
        <!-- 只需声明 MCP Server 特有依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
    </dependencies>
</project>
<!-- spring-ai-mcp-client/pom.xml -->
<project>
    <parent>
        <groupId>cn.dianyu.ai</groupId>
        <artifactId>spring-ai-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    
    <artifactId>spring-ai-mcp-client</artifactId>
    
    <dependencies>
        <!-- 只需声明 MCP Client 特有依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client</artifactId>
        </dependency>
    </dependencies>
</project>

优势:

  • ✅ 避免版本冲突:所有模块使用统一的 Spring AI 版本
  • ✅ 简化子模块配置:子模块只需关注自己的特有依赖
  • ✅ 便于升级:修改父 POM 中的版本号即可升级所有模块
  • ✅ 符合 Maven 最佳实践:采用父子 POM 结构

🚀 第一部分:构建 MCP Server

1.1 添加依赖

spring-ai-mcp-server/pom.xml 中添加 MCP Server Starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

支持的传输协议:

  • spring-ai-starter-mcp-server - STDIO 传输
  • spring-ai-starter-mcp-server-webmvc - WebMVC(SSE/Streamable-HTTP/Stateless)
  • spring-ai-starter-mcp-server-webflux - WebFlux(响应式)

1.2 配置文件

application.yaml 关键配置:

server:
  port: 8100

spring:
  ai:
    mcp:
      server:
        enabled: true
        protocol: STREAMABLE  # 使用 Streamable-HTTP 协议
        name: my-spring-ai-mcp-server
        version: 1.0.0
        type: SYNC            # 同步服务器类型
        instructions: "该服务器提供天气查询工具、系统信息资源和问候提示"
        
        # 启用所有能力
        capabilities:
          resource: true
          tool: true
          prompt: true
          completion: true
        
        # 变更通知
        resource-change-notification: true
        tool-change-notification: true
        prompt-change-notification: true
        
        # Streamable-HTTP 端点
        streamable-http:
          mcp-endpoint: /mcp
          keep-alive-interval: 30s
        
        # 启用注解自动扫描
        annotation-scanner:
          enabled: true
    
    zhipuai:
      api-key: your-api-key
      chat:
        enabled: true
        options:
          model: glm-4.5-air
          temperature: 0.7

1.3 创建 MCP Tools

使用 @McpTool 注解定义工具方法:

@Component
public class McpWeatherTools {

    private final Random random = new Random();

    /**
     * 获取指定城市的当前天气
     */
    @McpTool(name = "mcp_get_current_weather", 
             description = "获取指定城市的当前天气信息")
    public String getCurrentWeather(
            @McpToolParam(description = "城市名称,例如:北京、上海、广州", 
                         required = true) String cityName) {
        
        String[] conditions = {"晴", "多云", "小雨", "大雨", "雷阵雨", "雪"};
        String condition = conditions[random.nextInt(conditions.length)];
        int temperature = random.nextInt(35) - 5;
        int humidity = random.nextInt(60) + 30;

        return String.format("【%s】当前天气:%s,温度:%d°C,湿度:%d%%,更新时间:%s",
                cityName, condition, temperature, humidity, LocalDateTime.now());
    }

    /**
     * 获取天气预报(未来多天)
     */
    @McpTool(name = "mcp_get_weather_forecast", 
             description = "获取指定城市的天气预报(未来多天)")
    public String getWeatherForecast(
            @McpToolParam(description = "城市名称", required = true) String cityName,
            @McpToolParam(description = "预报天数(1-7天),默认5天", 
                         required = false) Integer days) {
        
        if (days == null || days < 1) days = 5;
        if (days > 7) days = 7;

        StringBuilder forecast = new StringBuilder();
        forecast.append(String.format("【%s】未来%d天天气预报:\n", cityName, days));

        String[] conditions = {"晴", "多云", "小雨", "大雨", "雷阵雨", "雪"};
        for (int i = 1; i <= days; i++) {
            String condition = conditions[random.nextInt(conditions.length)];
            int highTemp = random.nextInt(15) + 20;
            int lowTemp = highTemp - random.nextInt(10) - 5;
            forecast.append(String.format("  第%d天:%s,%d°C ~ %d°C\n",
                    i, condition, lowTemp, highTemp));
        }

        return forecast.toString();
    }

    /**
     * 比较两个城市的天气
     */
    @McpTool(name = "mcp_compare_weather", 
             description = "比较两个城市的天气情况")
    public String compareWeather(
            @McpToolParam(description = "第一个城市名称", required = true) String city1,
            @McpToolParam(description = "第二个城市名称", required = true) String city2) {
        
        String weather1 = getCurrentWeather(city1);
        String weather2 = getCurrentWeather(city2);
        return String.format("天气对比:\n%s\n%s", weather1, weather2);
    }

    /**
     * 获取空气质量指数
     */
    @McpTool(name = "mcp_get_air_quality", 
             description = "获取指定城市的空气质量指数(AQI)")
    public Map<String, Object> getAirQuality(
            @McpToolParam(description = "城市名称", required = true) String cityName) {
        
        Map<String, Object> result = new HashMap<>();
        int aqi = random.nextInt(200) + 10;
        
        String level;
        String color;
        if (aqi <= 50) {
            level = "优";
            color = "绿色";
        } else if (aqi <= 100) {
            level = "良";
            color = "黄色";
        } else if (aqi <= 150) {
            level = "轻度污染";
            color = "橙色";
        } else {
            level = "中度污染";
            color = "红色";
        }

        result.put("city", cityName);
        result.put("aqi", aqi);
        result.put("level", level);
        result.put("color", color);
        result.put("timestamp", LocalDateTime.now().toString());

        return result;
    }
}

关键点:

  • 使用 @Component 标记为 Spring Bean
  • @McpTool 定义工具名称和描述
  • @McpToolParam 定义参数及其描述,支持 required 属性
  • 支持返回字符串或结构化数据(Map、List 等)
  • Spring Boot 会自动扫描并注册这些工具

1.4 创建 MCP Resources

使用 @McpResource 注解定义资源访问:

@Component
public class SystemInfoResource {

    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 获取系统信息资源
     * 支持的 infoType:memory、cpu、system、jvm、all
     */
    @McpResource(
            uri = "system://{infoType}",
            name = "系统信息",
            description = "获取系统运行时信息,支持 memory、cpu、system、jvm、all 等类型"
    )
    public String getSystemInfo(String infoType) {
        try {
            Map<String, Object> systemInfo = new HashMap<>();

            switch (infoType.toLowerCase()) {
                case "memory":
                    systemInfo.put("type", "memory");
                    systemInfo.putAll(getMemoryInfo());
                    break;
                case "cpu":
                    systemInfo.put("type", "cpu");
                    systemInfo.putAll(getCpuInfo());
                    break;
                case "system":
                    systemInfo.put("type", "system");
                    systemInfo.putAll(getSystemInfo());
                    break;
                case "jvm":
                    systemInfo.put("type", "jvm");
                    systemInfo.putAll(getJvmInfo());
                    break;
                case "all":
                    systemInfo.put("type", "all");
                    systemInfo.put("memory", getMemoryInfo());
                    systemInfo.put("cpu", getCpuInfo());
                    systemInfo.put("system", getSystemInfo());
                    systemInfo.put("jvm", getJvmInfo());
                    break;
                default:
                    systemInfo.put("error", "不支持的信息类型: " + infoType);
                    systemInfo.put("supportedTypes", 
                        new String[]{"memory", "cpu", "system", "jvm", "all"});
            }

            systemInfo.put("timestamp", LocalDateTime.now().toString());
            return objectMapper.writeValueAsString(systemInfo);

        } catch (Exception e) {
            throw new RuntimeException("无法生成系统信息: " + infoType, e);
        }
    }

    private Map<String, Object> getMemoryInfo() {
        Map<String, Object> memoryInfo = new HashMap<>();
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        memoryInfo.put("heapUsed", 
            memoryMXBean.getHeapMemoryUsage().getUsed() / (1024 * 1024) + " MB");
        memoryInfo.put("heapMax", 
            memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + " MB");
        memoryInfo.put("nonHeapUsed", 
            memoryMXBean.getNonHeapMemoryUsage().getUsed() / (1024 * 1024) + " MB");
        return memoryInfo;
    }

    private Map<String, Object> getCpuInfo() {
        Map<String, Object> cpuInfo = new HashMap<>();
        OperatingSystemMXBean osMXBean = 
            ManagementFactory.getOperatingSystemMXBean();
        cpuInfo.put("availableProcessors", osMXBean.getAvailableProcessors());
        cpuInfo.put("systemLoadAverage", osMXBean.getSystemLoadAverage());
        return cpuInfo;
    }

    private Map<String, Object> getSystemInfo() {
        Map<String, Object> sysInfo = new HashMap<>();
        sysInfo.put("osName", System.getProperty("os.name"));
        sysInfo.put("osVersion", System.getProperty("os.version"));
        sysInfo.put("osArch", System.getProperty("os.arch"));
        sysInfo.put("userName", System.getProperty("user.name"));
        sysInfo.put("userHome", System.getProperty("user.home"));
        return sysInfo;
    }

    private Map<String, Object> getJvmInfo() {
        Map<String, Object> jvmInfo = new HashMap<>();
        jvmInfo.put("javaVersion", System.getProperty("java.version"));
        jvmInfo.put("javaVendor", System.getProperty("java.vendor"));
        jvmInfo.put("javaHome", System.getProperty("java.home"));
        jvmInfo.put("vmName", System.getProperty("java.vm.name"));
        jvmInfo.put("startTime", ManagementFactory.getRuntimeMXBean().getStartTime());
        jvmInfo.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
        return jvmInfo;
    }
}

关键点:

  • URI 模板支持动态参数:system://{infoType}
  • 返回 JSON 格式的结构化数据
  • 适合提供实时数据、文件内容、数据库查询结果等
  • 资源通过 URI 访问,客户端可以动态读取

1.5 创建 MCP Prompts

使用 @McpPrompt 注解定义提示模板:

@Component
public class GreetingPrompt {

    /**
     * 生成个性化问候提示
     */
    @McpPrompt(
            name = "greeting",
            description = "生成个性化的问候语,支持多种语言"
    )
    public String generateGreeting(
            @McpArg(name = "name", 
                   description = "要问候的姓名或称呼", 
                   required = true) String name,
            @McpArg(name = "language", 
                   description = "语言类型:zh-中文, en-英文", 
                   required = false) String language) {

        if (name == null || name.trim().isEmpty()) name = "朋友";
        if (language == null || language.trim().isEmpty()) language = "zh";

        if ("en".equalsIgnoreCase(language)) {
            return String.format("Hello %s! How can I assist you today?", name);
        } else {
            return String.format("你好,%s!今天我能帮你什么?", name);
        }
    }

    /**
     * 生成商务问候提示
     */
    @McpPrompt(
            name = "business_greeting",
            description = "生成专业的商务问候语"
    )
    public String generateBusinessGreeting(
            @McpArg(name = "recipientName", 
                   description = "收件人姓名", 
                   required = true) String recipientName,
            @McpArg(name = "senderName", 
                   description = "发件人姓名", 
                   required = true) String senderName) {

        if (recipientName == null || recipientName.trim().isEmpty()) 
            recipientName = "尊敬的客户";
        if (senderName == null || senderName.trim().isEmpty()) 
            senderName = "客服代表";

        return String.format(
                "尊敬的 %s:\n\n您好!我是 %s,很高兴为您服务。请问有什么可以帮助您的吗?",
                recipientName, senderName
        );
    }

    /**
     * 生成节日祝福提示
     */
    @McpPrompt(
            name = "festival_greeting",
            description = "生成节日祝福问候语"
    )
    public String generateFestivalGreeting(
            @McpArg(name = "recipientName", 
                   description = "收件人姓名", 
                   required = true) String recipientName,
            @McpArg(name = "festival", 
                   description = "节日名称:春节、中秋、圣诞、元旦等", 
                   required = true) String festival) {

        if (recipientName == null || recipientName.trim().isEmpty()) 
            recipientName = "朋友";
        if (festival == null || festival.trim().isEmpty()) 
            festival = "节日";

        return String.format(
                "亲爱的 %s:\n\n祝您%s快乐!愿您在新的一年里身体健康、工作顺利、万事如意!",
                recipientName, festival
        );
    }
}

关键点:

  • @McpPrompt 定义提示模板名称和描述
  • @McpArg 定义参数及其是否必需
  • 适合生成邮件模板、对话开场白、特定场景的提示等
  • 提示模板可以被客户端重复使用,支持参数化定制

1.6 启动 MCP Server

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

启动后,MCP Server 将在 http://localhost:8100/mcp 提供服务。

验证服务器:

# 检查服务器是否启动
curl http://localhost:8100/mcp

🔌 第二部分:构建 MCP Client

2.1 添加依赖

spring-ai-mcp-client/pom.xml 中添加 MCP Client Starter:

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

支持的传输协议:

  • spring-ai-starter-mcp-client - STDIO、HTTP/SSE、Streamable-HTTP(基于 JDK HttpClient)
  • spring-ai-starter-mcp-client-webflux - WebFlux 传输(推荐生产环境)

2.2 配置文件

application.yaml 关键配置:

server:
  port: 8200

spring:
  ai:
    mcp:
      server:
        enabled: false  # 客户端不需要启用 Server
      
      client:
        enabled: true
        name: my-mcp-client
        version: 1.0.0
        type: SYNC
        request-timeout: 30s
        
        # 启用工具回调集成
        toolcallback:
          enabled: true
        
        # 启用注解扫描
        annotation-scanner:
          enabled: true
        
        # 连接到本地 MCP Server
        streamable-http:
          connections:
            local-server:
              url: http://localhost:8100
              endpoint: /mcp
    
    zhipuai:
      api-key: your-api-key
      chat:
        enabled: true
        options:
          model: glm-4.5-air
          temperature: 0.7

2.3 基础聊天控制器

@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ZhiPuAiChatModel chatModel;
    private final SyncMcpToolCallbackProvider toolCallbackProvider;
    private final List<McpSyncClient> mcpSyncClients;

    public ChatController(ZhiPuAiChatModel chatModel,
                          SyncMcpToolCallbackProvider toolCallbackProvider,
                          List<McpSyncClient> mcpSyncClients) {
        this.chatModel = chatModel;
        this.toolCallbackProvider = toolCallbackProvider;
        this.mcpSyncClients = mcpSyncClients;
    }

    /**
     * 基础聊天:使用 MCP Tools
     */
    @GetMapping
    public ChatResponse chat(String message) {
        // 获取 MCP 工具作为 Spring AI 函数回调
        ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();

        ChatClient chatClient = ChatClient.create(chatModel);
        Prompt prompt = new Prompt(message, ChatOptions.builder().build());

        // 使用自定义的工具名称(由 Resource/Prompt 转换而来)
        return chatClient.prompt(prompt)
                .toolNames("system_info_reader", "greeting_generator", 
                          "business_greeting_generator", "festival_greeting_generator")
                .toolCallbacks(toolCallbackProvider)
                .call()
                .chatResponse();
    }
}

测试示例:

# 使用天气工具
curl "http://localhost:8200/chat?message=北京的天气怎么样?"

# 使用系统信息工具
curl "http://localhost:8200/chat?message=当前系统的内存使用情况如何?"

2.4 访问 MCP Resources

/**
 * 获取所有可用的 MCP Resources 列表
 */
@GetMapping("/resources")
public McpSchema.ListResourceTemplatesResult getResources() {
    if (mcpSyncClients.isEmpty()) {
        throw new IllegalStateException("没有可用的 MCP 客户端连接");
    }
    return mcpSyncClients.get(0).listResourceTemplates();
}

/**
 * 读取指定的 MCP Resource
 */
@GetMapping("/resource/read")
public McpSchema.ReadResourceResult readResource(@RequestParam String uri) {
    if (mcpSyncClients.isEmpty()) {
        throw new IllegalStateException("没有可用的 MCP 客户端连接");
    }
    
    McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest(uri);
    return mcpSyncClients.get(0).readResource(request);
}

测试示例:

# 获取所有资源模板
curl http://localhost:8200/chat/resources

# 读取系统内存信息
curl "http://localhost:8200/chat/resource/read?uri=system://memory"

# 读取所有系统信息
curl "http://localhost:8200/chat/resource/read?uri=system://all"

返回示例:

{
  "type": "memory",
  "heapUsed": "128 MB",
  "heapMax": "4096 MB",
  "nonHeapUsed": "45 MB",
  "timestamp": "2026-06-16T10:30:00"
}

2.5 使用 MCP Prompts

/**
 * 获取所有可用的 MCP Prompts 列表
 */
@GetMapping("/prompts")
public McpSchema.ListPromptsResult getPrompts() {
    if (mcpSyncClients.isEmpty()) {
        throw new IllegalStateException("没有可用的 MCP 客户端连接");
    }
    return mcpSyncClients.get(0).listPrompts();
}

/**
 * 使用 MCP Prompt 生成消息
 */
@PostMapping("/prompt/use")
public String usePrompt(@RequestParam String promptName,
                       @RequestParam(required = false) String name,
                       @RequestParam(required = false) String language) {
    if (mcpSyncClients.isEmpty()) {
        throw new IllegalStateException("没有可用的 MCP 客户端连接");
    }

    // 构建参数 Map
    Map<String, Object> arguments = new HashMap<>();
    if (name != null) arguments.put("name", name);
    if (language != null) arguments.put("language", language);

    McpSchema.GetPromptRequest request = 
        new McpSchema.GetPromptRequest(promptName, arguments);
    McpSchema.GetPromptResult result = 
        mcpSyncClients.get(0).getPrompt(request);

    // 将生成的提示内容发送给 LLM
    ChatClient chatClient = ChatClient.create(chatModel);
    String promptMessage = result.messages().get(0).content().toString();

    return chatClient.prompt(promptMessage).call().content();
}

测试示例:

# 获取所有提示模板
curl http://localhost:8200/chat/prompts

# 使用问候提示模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=greeting&name=张三&language=zh"

# 使用商务问候模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=business_greeting&recipientName=李总&senderName=王经理"

# 使用节日祝福模板
curl -X POST "http://localhost:8200/chat/prompt/use?promptName=festival_greeting&recipientName=赵先生&festival=春节"

🔄 第三部分:将 Resources 和 Prompts 转换为 Tools

这是一个高级技巧!通过将 Resources 和 Prompts 包装成 Spring AI Tools,我们可以让 LLM 自主决定何时调用它们,而不是硬编码调用逻辑。

3.1 Resource 转 Tool

创建 McpResourceToolConfig.java

@Component
public class McpResourceToolConfig {

    private final List<McpSyncClient> mcpSyncClients;
    private static final Logger log = LoggerFactory.getLogger(McpResourceToolConfig.class);

    public McpResourceToolConfig(List<McpSyncClient> mcpSyncClients) {
        this.mcpSyncClients = mcpSyncClients;
    }

    /**
     * 系统信息资源读取工具
     * 将 Resource 访问转换为 Spring AI Tool
     */
    @Bean("system_info_reader")
    @Description("读取系统运行时信息,支持 memory、cpu、system、jvm、all 等类型")
    public Function<SystemInfoRequest, SystemInfoResponse> systemInfoReader() {
        log.info("注册系统信息读取工具: systemInfoReader");

        return request -> {
            log.info("读取系统信息: infoType={}", request.infoType());

            if (mcpSyncClients.isEmpty()) {
                return new SystemInfoResponse("错误:没有可用的 MCP 客户端连接", false);
            }

            try {
                // 构建资源 URI
                String resourceUri = "system://" + request.infoType();
                
                // 调用 MCP 客户端读取资源
                McpSchema.ReadResourceRequest readRequest = 
                    new McpSchema.ReadResourceRequest(resourceUri);
                McpSchema.ReadResourceResult result = 
                    mcpSyncClients.get(0).readResource(readRequest);

                // 提取资源内容
                String content = result.contents().stream()
                        .findFirst()
                        .map(contentItem -> {
                            if (contentItem instanceof McpSchema.TextResourceContents textContent) {
                                return textContent.text();
                            }
                            return contentItem.toString();
                        })
                        .orElse("资源内容为空");

                log.info("成功读取系统信息: {}", request.infoType());
                return new SystemInfoResponse(content, true);

            } catch (Exception e) {
                log.error("读取系统信息失败: infoType={}", request.infoType(), e);
                return new SystemInfoResponse("读取失败: " + e.getMessage(), false);
            }
        };
    }

    /**
     * 系统信息请求 DTO
     */
    public record SystemInfoRequest(String infoType) {}

    /**
     * 系统信息响应 DTO
     */
    public record SystemInfoResponse(String content, boolean success) {}
}

工作原理:

  1. 创建一个 Spring Bean,类型为 Function<Request, Response>
  2. 使用 @Bean("system_info_reader") 指定工具名称
  3. 使用 @Description 提供工具描述(LLM 根据描述决定是否调用)
  4. 在函数内部调用 McpSyncClient.readResource() 读取资源
  5. LLM 可以根据用户需求自主调用这个工具

3.2 Prompt 转 Tool

创建 McpPromptToolConfig.java

@Component
public class McpPromptToolConfig {

    private final List<McpSyncClient> mcpSyncClients;
    private static final Logger log = LoggerFactory.getLogger(McpPromptToolConfig.class);

    public McpPromptToolConfig(List<McpSyncClient> mcpSyncClients) {
        this.mcpSyncClients = mcpSyncClients;
    }

    /**
     * 通用问候语生成工具
     */
    @Bean("greeting_generator")
    @Description("生成个性化问候语,支持中英文,参数:name-姓名, language-语言(zh/en)")
    public Function<GreetingRequest, GreetingResponse> greetingGenerator() {
        log.info("注册问候语生成工具: greetingGenerator");

        return request -> {
            log.info("生成问候语: name={}, language={}", request.name(), request.language());

            if (mcpSyncClients.isEmpty()) {
                return new GreetingResponse("错误:没有可用的 MCP 客户端连接", false);
            }

            try {
                // 构建参数 Map
                Map<String, Object> arguments = new HashMap<>();
                if (request.name() != null && !request.name().isEmpty()) {
                    arguments.put("name", request.name());
                }
                if (request.language() != null && !request.language().isEmpty()) {
                    arguments.put("language", request.language());
                }

                // 调用 MCP 客户端获取提示
                McpSchema.GetPromptRequest promptRequest = 
                    new McpSchema.GetPromptRequest("greeting", arguments);
                McpSchema.GetPromptResult result = 
                    mcpSyncClients.get(0).getPrompt(promptRequest);

                // 提取提示消息内容
                String content = result.messages().stream()
                        .findFirst()
                        .map(message -> message.content().toString())
                        .orElse("提示内容为空");

                log.info("成功生成问候语: {}", request.name());
                return new GreetingResponse(content, true);

            } catch (Exception e) {
                log.error("生成问候语失败: name={}", request.name(), e);
                return new GreetingResponse("生成失败: " + e.getMessage(), false);
            }
        };
    }

    /**
     * 商务问候语生成工具
     */
    @Bean("business_greeting_generator")
    @Description("生成商务问候语,参数:recipientName-收件人姓名, senderName-发件人姓名")
    public Function<BusinessGreetingRequest, BusinessGreetingResponse> businessGreetingGenerator() {
        log.info("注册商务问候语生成工具: businessGreetingGenerator");

        return request -> {
            log.info("生成商务问候语: recipientName={}, senderName={}", 
                    request.recipientName(), request.senderName());

            if (mcpSyncClients.isEmpty()) {
                return new BusinessGreetingResponse("错误:没有可用的 MCP 客户端连接", false);
            }

            try {
                Map<String, Object> arguments = new HashMap<>();
                if (request.recipientName() != null && !request.recipientName().isEmpty()) {
                    arguments.put("recipientName", request.recipientName());
                }
                if (request.senderName() != null && !request.senderName().isEmpty()) {
                    arguments.put("senderName", request.senderName());
                }

                McpSchema.GetPromptRequest promptRequest = 
                    new McpSchema.GetPromptRequest("business_greeting", arguments);
                McpSchema.GetPromptResult result = 
                    mcpSyncClients.get(0).getPrompt(promptRequest);

                String content = result.messages().stream()
                        .findFirst()
                        .map(message -> message.content().toString())
                        .orElse("提示内容为空");

                log.info("成功生成商务问候语");
                return new BusinessGreetingResponse(content, true);

            } catch (Exception e) {
                log.error("生成商务问候语失败", e);
                return new BusinessGreetingResponse("生成失败: " + e.getMessage(), false);
            }
        };
    }

    /**
     * 节日祝福生成工具
     */
    @Bean("festival_greeting_generator")
    @Description("生成节日祝福问候语,参数:recipientName-收件人姓名, festival-节日名称")
    public Function<FestivalGreetingRequest, FestivalGreetingResponse> festivalGreetingGenerator() {
        log.info("注册节日祝福生成工具: festivalGreetingGenerator");

        return request -> {
            log.info("生成节日祝福: recipientName={}, festival={}", 
                    request.recipientName(), request.festival());

            if (mcpSyncClients.isEmpty()) {
                return new FestivalGreetingResponse("错误:没有可用的 MCP 客户端连接", false);
            }

            try {
                Map<String, Object> arguments = new HashMap<>();
                if (request.recipientName() != null && !request.recipientName().isEmpty()) {
                    arguments.put("recipientName", request.recipientName());
                }
                if (request.festival() != null && !request.festival().isEmpty()) {
                    arguments.put("festival", request.festival());
                }

                McpSchema.GetPromptRequest promptRequest = 
                    new McpSchema.GetPromptRequest("festival_greeting", arguments);
                McpSchema.GetPromptResult result = 
                    mcpSyncClients.get(0).getPrompt(promptRequest);

                String content = result.messages().stream()
                        .findFirst()
                        .map(message -> message.content().toString())
                        .orElse("提示内容为空");

                log.info("成功生成节日祝福");
                return new FestivalGreetingResponse(content, true);

            } catch (Exception e) {
                log.error("生成节日祝福失败", e);
                return new FestivalGreetingResponse("生成失败: " + e.getMessage(), false);
            }
        };
    }

    // DTO 定义
    public record GreetingRequest(String name, String language) {}
    public record GreetingResponse(String content, boolean success) {}
    
    public record BusinessGreetingRequest(String recipientName, String senderName) {}
    public record BusinessGreetingResponse(String content, boolean success) {}
    
    public record FestivalGreetingRequest(String recipientName, String festival) {}
    public record FestivalGreetingResponse(String content, boolean success) {}
}

3.3 为什么需要转换?

传统方式(硬编码):

// 开发者需要手动判断何时调用 Resource/Prompt
if (userAsksForSystemInfo()) {
    readResource("system://memory");
}

转换后的方式(LLM 自主决策):

// LLM 根据用户意图自主决定调用哪个工具
ChatClient chatClient = ChatClient.create(chatModel);
return chatClient.prompt(userMessage)
        .toolCallbacks(toolCallbackProvider)  // 包含所有转换后的工具
        .call()
        .chatResponse();

优势:

  • ✅ LLM 可以根据自然语言理解自主选择合适的工具
  • ✅ 减少硬编码的条件判断逻辑
  • ✅ 更符合 AI Agent 的设计理念
  • ✅ 提高系统的灵活性和可扩展性

🎨 第四部分:综合应用场景

4.1 高级聊天控制器

创建一个同时使用 Tools、Resources 和 Prompts 的高级聊天接口:

/**
 * 综合示例:同时使用 Tools、Resources 和 Prompts
 */
@PostMapping("/advanced")
public ChatResponse advancedChat(@RequestBody AdvancedChatRequest request) {
    // 1. 获取工具
    ToolCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();

    // 2. 可选:先读取某个资源作为上下文
    String enhancedMessage = request.getMessage();
    if (request.getResourceUri() != null && !mcpSyncClients.isEmpty()) {
        try {
            McpSchema.ReadResourceRequest resourceRequest =
                new McpSchema.ReadResourceRequest(request.getResourceUri());
            McpSchema.ReadResourceResult resourceResult =
                mcpSyncClients.get(0).readResource(resourceRequest);
            // 将资源内容添加到用户消息中
            enhancedMessage = request.getMessage() + "\n\n参考资源: " + resourceResult.contents();
        } catch (Exception e) {
            // 资源读取失败时继续执行
            System.err.println("读取资源失败: " + e.getMessage());
        }
    }

    // 3. 可选:使用提示模板
    if (request.getPromptName() != null && !mcpSyncClients.isEmpty()) {
        try {
            McpSchema.GetPromptRequest promptRequest =
                new McpSchema.GetPromptRequest(request.getPromptName(), request.getPromptArgs());
            McpSchema.GetPromptResult promptResult = 
                mcpSyncClients.get(0).getPrompt(promptRequest);
            enhancedMessage = promptResult.messages().get(0).content().toString();
        } catch (Exception e) {
            System.err.println("使用提示模板失败: " + e.getMessage());
        }
    }

    // 4. 调用 LLM
    ChatClient chatClient = ChatClient.create(chatModel);
    Prompt prompt = new Prompt(enhancedMessage, ChatOptions.builder().build());

    return chatClient.prompt(prompt)
            .toolCallbacks(mcpTools)
            .call()
            .chatResponse();
}

/**
 * 高级聊天请求 DTO
 */
@Data
public static class AdvancedChatRequest {
    private String message;
    private String resourceUri;      // 可选:资源 URI
    private String promptName;       // 可选:提示模板名称
    private Map<String, Object> promptArgs;  // 可选:提示模板参数
}

测试示例:

curl -X POST http://localhost:8200/chat/advanced \
  -H "Content-Type: application/json" \
  -d '{
    "message": "请帮我分析当前系统状态",
    "resourceUri": "system://all",
    "promptName": "greeting",
    "promptArgs": {
      "name": "管理员",
      "language": "zh"
    }
  }'

4.2 实际应用场景

场景 1:智能客服系统
// 用户提问:"我需要给张总发送一封商务邮件"
// LLM 自动调用:
// 1. business_greeting_generator(recipientName="张总", senderName="客服")
// 2. 生成完整的商务邮件内容
场景 2:系统监控助手
// 用户提问:"当前服务器的负载情况如何?"
// LLM 自动调用:
// 1. system_info_reader(infoType="cpu")
// 2. system_info_reader(infoType="memory")
// 3. 综合分析并给出建议
场景 3:节日营销自动化
// 用户提问:"春节快到了,帮我给客户王先生写个祝福"
// LLM 自动调用:
// 1. festival_greeting_generator(recipientName="王先生", festival="春节")
// 2. 生成个性化的节日祝福

🔧 第五部分:进阶配置与最佳实践

5.1 同步 vs 异步服务器

同步服务器(SYNC):

spring:
  ai:
    mcp:
      server:
        type: SYNC  # 默认
  • 适用于传统的请求-响应模式
  • 仅注册同步的 MCP 注解方法
  • 简单易用,适合大多数场景

异步服务器(ASYNC):

spring:
  ai:
    mcp:
      server:
        type: ASYNC
  • 适用于响应式应用程序
  • 仅注册异步的 MCP 注解方法
  • 非阻塞操作,高吞吐量

5.2 协议选择

Streamable-HTTP(推荐):

spring:
  ai:
    mcp:
      server:
        protocol: STREAMABLE
  • 替代旧的 SSE 传输
  • 支持 HTTP POST 和 GET
  • 可选择使用 SSE 流式传输

无状态服务器(STATELESS):

spring:
  ai:
    mcp:
      server:
        protocol: STATELESS
  • 不维护会话状态
  • 适合微服务架构和云原生部署
  • 简化部署模型

5.3 工具名称前缀生成

当连接多个 MCP 服务器时,可能出现工具名称冲突。可以使用自定义前缀生成器:

@Component
public class CustomToolNamePrefixGenerator implements McpToolNamePrefixGenerator {

    @Override
    public String prefixedToolName(McpConnectionInfo connectionInfo, Tool tool) {
        // 使用服务器名称作为前缀
        String serverName = connectionInfo.initializeResult().serverInfo().name();
        return serverName + "_" + tool.name();
    }
}

5.4 工具过滤

选择性包含/排除工具:

@Component
public class CustomMcpToolFilter implements McpToolFilter {

    @Override
    public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {
        // 排除实验性工具
        if (tool.description() != null && 
            tool.description().contains("experimental")) {
            return false;
        }
        return true;
    }
}

5.5 客户端定制

自定义客户端行为:

@Component
public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer {
    @Override
    public void customize(String serverConfigurationName, McpClient.SyncSpec spec) {
        // 自定义请求超时
        spec.requestTimeout(Duration.ofSeconds(30));

        // 添加工具变更监听
        spec.toolsChangeConsumer((List<McpSchema.Tool> tools) -> {
            System.out.println("工具列表已更新: " + tools.size() + " 个工具可用");
        });

        // 添加日志消息处理
        spec.loggingConsumer((McpSchema.LoggingMessageNotification log) -> {
            System.out.println("收到日志: " + log.level() + " - " + log.data());
        });
    }
}

🎓 总结

通过本篇教程,我们全面学习了 Spring AI MCP 的核心概念和实践应用:

核心要点回顾

  1. MCP 三大能力:

    • Tools:执行操作
    • Resources:数据访问
    • Prompts:提示模板
  2. Server 开发:

    • 使用 @McpTool@McpResource@McpPrompt 注解
    • 支持同步/异步两种模式
    • 支持多种传输协议(STDIO、SSE、Streamable-HTTP)
  3. Client 开发:

    • 自动发现和使用 Server 暴露的能力
    • 可以将 Resources 和 Prompts 转换为 Tools
    • 支持工具过滤和前缀生成
  4. 最佳实践:

    • 优先使用 Streamable-HTTP 协议
    • 生产环境使用 WebFlux 传输
    • 合理配置超时和重试策略
    • 做好日志记录和监控

参考资源

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐