☰
Java 手写第一个 MCP Server:Spring AI MCP 半小时跑通 TaoToken 接入
2026/10/3 19:22:05 网站建设 项目流程

1. Java 开发者为什么需要一个自己的 MCP Server

MCP Server 说白了就是给大模型开的一扇「工具窗口」:模型本身只会输出文本,它没法直接读你的数据库、查你的规则库、调你的内部接口。MCP 协议做的事,就是把这层「模型想调工具」的意图,翻译成一次真实的函数调用,再把结果回填给模型。对 Java 开发者来说,这件事以前要么用 Python 写个脚本凑合,要么干脆放弃,让模型凭记忆瞎编。Spring AI 2.0 把 MCP Server 的 starter 做进了 Spring Boot 体系,意味着你可以用最熟的@Service、@Bean、application.yml这套东西,半小时内把一个能对外提供工具的服务跑起来。

这篇要解决的具体场景是:你手头有一份团队内部的代码审查规则库,41 条规则,分并发安全、事务、安全、空指针、需求评审五关。你希望 AI 在审查代码时,能实时按规则 id 拉回规则全文,而不是靠它自己脑补。做法就是把这套规则库包装成一个 MCP Server,对外暴露三个查询工具,然后用一个客户端真调一把,验证端到端链路通。

适合谁看:有 Spring Boot 基础、想给自己的 AI 工作流接一个自定义工具的 Java 后端;或者你已经在用 Cline、Claude Code 这类客户端,想给它们挂一个自己写的 MCP Server。全程不需要你懂 MCP 协议的报文细节,Spring AI 把序列化、握手、工具注册都封好了,你只需要写业务方法。

版本这块先钉死,因为 MCP 和 Spring AI 这两条线今年都变得快。Spring Boot 用 4.1.1,Spring AI 用 2.0.1,Java 21。有个坑要提前说:Spring AI 2.0.1 已经不支持 Boot 3.x,网上 2025 年那批教程大多是 Boot 3.x 配 1.0.0-M 系列的版本号,依赖坐标对不上,照抄容易起不来。另外传输协议的口径也变了,Spring AI 2.0 里 SSE 传输已标记 deprecated,官方推荐 Streamable HTTP,端点是POST /mcp。我这回为了配合经典的SSEClientTransport客户端写法,显式配了spring.ai.mcp.server.protocol: SSE,用回老的/sse端点。新工程建议直接上 Streamable HTTP,这里只是为了让客户端代码最短。

2. TaoToken 前置:统一 Key 与 API 通道怎么准备

在写代码之前,先把模型侧的通道准备好。MCP Server 本身不调模型,它只负责提供工具;真正调模型的是客户端那一侧。但如果你想让自己的验证脚本或者后续的 AI 应用能统一走一个 Key、一个 Base URL,那 TaoToken 这层就值得先配好。它的作用是给你一个统一的 API 入口,模型对话、编码 Agent、工具调用都从这一个口子走,省得每个客户端各配一套 Key。

第一步是拿 Key。打开https://taotoken.net/api-keys,登录后创建一个 API Key,复制出来存好。这个 Key 就是你后面所有请求的凭证,别写进代码里提交到 Git,用环境变量或者本地配置文件。

第二步是确认 Base URL。TaoToken 的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,就是干干净净的根路径。你在客户端里配置的时候,Base URL 填这个,Key 填上一步拿到的,Model ID 按你要用的模型填,比如claude-sonnet-4-5或者gpt-4o这类。这三件套——Base URL、Key、Model ID——是任何 OpenAI 兼容客户端接入的标配,缺一不可。

第三步是验证通道通不通。最直接的办法是用 curl 打一发模型对话请求:

curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "回复一个字:通"}], "max_tokens": 16 }'

如果返回里能看到choices数组和一段文本,说明 Key 和通道都没问题。这一步别跳过,后面 MCP 客户端调工具时如果报 401,你至少能确定不是 Key 的问题。

如果你用的是 Claude Code 这类编码 Agent,配置方式略有不同。Claude Code 走的是 Anthropic 协议,需要在 settings 里配ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY,Base URL 同样填https://taotoken.net/api。配完之后跑一个简单的对话测试,确认 Agent 能正常响应。这一步做完,你就有了一条稳定的模型通道,接下来写 MCP Server 的时候心里有底。

3. 可复制配置:pom 依赖与 application.yml 片段

建工程这块,用 Spring Initializr 生成一个最简的 Boot 4.1.1 工程,Java 21,然后改 pom。关键依赖就两个,但有个细节容易漏:spring-ai-starter-mcp-server-webmvc只带spring-webmvc,嵌入式 Tomcat 得靠spring-boot-starter-web提供。少了后者,启动时连 Servlet 容器都找不到。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> </parent> <properties> <java.version>21</java.version> <spring-ai.version>2.0.1</spring-ai.version> </properties> <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> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

然后是application.yml。这里每一行都有讲究,尤其是 logging 那块,是踩坑之后补上的:

server: port: 8080 spring: main: banner-mode: "off" web-application-type: servlet ai: mcp: server: name: review-rules-server version: 0.0.1 protocol: SSE logging: level: root: warn com.example.reviewrules: info org.springframework.boot: info org.apache.catalina.core: info io.modelcontextprotocol: info org.springframework.ai.mcp: info

name和version会在握手时作为服务端身份报给客户端。protocol: SSE是显式声明,不写的话 2.0 默认走 Streamable HTTP。com.example.reviewrules: info这行必须放行,原因在坑一里讲——Boot 的Started ...日志挂在主类 logger 下,root 调成 warn 之后这行日志会被吞掉,你会以为服务没起来。

工具类本身是个普通@Service,规则数据用 Map 内置。每个工具是一个加了@Tool注解的方法:

@Tool(name = "getReviewRule", description = "按规则 id 获取单条审查规则全文,四段式:适用条件、决策树、禁止写法、正反例", resultConverter = PlainTextResultConverter.class) public String getReviewRule( @ToolParam(description = "规则 id,如 concurrent-stock-deduct、tx-rpc-after-commit、ssrf-metadata") String ruleId) { Rule rule = rules.get(ruleId); if (rule == null) { return "未找到规则 [" + ruleId + "]。本 demo 可用 id:" + rules.keySet().stream().collect(Collectors.joining(", ")); } return "规则 " + rule.id() + "(" + rule.gate() + " · " + rule.title() + ")\n" + "来源:" + rule.origin() + "\n\n" + rule.body(); }

@Tool上三个属性值得说。name是工具对外的名字。description是给模型看的——模型看到的工具定义本质是一段 prompt 文本,它靠读 description 决定调不调、传什么参数,所以这句描述我写得比 JavaDoc 还认真。resultConverter先记住它出现过,坑二会讲:不加这个参数,客户端收到的文本没法直接看。方法参数上的@ToolParam同理,它的 description 会进 JSON Schema,模型靠它知道 ruleId 该填什么格式。

光有@Tool方法还不够,得告诉 Spring AI 把它们注册成 MCP 工具。一个配置类搞定:

@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider reviewRulesTools(ReviewRulesService reviewRulesService) { return MethodToolCallbackProvider.builder().toolObjects(reviewRulesService).build(); } }

4. 验证请求:Node 客户端真调一把

mvn package之后java -jar启动,日志里这三行最关键:

Tomcat initialized with port 8080 (http) Registered tools: 3 Started McpReviewRulesApplication in 5.646 seconds

Registered tools: 3,三个工具注册成功。你可能要问了:日志里还有一行 WARN 写着No tool methods found in the provided tool objects: [],是不是有东西没扫到?别怕。那是 Spring AI 2.0 新增的@McpTool注解扫描器在找另一种注解,我们没用它,扫不到属正常;我们的@Tool走的是上面ToolCallbackProvider这条注册路径,两码事。

客户端我故意没用 Java 写,用了 Node 加官方 JS SDK。Java 写的 Server 被另一门语言调通,「协议」两个字才算坐实。核心代码如下:

import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const transport = new SSEClientTransport(new URL("http://localhost:8080/sse")); const client = new Client( { name: "review-rules-demo-client", version: "0.0.1" }, { capabilities: {} } ); await client.connect(transport); const { tools } = await client.listTools(); console.log("tools/list 结果:共", tools.length, "个工具"); tools.forEach(t => console.log(" -", t.name, ":", t.description)); const result = await client.callTool({ name: "getReviewRule", arguments: { ruleId: "ssrf-metadata" } }); console.log(result.content[0].text);

跑起来,真实输出原样贴在这里:

[client] 连接 MCP Server: http://localhost:8080/sse ... [client] SSE 连接成功 [client] tools/list 结果:共 3 个工具 - getReviewRule : 按规则 id 获取单条审查规则全文,四段式:适用条件、决策树、禁止写法、正反例 - listReviewRules : 列出团队 AI 代码审查规则库的完整目录:五关各多少条、规则总量、来源构成 - searchReviewRules : 按关键词模糊搜索审查规则,返回命中规则的 id、所属关卡和摘要 [client] callTool getReviewRule({ ruleId: "ssrf-metadata" }) 返回: 规则 ssrf-metadata(安全关 · SSRF 白名单必须拦内网段与云元数据接口) 来源:推演立规 【适用条件】 任何由用户传入 URL、由服务端发起请求的代码:头像抓取、网页摘要、 webhook 回调、图片转存。攻击面是服务端代替攻击者访问内网。 【决策树】 1. URL 是用户可控的吗? ├─ 否 → 走普通 HTTP 审查 └─ 是 → 2 2. 白名单校验覆盖了哪些目标? ├─ 只拦 127.0.0.1 → 不合格,必须全量拦截: │ 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、 │ 169.254.169.254(云元数据接口)、[::1]、0.0.0.0, │ 以及 DNS 重绑定(解析后再校验一次) └─ 域名白名单 + 解析后 IP 二次校验 → 合格 【禁止写法】 禁止只拦 127.0.0.1。云上 SSRF 的头号目标是 169.254.169.254, 拿到临时凭证等于拿到整台机器的权限。也禁止只校验域名不校验解析结果。 [client] 连接已关闭,demo 结束

