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

当 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 分钟理解三个关键概念:

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

一个 MCP Tool 的 JSON Schema 长这样:

json 复制代码
{
  "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 无缝集成 | 要求 JDK 17+,Spring Boot 3.x |
| 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 添加依赖

xml 复制代码
<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 配置

yaml 复制代码
# application.yml
spring:
  ai:
    mcp:
      server:
        name: legacy-order-mcp
        version: 1.0.0
        type: SYNC
        # stdio: 本地CLI场景
        # sse: 远程部署,供多个Agent调用
        transport: sse
        sse-port: 8090

5.3 编写 Tool(核心代码)

java 复制代码
@Service
public class LegacyOrderTools {

    private final WebClient webClient;

    public LegacyOrderTools(WebClient.Builder builder) {
        this.webClient = builder
                .baseUrl("http://legacy-order-service:8080")
                .defaultHeader("Authorization", "Bearer " + getInternalToken())
                .build();
    }

    /**
     * 查询订单 ------ 对应旧接口 GET /api/v1/orders/{id}
     */
    @Tool(description = "根据订单号查询订单详情。返回订单状态、总金额、" +
                        "商品列表。适用于用户询问订单进度、物流状态等场景。")
    public String queryOrder(
            @ToolParam(description = "订单编号,格式如 ORD-2024-001234") 
            String orderId) {
        
        try {
            OrderDTO order = webClient.get()
                    .uri("/api/v1/orders/{id}", orderId)
                    .retrieve()
                    .onStatus(HttpStatusCode::is4xxClientError, resp -> {
                        if (resp.statusCode().value() == 404) {
                            return Mono.error(new OrderNotFoundException(orderId));
                        }
                        return Mono.error(new RuntimeException("查询失败"));
                    })
                    .bodyToMono(OrderDTO.class)
                    .block(Duration.ofSeconds(10));

            // 关键:裁剪响应,只返回LLM需要的字段
            return formatOrderSummary(order);
            
        } catch (OrderNotFoundException e) {
            return "未找到订单号 " + orderId + ",请确认订单号是否正确。";
        } catch (Exception e) {
            return "查询订单时系统繁忙,请稍后重试。";
        }
    }

    /**
     * 取消订单 ------ 对应旧接口 POST /api/v1/orders/{id}/cancel
     */
    @Tool(description = "取消指定订单。仅支持状态为'待付款'或'待发货'的订单。" +
                        "此操作不可逆,调用前请与用户确认。")
    public String cancelOrder(
            @ToolParam(description = "要取消的订单编号") String orderId,
            @ToolParam(description = "取消原因,如:不想要了、买错了") String reason) {
        
        try {
            CancelRequest req = new CancelRequest(reason);
            CancelResult result = webClient.post()
                    .uri("/api/v1/orders/{id}/cancel", orderId)
                    .contentType(MediaType.APPLICATION_JSON)
                    .bodyValue(req)
                    .retrieve()
                    .bodyToMono(CancelResult.class)
                    .block(Duration.ofSeconds(15));

            return result.isSuccess() 
                    ? "订单 " + orderId + " 已成功取消。" 
                    : "取消失败:" + result.getMessage();
                    
        } catch (Exception e) {
            return "取消订单操作失败,请联系客服处理。";
        }
    }

    private String formatOrderSummary(OrderDTO order) {
        StringBuilder sb = new StringBuilder();
        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 (OrderItem item : order.getItems()) {
            sb.append("  - ").append(item.getName())
              .append(" x").append(item.getQty())
              .append(" ¥").append(item.getPrice()).append("\n");
        }
        return sb.toString();
    }
}

5.4 注册 Tool Provider

java 复制代码
@Configuration
public class McpToolConfig {

    @Bean
    public ToolCallbackProvider orderToolProvider(LegacyOrderTools tools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(tools)
                .build();
    }
}

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

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

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

xml 复制代码
<dependency>
    <groupId>io.modelcontextprotocol</groupId>
    <artifactId>mcp</artifactId>
    <version>0.10.0</version>
</dependency>
java 复制代码
public class LegacyApiMcpServer {

    private static final HttpClient HTTP = HttpClient.newHttpClient();
    private static final String BASE_URL = "http://legacy:8080";

    public static void main(String[] args) {
        // 创建 stdio 传输的 MCP Server
        var transport = new StdioServerTransport();
        var server = McpServer.sync(transport)
                .serverInfo("legacy-api-mcp", "1.0.0")
                .capabilities(ServerCapabilities.builder().tools(true).build())
                .build();

        // 注册 Tool
        server.addTool(new McpServerFeatures.SyncToolSpecification(
            new Tool("query_user",
                     "根据用户ID查询用户基本信息,包括姓名、手机号、注册时间",
                     buildQueryUserSchema()),
            (exchange, arguments) -> {
                String userId = (String) arguments.get("userId");
                String result = callLegacyApi("/api/v1/users/" + userId);
                return new CallToolResult(List.of(new TextContent(result)), false);
            }
        ));

        System.err.println("MCP Server started on stdio");
    }

    private static String callLegacyApi(String path) {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(BASE_URL + path))
                    .header("Authorization", "Bearer internal-token")
                    .GET()
                    .build();
            HttpResponse<String> response = HTTP.send(request, 
                    HttpResponse.BodyHandlers.ofString());
            return simplifyResponse(response.body());
        } catch (Exception e) {
            return "调用失败: " + e.getMessage();
        }
    }

    private static String simplifyResponse(String json) {
        // 解析JSON,只提取关键字段,避免Token浪费
        // 实际项目中可用 Jackson / Gson
        return json; // 示意
    }
}

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

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

