1.什么是MCP

MCP是AI界的USB标准接口,可以连接数据库,浏览器,GIT,API接口等等。
工具方按 MCP 协议暴露服务 → MCP Server
AI 应用按 MCP 协议调用工具 → MCP Client
任何 MCP Client 可以连任何 MCP Server,不需要额外适配

Function Calling 是 MCP 的底层机制——MCP Server 注册的工具,最终还是通过 Function Calling 的方式被模型调用。MCP 是在上面加了一层标准化的传输和发现协议。两个概念不冲突,是不同层次的东西。
MCP暴露了三种能力,tools,resource,prompt,

MCP的架构是什么样的

1.三个角色,宿主机,客户端,服务端
2.两种传输模式
stdio——本地模式
Host 进程
└── 启动 MCP Server 子进程
└── 通过 stdin/stdout 收发 JSON 消息
特点:
● Server 和 Client 在同一台机器
● Server 随 Host 启动,Host 关闭时 Server 也退出
● 没有网络开销,延迟低
适合: 开发调试、本地工具、Cursor 连本地 Server
SSE——远程模式
MCP Client(HTTP 请求)
→ MCP Server(Web 服务,跑在 8080 端口)
← SSE 长连接推送响应
特点:
● Server 独立部署,可以跑在任何地方
● 多个 Client 可以连同一个 Server
● 支持认证、权限控制
适合: 生产环境、公司内部多团队共享的工具服务

3.通信协议
MCP 基于 JSON-RPC 2.0 协议,所有消息都是 JSON。

// Client → Server:初始化请求
{
  "jsonrpc": "2.0",    // 固定值,使用 JSON-RPC 2.0 规范
  "id": 1,             // 请求 ID,Server 响应时会带同一个 id,用来对应请求和响应
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",  // MCP 协议版本,双方用它协商兼容性
    "clientInfo": {
      "name": "my-agent",     // Client 的名称,调试日志里能看到是谁在连
      "version": "1.0.0"
    },
    "capabilities": {
      "tools": {}   // 声明 Client 支持工具调用能力(空对象表示支持但无额外配置)
    }
  }
}

// Server → Client:初始化响应
{
  "jsonrpc": "2.0",
  "id": 1,          // 和请求的 id 一致,Client 靠这个对应是哪条请求的回包
  "result": {
    "protocolVersion": "2025-03-26",  // Server 确认使用的协议版本
    "serverInfo": {
      "name": "my-mcp-server",
      "version": "1.0.0"
    },
    "capabilities": {
      "tools": { "listChanged": true },  // 支持工具列表,listChanged=true 表示工具可能动态变化,Client 可监听变更通知
      "resources": {},   // 支持 Resources(文件/数据暴露),空对象表示支持
      "prompts": {}      // 支持 Prompts(可复用提示词模板),空对象表示支持
    }
  }
}

capabilities 是双方的"能力握手"——Client 声明自己能用哪些功能,Server 声明自己提供哪些功能,两边取交集,后续通信只用双方都支持的部分。不声明的功能直接跳过,不会报错。
发现工具列表

// Client → Server:查询有哪些工具
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"   // 固定方法名,握手完成后第一件事就是调这个,拿到工具清单
}

// Server → Client:返回工具列表
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "query_orders",              // 工具名,LLM 决策时用这个名字来选工具
        "description": "查询订单数据,支持按日期范围、品类筛选",  // 极其重要:LLM 靠这段描述判断要不要调这个工具
        "inputSchema": {                     // JSON Schema 格式,定义入参结构
          "type": "object",
          "properties": {
            "startDate": { "type": "string", "description": "开始日期 yyyy-MM-dd" },
            "endDate":   { "type": "string", "description": "结束日期 yyyy-MM-dd" }
          },
          "required": ["startDate", "endDate"]  // 必填参数,LLM 必须提供这两个才能调用
        }
      }
    ]
  }
}

调用工具

// Client → Server:调用工具
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "query_orders",    // 要调用哪个工具,对应 tools/list 返回的 name
    "arguments": {             // LLM 生成的入参,结构必须符合该工具的 inputSchema
      "startDate": "2026-03-01",
      "endDate": "2026-03-26"
    }
  }
}

// Server → Client:工具执行结果
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [               // 结果是数组,支持多段内容(文本、图片等混合返回)
      {
        "type": "text",        // 内容类型,还可以是 image、resource 等
        "text": "2026-03-01 至 2026-03-26 订单数据:总销售额 ¥368,200,订单量 1,547 单..."
      }
    ],
    "isError": false           // false=正常;true=工具执行出错,text 里是错误信息,LLM 会据此决定下一步
  }
}

交互流程

1. Host 启动 MCP Server
   (stdio:fork 子进程;SSE:建立 HTTP 长连接)

2. Client 发送 initialize 请求
   Server 返回自己支持的 capabilities{
  "capabilities": {
    "tools": {
      "listChanged": true   // 工具列表可能动态变化,支持推送通知
    },
    "resources": {
      "subscribe": true,    // 支持资源变更订阅
      "listChanged": true
    },
    "prompts": {
      "listChanged": false
    },
    "logging": {}           // 支持日志消息推送
  }
}

3. Client 发送 initialized 通知(确认握手完成)

4. Client 查询工具列表(tools/list)
   → 把工具信息传给 AI 模型作为 Function Calling 定义

5. 用户发消息,模型决定调某个工具

6. Client 发起工具调用(tools/call)
   Server 执行,返回结果

7. 结果作为 Observation 传回给模型
   模型继续推理,直到任务完成

8. 连接关闭

Capabilities 协商
初始化时,Client 和 Server 互相声明各自支持哪些能力,这个机制叫 Capabilities 协商:

{
  "capabilities": {
    "tools": {
      "listChanged": true   // 工具列表可能动态变化,支持推送通知
    },
    "resources": {
      "subscribe": true,    // 支持资源变更订阅
      "listChanged": true
    },
    "prompts": {
      "listChanged": false
    },
    "logging": {}           // 支持日志消息推送
  }
}

