1. 存量 Java 接口迁移 MCP Server 的真实痛点
如果你手上有一套跑了三五年的 Spring Boot 微服务,Controller 里躺着几十上百个 REST 接口,现在团队要求把这些能力开放给大模型工作流调用,你大概率会经历这么几个阶段:先兴奋,觉得不就是包一层协议吗;然后打开 IDE,发现每个接口的入参、出参、鉴权、异常处理都不一样;最后陷入沉默,因为人工逐个改造的工时根本排不出来。
这就是存量 Java 接口批量迁移 MCP Server 的核心矛盾:MCP 协议本身不复杂,复杂的是你已有的接口资产太"脏"。命名风格不统一,有的用getUserInfo,有的用queryUserDetail;参数校验有的靠@Valid,有的在 Service 层手写 if;返回结构有的包ResponseEntity,有的直接返回对象。你不可能为了接 MCP 把这些接口全部重写一遍,业务方不会给你这个窗口期。
我试过的思路是:把迁移拆成"扫描—描述—注册—验证"四段流水线,让机器去干重复劳动,人只负责审核工具描述和边界情况。MCP Server 在这里扮演的角色,是把原本散落在各个 Controller 里的方法,统一封装成模型可发现、可调用的工具集。模型不需要知道你的接口是 GET 还是 POST,它只需要知道"有个工具叫 query_weather,输入城市名,返回天气数据"。
适合读这篇的人有三类:一是手里有 Spring Boot 单体或微服务、需要快速接入 MCP 的 Java 后端;二是正在做 AI Agent 平台、需要把内部系统能力暴露给模型的架构师;三是想搞清楚 MCP 工具描述到底该怎么写才不容易被模型误调用的工程师。下面我会给出可复制的扫描脚本、工具描述模板、批量注册配置,以及迁移前后调用一致性的验证动作。整套流程的目标,是把"人工逐个改造"压缩成"跑一遍脚本 + 人工审核描述"。
在动手之前,先明确一个边界:MCP Server 不是替代你的业务逻辑,它是一层适配。你的 Service 层、DAO 层、事务控制都不动,MCP 只负责把方法签名翻译成模型能理解的工具定义,再把模型的调用请求路由回你的方法。想清楚这一点,后面的工程化路径就顺了。
2. TaoToken 前置准备:MCP 工具调用链的模型侧配置
批量迁移出来的 MCP Server 最终是要被模型调用的,所以你得先把模型侧的通道打通。这里用 TaoToken 作为模型接入层,它的作用是让你在验证 MCP 工具时不用来回切换多个模型供应商的 Key,一个 Base URL 就能覆盖 Claude、GPT 等常用模型。
先说清楚要准备什么。你需要一个 API Key,以及三个关键配置项:Base URL、Key、Model ID。这三件套在后面的 Cline MCP、Claude Code、Codex 配置里都会反复出现,建议先记下来。
Base URL 填https://taotoken.net/api,注意这个地址不带任何查询参数。API Key 在控制台的 API Keys 页面创建,创建后只显示一次,复制到安全的地方。Model ID 根据你要验证的模型来选,比如验证工具调用能力时用 Claude 系列比较稳,因为它的 tool use 格式和 MCP 契合度高。
如果你只是临时验证 MCP 工具能不能被正确调用,用模型对话页面就够了,把工具描述贴进去,看模型能不能理解参数含义。但如果你要做长期的编码和 Agent 调试,建议直接上 Coding Plan,因为 MCP 工具的调试往往需要多轮对话,按量计费在密集调试时成本不好控。
这里有个容易踩的坑:很多人以为 MCP Server 跑起来就完事了,结果模型侧根本没配好,调用一直失败还以为是 Server 的问题。实际上 MCP 的调用链是"模型 → 模型接入层 → MCP Client → MCP Server → 你的 Java 接口",任何一环断了都调不通。所以先把模型侧的 Base URL 和 Key 配好,再去调 Server,排障时能少走一半弯路。
配置的时候注意,Base URL 后面不要手动加/v1之类的路径,TaoToken 的接入地址已经包含了必要的路由。Key 的权限范围也要确认,有些 Key 只开了对话权限,调工具时会报权限不足。这些细节在接入文档里都有说明,配之前扫一眼能省不少事。
3. 可复制配置:接口扫描脚本与 MCP 工具描述模板
这一节是整篇的核心,给你能直接跑的代码和配置。先解决"怎么把存量接口的元数据批量抠出来"。
假设你的项目用 Swagger/OpenAPI 注解标注了接口,那扫描就简单很多。下面这个脚本基于 Spring 的RequestMappingHandlerMapping遍历所有注册的 Handler,把路径、方法、参数、注解信息导出成 JSON。把它放在一个独立的@Component里,启动时跑一次即可。
@Component public class McpInterfaceScanner implements ApplicationRunner { @Autowired private RequestMappingHandlerMapping handlerMapping; @Override public void run(ApplicationArguments args) throws Exception { List<Map<String, Object>> tools = new ArrayList<>(); for (RequestMappingInfo info : handlerMapping.getHandlerMethods().keySet()) { HandlerMethod handler = handlerMapping.getHandlerMethods().get(info); Map<String, Object> tool = new LinkedHashMap<>(); tool.put("className", handler.getBeanType().getSimpleName()); tool.put("methodName", handler.getMethod().getName()); tool.put("paths", info.getPatternValues()); tool.put("httpMethods", info.getMethodsCondition().getMethods()); tool.put("params", extractParams(handler)); tool.put("description", resolveDescription(handler)); tools.add(tool); } Files.write(Paths.get("mcp-tools-scan.json"), new ObjectMapper().writerWithDefaultPrettyPrinter() .writeValueAsBytes(tools)); } private List<Map<String, String>> extractParams(HandlerMethod handler) { List<Map<String, String>> params = new ArrayList<>(); for (MethodParameter mp : handler.getMethodParameters()) { Map<String, String> p = new LinkedHashMap<>(); p.put("name", mp.getParameterName()); p.put("type", mp.getParameterType().getSimpleName()); RequestParam rp = mp.getParameterAnnotation(RequestParam.class); if (rp != null) { p.put("required", String.valueOf(rp.required())); p.put("defaultValue", rp.defaultValue()); } params.add(p); } return params; } private String resolveDescription(HandlerMethod handler) { ApiOperation op = handler.getMethodAnnotation(ApiOperation.class); return op != null ? op.value() : handler.getMethod().getName(); } }跑完之后你会得到一个mcp-tools-scan.json,里面是全部接口的元数据。接下来是把它转成 MCP 工具描述。MCP 的工具描述质量直接决定模型调用准确率,所以模板要写清楚三件事:工具名、用途、参数含义。
下面是一个工具描述的 JSON 模板,你可以用脚本批量套用:
{ "name": "query_weather_current", "description": "查询指定城市的实时天气。当用户询问某地当前天气、温度、湿度时调用此工具。", "inputSchema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如 北京、上海、Shenzhen" } }, "required": ["city"] } }注意description里我特意写了"当用户询问某地当前天气时调用",这是给模型的触发提示。很多迁移失败案例不是代码问题,而是工具描述太干,模型不知道什么时候该用它。批量生成时,可以用接口的@ApiOperation值作为基础,再补一句触发场景。
然后是 MCP Server 的注册配置。如果你用 Spring AI 的 MCP 支持,配置长这样:
spring: ai: mcp: server: name: legacy-java-mcp version: 1.0.0 type: SYNC sse-endpoint: /mcp/sse capabilities: tool: true resource: false prompt: false如果你用的是独立的 MCP Server 框架,把扫描出来的工具逐个注册进去即可。批量注册的关键是让工具名和你的 Java 方法建立映射,调用时能路由回去。建议在工具名里保留原方法名的语义,比如query_weather_current对应WeatherController#getCurrentWeather,这样排障时一眼能对上。
4. 验证请求:迁移前后调用一致性怎么测
工具注册完,别急着接模型,先做一致性验证。这一步的目的是确认 MCP 调用和原来的 REST 调用返回结果一致,避免迁移引入隐性 bug。
验证分两层。第一层是直接调 MCP Server 的 SSE 端点,模拟一次工具调用,看返回结构。第二层是接上模型,让模型根据自然语言触发工具,看它选的工具和参数对不对。
第一层可以用 curl 快速验证。假设你的 MCP Server 跑在 8080,SSE 端点是/mcp/sse:
curl -N http://localhost:8080/mcp/sse \ -H "Accept: text/event-stream"拿到 session 后,发送工具调用请求:
curl -X POST http://localhost:8080/mcp/message?sessionId=YOUR_SESSION \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "query_weather_current", "arguments": {"city": "北京"} } }'把返回结果和你直接调原 REST 接口的结果对比。重点看三个地方:字段名是否一致、数据类型是否一致、空值处理是否一致。我踩过的坑是原接口返回null时 MCP 序列化成了空字符串,模型拿到后判断出错。这种问题只能靠对比测出来。
第二层验证接模型。在 TaoToken 的模型对话页面,把工具描述贴进去,然后问"北京现在天气怎么样",看模型是否调用了query_weather_current且参数是"北京"。如果模型没调用,说明描述里的触发场景不够明确;如果参数错了,说明inputSchema的 description 写得不够清楚。
批量验证时,可以写个脚本遍历mcp-tools-scan.json,对每个工具构造一条自然语言 query,自动跑一遍看命中率。命中率低于 80% 的工具,回去改描述。这个过程听起来笨,但比上线后被用户发现"模型不听话"要划算得多。
一致性验证通过后,再考虑灰度。先放非核心接口给模型用,观察一段时间调用日志,确认没有异常路由和超时,再逐步放开核心接口。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
迁移过程中报错集中在几个地方,我按出现频率排一下。
401 Unauthorized最常见,八成是 Key 或 Base URL 配错了。检查三件套:Base URL 是不是https://taotoken.net/api,Key 有没有多余空格,Model ID 是不是当前 Key 有权限的模型。如果 MCP Server 自己也做了鉴权,确认请求头里的Authorization格式是Bearer <key>。还有一种情况是 Key 创建后没启用,去控制台确认状态。
local proxy failed通常出现在 MCP Client 侧,说明 Client 连不上 Server。先确认 Server 进程活着,端口没被占。然后看 Client 配置里的 Server 地址是不是localhost,有些环境里localhost解析到 IPv6 而 Server 只监听了 IPv4,改成127.0.0.1就好了。如果是容器环境,注意网络模式,localhost在容器里指向容器自己,不是宿主机。
reading choices 相关报错一般出现在模型返回结构解析阶段,说明模型接入层返回的格式和 Client 预期不一致。检查 Model ID 是否选对,有些模型不支持 tool use,硬调就会返回纯文本,Client 解析choices时找不到工具调用字段就报错。换成支持 function calling 的模型即可。
OAuth 报错多出现在 Claude Code 或 Codex 这类工具的认证环节。如果你用 API Key 方式接入,确认配置里没有残留的 OAuth 配置项。以 Codex 的auth.json为例,正确写法是:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "claude-sonnet-4-20250514" }三件套缺一不可。如果之前配过 OAuth,把旧的 token 字段删干净,否则工具会优先走 OAuth 流程然后失败。
排障的通用思路是分层定位:先确认模型接入层通不通(用模型对话页面发一条普通消息),再确认 MCP Server 通不通(用 curl 直接调),最后确认两者之间的 Client 配置对不对。一层层排除,比盲目改配置快得多。
6. 长期编码与 Agent 场景的接入建议
如果你迁移 MCP Server 的目的是做长期编码助手或自动化 Agent,那配置方式要和临时验证区分开。临时验证用模型对话页面就行,但长期跑建议用 Coding Plan,因为 Agent 场景的调用密度高,按量计费容易失控,包月方案更稳。
Cline MCP 的配置是个典型例子。在 Cline 的 MCP 设置里,你需要填 Server 的启动命令或 SSE 地址,同时确保模型侧的 Base URL 和 Key 已经配好。Cline 会同时用到模型接入和 MCP Server,两边都要通。配置片段如下:
{ "mcpServers": { "legacy-java": { "url": "http://127.0.0.1:8080/mcp/sse", "disabled": false } } }模型侧在 Cline 的设置里填 Base URLhttps://taotoken.net/api、API Key、Model ID。这样 Cline 在编码时既能调模型,又能通过 MCP 调你的 Java 接口。
Claude Code 的接入类似,在配置里指定 Base URL 和 Key,然后把 MCP Server 注册进去。注意 Claude Code 对工具描述的格式要求比较严,inputSchema必须是合法的 JSON Schema,参数类型别写错。
Codex 的auth.json前面给过了,三件套填全就行。如果你在多个工具之间切换,建议把三件套统一记在一个地方,避免每个工具配一遍还配错。
长期运行的另一个建议是给 MCP Server 加调用日志。记录每次工具调用的入参、出参、耗时,出问题时能快速定位是模型选错了工具,还是你的接口返回了异常。日志格式建议结构化,方便后续做调用分析。
最后说个经验:批量迁移不要追求一次全量。先把调用频率最高的 20% 接口迁完,验证稳定后再迁剩下的。MCP 工具描述的质量需要根据实际调用反馈迭代,一次全量上线,出了问题排查面太大。分批次迁移,每批跑一遍一致性验证,稳扎稳打比赶进度靠谱。