
1. 为什么要在 SpringAI 里自己写 MCP 服务如果你最近在折腾 Java 侧的 AI 应用大概率会遇到一个尴尬模型本身很聪明但它不知道你公司内部的订单系统长什么样也读不到你本地那台机器上的日志文件。Function Calling 能解决一部分问题可每接一个新数据源就要写一套函数定义、参数校验、结果解析接三个系统之后代码就开始失控。MCPModel Context Protocol模型上下文协议就是冲着这个痛点来的。你可以把它理解成 AI 世界里的 USB-C 接口主机比如你的 Spring Boot 应用或聊天客户端通过标准协议去发现和调用服务器暴露出来的工具、资源、提示模板不用再为每个数据源单独写胶水代码。它基于 JSON-RPC 2.0支持 Stdio 和 HTTP SSE 两种传输方式一次开发就能被多个支持 MCP 的客户端复用。那为什么还要跟 Qwen 集成因为 MCP 只解决了工具怎么暴露没解决模型怎么调用工具。Qwen 系列里像 Qwen2.5 这类模型已经原生支持工具调用能读懂 MCP 服务器返回的工具列表并自主决定调哪个。把 SpringAI 写的 MCP 服务挂到 Qwen 上你就得到了一个能查天气、能抓网页、能查热点趋势的 Java 后端智能体。这篇要解决的核心问题是用 SpringAI 搭一个 MCP 服务端再通过 TaoToken 的统一 Key 通道把 Qwen 接进来让模型真正调用到你写的 Java 工具。适合有 Spring Boot 基础、想快速跑通 MCP Qwen 链路的后端同学。全程给可复制的application.yml、MCP 服务端配置和一次本地启动验证跟着敲就能出结果。2. TaoToken 前置准备统一 Key 与 Qwen 接入通道在写代码之前先把模型调用这条链路理顺。很多同学卡在第一步不是因为 SpringAI 难而是因为模型 API 的 Key 管理太碎今天用这家、明天换那家Base URL 和模型 ID 到处改配置文件里一堆硬编码。TaoToken 在这里扮演的角色是统一入口。它提供一个兼容 OpenAI 风格的 API 通道你只需要拿一个 Key就能在同一个 Base URL 下切换不同模型。对 SpringAI 来说这意味着application.yml里的base-url和api-key基本不用动换模型只改model字段。先拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 进去之后左侧菜单找 API Keys点新建复制那串sk-开头的字符串。这个 Key 只显示一次建议先贴到本地临时文件里。拿到 Key 之后记下两个地址Base URLhttps://taotoken.net/api注意这个不带 UTM 参数直接用于代码配置模型 IDQwen 系列填qwen2.5-72b-instruct或你账号下可用的 Qwen 模型标识具体以模型列表页为准这里有个容易踩的坑Base URL 末尾不要带/v1还是不带取决于 SpringAI 的 OpenAI starter 版本。SpringAI 1.0.0 的OpenAiApi默认会在 base-url 后面拼/v1/chat/completions所以你的base-url填https://taotoken.net/api即可让它自己拼。如果你填成https://taotoken.net/api/v1最后会变成/api/v1/v1/chat/completions直接 404。想先确认 Key 能不能用最省事的办法是去模型对话页 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 发一条消息试试。能正常回复说明 Key 和通道都没问题再往下写代码。如果你后面要做长期的编码 Agent 或者多轮工具调用可以考虑 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 额度模型更适合高频调用场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到参数对不上时翻一下比猜快。3. 可复制配置application.yml 与 MCP 服务端这一节是全文的核心给两份能直接抄的配置一份是 Spring Boot 的application.yml一份是 MCP 服务端的工具注册代码。先看pom.xml的依赖。SpringAI 的 MCP 服务端 starter 有两个版本WebMVC 走 SSE另一个走 Stdio。我们两个都演示所以引入 WebMVC 版本dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency /dependencies注意版本号网上很多老教程写1.0.0-SNAPSHOT那个快照仓库现在不一定能拉到直接用1.0.0正式版更稳。然后是application.yml。这份配置同时管了 MCP 服务端和 Qwen 模型客户端两件事server: port: 8080 spring: application: name: spring-ai-mcp-qwen-demo ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: qwen2.5-72b-instruct temperature: 0.7 mcp: server: name: weather-mcp-server version: 1.0.0 protocol: SSE sse-endpoint: /sse sse-message-endpoint: /mcp/message几个关键点解释一下。base-url填 TaoToken 的 API 地址api-key用环境变量注入别把 Key 硬编码进仓库这是基本安全习惯。model填 Qwen 的模型 ID。MCP 服务端这边protocol: SSE表示走 HTTP SSE 传输sse-endpoint是客户端建立连接的路径sse-message-endpoint是后续消息回传的路径这两个路径客户端要对应上。接下来写 MCP 工具。新建一个WeatherService用Tool注解暴露方法Service public class WeatherService { Tool(description 根据城市名称获取天气预报) public String getWeatherByCity(ToolParam(description 城市名称) String city) { if (city null || city.isBlank()) { return 抱歉城市名称不能为空; } MapString, String mockData Map.of( 西安, 晴天25℃, 北京, 小雨18℃, 上海, 大雨22℃ ); return mockData.getOrDefault(city, 抱歉未查询到该城市天气); } }再写一个配置类把工具注册成ToolCallbackProviderConfiguration public class McpToolConfig { Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }启动类就是普通的 Spring Boot 启动类不用额外加注解。到这里MCP 服务端就写完了。SSE 模式下服务会随应用一起启动不需要手动调main。如果你要用 Stdio 模式比如被 Claude Desktop 这类客户端以子进程方式拉起把application.yml里的protocol改成STDIO并在启动参数里加-Dspring.ai.mcp.server.stdiotrue -Dspring.main.web-application-typenone。Stdio 模式下服务不监听端口通过标准输入输出通信。4. 验证请求本地启动与接口调用配置写完跑起来验证。先在终端设置环境变量把 Key 注入进去export TAOTOKEN_API_KEYsk-你的实际Key然后启动应用mvn spring-boot:run看到日志里出现Started ServerApplication和 MCP server 注册成功的提示说明服务起来了。默认监听 8080 端口。先验证 MCP 服务端本身。用 curl 建立 SSE 连接看看工具列表能不能拉出来curl -N http://localhost:8080/sse-N是关闭缓冲让你能实时看到 SSE 推送。正常的话会先收到一个endpoint事件里面带着 message 路径类似event: endpoint data: /mcp/message?sessionIdxxxxx拿到sessionId之后另开一个终端发 JSON-RPC 请求去列工具curl -X POST http://localhost:8080/mcp/message?sessionIdxxxxx \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: tools/list, params: {} }返回里应该能看到getWeatherByCity这个工具带 description 和 inputSchema。这一步通了说明 MCP 服务端没问题。再验证工具调用curl -X POST http://localhost:8080/mcp/message?sessionIdxxxxx \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 2, method: tools/call, params: { name: getWeatherByCity, arguments: {city: 上海} } }预期返回{ jsonrpc: 2.0, id: 2, result: { content: [{type: text, text: 大雨22℃}], isError: false } }到这里 MCP 服务端验证完毕。接下来验证 Qwen 能不能通过 TaoToken 通道调用。写一个简单的测试接口用 SpringAI 的ChatClient发起一次带工具的对话RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder, ToolCallbackProvider weatherTools) { this.chatClient builder .defaultToolCallbacks(weatherTools) .build(); } GetMapping(/chat) public String chat(RequestParam String q) { return chatClient.prompt(q).call().content(); } }重启应用访问curl http://localhost:8080/chat?q上海今天天气怎么样如果 Qwen 正确识别了工具并调用你会看到类似上海今天是大雨气温 22℃的回复。日志里会打印出模型决定调用getWeatherByCity的过程。这一步成功整条链路就通了Qwen 通过 TaoToken 通道拿到工具列表自主决定调用你写的 Java 方法再把结果组织成自然语言返回。5. 本篇常见错误排查跑不通的时候对照下面几个真实报错看。401 Unauthorized / invalid api key。这个最常见八成是 Key 没注入或者复制时带了空格。先确认echo $TAOTOKEN_API_KEY有值再检查application.yml里是不是写成了${TAOTOKEN_API_KEY:}这种带默认空值的写法。另外注意 Key 有没有过期去控制台 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一个试试。local proxy failed / connection refused。这个报错通常出现在你本地网络环境有额外代理设置的时候。检查一下JAVA_TOOL_OPTIONS或者系统环境变量里有没有http_proxy、https_proxy指向本地某个端口。如果有临时 unset 掉再启动。SpringAI 的 HTTP 客户端会读取这些环境变量代理不通就会报这个。reading choices 时返回空 / NullPointerException。这个多半是 Base URL 拼错了。前面说过base-url填https://taotoken.net/api不要带/v1。如果你填了/v1实际请求路径会变成/api/v1/v1/chat/completions服务端返回 404 或者一个非标准 JSONSpringAI 解析choices字段时就炸了。把/v1去掉重启即可。OAuth / token exchange failed。如果你用的是某些需要 OAuth 流程的客户端比如 Claude Code 接入场景报这个说明认证方式选错了。TaoToken 的 API 通道用的是 API Key 认证不是 OAuth。在客户端配置里选 API Key 模式把 Key 填进去Base URL 填https://taotoken.net/api。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 有详细说明。MCP 工具列表为空。检查ToolCallbackProvider那个Bean有没有被扫描到。常见原因是配置类和启动类不在同一个包路径下或者Configuration注解漏了。另外Tool注解的方法必须是 public 的参数上要有ToolParam。SSE 连接建立后收不到 endpoint 事件。确认sse-endpoint和客户端请求的路径一致。如果你改了application.yml里的sse-endpoint客户端也要跟着改。另外 SSE 是长连接curl 要加-N浏览器里用EventSource时注意跨域配置。模型不调用工具直接瞎编答案。这个不是报错但很常见。原因是模型没收到工具定义或者工具描述写得太模糊。检查ChatClient构建时有没有.defaultToolCallbacks(weatherTools)。另外Tool的 description 要写清楚用途比如根据城市名称获取天气预报就比查天气好模型靠这个判断该不该调。6. 把 Qwen 和 MCP 用起来下一步怎么走链路跑通之后你可以把天气工具换成真实业务。比如接公司内部的订单查询接口写一个OrderService用Tool暴露getOrderStatus(orderId)Qwen 就能在对话里直接查订单。或者接日志系统暴露searchLogs(keyword, timeRange)让模型帮你定位线上问题。MCP 的价值在于一次开发、多处复用。你写的这个 SpringAI MCP 服务端不光能被自己的 Spring Boot 应用调用还能挂到任何支持 MCP 的客户端上。比如在 Cline 或者 Claude Code 里配置 MCP 服务器把 SSE 地址填进去就能在编码时直接调用你的 Java 工具。配置的时候记住三件套Base URL 填https://taotoken.net/apiKey 填你的 TaoToken API KeyModel ID 填 Qwen 的模型标识三个缺一不可。如果你要做的工具比较多建议按领域拆成多个 MCP 服务端每个服务端只管一类工具。这样客户端可以按需挂载不会一次性拉一堆用不上的工具定义既省 token 又减少模型选错工具的概率。最后提醒一个实操细节MCP 服务端的工具方法尽量保持无副作用或者幂等。模型可能会在推理过程中多次调用同一个工具如果你的方法有写操作要做好去重或者加确认机制。读操作随便调写操作要谨慎这是把 MCP 用到生产环境的基本纪律。