tools.listChanged: true 的意思是:如果 Server 上的工具列表运行时发生了变化,Server 会主动推送通知给 Client,Client 重新拉取工具列表。动态工具注册场景用得上。

MCP Tools——实现工具调用服务端(stdio 模式)

MCP Server 和 Host(Cursor)之间需要通信,通信方式有两种:
stdio 模式:Host 直接在本地启动 MCP Server 进程,双方通过标准输入输出(stdin/stdout)管道通信。Cursor 配置文件里写一条启动命令,Cursor 启动时把 MCP Server 进程拉起来,消息来回通过管道传。整个过程在本地,不走网络。
SSE 模式:MCP Server 单独部署成一个 HTTP 服务,Host 通过网络连过去,消息用 Server-Sent Events 推送。适合生产环境、多个 Client 共享一个 Server 的场景。
鸡哥的策略是先跑通 stdio 模式——本地调试最方便,不用起服务不用配端口,验证工具能被正确调用之后,后面再改成 SSE 生产部署。

<?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.11</version>
    </parent>
    <groupId>com.jichi</groupId>
    <artifactId>mcp-tools-server</artifactId>
    <version>1.0.0</version>
    <name>mcp-tools-server</name>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.1.2</spring-ai.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!-- Spring AI BOM,统一管理所有 spring-ai-* 依赖版本 -->
            <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>
    <dependencies>
        <!-- MCP Server 核心:包含协议实现 + Spring 自动装配 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server</artifactId>
        </dependency>
        <!--
            Spring Boot WebMCP 自动配置内部依赖 spring-web 类(StandardServletEnvironment),
            不引入会报 ClassNotFoundException。
            通过 application.yml 里的 web-application-type: none,Tomcat 不会真正启动。
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Lombok@Slf4j@Component 等注解简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 打可执行 fat jar,Host 用 java -jar 启动 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意两个关键点:
● 要加 spring-boot-starter-web,但要关掉 Tomcat:Spring AI MCP 自动配置内部用到了 spring-web 里的类,不引入会报 ClassNotFoundException: StandardServletEnvironment。但 stdio 模式不需要 HTTP 服务器,用 web-application-type: none 阻止 Tomcat 启动就行,spring-web 的类还在,MCP 能正常工作。
● 必须打 fat jar:Host(Cursor)通过 java -jar 命令启动 Server,依赖要全部打进去

# application.yml
spring:
  main:
    web-application-type: none    # 不启动 Web 服务器
    banner-mode: off              # 关掉 Banner,避免污染 stdio 输出
  ai:
    mcp:
      server:
        name: wjl-tools-server
        version: 1.0.0
        type: SYNC                # SYNCASYNC,初学用 SYNC

logging:
  config: classpath:logback-spring.xml  # 强制用自定义配置,日志走 stderr + 文件,绝不写 stdout

还需要在 src/main/resources/ 下新建 logback-spring.xml:host靠解析stdout里面的json通信,日志必须写道文件里,否则解析协议直接崩溃,

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- 控制台输出到 System.err,不能用 System.out,否则会污染 MCP stdio 通道 -->
    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 文件输出,方便排查问题 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>mcp-server.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>mcp-server.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="WARN">
        <appender-ref ref="STDERR"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

MCP Server 的工具实现方式和 Spring AI 的 @Tool 完全一样,

package com.jichi.mcp;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

@Component
public class BusinessTools {

    @Tool(description = """
            查询指定日期范围内的销售汇总数据。
            返回:总销售额、订单量、客单价、环比变化。
            适用于:分析销售趋势、生成数据报告。
            """)
    public String querySales(
            @ToolParam(description = "开始日期,格式 yyyy-MM-dd") String startDate,
            @ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate) {

        // 演示用模拟数据,实际接数据库
        return String.format("""
                %s 至 %s 销售数据:
                总销售额:¥128,450.00
                订单量:543 单
                客单价:¥236.56
                环比上期:+8.3%%
                """, startDate, endDate);
    }

    @Tool(description = """
            查询商品当前库存状态。
            返回:商品名称、当前库存量、库存状态(充足/预警/缺货)。
            """)
    public String queryInventory(
            @ToolParam(description = "商品名称关键词,支持模糊匹配") String keyword) {

        return String.format("""
                搜索「%s」的库存结果:
                无线耳机 Pro:库存 45 件(充足)
                无线耳机 Lite:库存 8 件(预警,安全线 20 件)
                有线耳机 X1:库存 0 件(缺货)
                """, keyword);
    }

    @Tool(description = "获取当前日期,以及本周、上周、本月的起止日期。用于其他工具需要日期参数时辅助计算。")
    public String getDateInfo() {
        LocalDate today = LocalDate.now();
        LocalDate monday = today.with(java.time.DayOfWeek.MONDAY);
        LocalDate lastMonday = monday.minusWeeks(1);

        return String.format("""
                今天:%s
                本周:%s 至 %s
                上周:%s 至 %s
                本月:%s 至今
                """,
                today,
                monday, today,
                lastMonday, monday.minusDays(1),
                today.withDayOfMonth(1));
    }
}

description 写好是关键——模型完全靠这段描述来判断要不要调用这个工具、怎么传参。写得含糊,工具调用就会出问题。
Spring AI MCP 不会自动扫描 @Tool,需要手动注册成 ToolCallbackProvider:

package com.jichi.mcp;

import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class McpServerApplication {

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

    @Bean
    public ToolCallbackProvider businessToolCallbacks(BusinessTools tools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(tools)
                .build();
    }
}

MCP Server 以 jar 包形式运行,Host 通过命令行启动它:

mvn clean package -DskipTests

# 验证能否正常启动(不会打印 "Started on port 8080",这是正常的)
# stdio 模式没有 HTTP 端口,程序启动后静待 stdin 输入
# 实际使用时由 Cursor 自动拉起这个进程,不需要手动运行
java -jar target/mcp-tools-server-1.0.0.jar

Cursor 从 0.43 版本开始原生支持 MCP。打开 Cursor Settings → MCP,点击 Add new MCP server,填写配置:

{
  "mcpServers": {
    "jichi-tools": {
      "command": "java",
      "args": [
        "-jar",
        "/Users/yourname/projects/mcp-server/target/mcp-tools-server-1.0.0.jar"
      ],
      "env": {
        "JAVA_OPTS": "-Xmx256m"
      }
    }
  }
}

在 Cursor 的 Agent 模式里直接提问,就能触发工具调用:
请查一下上周的销售数据
Cursor Agent 会自动调用 getDateInfo() 和 querySales(),执行结果会内联显示在对话里。

工具的错误处理

@Tool(description = "查询指定订单详情")
public String getOrderDetail(
        @ToolParam(description = "订单号,格式 ORD + 数字,如 ORD20240115001") String orderId) {

    if (!orderId.matches("ORD\\d+")) {
        return "订单号格式不正确,应为 ORD + 数字,例如 ORD20240115001";
    }

    try {
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null) {
            return "未找到订单:" + orderId;
        }
        return formatOrder(order);
    } catch (Exception e) {
        log.error("查询订单失败", e);  // 日志写文件!
        return "查询订单时发生错误,请稍后重试";
    }
}

为什么要返回错误文字而不是抛异常?因为抛异常模型什么都不知道,只会看到工具调用失败;返回有意义的错误信息,模型可以根据错误内容决定下一步——比如提示用户订单号格式不对、或者换个方式重试。
如果确实需要让 MCP 协议层感知到错误(isError: true),可以用 McpException:

import org.springframework.ai.mcp.McpException;

throw new McpException("数据库连接失败,请检查数据库状态");

MCP Resources——把数据和文档暴露给模型

Resources 的两个核心概念:
每个资源有一个唯一的 URI,格式自由,遵循 scheme://path 就行:

file:///etc/config.yaml          → 读取本地配置文件
db://orders/ORD20240115001       → 读取某条订单记录
docs://wiki/onboarding           → 读取员工手册
https://api.example.com/status   → 读取 API 状态

URI 的 scheme 可以自定义,docs://、db://、config:// 都是大家自己定的,只要系统内一致就行。

Resource Template(资源模板)
资源是动态的(需要参数)时,用 URI Template(RFC 6570)定义:

db://orders/{orderId}           → orderId 是参数
docs://wiki/{category}/{title}  → 两个参数
file://logs/{date}              → 按日期读日志

静态资源就是 URI 固定、不需要参数的资源,比如公司手册、API 文档总览。

package com.jichi.mcp;

import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class McpResourcesConfig {

    @Bean
    public List<McpServerFeatures.SyncResourceSpecification> staticResources() {
        return List.of(
                new McpServerFeatures.SyncResourceSpecification(
                        new McpSchema.Resource(
                                "docs://handbook/onboarding",
                                "员工入职手册",
                                "公司员工入职流程和规范文档",
                                "text/plain",
                                null
                        ),
                        (exchange, resourceRequest) -> readStaticResource(resourceRequest)
                ),
                new McpServerFeatures.SyncResourceSpecification(
                        new McpSchema.Resource(
                                "docs://api/overview",
                                "API 接口文档总览",
                                "后端 API 接口清单和说明",
                                "text/markdown",
                                null
                        ),
                        (exchange, resourceRequest) -> readStaticResource(resourceRequest)
                ),
                new McpServerFeatures.SyncResourceSpecification(
                        new McpSchema.Resource(
                                "config://app/production",
                                "生产环境配置",
                                "应用生产环境的配置参数(脱敏版)",
                                "application/json",
                                null
                        ),
                        (exchange, resourceRequest) -> readStaticResource(resourceRequest)
                )
        );
    }

    private McpSchema.ReadResourceResult readStaticResource(McpSchema.ReadResourceRequest resourceRequest) {
        String uri = resourceRequest.uri();
        String content = switch (uri) {
            case "docs://handbook/onboarding" -> readOnboardingDoc();
            case "docs://api/overview"        -> readApiDoc();
            case "config://app/production"    -> readProductionConfig();
            default -> "资源不存在:" + uri;
        };

        return new McpSchema.ReadResourceResult(
                List.of(new McpSchema.TextResourceContents(uri, "text/plain", content))
        );
    }

    private String readOnboardingDoc() {
        // 实际可以从文件、数据库、CMS 读取,这里用硬编码演示
        return """
                # 员工入职手册
                
                ## 入职第一天
                1. 领取工牌和电脑
                2. 配置 VPN 和开发环境
                3. 阅读代码规范文档
                
                ## 常用系统
                - OA 系统:https://oa.company.com
                - 代码仓库:https://git.company.com
                - 知识库:https://wiki.company.com
                """;
    }

    private String readApiDoc() {
        return """
                # API 接口总览
                
                ## 订单模块
                - GET /api/orders/{id} 查询订单详情
                - POST /api/orders 创建订单
                - PUT /api/orders/{id}/cancel 取消订单
                
                ## 用户模块
                - GET /api/users/{id} 查询用户信息
                """;
    }

    private String readProductionConfig() {
        return """
                {
                  "app.timeout": 30,
                  "cache.ttl": 3600,
                  "rate.limit": 100,
                  "feature.new_checkout": true
                }
                """;
    }
}

订单详情、用户信息这类需要传 ID 的场景,用模板定义。对应类型是 McpServerFeatures.SyncResourceTemplateSpecification(每个模板一条元数据 + 读取函数;旧版 SyncResourceTemplateRegistrationCallback 已不存在)。

package com.jichi.mcp;

import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

/** 演示用订单快照;正式项目可改为 JPA 实体。 */
record DemoOrder(
        String id,
        String statusText,
        String productName,
        double amount,
        String createdAt,
        String address
) {}

/** 演示用用户快照;正式项目可改为 JPA 实体。 */
record DemoUser(
        long id,
        String nickname,
        String memberLevel,
        LocalDate createdAt,
        int orderCount
) {}

@Configuration
public class McpResourceTemplatesConfig {

    private final Map<String, DemoOrder> ordersById;
    private final Map<String, DemoUser> usersById;

