MCP协议是什么?Java后端五分钟搞懂Model Context Protocol,把自己变成LLM的工具

MCP协议是什么?Java后端五分钟搞懂Model Context Protocol,把自己变成LLM的工具

MCP的本质是"LLM调工具的标准化协议"。对Java后端来说,开发MCP Server就是写Spring Bean加​​@Tool​​注解,简单得很。难点在协议理解、传输模式选择、客户端适配、踩坑处理。

写过Spring AI的Function Calling,是不是觉得LLM调工具这事儿已经搞定了?我去年也是这么想的。直到Anthropic推了MCP,Cursor、Claude Desktop、Windsurf全跟进,我才反应过来--Function Calling是"能调",MCP是"标准地调"。这俩不是替代关系,是层次不同。

这篇文章讲Java后端怎么开发MCP Server,让自家业务系统变成Claude/Cursor能直接用的工具。适合正在搞AI工程化的Java开发,读完能跑通第一个MCP Server。

MCP到底是个啥

Model Context Protocol,Anthropic 2024年11月推出的开放协议。说白了就是LLM与应用系统之间的通信标准

后端视角理解:MCP之于LLM,相当于JDBC之于数据库。JDBC让Java代码用统一接口操作不同数据库,MCP让LLM用统一协议调用不同应用系统。一次开发MCP Server,所有支持MCP的客户端(Claude Desktop、Cursor、Windsurf、Continue)都能用你的工具。

协议层基于JSON-RPC 2.0,三大原语:Tools(工具调用)、Resources(资源读取)、Prompts(提示词模板)。传输层两种:stdio(本地进程通信)和SSE/HTTP(远程通信)。

我的体感是,MCP不是又一个Function Calling,它是把"LLM调工具"这件事协议化、标准化。以前每家LLM厂商自己定义工具调用格式,OpenAI一套、Anthropic一套、Google一套,开发者要适配多次。MCP出来后,写一次MCP Server,所有客户端通吃。

为啥需要MCP

Function Calling的问题很明显。第一,每个LLM厂商格式不同,OpenAI的tools参数结构和Anthropic的tool_use完全两套,业务代码要写多份适配。第二,工具定义散落在应用代码里,没法复用--A项目写的"查订单"工具,B项目要重新写。第三,工具调用和业务系统耦合,没法独立部署、独立升级。

MCP的解法是解耦。把工具能力抽成独立的MCP Server,业务系统专注于实现工具逻辑,LLM客户端专注于调用。MCP Server就像微服务架构里的一个独立服务,有自己的进程、自己的生命周期。

举个具体场景。我们有个内部工单系统,以前接Claude要写一套Function Calling,接Cursor要再写一套,接自家产品还要再适配。改成MCP Server后,写一次,三个客户端都能用,维护成本降了三分之二。

扯远了,说回MCP本身。这玩意儿2024年底推出,2025年生态爆发,2026年Spring AI 1.0正式集成MCP Server支持。现在是上车的最佳时机--早了没受众,晚了烂大街。

Java开发MCP Server

Spring AI 1.0提供了spring-ai-mcp-server-spring-boot-starter,开发MCP Server跟写普通Spring Bean一样简单。依赖就引这一个starter,版本由Spring AI BOM统一管,别单独指定版本号。

application.yml关键配置:

spring: ai: mcp: server: name: order-query-server version: 1.0.0 # stdio模式适合本地客户端(Claude Desktop) # sse模式适合远程客户端(Cursor云端) type: STDIO # 工具调用超时,别设太长,LLM等不起 request-timeout: 30s

核心MCP Server代码,用真实业务对象:

@Service public class OrderQueryService { private final OrderMapper orderMapper; private final OrderStatusDecoder statusDecoder; public OrderQueryService(OrderMapper orderMapper, OrderStatusDecoder statusDecoder) { this.orderMapper = orderMapper; this.statusDecoder = statusDecoder; } /** * @Tool注解标记这是MCP工具方法,LLM能调用 * name和description必填,LLM靠description判断何时调用 */ @Tool(name = "query_order", description = "根据订单号查询订单状态、金额、物流信息。输入订单号,返回订单详情。") public OrderDetail queryOrder(@ToolParam(description = "订单号,纯数字") String orderId) { // MCP工具方法本质就是普通Java方法,LLM传入参数,返回结果 // 别在这里写复杂逻辑,LLM等不起,超时30秒就断了 Order order = orderMapper.selectById(orderId); if (order == null) { throw new ToolExecutionException("订单不存在: " + orderId); } return OrderDetail.builder() .orderId(order.getId()) .status(statusDecoder.decode(order.getStatus())) .amount(order.getAmount()) .logisticsNo(order.getLogisticsNo()) .build(); } @Tool(name = "refund_order", description = "申请订单退款。需要订单号和退款原因。仅支持已发货前的订单。") public RefundResult refundOrder(@ToolParam(description = "订单号") String orderId, @ToolParam(description = "退款原因,不超过200字") String reason) { // 退款涉及状态变更,必须做幂等校验 // MCP工具被LLM重复调用是常事,别假设只调一次 return refundService.process(orderId, reason); } }

这里边有几个坑得提一下。

@Tooldescription是LLM判断何时调用的唯一依据,必须写清楚"这个工具干什么、输入什么、输出什么"。我一开始写得简略,Claude老调错工具--用户问"查物流",它调了refund_order。后来把description改成"根据订单号查询订单状态、金额、物流信息",调用准确率从70%到95%。

@ToolParamdescription同样重要,LLM靠它理解参数含义。订单号要写"纯数字",否则Claude会把"ORD-2026-001"这种带前缀的字符串传进来,数据库查不到。

工具方法里别写复杂逻辑。MCP工具本质是LLM的外挂函数,调用链路是"LLM->MCP协议->Java方法->返回结果"。中间任何一步慢了,LLM会超时重试,重复调用。30秒超时是我的经验值,再长Claude Desktop会断开。

异常必须用ToolExecutionException包装。我踩过坑,直接抛RuntimeException,Claude收到的是空响应,无法理解失败原因,会一直重试。改成ToolExecutionException后,错误信息会传回LLM,它能看到"订单不存在"然后告诉用户。

接入Claude Desktop测试

MCP Server开发完,要接到客户端测试。Claude Desktop是最常用的本地客户端。

配置文件在claude_desktop_config.json,位置:Mac是~/Library/Application Support/Claude/,Windows是%APPDATA%\Claude\

{ "mcpServers": { "order-query-server": { "command": "java", "args": ["-jar", "/path/to/order-mcp-server.jar"], "env": { "DEEPSEEK_API_KEY": "sk-xxx" } } } }

配置完重启Claude Desktop,在对话框里看到工具图标亮起,就说明MCP Server连上了。

实测效果。我问Claude"帮我查下订单20260723001的状态",它会自动调用query_order工具,拿到结果后用自然语言回复"您的订单已发货,物流单号SF1234567890,预计明天送达"。整个过程用户无感知,就像Claude自己知道订单信息一样。

真香。第一次跑通的时候我在工位上乐了半天,旁边同事以为我中奖了。

踩过的坑

坑一:stdio模式下日志不能打到stdout。stdio传输用stdout传JSON-RPC消息,日志打到stdout会污染协议流,Claude Desktop直接断连。必须把日志打到stderr或文件。这个坑我查了两天,日志一加就挂,最后才反应过来。

坑二:工具方法不能有同名重载。MCP协议用方法名做唯一标识,Java的重载在MCP层面会冲突。两个queryOrder方法,一个传String一个传Long,MCP Server启动报错。解法是改名--queryOrderByIdqueryOrderByName

坑三:返回对象必须可序列化。LLM拿到的结果是JSON,返回对象的字段要全部可序列化。我返回过一个含LocalDateTime的对象,Claude收到的是空对象,因为Jackson默认不认LocalDateTime。加@JsonFormat或配jackson-datatype-jsr310模块。

坑四:sse模式要单独暴露端口。stdio模式是本地进程通信,sse模式是HTTP服务,要单独开端口。我一开始把MCP Server和业务系统塞一个进程,结果MCP端口被业务流量冲垮。后来拆成独立进程,问题解决。

坑五:权限沙箱。Claude Desktop对MCP Server有权限限制,文件系统访问、网络访问都受控。我的工具要读/etc/config/order.json,被沙箱拦了。解法是把配置改成环境变量传入,或者用Claude Desktop的权限配置显式授权。

MCP vs Function Calling

这俩不是替代关系,是层次不同。Function Calling是LLM厂商定义的工具调用格式,OpenAI、Anthropic、Google各有各的。MCP是Anthropic推出的开放协议,标准化了"LLM怎么发现工具、怎么调用工具、怎么传参、怎么返回结果"。

选型上,如果只用一家LLM,Function Calling够用,简单直接。如果要支持多客户端(Claude Desktop、Cursor、自家产品),MCP一次开发通吃,维护成本低。我的判断是,MCP会成为事实标准,就像REST之于Web API。现在投入MCP,是给未来铺路。

面试官会怎么问

这块的问题套路我面过几个候选人,整理一下。

Q1:MCP和Function Calling什么关系?

Function Calling是LLM厂商定义的工具调用格式,每家不同。MCP是Anthropic推出的开放协议,标准化了工具发现、调用、传参、返回的完整流程。MCP在协议层,Function Calling在能力层。MCP Server一次开发,所有支持MCP的客户端通吃。

Q2:MCP三大原语是什么?

Tools(工具调用)、Resources(资源读取)、Prompts(提示词模板)。Tools是LLM调用业务方法,比如查订单。Resources是LLM读取静态资源,比如配置文件。Prompts是LLM使用预定义的提示词模板,比如代码review模板。生产中Tools用得最多。

Q3:stdio和sse两种传输模式怎么选?

stdio是本地进程通信,适合Claude Desktop这种本地客户端,延迟低但只能本机用。sse是HTTP服务,适合远程客户端(Cursor云端、自家产品),能跨网络但延迟高。本地工具用stdio,对外服务用sse。

Q4:MCP Server开发有什么坑?

五个坑。stdio模式日志不能打stdout会污染协议流。工具方法不能同名重载MCP按名识别。返回对象必须可序列化LocalDateTime要加注解。sse模式要单独端口别和业务塞一起。Claude Desktop权限沙箱限制文件和网络访问。

Q5(大厂追问):字节/百度会问什么?

字节会问MCP协议的JSON-RPC 2.0底层、sse模式的长连接管理。百度会问MCP与Function Calling的协议差异、MCP在RAG中的应用。核心都是"你有没有真的开发过MCP Server、踩过哪些坑"。

回头看

MCP的本质是"LLM调工具的标准化协议"。对Java后端来说,开发MCP Server就是写Spring Bean加@Tool注解,简单得很。难点在协议理解、传输模式选择、客户端适配、踩坑处理。

我的判断是MCP会成事实标准。现在投入,是给未来铺路。等Claude Desktop、Cursor、Windsurf都普及了,再写MCP就跟现在写REST API一样自然。