ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

旧 REST 接口封装成 MCP 服务:完整实战指南

旧 REST 接口封装成 MCP 服务:完整实战指南 当 AI Agent 需要调用你五年前写的 Spring MVC 接口你该怎么办一、背景一个真实的痛点相信很多团队都有这样的困境公司有一套运行多年的 Java REST 服务Spring MVC / JAX-RS / 甚至裸 Servlet业务逻辑稳定不想也不能大动但老板说“我们的系统要接入 AI Agent让大模型能调用这些接口”于是你打开 MCPModel Context Protocol的文档一脸懵这玩意儿怎么跟我的老项目对接MCP 是 Anthropic 提出的开放协议定义了 LLM 与外部工具之间的标准通信方式。它不关心你的后端是 Java、Python 还是 COBOL——它只关心你能不能暴露一组符合规范的Tool。所以核心问题变成了如何在不重写旧系统的前提下把 REST 接口翻译成 MCP Tool二、MCP 核心概念速览在动手之前花 2 分钟理解三个关键概念概念说明类比ToolLLM 可调用的函数有名称、描述、参数 Schema一个 REST API 端点Transport通信方式stdio本地进程或 SSE/HTTP远程HTTP vs gRPCJSON-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 核心 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-mcp-server-spring-boot-starter/artifactIdversion1.0.0/version/dependency!-- 用于调用旧REST接口的HTTP客户端 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-webflux/artifactId/dependency/dependencies5.2 配置# application.ymlspring:ai:mcp:server:name:legacy-order-mcpversion:1.0.0type:SYNC# stdio: 本地CLI场景# sse: 远程部署供多个Agent调用transport:ssesse-port:80905.3 编写 Tool核心代码ServicepublicclassLegacyOrderTools{privatefinalWebClientwebClient;publicLegacyOrderTools(WebClient.Builderbuilder){this.webClientbuilder.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{OrderDTOorderwebClient.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{CancelRequestreqnewCancelRequest(reason);CancelResultresultwebClient.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){StringBuildersbnewStringBuilder();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 ProviderConfigurationpublicclassMcpToolConfig{BeanpublicToolCallbackProviderorderToolProvider(LegacyOrderToolstools){returnMethodToolCallbackProvider.builder().toolObjects(tools).build();}}启动后MCP Server 自动在 8090 端口暴露 SSE 端点任何 MCP ClientClaude Desktop、Cursor、自研 Agent都能发现并调用这些 Tool。六、方案二MCP Java SDK轻量/非 Spring适用于 JDK 11 或不想引入 Spring Boot 的场景。dependencygroupIdio.modelcontextprotocol/groupIdartifactIdmcp/artifactIdversion0.10.0/version/dependencypublicclassLegacyApiMcpServer{privatestaticfinalHttpClientHTTPHttpClient.newHttpClient();privatestaticfinalStringBASE_URLhttp://legacy:8080;publicstaticvoidmain(String[]args){// 创建 stdio 传输的 MCP ServervartransportnewStdioServerTransport();varserverMcpServer.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);StringresultcallLegacyApi(/api/v1/users/userId);returnnewCallToolResult(List.of(newTextContent(result)),false);}));System.err.println(MCP Server started on stdio);}privatestaticStringcallLegacyApi(Stringpath){try{HttpRequestrequestHttpRequest.newBuilder().uri(URI.create(BASE_URLpath)).header(Authorization,Bearer internal-token).GET().build();HttpResponseStringresponseHTTP.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 mcpFastMCP(legacy-java-proxy)LEGACY_BASEhttp://legacy-java:8080/api/v1mcp.tool()asyncdefquery_order(order_id:str)-str:根据订单号查询订单详情。订单号格式如 ORD-2024-001234。 返回订单状态、金额和商品清单。asyncwithhttpx.AsyncClient(timeout10)asclient:respawaitclient.get(f{LEGACY_BASE}/orders/{order_id},headers{Authorization:Bearer internal-token})ifresp.status_code404:returnf订单{order_id}不存在请核实订单号。dataresp.json()return(f订单:{data[orderId]}\nf状态:{data[statusText]}\nf金额: ¥{data[totalAmount]}\nf商品:{, .join(i[name]foriindata[items])})mcp.tool()asyncdefsearch_products(keyword:str,category:str)-str:搜索商品。keyword为必填搜索关键词category可选如电子、服装、食品。params{keyword:keyword}ifcategory:params[category]categoryasyncwithhttpx.AsyncClient(timeout10)asclient:respawaitclient.get(f{LEGACY_BASE}/products/search,paramsparams)dataresp.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(transportstdio)八、关键设计要点踩坑总结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 resulttools.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 语义化翻译不改旧系统在中间加一层薄适配Tool 描述面向 AI写清楚什么时候用、怎么用、返回什么响应做裁剪只给 LLM 它需要的信息错误用自然语言别抛 JSON 错误码写操作加防护读操作先上线技术栈选择上能上 Spring AI 就上 Spring AI开发体验最好上不了就用 Python 代理最稳妥。不要为了纯 Java而硬凑MCP 是协议层的事跟语言无关。旧系统不是包袱它是你 MCP 服务的坚实后端。你只需要给它穿上一件AI 能看懂的外套。
返回列表