旧 REST 接口封装成 MCP 服务:完整实战指南
2026/8/21 4:05:17 网站建设 项目流程

当 AI Agent 需要调用你五年前写的 Spring MVC 接口,你该怎么办?

一、背景:一个真实的痛点

相信很多团队都有这样的困境:

  • 公司有一套运行多年的 Java REST 服务(Spring MVC / JAX-RS / 甚至裸 Servlet)
  • 业务逻辑稳定,不想也不能大动
  • 但老板说:“我们的系统要接入 AI Agent,让大模型能调用这些接口”
  • 于是你打开 MCP(Model Context Protocol)的文档,一脸懵:这玩意儿怎么跟我的老项目对接?

MCP 是 Anthropic 提出的开放协议,定义了 LLM 与外部工具之间的标准通信方式。它不关心你的后端是 Java、Python 还是 COBOL——它只关心你能不能暴露一组符合规范的Tool
所以核心问题变成了:如何在不重写旧系统的前提下,把 REST 接口"翻译"成 MCP Tool?

二、MCP 核心概念速览

在动手之前,花 2 分钟理解三个关键概念:

概念说明类比
ToolLLM 可调用的函数,有名称、描述、参数 Schema一个 REST API 端点
Transport通信方式:stdio(本地进程)或 SSE/HTTP(远程)HTTP vs gRPC
JSON-RPCMCP 底层通信协议REST 底层的 HTTP

一个 MCP Tool 的 JSON Schema 长这样:

{"name":"queryOrder","description":"根据订单号查询订单详情,包含状态、金额、商品列表","inputSchema":{"type":"object","properties":{"orderId":{"type":"string","description":"订单编号,格式 ORD-2024-XXXXX"}},"required":["orderId"]}}

LLM 看到这个描述后,就知道什么时候该调它、怎么传参。这就是 MCP 与传统 API 网关的本质区别:接口的语义描述是面向 AI 的,不是面向前端开发者的。

三、整体架构

┌─────────────┐ MCP Protocol ┌──────────────────┐ HTTP ┌─────────────────┐ │ LLM / │ ◄──────────────────────► │ MCP Server │ ◄────────────► │ 旧 Java REST │ │ AI Agent │ (stdio / SSE / HTTP) │ (适配层) │ (REST调用) │ 服务 (不动) │ └─────────────┘ └──────────────────┘ └─────────────────┘

关键原则:旧系统零改动,所有适配逻辑收敛在 MCP Server 层。

四、方案选型

方案对比

方案适用场景优点 缺点
Spring AI MCP Server已有 Spring Boot 项目,JDK 17+注解驱动,生态好,与 Spring 无缝集成
MCP Java SDK(官方)非 Spring 项目或需轻量部署无框架绑定,灵活 需手动注册 Tool、管理生命周期
Python/TS 代理旧系统 JDK 8/11 无法升级零侵入,生态最成熟

怎么选?

旧系统能升级 JDK 17 + Spring Boot 3? ├── 是 → Spring AI MCP(首选) └── 否 → 旧系统能加一个独立 Java 模块? ├── 是 → MCP Java SDK 独立进程 └── 否 → Python/TS 代理(最稳妥)

下面分别给出实现。

五、方案一:Spring AI MCP Server(推荐)

5.1 添加依赖

<dependencies><!-- MCP Server 核心 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId><version>1.0.0</version></dependency><!-- 用于调用旧REST接口的HTTP客户端 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency></dependencies>

5.2 配置

# application.ymlspring:ai:mcp:server:name:legacy-order-mcpversion:1.0.0type:SYNC# stdio: 本地CLI场景# sse: 远程部署,供多个Agent调用transport:ssesse-port:8090

5.3 编写 Tool(核心代码)