    public McpResourceTemplatesConfig() {
        this.ordersById = Map.of(
                "ORD20240115001",
                new DemoOrder(
                        "ORD20240115001",
                        "已发货",
                        "华为 Mate 70 Pro",
                        13999.0,
                        "2026-01-15 14:30",
                        "上海市浦东新区世纪大道100号"
                )
        );
        this.usersById = Map.of(
                "1001",
                new DemoUser(1001L, "阿强", "黄金会员", LocalDate.of(2023, 6, 1), 28)
        );
    }

    @Bean
    public List<McpServerFeatures.SyncResourceTemplateSpecification> resourceTemplates() {
        return List.of(
                new McpServerFeatures.SyncResourceTemplateSpecification(
                        new McpSchema.ResourceTemplate(
                                "db://orders/{orderId}",
                                "订单详情",
                                "根据订单号读取完整的订单信息",
                                "text/plain",
                                null
                        ),
                        (exchange, request) -> readTemplatedResource(request)
                ),
                new McpServerFeatures.SyncResourceTemplateSpecification(
                        new McpSchema.ResourceTemplate(
                                "db://users/{userId}",
                                "用户信息",
                                "根据用户 ID 读取用户基本信息",
                                "text/plain",
                                null
                        ),
                        (exchange, request) -> readTemplatedResource(request)
                ),
                new McpServerFeatures.SyncResourceTemplateSpecification(
                        new McpSchema.ResourceTemplate(
                                "file://logs/{date}",
                                "应用日志",
                                "读取指定日期的应用日志,date 格式 yyyy-MM-dd",
                                "text/plain",
                                null
                        ),
                        (exchange, request) -> readTemplatedResource(request)
                )
        );
    }

    private McpSchema.ReadResourceResult readTemplatedResource(McpSchema.ReadResourceRequest request) {
        String uri = request.uri();
        String content;

        if (uri.startsWith("db://orders/")) {
            String orderId = uri.substring("db://orders/".length());
            content = readOrderById(orderId);
        } else if (uri.startsWith("db://users/")) {
            String userId = uri.substring("db://users/".length());
            content = readUserById(userId);
        } else if (uri.startsWith("file://logs/")) {
            String date = uri.substring("file://logs/".length());
            content = readLogByDate(date);
        } else {
            content = "未知资源 URI:" + uri;
        }

        return new McpSchema.ReadResourceResult(
                List.of(new McpSchema.TextResourceContents(uri, "text/plain", content))
        );
    }

    private String readOrderById(String orderId) {
        DemoOrder order = ordersById.get(orderId);
        if (order == null) {
            return "未找到订单:" + orderId;
        }

        return String.format("""
                订单号:%s
                状态:%s
                商品:%s
                金额:¥%.2f
                下单时间:%s
                收货地址:%s
                """,
                order.id(), order.statusText(),
                order.productName(), order.amount(),
                order.createdAt(), order.address());
    }

    private String readUserById(String userId) {
        DemoUser user = usersById.get(userId);
        if (user == null) {
            return "未找到用户:" + userId;
        }

        return String.format("""
                用户 ID:%s
                昵称:%s
                会员等级:%s
                注册时间:%s
                累计订单:%d 单
                """,
                user.id(), user.nickname(),
                user.memberLevel(), user.createdAt(),
                user.orderCount());
    }

    private String readLogByDate(String date) {
        java.nio.file.Path logPath = java.nio.file.Paths.get(
                "/var/log/app/app-" + date + ".log");

        try {
            if (!java.nio.file.Files.exists(logPath)) {
                return date + " 的日志文件不存在";
            }
            List<String> lines = java.nio.file.Files.readAllLines(logPath);
            // 只返回最后 100 行,避免内容太长
            int start = Math.max(0, lines.size() - 100);
            return String.join("\n", lines.subList(start, lines.size()));
        } catch (Exception e) {
            return "读取日志失败:" + e.getMessage();
        }
    }
}

Resources 的使用方式和 Tools 不一样:Tools 是模型自动决定要不要调用,Resources 需要用户或模型主动引用 URI。
前提:MCP Server 必须实现了对应的 Resource,才能被读取。
这节的演示基于前面实现的 McpResourcesConfig 和 McpResourceTemplatesConfig——它们把 db://orders/{orderId}、docs://handbook/onboarding 等挂成了可读资源。如果你的 MCP Server 只注册了 Tools,没有实现 Resource,说出 URI 会直接报「资源不存在」,这是正常的,不是 Cursor 的问题。

部署后必做:刷新 CursorMCP 连接
重新打 jar、重启进程之后,Cursor 缓存的还是旧的能力列表,不会自动感知新注册的 Resource。每次 MCP Server 更新都要手动刷新:
1. Cursor 左下角 → SettingsMCP
2. 找到对应的 MCP Server(如 user-jichi-tools)
3. 点击 disable → 再点 enable(或直接点刷新图标)
4. 等连接恢复后再测试

确认 Server 实现了对应 Resource、Cursor 连接已刷新后,在 Cursor Chat/Agent 里直接说出 URI,模型就会去读:

请读取 db://orders/ORD20240115001 这个订单的信息,帮我分析一下
把 docs://handbook/onboarding 里的入职流程给我总结一下,我明天入职

Cursor Agent 会自动调用 resources/read,拿到内容后作为上下文继续推理,和调用 Tools 的体验基本一致。

MCP Prompts——企业级可复用提示词模板服务

MCP 的第三个能力:Prompts(提示词模板)。
在企业场景里很有价值——把公司沉淀的高质量提示词集中管理,通过 MCP Server 提供给所有 AI 应用复用。
MCP Prompts 是带参数的提示词模板,由 Server 管理,Client 可以按需获取。
典型使用场景:
● 公司统一的代码审查提示词(所有 AI 工具共享)
● 各业务线的分析报告模板
● 客服话术模板
● 数据提取的结构化输出模板

package com.jichi.mcp;

import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;

@Configuration
public class McpPromptsConfig {

