1. 从一次真实踩坑说起:为什么 Java 开发者需要自己的 MCP Server
MCP Server 是什么?简单说,它是把「模型能调用的工具」用一套标准协议暴露出来的服务端。AI Agent 是什么?就是那个会自己判断「现在该调哪个工具」的调度大脑。而 Spring AI 让 Java 开发者不用去啃 Node.js 生态,直接用注解就能把方法注册成 MCP 工具。这套组合适合谁?适合手上已经有 Spring Boot 项目、想让自己业务能力被 Claude、Cursor、CodeBuddy 这类客户端直接调用的后端同学。
我试过最原始的玩法:在客户端里写一堆 function call 的 JSON Schema,每换一个模型平台就重写一遍集成代码。后来 MCP 协议出来,理论上一次开发到处复用,但早期生态几乎全是 Node.js 的 Server,Java 这边只能干看着。直到 Spring AI 把spring-ai-mcp-server-spring-boot-starter放出来,Java 才算真正有了「春天」。
这篇要做的闭环很具体:先讲清 MCP 协议里 Client、Server、Tool 三者怎么串起来,再落地一个能跑的 Spring AI MCP Server 项目,最后用客户端真实调用验证。中间会给出可复制的 Maven 依赖、@Tool注册代码、application.yml配置,以及统一 Key 通道的 endpoint 写法。目标只有一个——你照着敲,能一次跑通。
需要提前说明的是,MCP 的传输方式有好几种,Stdio、SSE、Streamable HTTP 各有适用场景。本文聚焦 Stdio,因为它最适合本地集成和命令行工具开发:客户端启动时自动拉起 Server 进程,通过标准输入输出通信,不需要额外开端口,调试也直观。理解了 Stdio 这一种,其他传输方式的思路是相通的。
2. 前置准备:用 TaoToken 统一 Key 打通模型通道与 MCP 调用链路
在动手写 Server 之前,先把「模型从哪来」这件事解决掉。MCP Server 本身只负责暴露工具,真正决定调不调、怎么调的是背后的大模型。如果你同时用 Claude、GPT、国产模型,每个平台一套 Key、一套 Base URL,管理起来很烦。TaoToken 在这里的作用就是提供统一的 API 通道,一个 Key 走通多家模型,省掉反复切换配置的麻烦。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 列表在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完记得复制保存,页面刷新后就不再完整显示。
拿到 Key 之后,模型调用的 Base URL 统一填https://taotoken.net/api,注意这个地址不带任何查询参数。如果你用的是 OpenAI 兼容的 SDK,直接把它当作base_url即可;如果是 Claude Code 这类工具,走的是 Anthropic 兼容通道,配置方式略有不同,可以参考接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
这里要区分两个概念,别搞混:
| 用途 | 地址 | 说明 |
|---|---|---|
| 模型对话 / API 调用 | https://taotoken.net/api | 统一 Key 通道,供 SDK、客户端调用模型 |
| MCP Server 本地服务 | 无固定地址(Stdio) | 由客户端进程拉起,不走网络端口 |
也就是说,TaoToken 负责「模型侧」的通道,MCP Server 负责「工具侧」的能力,两者通过客户端(AI Agent)串起来。客户端一边用 TaoToken 的 Key 调模型,一边通过 Stdio 调你的 MCP Server,模型决定调用哪个工具,客户端负责转发。
如果你打算长期做编码类 Agent 开发,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合需要稳定额度、频繁调用的场景。想先验证模型通不通,用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 发一条消息即可,能正常返回就说明 Key 和通道没问题。
前置准备清单:
- JDK 17 或以上(Spring AI 1.0.0-M6 要求)
- Maven 3.9+
- 一个支持 MCP 的客户端(CodeBuddy、Cursor、Claude Desktop 等)
- TaoToken API Key(用于模型侧调用)
- 一个你熟悉的业务接口,本文以「发布文章」为例
把这些准备好,后面写代码就不会卡在环境上。
3. 可复制配置:Spring AI MCP Server 的 pom.xml 与 application.yml 完整片段
这一节是全文最核心的可复制部分。先给pom.xml,再给application.yml,最后给工具注册代码。路径和文件名都按实际项目结构写,你直接对照改。
3.1 pom.xml 依赖配置
关键依赖是spring-ai-mcp-server-spring-boot-starter,配合spring-ai-bom统一版本。完整片段如下:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <spring-ai.version>1.0.0-M6</spring-ai.version> <spring-boot.version>3.4.5</spring-boot.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>retrofit</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>converter-jackson</artifactId> <version>2.9.0</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>注意spring-ai-bom必须放在dependencyManagement里,否则 starter 的版本会解析失败。Retrofit 是我用来调外部 HTTP 接口的,你可以换成 WebClient 或 RestTemplate,不影响 MCP 部分。
3.2 application.yml 配置
Stdio 模式下有几个必填项,漏一个都启动不了:
spring: application: name: tencent-send-article-mcp-server main: web-application-type: none banner-mode: off ai: mcp: server: stdio: true name: tencent-send-article-mcp-server version: 1.0.0 logging: pattern: console: "" file: name: data/log/${spring.application.name}.log逐项说明:web-application-type: none表示这不是 Web 应用,不启 Tomcat;banner-mode: off关掉启动 banner,因为 Stdio 模式下任何多余输出都会污染 JSON-RPC 通道;stdio: true是核心开关;name和version会出现在客户端看到的 Server 信息里。日志那块建议保留,Stdio 模式下控制台输出会干扰协议通信,写到文件里更安全。
3.3 工具注册代码
定义一个 Service,用@Tool注解标记要暴露的方法:
@Service public class TencentArticleToolService { @Resource private ITencentService iTencentService; @Resource private TencentApiProperties tencentApiProperties; @Tool(description = "发布文章到腾讯云开发者社区") public ArticleFunctionResponse saveArticle(ArticleFunctionRequest request) { try { AddArticleRequest addArticleRequest = new AddArticleRequest(); addArticleRequest.setTitle(request.getTitle()); addArticleRequest.setPlain(request.getMarkdowncontent()); addArticleRequest.setContent(addArticleRequest.getContent()); addArticleRequest.setSourceType(1); addArticleRequest.setUserSummary(request.getUserSummary()); Call<AddArticleResponse> call = iTencentService.addArticle( tencentApiProperties.getCookie(), addArticleRequest); Response<AddArticleResponse> response = call.execute(); ArticleFunctionResponse result = new ArticleFunctionResponse(); if (response.isSuccessful() && response.body() != null) { result.setStatus(response.body().getStatus()); result.setArticleId(response.body().getArticleId()); result.setUrl("https://cloud.tencent.com/developer/article/" + response.body().getArticleId()); } return result; } catch (Exception e) { log.error("发布失败", e); } return null; } }@Tool的description很关键,模型就是靠这句话判断「什么时候该调这个工具」。写得太模糊,模型可能永远不调;写得太长,又浪费 token。建议一句话说清「做什么 + 对谁做」。
然后在启动类里注册 ToolCallbackProvider:
@Bean public ToolCallbackProvider articleTools(TencentArticleToolService service) { return MethodToolCallbackProvider.builder() .toolObjects(service) .build(); }到这里,一个最小可用的 MCP Server 就成型了。打包命令:
./mvnw clean install -DskipTests产物在target/目录下,记住这个 jar 的绝对路径,下一步客户端配置要用。
4. 验证请求:本地启动 MCP Server 并用客户端完成一次真实工具调用
代码写完不算完,得真跑起来。这一节分两步:先本地启动确认 Server 能拉起,再配客户端完成一次端到端调用。
4.1 本地启动验证
Stdio 模式的 Server 不能直接java -jar跑,因为它等的是标准输入。你直接运行会看到进程挂起,这是正常的。正确做法是通过客户端拉起,或者用官方的 MCP Inspector 工具测试。
先确认 jar 能正常加载:
java -Dfile.encoding=UTF-8 -jar target/tencent-send-article-mcp-server-1.0.0.jar如果卡住不动、没有报错,说明 Server 已经进入等待输入状态,Ctrl+C 退出即可。如果报NoClassDefFoundError或ClassNotFoundException,多半是依赖没打进去,检查spring-boot-maven-plugin是否配置了 repackage。
4.2 客户端配置
以 CodeBuddy 为例,在 MCP 配置里新增一段:
{ "mcpServers": { "tencent-send-article-mcp-server": { "command": "/你的JDK17路径/bin/java", "args": [ "-Dfile.encoding=UTF-8", "-jar", "/你的项目路径/target/tencent-send-article-mcp-server-1.0.0.jar", "--tencent.api.cookie=你的Cookie值" ] } } }三个关键点:command必须指向 JDK 17 以上的 java,如果你系统默认是 JDK 8,这里要写绝对路径;args里的-Dfile.encoding=UTF-8防止中文乱码;--tencent.api.cookie是通过命令行参数注入配置,对应@ConfigurationProperties(prefix = "tencent.api")那个类。
配置保存后,客户端侧边栏应该能看到 Server 亮起。点开工具列表,能看到saveArticle这个工具,说明注册成功。
4.3 发起一次真实调用
在对话里输入类似这样的指令:
帮我发布一篇文章,标题是「MCP Server 实战记录」,内容是 Markdown 格式的一段介绍,摘要是「一次跑通 Spring AI MCP Server」。
模型会判断需要调用saveArticle,然后客户端把参数通过 Stdio 传给 Server。Server 执行完返回结果,客户端展示给你。如果返回里带了文章 ID 和 URL,说明整条链路通了。
这里有个细节:模型传过来的markdowncontent是 Markdown 文本,但目标平台要的是 ProseMirror 格式的 JSON。所以我在AddArticleRequest.getContent()里做了一层转换,把 Markdown 转成 ProseMirror。这个转换器可以自己写,也可以让 AI 帮你生成,核心是把标题、段落、列表、代码块这些元素映射成对应的 JSON 节点。
验证成功的标志有三个:客户端显示工具调用成功、返回结果里有文章 ID、去目标平台能看到刚发布的文章。三个都满足,闭环就算跑通了。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth 报错对照
跑不通是常态,这一节把最常见的几类报错和排查思路列出来,对照着看。
5.1 401 Unauthorized
这个报错分两种场景。如果是模型侧返回 401,说明 TaoToken 的 Key 不对或过期了,去控制台重新生成一个,确认请求头里Authorization: Bearer <key>拼写正确。如果是工具侧返回 401,说明你调的外部接口(比如发布文章那个)Cookie 失效了,重新登录目标平台抓一份新的 Cookie,更新到--tencent.api.cookie参数里。
5.2 local proxy failed
这个报错通常出现在客户端连不上 Server 的时候。排查顺序:先确认command路径下的 java 能执行,在终端里手动跑一遍java -version;再确认 jar 路径是绝对路径且文件存在;最后看args数组里的参数顺序,-jar必须在 jar 路径前面。如果用了相对路径,客户端的工作目录可能和你预期的不一样,一律改成绝对路径最稳。
5.3 reading choices 相关报错
这类报错一般出现在模型返回结构解析失败的时候。常见原因是 Stdio 通道被污染了——比如 Server 启动时打印了 banner、日志输出到了 stdout。检查application.yml里banner-mode: off和web-application-type: none是否都配了,日志是否写到了文件而不是控制台。Stdio 模式下,stdout 只能走 JSON-RPC 消息,任何多余输出都会导致客户端解析失败。
5.4 OAuth 相关报错
如果你用的是 Claude Code 这类走 Anthropic 通道的工具,可能会遇到 OAuth 报错。这类问题多半是认证方式没选对。Claude Code 的接入方式参考文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面区分了 API Key 和 OAuth 两种模式,按你的场景选。如果只是本地开发调试,用 API Key 模式更简单。
5.5 工具注册了但模型不调用
这不是报错,但比报错更让人抓狂。原因通常是@Tool的description写得太模糊,模型不知道什么时候该用。解决办法是把描述写具体,比如「发布文章到腾讯云开发者社区,需要标题、Markdown 内容和摘要三个参数」,让模型能明确判断触发条件。另外确认ToolCallbackProvider的 Bean 确实被 Spring 扫描到了,可以在启动日志里搜一下工具注册信息。
5.6 中文乱码
Stdio 模式下中文乱码很常见,根源是编码不一致。三处都要设成 UTF-8:pom.xml里的project.build.sourceEncoding、启动参数里的-Dfile.encoding=UTF-8、以及application.yml里的server.servlet.encoding.charset。三处对齐基本就不会乱码了。
排查这类问题的通用思路是:先看客户端报错信息,定位是模型侧还是工具侧;再看 Server 日志文件,确认请求有没有到达;最后用最小化配置复现,逐步加回参数。别一上来就怀疑代码,八成是配置问题。
6. 把 MCP Server 接进你的 AI Agent 工作流
到这里,一个能跑的 Spring AI MCP Server 已经完成了。但单个 Server 只是起点,真正的价值在于把多个 Server 串起来,让 AI Agent 自己调度。
回到开头的场景:你有一个发布文章的 Server、一个操作文件的 Server、一个查数据库的 Server。客户端(AI Agent)拿到用户指令后,会自己判断该调哪个、按什么顺序调。比如「把这篇草稿发布出去,然后在本地的记录文件里追加一条日志」,Agent 会先调发布工具,再调文件工具,全程不需要你写编排代码。这就是 MCP 协议最大的意义——把「工具」和「调度」解耦。
如果你想继续深入,几个方向可以试试。一是把 Stdio 换成 SSE 或 Streamable HTTP,让 Server 能远程部署,多个客户端共享;二是把工具的参数校验做严,用 JSON Schema 约束输入,减少模型传错参数的概率;三是把多个工具按业务域拆成多个 Server,客户端按需加载,避免一次性暴露太多工具导致模型选择困难。
模型侧的统一通道还是用 TaoToken,Base URL 固定https://taotoken.net/api,换模型不用改代码。工具侧就是你刚写的这个 Server,加新能力就是加一个@Tool方法。两边通过客户端串起来,这套结构跑顺了,后面加什么业务都只是重复这个模式。
最后留一个实用技巧:开发阶段把 Server 日志级别调到 DEBUG,能看到完整的 JSON-RPC 请求和响应,排查问题效率翻倍。上线前再调回 INFO,避免日志文件膨胀。工具描述定期 review 一遍,模型调用的准确率会随着描述优化明显提升。