@ServicepublicclassLegacyOrderTools{privatefinalWebClientwebClient;publicLegacyOrderTools(WebClient.Builderbuilder){this.webClient=builder.baseUrl("http://legacy-order-service:8080").defaultHeader("Authorization","Bearer "+getInternalToken()).build();}/** * 查询订单 —— 对应旧接口 GET /api/v1/orders/{id} */@Tool(description="根据订单号查询订单详情。返回订单状态、总金额、"+"商品列表。适用于用户询问订单进度、物流状态等场景。")publicStringqueryOrder(@ToolParam(description="订单编号,格式如 ORD-2024-001234")StringorderId){try{OrderDTOorder=webClient.get().uri("/api/v1/orders/{id}",orderId).retrieve().onStatus(HttpStatusCode::is4xxClientError,resp->{if(resp.statusCode().value()==404){returnMono.error(newOrderNotFoundException(orderId));}returnMono.error(newRuntimeException("查询失败"));}).bodyToMono(OrderDTO.class).block(Duration.ofSeconds(10));// 关键:裁剪响应,只返回LLM需要的字段returnformatOrderSummary(order);}catch(OrderNotFoundExceptione){return"未找到订单号 "+orderId+",请确认订单号是否正确。";}catch(Exceptione){return"查询订单时系统繁忙,请稍后重试。";}}/** * 取消订单 —— 对应旧接口 POST /api/v1/orders/{id}/cancel */@Tool(description="取消指定订单。仅支持状态为'待付款'或'待发货'的订单。"+"此操作不可逆,调用前请与用户确认。")publicStringcancelOrder(@ToolParam(description="要取消的订单编号")StringorderId,@ToolParam(description="取消原因,如:不想要了、买错了")Stringreason){try{CancelRequestreq=newCancelRequest(reason);CancelResultresult=webClient.post().uri("/api/v1/orders/{id}/cancel",orderId).contentType(MediaType.APPLICATION_JSON).bodyValue(req).retrieve().bodyToMono(CancelResult.class).block(Duration.ofSeconds(15));returnresult.isSuccess()?"订单 "+orderId+" 已成功取消。":"取消失败:"+result.getMessage();}catch(Exceptione){return"取消订单操作失败,请联系客服处理。";}}privateStringformatOrderSummary(OrderDTOorder){StringBuildersb=newStringBuilder();sb.append("订单号: ").append(order.getId()).append("\n");sb.append("状态: ").append(order.getStatusText()).append("\n");sb.append("总金额: ¥").append(order.getTotalAmount()).append("\n");sb.append("下单时间: ").append(order.getCreateTime()).append("\n");sb.append("商品:\n");for(OrderItemitem:order.getItems()){sb.append(" - ").append(item.getName()).append(" x").append(item.getQty()).append(" ¥").append(item.getPrice()).append("\n");}returnsb.toString();}}

5.4 注册 Tool Provider

@ConfigurationpublicclassMcpToolConfig{@BeanpublicToolCallbackProviderorderToolProvider(LegacyOrderToolstools){returnMethodToolCallbackProvider.builder().toolObjects(tools).build();}}

启动后,MCP Server 自动在 8090 端口暴露 SSE 端点,任何 MCP Client(Claude Desktop、Cursor、自研 Agent)都能发现并调用这些 Tool。

六、方案二:MCP Java SDK(轻量/非 Spring)

适用于 JDK 11 或不想引入 Spring Boot 的场景。

<dependency><groupId>io.modelcontextprotocol</groupId><artifactId>mcp</artifactId><version>0.10.0</version></dependency>
publicclassLegacyApiMcpServer{privatestaticfinalHttpClientHTTP=HttpClient.newHttpClient();privatestaticfinalStringBASE_URL="http://legacy:8080";publicstaticvoidmain(String[]args){// 创建 stdio 传输的 MCP Servervartransport=newStdioServerTransport();varserver=McpServer.sync(transport).serverInfo("legacy-api-mcp","1.0.0").capabilities(ServerCapabilities.builder().tools(true).build()).build();// 注册 Toolserver.addTool(newMcpServerFeatures.SyncToolSpecification(newTool("query_user","根据用户ID查询用户基本信息,包括姓名、手机号、注册时间",buildQueryUserSchema()),(exchange,arguments)->{StringuserId=(String)arguments.get("userId");Stringresult=callLegacyApi("/api/v1/users/"+userId);returnnewCallToolResult(List.of(newTextContent(result)),false);}));System.err.println("MCP Server started on stdio");}privatestaticStringcallLegacyApi(Stringpath){try{HttpRequestrequest=HttpRequest.newBuilder().uri(URI.create(BASE_URL+path)).header("Authorization","Bearer internal-token").GET().build();HttpResponse<String>response=HTTP.send(request,HttpResponse.BodyHandlers.ofString());returnsimplifyResponse(response.body());}catch(Exceptione){return"调用失败: "+e.getMessage();}}privatestaticStringsimplifyResponse(Stringjson){// 解析JSON,只提取关键字段,避免Token浪费// 实际项目中可用 Jackson / Gsonreturnjson;// 示意}}

七、方案三:Python 代理(旧系统完全不动)

当旧系统是 JDK 8 的 WAR 包、部署在 Tomcat 上、没人敢碰时:

frommcp.server.fastmcpimportFastMCPimporthttpx mcp=FastMCP("legacy-java-proxy")LEGACY_BASE="http://legacy-java:8080/api/v1"@mcp.tool()asyncdefquery_order(order_id:str)->str:"""根据订单号查询订单详情。订单号格式如 ORD-2024-001234。 返回订单状态、金额和商品清单。"""asyncwithhttpx.AsyncClient(timeout=10)asclient:resp=awaitclient.get(f"{LEGACY_BASE}/orders/{order_id}",headers={"Authorization":"Bearer internal-token"})ifresp.status_code==404:returnf"订单{order_id}不存在,请核实订单号。"data=resp.json()return(f"订单:{data['orderId']}\n"f"状态:{data['statusText']}\n"f"金额: ¥{data['totalAmount']}\n"f"商品:{', '.join(i['name']foriindata['items'])}")@mcp.tool()asyncdefsearch_products(keyword:str,category:str="")->str:"""搜索商品。keyword为必填搜索关键词,category可选(如:电子、服装、食品)。"""params={"keyword":keyword}ifcategory:params["category"]=categoryasyncwithhttpx.AsyncClient(timeout=10)asclient:resp=awaitclient.get(f"{LEGACY_BASE}/products/search",params=params)data=resp.json()ifnotdata:returnf"未找到与'{keyword}'相关的商品。"lines=[f"找到{len(data)}件商品:"]forpindata[:5]:# 最多返回5条,控制Tokenlines.append(f" -{p['name']}¥{p['price']}({p['category']})")return"\n".join(lines)if__name__=="__main__":mcp.run(transport="stdio")

八、关键设计要点(踩坑总结)

8.1 Tool 描述是给 AI 看的,不是给开发者看的

❌ 错误:

@Tool(description="调用订单查询接口")

✅ 正确:

@Tool(description="根据订单号查询订单详情。当用户询问'我的订单到哪了'、"+"'订单什么时候发货'时使用此工具。返回物流状态和预计到达时间。"+"订单号格式:ORD-YYYY-NNNNNN")

8.2 响应裁剪:别把整个 JSON 扔给 LLM

旧接口可能返回 200 个字段(含前端渲染用的 cssClass、trackingParams)。必须过滤:

// 只提取 LLM 推理需要的字段returnString.format("订单%s,状态:%s,金额:%.2f元",order.getId(),order.getStatusText(),order.getAmount());

8.3 错误信息用自然语言

// ❌ 不要这样thrownewRuntimeException("{\"code\":50023,\"msg\":\"ORD_NOT_EXIST\"}");// ✅ 要这样return"订单号 ORD-2024-999999 不存在。请检查是否有拼写错误,或联系人工客服。";

8.4 写操作加防护

@Tool(description="删除用户账户。【危险操作】此操作不可逆,"+"必须在用户明确确认后才能调用。")publicStringdeleteUser(StringuserId){// 实际生产中可加二次确认机制}

8.5 超时与重试

旧系统可能响应慢,务必设置超时:

webClient.get().uri(...).retrieve().bodyToMono(String.class).timeout(Duration.ofSeconds(10))// 10秒超时.onErrorResume(TimeoutException.class,e->Mono.just("查询超时,旧系统响应较慢,请稍后再试。"));

九、测试与调试

9.1 MCP Inspector(可视化调试)

npx @modelcontextprotocol/inspector

打开浏览器界面,可以:

  • 查看注册的 Tool 列表
  • 手动输入参数调用 Tool
  • 查看原始 JSON-RPC 请求/响应

9.2 接入 Claude Desktop 测试

编辑 claude_desktop_config.json:

{"mcpServers":{"legacy-order":{"command":"java","args":["-jar","legacy-order-mcp.jar"],"env":{"LEGACY_API_TOKEN":"your-token"}}}}然后在对话中测试:"帮我查一下订单 ORD-2024-001234 的物流状态"。 ###9.3单元测试```java @TestvoidtestQueryOrderTool(){// Mock 旧接口返回when(webClient.get()).thenReturn(mockOrderResponse());String result=tools.queryOrder("ORD-2024-001234");assertThat(result).contains("已发货");assertThat(result).contains("¥299.00");assertThat(result).doesNotContain("cssClass");// 确认冗余字段已过滤}

十、生产部署建议

关注点建议
认证旧接口 Token 通过环境变量注入,不硬编码
限流MCP 层加 RateLimiter,防止 LLM 循环调用打垮旧系统
日志记录每次 Tool 调用的入参、耗时、响应摘要
监控暴露 Prometheus metrics:调用次数、错误率、P99延迟
灰度先只暴露只读接口(GET),验证稳定后再开放写操作
版本管理Tool 的 description 变更需走 Review,因为直接影响 LLM 行为

十一、总结

把旧 REST 接口封装成 MCP 服务,本质上是在做API 语义化翻译

  1. 不改旧系统,在中间加一层薄适配
  2. Tool 描述面向 AI,写清楚"什么时候用、怎么用、返回什么"
  3. 响应做裁剪,只给 LLM 它需要的信息
  4. 错误用自然语言,别抛 JSON 错误码
  5. 写操作加防护,读操作先上线

技术栈选择上:能上 Spring AI 就上 Spring AI(开发体验最好);上不了就用 Python 代理(最稳妥)。不要为了"纯 Java"而硬凑,MCP 是协议层的事,跟语言无关。
旧系统不是包袱,它是你 MCP 服务的坚实后端。你只需要给它穿上一件"AI 能看懂的外套"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询