Java 实现 MCP 客户端——连接 AI 工具生态的协议实战
Java 实现 MCP 客户端——连接 AI 工具生态的协议实战
适读人群:Java 工程师、AI 应用架构师 | 阅读时长:约16分钟 | 核心价值:理解 MCP 协议原理,用 Java 实现 MCP 客户端对接工具生态
2025年最让我兴奋的技术之一是 MCP(Model Context Protocol)。不是因为它有多复杂,恰恰相反,是因为它足够简单、足够标准。
我第一次看到 Cursor 通过 MCP 直接调用我写的本地工具时,有一种"这才是 AI 应该有的样子"的感觉——模型不再是一个封闭的黑盒,而是一个可以调用任何工具的智能代理。
这篇文章讲如何用 Java 实现 MCP 客户端,以及如何把你的 Java 服务暴露为 MCP 工具。
MCP 是什么,解决什么问题
MCP(Model Context Protocol)是 Anthropic 开源的一个标准协议,让 AI 应用和外部工具/数据源之间有一种统一的通信方式。
在 MCP 之前,每个 AI 应用想集成外部工具都要自己写一套集成代码,接口不统一,工具不能跨应用复用。
MCP 之后:工具开发者实现一次 MCP Server,所有支持 MCP 的 AI 客户端(Cursor、Claude Desktop、你自己开发的应用)都能直接使用,不需要重复适配。
核心概念:
- MCP Server:提供工具、资源、提示词的服务端
- MCP Client:调用工具的客户端(你的 AI 应用)
- Transport:通信方式,支持 stdio 和 SSE 两种
Java MCP SDK
Anthropic 官方提供了 Java SDK:
<dependency>
<groupId>io.modelcontextprotocol</groupId>
<artifactId>java-sdk</artifactId>
<version>0.8.1</version>
</dependency>Spring AI 1.0 内置了对 MCP 的支持:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>实现 MCP 客户端
方式一:直接用 Java SDK
@Slf4j
public class McpClientExample {
public static void main(String[] args) throws Exception {
// 连接到本地的 MCP Server(stdio 方式)
// 这个 Server 是一个 Node.js 写的文件系统工具
ServerParameters serverParams = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-filesystem", "/tmp")
.build();
McpClient client = McpClient.sync(
new StdioClientTransport(serverParams)
).build();
// 初始化连接
client.initialize();
// 列出所有可用工具
ListToolsResult tools = client.listTools();
tools.tools().forEach(tool -> {
log.info("工具:{} - {}", tool.name(), tool.description());
});
// 调用工具
CallToolResult result = client.callTool(
new CallToolRequest("read_file",
Map.of("path", "/tmp/test.txt"))
);
result.content().forEach(content -> {
if (content instanceof TextContent textContent) {
System.out.println(textContent.text());
}
});
client.close();
}
}方式二:Spring AI 集成(推荐)
# application.yml
spring:
ai:
mcp:
client:
# 配置多个 MCP Server
servers:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
custom-java-server:
url: http://localhost:8080/mcp/sse # SSE 方式@Configuration
public class McpConfig {
// Spring AI 自动注册所有 MCP 工具
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
List<McpSyncClient> mcpClients) {
// 把所有 MCP Server 的工具都注册到 ChatClient
List<McpFunctionCallback> tools = mcpClients.stream()
.flatMap(client -> {
try {
return client.listTools().tools().stream()
.map(tool -> new McpFunctionCallback(client, tool));
} catch (Exception e) {
log.warn("获取 MCP 工具列表失败: {}", e.getMessage());
return Stream.empty();
}
})
.collect(toList());
log.info("注册了 {} 个 MCP 工具", tools.size());
return builder
.defaultTools(tools.toArray(new McpFunctionCallback[0]))
.build();
}
}实现 MCP Server(把你的 Java 服务暴露为工具)
这才是重点,让别人的 AI 客户端能调用你的服务。
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>spring:
ai:
mcp:
server:
name: java-business-tools
version: 1.0.0
# SSE 方式,支持远程连接
transport-type: SSE@Component
@Slf4j
public class BusinessMcpTools {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
/**
* 暴露为 MCP 工具
* Spring AI 自动将 @Tool 方法注册为 MCP Server 的工具
*/
@Tool(description = "查询订单状态和详情,需要提供订单号")
public String queryOrder(
@ToolParam(description = "订单号,格式ORD-XXXXXXXX") String orderId) {
try {
OrderDetail order = orderService.findById(orderId);
return String.format(
"订单%s状态:%s,金额:%.2f元,收货人:%s,地址:%s,预计%s送达",
order.getId(), order.getStatus(), order.getAmount(),
order.getReceiverName(), order.getAddress(), order.getEstimatedDelivery()
);
} catch (OrderNotFoundException e) {
return "未找到订单:" + orderId;
}
}
@Tool(description = "搜索商品,支持按名称、分类、价格范围筛选")
public String searchProducts(
@ToolParam(description = "搜索关键词") String keyword,
@ToolParam(description = "最高价格,不限制传0") double maxPrice,
@ToolParam(description = "分类,如电子/服装/食品,不限制传空字符串") String category) {
ProductSearchResult result = productService.search(keyword, maxPrice, category);
if (result.isEmpty()) {
return "未找到符合条件的商品";
}
StringBuilder sb = new StringBuilder("找到 ").append(result.total()).append(" 件商品:\n");
result.items().stream().limit(10).forEach(item -> {
sb.append(String.format("- %s,价格:%.2f元,库存:%d件\n",
item.getName(), item.getPrice(), item.getStock()));
});
return sb.toString();
}
@Tool(description = "获取销售数据报表,支持按时间段查询")
public String getSalesReport(
@ToolParam(description = "开始日期,格式 YYYY-MM-DD") String startDate,
@ToolParam(description = "结束日期,格式 YYYY-MM-DD") String endDate) {
SalesReport report = orderService.getSalesReport(startDate, endDate);
return String.format(
"%s 至 %s 销售数据:\n总订单数:%d\n总销售额:%.2f元\n平均客单价:%.2f元\n最畅销商品:%s",
startDate, endDate, report.totalOrders(), report.totalRevenue(),
report.avgOrderValue(), report.topProduct()
);
}
}现在,任何支持 MCP 的 AI 客户端都能通过 http://你的服务器:8080/mcp/sse 调用这些工具了。
踩坑实录
坑一:stdio 模式下工具调用超时
现象:通过 stdio 方式连接的 MCP Server,第一次调用工具经常超时(>10s)。
原因:stdio 方式启动 Server 进程需要时间(npm install、JVM 启动等),第一次调用时进程还没完全启动。
解法:增大初始化超时,并在 Spring 容器启动时预热:
@Bean
public McpSyncClient mcpClient() {
return McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(60)) // 增大超时
.build();
}
@Bean
public ApplicationRunner warmupMcp(McpSyncClient mcpClient) {
return args -> {
// 启动时预热,列出工具触发进程初始化
try {
mcpClient.initialize();
log.info("MCP 预热成功");
} catch (Exception e) {
log.warn("MCP 预热失败,将在首次使用时初始化: {}", e.getMessage());
}
};
}坑二:工具描述太长导致超 token
现象:注册了 30 个 MCP 工具,每个工具描述都有详细 JSON Schema,导致每次请求的 System Token 消耗 3000+,成本翻倍。
原因:所有工具的描述(包括参数 Schema)都会注入到每次请求里。
解法:按功能分组,动态选择工具集:
// 根据对话上下文动态注册工具,而不是全量注册
List<McpFunctionCallback> relevantTools = determineRelevantTools(userMessage);
chatClient.prompt()
.user(userMessage)
.tools(relevantTools.toArray(new McpFunctionCallback[0]))
.call()
.content();坑三:SSE 连接断开后工具调用失败
现象:SSE 模式下,连接偶尔会断开,断开后的工具调用直接失败而不是重试。
原因:Spring AI 的 MCP Client 默认没有自动重连机制。
解法:添加重连逻辑:
@Bean
public McpSyncClient robustMcpClient() {
return McpClient.sync(
new SseClientTransport(
WebClient.builder()
.baseUrl("http://localhost:8080/mcp/sse")
// 配置连接保活
.build()
)
)
.requestTimeout(Duration.ofSeconds(30))
.build();
}
// 用 @Retryable 包装工具调用
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public String callMcpTool(McpSyncClient client, String toolName, Map<String, Object> args) {
return client.callTool(new CallToolRequest(toolName, args))
.content().get(0).toString();
}MCP 生态现状(2025年4月)
目前已有大量开箱即用的 MCP Server:
- 文件系统:
@modelcontextprotocol/server-filesystem - GitHub:
@modelcontextprotocol/server-github - 数据库:PostgreSQL、SQLite、MongoDB 都有对应实现
- 搜索:Brave Search、Google Search
- 浏览器自动化:Playwright MCP
支持 MCP 的客户端:Claude Desktop、Cursor、Windsurf、Continue,以及 Spring AI、LangChain4j 等框架。
MCP 的价值:一次实现,处处可用。你公司的内部工具实现一个 MCP Server,所有 AI 工具都能调用,不需要为每个工具单独写集成代码。
MCP 的认证与安全
在企业环境中,MCP Server 需要认证机制,不能让任何人随意调用你的业务接口。
Spring AI 的 MCP Server 支持通过 HTTP Header 传递认证信息:
// 在 MCP 工具方法里获取当前调用者身份
@Component
public class AuthenticatedMcpTools {
@Tool(description = "查询当前用户的订单列表")
public String queryMyOrders(
@ToolParam(description = "查询条件:ALL/PENDING/SHIPPED/COMPLETED") String filter) {
// 从 HTTP 请求头获取调用者身份(由 AI 客户端传入)
String apiKey = getCurrentApiKey(); // 从 SecurityContext 或请求上下文获取
UserInfo user = apiKeyService.validate(apiKey);
if (user == null) {
return "认证失败:无效的 API Key";
}
// 只查询该用户的订单
List<Order> orders = orderService.findByUserIdAndFilter(user.getId(), filter);
return formatOrderList(orders);
}
}MCP 客户端在配置时传入认证信息:
spring:
ai:
mcp:
client:
servers:
company-tools:
url: https://internal-mcp.yourcompany.com/mcp/sse
headers:
X-API-Key: ${INTERNAL_MCP_API_KEY}构建 MCP 工具库的设计思路
如果你们公司要系统性地建设 MCP 工具库,以下是我的建议:
工具分层:
第一层是数据查询工具,比如订单查询、库存查询、用户信息查询。这类工具只读不写,风险低,可以给所有 AI 客户端使用。
第二层是操作执行工具,比如创建工单、发送通知、更新状态。这类工具有副作用,需要权限控制,不同的 AI 客户端只能使用有权限的工具子集。
第三层是管理类工具,比如查看系统配置、运行 SQL 脚本。只有特定的运维 AI 工具才能使用,普通业务 AI 禁止访问。
统一的工具描述规范:
工具描述的质量直接影响 AI 客户端的调用准确率。建议在公司内部建立统一的描述规范:
描述要包含:工具做什么、什么时候调用、输入参数的格式要求、可能的返回值格式、什么情况下不应该调用。
一个好的工具描述示例:"查询指定部门在指定月份的费用报销汇总。当用户询问某个部门的费用情况或需要生成费用报告时调用。department 参数传部门编号(如D001),不是部门名称;month 格式为 YYYY-MM。如果用户只说了部门名称没有编号,请先用 queryDepartments 工具查询编号再调用本工具。"
注意最后那句话——告诉模型如果缺少必要信息要怎么处理,这能大幅减少工具调用参数错误的情况。
版本化和兼容性:
工具一旦被多个 AI 客户端使用,就要考虑版本兼容性问题。如果你修改了工具的参数格式或行为,依赖这个工具的 AI 系统可能立刻出问题。
建议给工具名加版本号(如 queryOrder_v2),旧版本保留一段时间后再废弃,给所有依赖方充分的迁移时间。