python 复制代码
from mcp.server.fastmcp import FastMCP
import httpx

mcp = FastMCP("legacy-java-proxy")

LEGACY_BASE = "http://legacy-java:8080/api/v1"

@mcp.tool()
async def query_order(order_id: str) -> str:
    """根据订单号查询订单详情。订单号格式如 ORD-2024-001234。
    返回订单状态、金额和商品清单。"""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(
            f"{LEGACY_BASE}/orders/{order_id}",
            headers={"Authorization": "Bearer internal-token"}
        )
        if resp.status_code == 404:
            return f"订单 {order_id} 不存在,请核实订单号。"
        data = resp.json()
        return (
            f"订单: {data['orderId']}\n"
            f"状态: {data['statusText']}\n"
            f"金额: ¥{data['totalAmount']}\n"
            f"商品: {', '.join(i['name'] for i in data['items'])}"
        )

@mcp.tool()
async def search_products(keyword: str, category: str = "") -> str:
    """搜索商品。keyword为必填搜索关键词,category可选(如:电子、服装、食品)。"""
    params = {"keyword": keyword}
    if category:
        params["category"] = category
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(f"{LEGACY_BASE}/products/search", params=params)
        data = resp.json()
        if not data:
            return f"未找到与'{keyword}'相关的商品。"
        lines = [f"找到{len(data)}件商品:"]
        for p in data[:5]:  # 最多返回5条,控制Token
            lines.append(f"  - {p['name']} ¥{p['price']} ({p['category']})")
        return "\n".join(lines)

if __name__ == "__main__":
    mcp.run(transport="stdio")

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

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

❌ 错误:

java 复制代码
@Tool(description = "调用订单查询接口")

✅ 正确:

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

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

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

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

8.3 错误信息用自然语言

java 复制代码
// ❌ 不要这样
throw new RuntimeException("{\"code\":50023,\"msg\":\"ORD_NOT_EXIST\"}");

// ✅ 要这样
return "订单号 ORD-2024-999999 不存在。请检查是否有拼写错误,或联系人工客服。";

8.4 写操作加防护

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

8.5 超时与重试

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

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

九、测试与调试

9.1 MCP Inspector(可视化调试)

bash 复制代码
npx @modelcontextprotocol/inspector

打开浏览器界面,可以:

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

9.2 接入 Claude Desktop 测试

编辑 claude_desktop_config.json:

json 复制代码
{
  "mcpServers": {
    "legacy-order": {
      "command": "java",
      "args": ["-jar", "legacy-order-mcp.jar"],
      "env": {
        "LEGACY_API_TOKEN": "your-token"
      }
    }
  }
}
然后在对话中测试:"帮我查一下订单 ORD-2024-001234 的物流状态"。
### 9.3 单元测试
```java
@Test
void testQueryOrderTool() {
    // 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 能看懂的外套"。

相关推荐
2501_9381400741 分钟前
做内容很省心,2026好用AI笔记,支持抖音小红书视频无水印保存
人工智能
sell-cloud3 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国5 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI5 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区5 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
Java爱好狂.7 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
XCDoris7 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy8 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V8 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI8 小时前
AI英语教培智能体的开发
人工智能