tools/list拿回 3 个工具,callTool getReviewRule("ssrf-metadata")拿回安全关那条 SSRF 规则的完整全文。顺手又验了searchReviewRules("事务"),命中事务关那条tx-rpc-after-commit,摘要里写着「推演立规,未真炸,后拦回一次」——模糊搜索这条路径也是通的。

这里补一句模型请求的循环,因为很多人卡在「工具调了但模型没反应」。真实 AI 应用里,完整链条是两趟模型请求:第一趟,用户提问加上工具清单(启动时tools/list注入 system prompt),模型决定调getReviewRule,参数ruleId="ssrf-metadata";然后 Host 里的 MCP Client 发callTool,Server 跑 Java 方法返回规则全文;工具结果作为一条消息塞回对话上下文;第二趟模型请求,对话历史加工具结果,模型基于规则全文生成最终回答。之所以要两趟,是因为模型自己执行不了代码,它只能输出「我想调这个工具、参数是什么」这段结构化文本,真正动手的是 Client。这个循环有个实战推论:description 写得好不好,直接决定第一趟请求里模型选不选你的工具、参数填得对不对。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

跑通之后,把几个高频报错对照着过一遍,省得你卡住时到处搜。

401 Unauthorized。这个最常见,出现在客户端调模型那一侧,不是 MCP Server 本身。原因通常是 Key 没配、Key 过期、或者 Base URL 写错了。检查三件套:Base URL 是不是https://taotoken.net/api,Key 是不是从https://taotoken.net/api-keys拿的那串,Model ID 是不是客户端支持的。如果用的是 Claude Code,确认ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY都配了,且没有多余的空格或换行。

local proxy failed。这个报错通常出现在客户端尝试走本地代理但代理没起来的时候。如果你没配代理,检查客户端设置里是不是残留了http_proxy或https_proxy环境变量。清掉之后重启客户端。注意,这里说的是本地环境变量清理,不是让你去配什么网络工具,别搞混。

reading choices 报错。这个一般出现在你手动 curl 或者脚本解析响应时,返回的 JSON 里没有choices字段。原因可能是请求体格式不对,比如messages写成了字符串而不是数组,或者model字段填了一个不存在的模型名。把请求体对照文档检查一遍,messages必须是[{"role": "user", "content": "..."}]这种结构。

OAuth 相关报错。如果你用的是需要 OAuth 授权的客户端,报错里出现invalid_grant或者token expired,说明授权过期了。重新走一遍授权流程,或者在客户端里重新登录。MCP Server 本身不涉及 OAuth,这层是客户端和模型通道之间的事。

MCP Server 侧的错误。如果客户端连不上/sse,先确认服务真的起来了:curl http://localhost:8080/sse应该返回 200 并且保持连接。如果返回 404,检查protocol: SSE有没有配,不配的话默认走 Streamable HTTP,端点变成POST /mcp。如果返回 500,看服务端日志,多半是工具方法抛异常了。

工具调了但返回空。检查@Tool方法的返回值类型和resultConverter。如果返回 String 但没配PlainTextResultConverter,客户端收到的是带引号的 JSON 字符串,换行全变成\n。这个在坑二里详细讲了,配一个十几行的转换器就好。

6. 从验证到长期使用:把 MCP Server 接进你的编码工作流

验证通过只是第一步,真正有价值的是把它接进你日常的编码工作流。如果你用的是 Cline 或者 Claude Code 这类支持 MCP 的客户端,可以在配置里挂上这个 Server。以 Cline 的 MCP 配置为例,在cline_mcp_settings.json里加一段:

{ "mcpServers": { "review-rules": { "url": "http://localhost:8080/sse", "disabled": false, "autoApprove": ["getReviewRule", "listReviewRules", "searchReviewRules"] } } }

配好之后,Cline 在审查代码时就能实时查你的规则库。autoApprove里列的工具会自动执行,不用每次点确认。如果你用的是 Codex 那套,配置写在auth.json同级的 MCP 配置里,Base URL、Key、Model ID 三件套照旧。

长期跑的话,建议把 Server 做成一个常驻服务,用systemd或者nohup挂后台。日志级别保持root: warn加主类info,既干净又能看到关键启动信息。规则库更新的时候,重新打包重启就行,客户端不用动。

如果你想让模型通道也统一管理,TaoToken 的 Coding Plan 适合长期编码和 Agent 场景,一个 Key 覆盖多个客户端。模型对话可以在https://taotoken.net/models里试,接入文档在https://taotoken.net/doc,API Key 管理在https://taotoken.net/api-keys。这几个入口按需取用,别一次全打开。

最后说个真实经验:MCP Server 的骨架就三件事——两个依赖、几个@Tool方法、一个ToolCallbackProvider配置类,半小时够用。真正花时间的是那些教程里没有的细节,比如Started日志被吞、String 返回值被 JSON 序列化、Git Bash 后台化 kill 错进程。这些坑我都蹚过了,你照着这篇走,应该能省下那半天。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询