    // SyncPromptRegistrationCallback 已在新版 Spring AI MCP 中移除
    // 改用 List<SyncPromptSpecification>,每个 Prompt 自带独立的 handler,和 Resources 模式一致
    @Bean
    public List<McpServerFeatures.SyncPromptSpecification> prompts() {
        return List.of(
                new McpServerFeatures.SyncPromptSpecification(
                        new McpSchema.Prompt(
                                "code_review",
                                "代码审查模板",
                                "对 Java 代码进行全面的审查,包括代码质量、安全性、性能等维度",
                                List.of(
                                        new McpSchema.PromptArgument("code", "要审查的 Java 代码", true),
                                        new McpSchema.PromptArgument("focus",
                                                "重点关注方向:security/performance/readability/all,默认 all", false)
                                )
                        ),
                        (exchange, request) -> buildCodeReviewPrompt(request.arguments())
                ),
                new McpServerFeatures.SyncPromptSpecification(
                        new McpSchema.Prompt(
                                "sales_report",
                                "销售分析报告模板",
                                "基于销售数据生成结构化的分析报告,适合汇报给管理层",
                                List.of(
                                        new McpSchema.PromptArgument("data", "销售数据(文字描述或结构化数据)", true),
                                        new McpSchema.PromptArgument("period", "报告周期,例如:上周、上月、Q1", true),
                                        new McpSchema.PromptArgument("audience",
                                                "报告受众:executive/team/client,默认 team", false)
                                )
                        ),
                        (exchange, request) -> buildSalesReportPrompt(request.arguments())
                ),
                new McpServerFeatures.SyncPromptSpecification(
                        new McpSchema.Prompt(
                                "sql_generator",
                                "自然语言转 SQL 模板",
                                "根据自然语言描述生成对应的 SQL 查询语句",
                                List.of(
                                        new McpSchema.PromptArgument("question", "查询需求的自然语言描述", true),
                                        new McpSchema.PromptArgument("schema",
                                                "数据库表结构(DDL 或描述)", true),
                                        new McpSchema.PromptArgument("dialect",
                                                "SQL 方言:mysql/postgresql/h2,默认 mysql", false)
                                )
                        ),
                        (exchange, request) -> buildSqlGeneratorPrompt(request.arguments())
                ),
                new McpServerFeatures.SyncPromptSpecification(
                        new McpSchema.Prompt(
                                "customer_reply",
                                "客服回复话术模板",
                                "生成专业、友善的客服回复,适合电商客服场景",
                                List.of(
                                        new McpSchema.PromptArgument("issue", "用户反映的问题", true),
                                        new McpSchema.PromptArgument("context",
                                                "相关背景信息(订单状态、历史记录等)", false),
                                        new McpSchema.PromptArgument("tone",
                                                "语气风格:formal/friendly/apologetic,默认 friendly", false)
                                )
                        ),
                        (exchange, request) -> buildCustomerReplyPrompt(request.arguments())
                )
        );
    }

    // -------- 提示词内容构建 --------

    private McpSchema.GetPromptResult buildCodeReviewPrompt(Map<String, Object> args) {
        String code = String.valueOf(args.getOrDefault("code", ""));
        String focus = String.valueOf(args.getOrDefault("focus", "all"));

        String focusInstruction = switch (focus) {
            case "security"     -> "重点审查安全漏洞:SQL 注入、XSS、权限校验缺失、敏感信息泄露等。";
            case "performance"  -> "重点审查性能问题:N+1 查询、不必要的循环、内存泄漏风险、线程安全等。";
            case "readability"  -> "重点审查代码可读性:命名规范、注释质量、方法长度、职责单一等。";
            default             -> "全面审查:代码质量、安全性、性能、可读性、测试覆盖度。";
        };

        String systemPrompt = """
                你是一名资深 Java 开发工程师,有丰富的代码审查经验。
                审查要有理有据,指出具体问题位置,并给出改进建议和示例代码。
                发现严重问题时明确标注「严重」,一般建议标注「建议」。
                """;

        String userPrompt = focusInstruction + "\n\n请审查以下代码:\n\n```java\n" + code + "\n```";

        return new McpSchema.GetPromptResult(
                "Java 代码审查",
                List.of(
                        new McpSchema.PromptMessage(
                                McpSchema.Role.USER,
                                new McpSchema.TextContent(systemPrompt + "\n\n" + userPrompt)
                        )
                )
        );
    }

    private McpSchema.GetPromptResult buildSalesReportPrompt(Map<String, Object> args) {
        String data = String.valueOf(args.getOrDefault("data", ""));
        String period = String.valueOf(args.getOrDefault("period", ""));
        String audience = String.valueOf(args.getOrDefault("audience", "team"));

        String style = switch (audience) {
            case "executive" -> "简洁、重点突出,不超过 300 字,包含核心指标和关键结论";
            case "client"    -> "专业、正式,突出正向数据,措辞谨慎";
            default          -> "详细、有数据支撑,包含问题分析和改进建议";
        };

        String prompt = String.format("""
                请根据以下销售数据生成 %s 的销售分析报告。
                
                报告风格要求:%s
                
                报告结构:
                1. 核心指标(销售额、订单量、客单价)
                2. 对比分析(和上期对比的变化及原因)
                3. 异常和亮点
                4. 改进建议(如果适用)
                
                销售数据:
                %s
                """, period, style, data);

        return new McpSchema.GetPromptResult(
                period + "销售分析报告",
                List.of(new McpSchema.PromptMessage(
                        McpSchema.Role.USER,
                        new McpSchema.TextContent(prompt)))
        );
    }

    private McpSchema.GetPromptResult buildSqlGeneratorPrompt(Map<String, Object> args) {
        String question = String.valueOf(args.getOrDefault("question", ""));
        String schema = String.valueOf(args.getOrDefault("schema", ""));
        String dialect = String.valueOf(args.getOrDefault("dialect", "mysql"));

        String prompt = String.format("""
                你是一个 SQL 专家,擅长 %s。
                根据以下数据库结构和查询需求,生成准确的 SQL 语句。
                
                要求:
                - 只输出 SQL,不要解释
                - SQL 需要可直接执行
                - 涉及大表时加合适的 WHERE 条件和 LIMIT
                
                数据库结构:
                %s
                
                查询需求:%s
                """, dialect.toUpperCase(), schema, question);

        return new McpSchema.GetPromptResult(
                "SQL 查询语句",
                List.of(new McpSchema.PromptMessage(
                        McpSchema.Role.USER,
                        new McpSchema.TextContent(prompt)))
        );
    }

