MCP上下文协议
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 Web:MCP 自动配置内部依赖 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 # SYNC 或 ASYNC,初学用 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 的问题。
部署后必做:刷新 Cursor 的 MCP 连接
重新打 jar、重启进程之后,Cursor 缓存的还是旧的能力列表,不会自动感知新注册的 Resource。每次 MCP Server 更新都要手动刷新:
1. Cursor 左下角 → Settings → MCP
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)))
);
}
}
典型使用流程:
- 在 Cursor Chat 输入 /
- 看到 code_review、sales_report 等模板出现在列表里
- 选中 code_review,填入 code 和 focus 参数
- 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 正常启动。
连接流程分三步:
- 握手(initialize):Client 把自己的名字版本告诉 Server,Server 返回能力声明;SDK 内部自动完成握手通知
- 发现工具(listTools):Client 问 Server 有哪些工具可以用
- 调用工具(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 的内容"}'
# 让 Agent 查 GitHub(Agent 会自动调用 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 通信
Server 和 Client 必须在同一台机器
一个 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
检查两个地方:
- application.yml 里有没有留着 web-application-type: none——SSE 模式必须删掉这行,否则 Web 容器不启动
- 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 '{}'
更多推荐

所有评论(0)