1. 旧 Spring MVC 接口接 AI Agent,卡在哪一步
你手上有一套跑了五六年的 Spring MVC 服务,订单、用户、库存这些接口天天在扛线上流量,业务逻辑稳得很。现在团队要接 AI Agent,让大模型能直接调这些接口,问题就来了:MCP 协议是给 LLM 用的,你的接口是给前端和 App 用的,两边说的不是一种话。
MCP 全称 Model Context Protocol,它定义的是 LLM 和外部工具之间的标准通信方式。它不关心你后端是 Java、Python 还是别的,只关心你能不能暴露一组符合规范的 Tool。所以真正要解决的问题不是重写旧系统,而是在中间加一层适配,把 REST 接口翻译成 MCP Tool。
这篇面向的是已有 AI Agent 工具链、手里有存量 Spring MVC 项目的团队。我会给出可复制的 MCP 服务骨架、TaoToken 统一 Key 的配置片段,以及一次端到端调用验证。旧系统零改动,所有适配逻辑收敛在 MCP Server 层,这是贯穿全文的原则。
适合谁看:Java 后端想快速把接口暴露给 Agent 的;团队已经在用 Claude Code、Cursor 这类工具,想接自研接口的;以及被 JDK 版本卡住、旧系统不敢动的人。下面从架构到代码一步步来。
2. 前置准备:TaoToken 统一 Key 与 MCP 适配层定位
在写代码之前,先把两个东西理清楚:一个是 MCP 适配层放在哪,另一个是模型调用通道怎么统一。
架构上分三段。左边是 LLM / AI Agent,中间是 MCP Server 适配层,右边是旧 Java REST 服务。Agent 通过 MCP 协议(stdio 或 SSE/HTTP)连到适配层,适配层再用普通 HTTP 调旧接口。旧服务完全不知道 MCP 的存在,它只看到熟悉的 REST 请求。
┌─────────────┐ MCP Protocol ┌──────────────────┐ HTTP ┌─────────────────┐ │ LLM / │ ◄─────────────► │ MCP Server │ ◄───────► │ 旧 Java REST │ │ AI Agent │ (stdio/SSE/HTTP)│ (适配层) │ (REST) │ 服务 (不动) │ └─────────────┘ └──────────────────┘ └─────────────────┘模型调用通道这块,我建议用 TaoToken 统一 Key 来管。原因是团队里往往不止一个模型、不止一个 Agent 工具,每个工具各配一套 Key 和地址,换模型时到处改配置,很容易漏。TaoToken 提供统一的 API 通道,模型对话、编码计划、控制台、API Keys 都在一个入口下,配置一次就能被多个工具复用。
你需要准备的东西:一个 TaoToken 账号,拿到统一 Key;旧 REST 服务的地址和一个内部调用 Token(建议走环境变量注入,别硬编码);JDK 17+ 和 Maven(走 Spring AI 方案的话)。
注意:旧接口的鉴权 Token 和 TaoToken 的 Key 是两回事。前者是适配层调旧服务用的,后者是 Agent 调模型用的,别混在一个配置里。
3. 可复制配置:MCP 服务骨架与 TaoToken 接入片段
先给依赖。走 Spring AI MCP Server 方案的话,pom 里加两块:MCP Server starter 和用来调旧接口的 WebClient。
<dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> </dependencies>application.yml 里配 MCP Server 的基本信息和传输方式。本地 CLI 场景用 stdio,远程给多个 Agent 调用用 sse。
spring: ai: mcp: server: name: legacy-order-mcp version: 1.0.0 type: SYNC transport: sse sse-port: 8090接下来是 Tool 的核心代码。这里的关键是:Tool 的 description 是写给 AI 看的,不是写给开发者看的。它要回答“什么时候该调我、参数长什么样、返回什么”。
@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 " + System.getenv("LEGACY_API_TOKEN")) .build(); } @Tool(description = "根据订单号查询订单详情。当用户询问'我的订单到哪了'、" + "'订单什么时候发货'时使用此工具。返回订单状态、总金额和商品列表。" + "订单号格式:ORD-YYYY-NNNNNN") public String queryOrder( @ToolParam(description = "订单编号,格式如 ORD-2024-001234") String orderId) { try { OrderDTO order = webClient.get() .uri("/api/v1/orders/{id}", orderId) .retrieve() .bodyToMono(OrderDTO.class) .timeout(Duration.ofSeconds(10)) .block(); return formatOrderSummary(order); } catch (Exception e) { return "查询订单时系统繁忙,请稍后重试。"; } } private String formatOrderSummary(OrderDTO order) { return String.format("订单%s,状态:%s,金额:%.2f元", order.getId(), order.getStatusText(), order.getAmount()); } }注册 Tool Provider,让 Spring 自动扫描到这些方法:
@Configuration public class McpToolConfig { @Bean public ToolCallbackProvider orderToolProvider(LegacyOrderTools tools) { return MethodToolCallbackProvider.builder() .toolObjects(tools) .build(); } }然后是 TaoToken 的配置片段。如果你用 Claude Code 这类工具,settings.json 里这样写:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "你的TaoToken统一Key" } }如果用支持 config.toml 的客户端,等价写法:
[provider] base_url = "https://taotoken.net/api" api_key = "你的TaoToken统一Key"这样模型调用走统一通道,MCP Server 只管把旧接口翻译成 Tool,两件事解耦,换模型不用动适配层代码。
4. 验证请求:一次端到端调用跑通
配置写完,先别急着接 Agent,用 MCP Inspector 单独验证 Tool 能不能被正确发现和调用。
npx @modelcontextprotocol/inspector打开浏览器界面后,你会看到注册的 Tool 列表,点进 queryOrder,手动输入ORD-2024-001234,观察原始 JSON-RPC 请求和响应。这一步能确认三件事:Tool 有没有注册成功、参数 Schema 对不对、旧接口返回有没有被正确裁剪。
Inspector 通过后,再接到真实 Agent 里测。以 Claude Desktop 为例,编辑claude_desktop_config.json:
{ "mcpServers": { "legacy-order": { "command": "java", "args": ["-jar", "legacy-order-mcp.jar"], "env": { "LEGACY_API_TOKEN": "your-internal-token" } } } }重启后在对话里输入“帮我查一下订单 ORD-2024-001234 的物流状态”。如果 Agent 正确调用了 queryOrder 并返回了裁剪后的摘要,说明整条链路通了。
单元测试也别省,重点验证响应裁剪有没有生效:
@Test void testQueryOrderTool() { String result = tools.queryOrder("ORD-2024-001234"); assertThat(result).contains("已发货"); assertThat(result).doesNotContain("cssClass"); }doesNotContain("cssClass")这行很关键,它保证旧接口里那些前端渲染用的冗余字段没被扔给 LLM。
5. 本篇常见错排查:Tool 不触发、超时、Token 浪费
Tool 描述太笼统,Agent 不调用。写成“调用订单查询接口”,LLM 不知道什么时候该用。改成“当用户询问订单进度、物流状态时使用”,并带上订单号格式示例,命中率会明显提升。
旧接口响应慢导致超时。旧系统可能扛着历史包袱,响应不稳定。WebClient 一定要设 timeout,并做降级:
.timeout(Duration.ofSeconds(10)) .onErrorResume(TimeoutException.class, e -> Mono.just("查询超时,旧系统响应较慢,请稍后再试。"))把整个 JSON 扔给 LLM,Token 烧得飞快。旧接口可能返回两百个字段,含大量前端用的样式类、埋点参数。必须在适配层裁剪,只留 LLM 推理需要的字段。上面formatOrderSummary就是干这个的。
错误信息抛 JSON 错误码。别返回{"code":50023,"msg":"ORD_NOT_EXIST"},LLM 看不懂。改成自然语言:“订单号 ORD-2024-999999 不存在,请检查是否有拼写错误。”
写操作没防护。取消订单、删除账户这类 Tool,description 里要明确标注危险,并在业务层加二次确认。读操作先上线,稳定后再开放写操作。
TaoToken Key 和旧接口 Token 混用。两者作用域不同,配置里分开写,旧接口 Token 走环境变量,别提交到仓库。
6. 把旧接口稳定交给 Agent 调用
走到这里,你已经有了一个能跑的 MCP 适配层:旧 Spring MVC 服务零改动,Tool 描述面向 AI 写清楚,响应做了裁剪,错误用自然语言,超时和降级都配上了。模型调用侧用 TaoToken 统一 Key 管起来,换模型、加 Agent 工具都不用动适配层。
后续要接更多旧接口,就是复制 Tool 方法、写好 description、注册 Provider 这三步。生产上记得加限流,防止 LLM 循环调用把旧系统打垮;日志记录每次 Tool 调用的入参、耗时和响应摘要,方便排查。
如果你还没配好统一 Key,可以从 API Keys 页面拿到 Key,再对照接入文档把 settings.json 或 config.toml 补全。想先验证模型通道通不通,去模型对话里发一条消息试试;如果团队要长期跑编码和 Agent 任务,Coding Plan 会更省心。旧系统不是包袱,给它穿一件 AI 能看懂的外套就行。