    private McpSchema.GetPromptResult buildCustomerReplyPrompt(Map<String, Object> args) {
        String issue = String.valueOf(args.getOrDefault("issue", ""));
        String context = String.valueOf(args.getOrDefault("context", ""));
        String tone = String.valueOf(args.getOrDefault("tone", "friendly"));

        String toneInstruction = switch (tone) {
            case "formal"     -> "正式、专业,保持商务语气";
            case "apologetic" -> "诚恳致歉,展现责任担当,安抚情绪";
            default           -> "友善、亲切,像和朋友对话一样自然";
        };

        String prompt = String.format("""
                你是一名专业的电商客服,请用以下语气风格回复用户:%s
                
                回复要求:
                - 直接解决用户问题,不说废话
                - 如果需要用户提供信息,明确说明需要什么
                - 如果问题无法立即解决,给出明确的时间预期
                - 字数控制在 150 字以内
                
                用户问题:%s
                
                %s
                """,
                toneInstruction, issue,
                context.isBlank() ? "" : "背景信息:\n" + context);

        return new McpSchema.GetPromptResult(
                "客服回复",
                List.of(new McpSchema.PromptMessage(
                        McpSchema.Role.USER,
                        new McpSchema.TextContent(prompt)))
        );
    }
}

典型使用流程:

  1. 在 Cursor Chat 输入 /
  2. 看到 code_review、sales_report 等模板出现在列表里
  3. 选中 code_review,填入 code 和 focus 参数
  4. Cursor 发出请求,拿回填好的提示词,开始审查

手搓 MCP Client——Java 应用连接任意 MCP Server

项目依赖
新建一个独立项目 mcp-tools-client, 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.11</version>
    </parent>
    <groupId>com.jichi</groupId>
    <artifactId>mcp-tools-client</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>21</java.version>
        <spring-ai.version>1.1.2</spring-ai.version>
    </properties>
    <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>
    <dependencies>
        <!-- MCP Client 核心:包含协议实现 + Spring 自动装配 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client</artifactId>
        </dependency>
        <!-- MCP JSON Jackson 实现:JacksonMcpJsonMapper 类在此模块,显式声明避免传递下载失败 -->
        <dependency>
            <groupId>io.modelcontextprotocol.sdk</groupId>
            <artifactId>mcp-json-jackson2</artifactId>
            <version>0.17.0</version>
        </dependency>
        <!-- Spring Boot Web:提供 HTTP 接口,Agent 通过 REST 接收用户请求 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring AI OpenAI:用于驱动 Agent 的大模型 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Client 和 Server 用的是不同的依赖:Server 用 spring-ai-starter-mcp-server,Client 用 spring-ai-starter-mcp-client。业务 Agent 是普通 Web 应用,Tomcat 正常启动。
连接流程分三步:

  1. 握手(initialize):Client 把自己的名字版本告诉 Server,Server 返回能力声明;SDK 内部自动完成握手通知
  2. 发现工具(listTools):Client 问 Server 有哪些工具可以用
  3. 调用工具(callTool):根据工具名和参数发起调用,拿回结果
package com.jichi.mcp.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class LocalMcpClientService {

    private final ObjectMapper objectMapper;   // Spring 自动装配的 Jackson ObjectMapper
    private McpSyncClient client;

    @PostConstruct
    public void connect() {
        // StdioClientTransport:Client 直接启动 Server 子进程,通过 stdin/stdout 通信
        // 和 Cursor 接 MCP 的原理完全一样,只是这里 Java 代码扮演了 Cursor 的角色
        StdioClientTransport transport = new StdioClientTransport(
                ServerParameters.builder("java")
                        .args("-jar", "/path/to/mcp-tools-server-1.0.1.jar")
                        .build(),
                new JacksonMcpJsonMapper(objectMapper)
        );

        client = McpClient.sync(transport)
                .clientInfo(new McpSchema.Implementation("jichi-agent", "1.0.0"))
                .build();

        // 第一步:握手,拿到 Server 的名字和版本
        McpSchema.InitializeResult initResult = client.initialize();
        log.info("[MCP] 已连接 Server:{} v{}",
                initResult.serverInfo().name(), initResult.serverInfo().version());

        // initialize() 内部已自动发送 initialized 通知,无需手动调用
    }

    /**
     * 列出 Server 提供的所有工具
     */
    public List<McpSchema.Tool> listTools() {
        McpSchema.ListToolsResult result = client.listTools();
        return result.tools();
    }

    /**
     * 调用指定工具
     */
    public String callTool(String toolName, Map<String, Object> arguments) {
        McpSchema.CallToolResult result = client.callTool(
                new McpSchema.CallToolRequest(toolName, arguments));

        // isError=true 表示工具执行出错,文字内容是错误信息
        if (Boolean.TRUE.equals(result.isError())) {
            log.warn("[MCP] 工具 {} 执行失败", toolName);
            return "工具调用失败";
        }

        // 提取文字内容(content 是数组,可能包含文字、图片等多种类型)
        return result.content().stream()
                .filter(c -> c instanceof McpSchema.TextContent)
                .map(c -> ((McpSchema.TextContent) c).text())
                .findFirst()
                .orElse("");
    }

    /**
     * 读取资源(需要 Server 实现了对应的 Resource)
     */
    public String readResource(String uri) {
        McpSchema.ReadResourceResult result = client.readResource(
                new McpSchema.ReadResourceRequest(uri));

        return result.contents().stream()
                .filter(c -> c instanceof McpSchema.TextResourceContents)
                .map(c -> ((McpSchema.TextResourceContents) c).text())
                .findFirst()
                .orElse("");
    }

    @PreDestroy
    public void disconnect() {
        if (client != null) {
            client.close();
            log.info("[MCP] 已断开本地 Server 连接");
        }
    }
}

暴露http接口测试

package com.jichi.mcp.client;

import io.modelcontextprotocol.spec.McpSchema;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/local-mcp")
@RequiredArgsConstructor
public class LocalMcpController {

    private final LocalMcpClientService localMcpClientService;

    @GetMapping("/tools")
    public List<String> listTools() {
        return localMcpClientService.listTools().stream()
                .map(t -> t.name() + ":" + t.description())
                .toList();
    }

    @GetMapping("/call")
    public String callTool(
            @RequestParam String tool,
            @RequestParam Map<String, Object> args) {
        return localMcpClientService.callTool(tool, args);
    }

    @GetMapping("/resource")
    public String readResource(@RequestParam String uri) {
        return localMcpClientService.readResource(uri);
    }
}

大家不只会用自己写的 Server,更多场景是接官方或社区提供的第三方 Server——文件系统、GitHub、数据库等,直接拿来用,不用自己实现。
第三方 Server 大多是 Node.js 写的,用 npx 启动,接入方式和 Java jar 完全一样,换个启动命令就行。

实际项目里会同时连多个 Server,封装一个统一的管理器,对上层 Agent 屏蔽"工具在哪个 Server 上"的细节。
工具名用 serverName.toolName 格式(例如 filesystem.read_file),管理器负责解析、路由到正确的 Server 执行。

package com.jichi.mcp.client;

import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
@Slf4j
public class McpClientManager {

    private final Map<String, McpSyncClient> clients;

    public McpClientManager(
            McpSyncClient filesystemMcpClient,
            McpSyncClient githubMcpClient) {

        this.clients = Map.of(
                "filesystem", filesystemMcpClient,
                "github",     githubMcpClient
        );
    }

    /**
     * 获取所有 Server 的工具列表,合并后供 Agent 使用
     * 工具名加上 serverName 前缀,避免不同 Server 的同名工具冲突
     */
    public List<McpSchema.Tool> getAllTools() {
        List<McpSchema.Tool> allTools = new ArrayList<>();
        clients.forEach((serverName, client) -> {
            try {
                client.listTools().tools().forEach(tool ->
                        // Tool 是 7 参数 record,必须用 builder,不能用构造函数
                        // 用双下划线 __ 分隔 serverName 和 toolName
                        // OpenAI/DeepSeek 工具名只允许 ^[a-zA-Z0-9_-]+$,点号非法
                        allTools.add(McpSchema.Tool.builder()
                                .name(serverName + "__" + tool.name())   // filesystem__list_directory
                                .description(tool.description())
                                .inputSchema(tool.inputSchema())
                                .build())
                );
            } catch (Exception e) {
                log.error("[MCP] {} 工具列表获取失败:{}", serverName, e.getMessage());
            }
        });
        return allTools;
    }

    /**
     * 按工具名路由到正确的 Server 执行
     * 格式:serverName.toolName,例如 filesystem.read_file
     */
    public String callTool(String qualifiedToolName, Map<String, Object> arguments) {
        String[] parts = qualifiedToolName.split("__", 2);
        if (parts.length != 2) {
            return "工具名格式错误,应为 serverName__toolName,例如 filesystem__read_file";
        }

        String serverName = parts[0];
        String toolName   = parts[1];

        McpSyncClient client = clients.get(serverName);
        if (client == null) {
            return "未找到 Server:" + serverName + ",可用 Server:" + clients.keySet();
        }

        McpSchema.CallToolResult result = client.callTool(
                new McpSchema.CallToolRequest(toolName, arguments));

        if (Boolean.TRUE.equals(result.isError())) {
            log.warn("[MCP] {}__{}  执行失败", serverName, toolName);
            return "工具执行失败";
        }

        return result.content().stream()
                .filter(c -> c instanceof McpSchema.TextContent)
                .map(c -> ((McpSchema.TextContent) c).text())
                .findFirst()
                .orElse("(无返回内容)");
    }
}

把从 MCP Server 发现的工具,转成 Spring AI 的 ToolCallback 格式,注入给 ChatClient,Agent 就能自动调用了。

package com.jichi.mcp.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class McpDrivenAgent {

    private final ChatClient chatClient;
    private final McpClientManager mcpManager;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public McpDrivenAgent(ChatClient.Builder builder, McpClientManager mcpManager) {
        this.mcpManager = mcpManager;

        // 启动时从所有 MCP Server 拉取工具列表,转成 ToolCallback 注入 ChatClient
        List<ToolCallback> mcpToolCallbacks = buildMcpToolCallbacks();
        log.info("[Agent] 从 MCP Server 加载了 {} 个工具", mcpToolCallbacks.size());

        this.chatClient = builder
                .defaultSystem("""
                        你是一个智能助手,可以访问文件系统和 GitHub。
                        工具名格式:serverName__toolName,例如 filesystem__read_file。
                        需要操作文件时用 filesystem 系列工具,需要查 GitHub 时用 github 系列工具。
                        """)
                .defaultToolCallbacks(mcpToolCallbacks.toArray(new ToolCallback[0]))
                .build();
    }

    private List<ToolCallback> buildMcpToolCallbacks() {
        return mcpManager.getAllTools().stream()
                .map(tool -> (ToolCallback) new ToolCallback() {
                    @Override
                    public ToolDefinition getToolDefinition() {
                        String schemaJson;
                        try {
                            // inputSchema() 返回 JsonSchema 对象,必须序列化成 JSON 字符串
                            schemaJson = objectMapper.writeValueAsString(tool.inputSchema());
                        } catch (Exception e) {
                            schemaJson = "{}";
                        }
                        return ToolDefinition.builder()
                                .name(tool.name())
                                .description(tool.description())
                                .inputSchema(schemaJson)
                                .build();
                    }

                    @Override
                    public String call(String toolInput) {
                        try {
                            Map<String, Object> args = objectMapper.readValue(toolInput, Map.class);
                            return mcpManager.callTool(tool.name(), args);
                        } catch (Exception e) {
                            log.error("[Agent] 工具 {} 调用失败:{}", tool.name(), e.getMessage());
                            return "工具调用失败:" + e.getMessage();
                        }
                    }
                })
                .toList();
    }

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

暴露接口

package com.jichi.mcp.client;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/mcp")
@RequiredArgsConstructor
public class McpAgentController {

    private final McpClientManager mcpManager;
    private final McpDrivenAgent agent;

    /** 查看当前 Agent 能用哪些工具 */
    @GetMapping("/tools")
    public List<String> listAllTools() {
        return mcpManager.getAllTools().stream()
                .map(t -> t.name() + ":" + t.description())
                .toList();
    }

    /** 和 Agent 对话,Agent 会自动调用 MCP 工具 */
    @PostMapping("/chat")
    public String chat(@RequestBody Map<String, String> body) {
        return agent.chat(body.get("message"));
    }
}

测试

# 查看所有可用工具
curl http://localhost:8080/api/mcp/tools

# 让 Agent 帮你读文件(Agent 会自动调用 filesystem.read_file)
curl -X POST http://localhost:8080/api/mcp/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "读一下 /Users/yourname/Documents/readme.txt 的内容"}'

# 让 AgentGitHubAgent 会自动调用 github 系列工具)
curl -X POST http://localhost:8080/api/mcp/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "帮我搜一下 spring-ai 在 GitHub 上最受欢迎的仓库"}'

远程 MCP Server——SSE 传输与生产部署

把 MCP Server 变成一个普通的 HTTP 服务,任意 Client 都可以通过网络连进来。

stdio 模式:
  Host/Client 进程 → fork → Server 子进程
  通过 stdin/stdout 通信
  ServerClient 必须在同一台机器
  一个 Server 只服务一个 Client

SSE 模式:
  MCP Server 独立部署(标准 Spring Boot Web 服务)
  Client 通过 HTTP 长连接(SSE)接收推送
  Server 独立运行,可以同时服务多个 Client
  Server 挂了不影响 Client 进程

直接在之前的 mcp-tools-server 项目上改,不用新建项目。工具代码一行都不用改,这是 Spring AI MCP 设计得很好的地方。

第一步换依赖
pom.xml 把原来的 spring-ai-starter-mcp-server 换成 spring-ai-starter-mcp-server-webmvc,webmvc 变体已经内置了 Web 容器,不需要再单独加 spring-boot-starter-web:

<!-- 去掉原来的 spring-ai-starter-mcp-server -->
<!-- 换成 webmvc 变体 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

第二部改配置
application.yml 去掉 web-application-type: none(SSE 模式需要启动 Web 容器),其他加上端口和 MCP 配置

server:
  port: 8090        # MCP Server 独立端口,避开 Agent 应用的 8080

spring:
  ai:
    mcp:
      server:
        name: jichi-remote-tools
        version: 1.0.0
        type: SYNC
        sse-message-endpoint: /mcp/messages   # SSE 消息端点路径

logging:
  config: classpath:logback-spring.xml  # SSE 模式日志不用限制,正常输出就行

主类和工具类不变,启动后会自动暴露两个端点:
● GET /sse:Client 建立 SSE 长连接,等待服务器推送
● POST /mcp/messages:Client 发送请求(工具调用等)

在之前的 mcp-tools-client 项目里新增文件,把连接本地 Server 的 stdio 传输层换成 SSE 传输层。
注意:项目里原来的 LocalMcpClientConfig 和 ThirdPartyMcpConfig 里的 @Bean 要先屏蔽掉,否则 Spring 启动时会同时初始化 stdio 连接,找不到本地 jar 就报 Stream closed。最简单的方式是把旧配置类的 @Bean 注释掉,或者给两套配置加 @Profile 区分。
新建 RemoteMcpClientConfig.java:
常见问题:改完启动报错或 curl 显示 ECONNREFUSED
检查两个地方:

  1. application.yml 里有没有留着 web-application-type: none——SSE 模式必须删掉这行,否则 Web 容器不启动
  2. pom.xml 有没有加 spring-boot-starter-web
    两项都确认后 mvn clean package 重新打包,启动日志里出现 Tomcat started on port 8090 说明 Server 正常了。
    验证服务是否启动正常:
package com.jichi.mcp.client;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RemoteMcpClientConfig {

    @Bean
    public McpSyncClient remoteToolsClient() {
        // SSE 传输层:只需要传 Server 的 URL
        HttpClientSseClientTransport transport =
                HttpClientSseClientTransport.builder("http://localhost:8090")
                        .build();

        McpSyncClient client = McpClient.sync(transport)
                .clientInfo(new McpSchema.Implementation("jichi-agent", "1.0.0"))
                .build();

        // initialize() 内部自动完成握手通知,不需要额外调用
        McpSchema.InitializeResult result = client.initialize();
        log.info("[MCP] 已连接远程 Server:{} v{}",
                result.serverInfo().name(), result.serverInfo().version());

        return client;
    }
}

测试controller

package com.jichi.mcp.client;

import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/remote-mcp")
@RequiredArgsConstructor
public class RemoteMcpController {

    private final McpSyncClient remoteToolsClient;

    @GetMapping("/tools")
    public List<String> listTools() {
        return remoteToolsClient.listTools().tools().stream()
                .map(t -> t.name() + ":" + t.description())
                .toList();
    }

    @PostMapping("/call")
    public String callTool(
            @RequestParam String toolName,
            @RequestBody Map<String, Object> args) {
        McpSchema.CallToolResult result = remoteToolsClient.callTool(
                new McpSchema.CallToolRequest(toolName, args));
        return result.content().stream()
                .filter(c -> c instanceof McpSchema.TextContent)
                .map(c -> ((McpSchema.TextContent) c).text())
                .findFirst().orElse("(无返回内容)");
    }
}

测试

# 查看远程 Server 提供的工具
curl http://localhost:8080/api/remote-mcp/tools

# 调用远程工具
curl -X POST "http://localhost:8080/api/remote-mcp/call?toolName=getDateInfo" \
  -H "Content-Type: application/json" \
  -d '{}'
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