YAOTU INSIGHTS

大模型之Spring AI实战系列(二十三):Spring AI + MCP + 自定义MCP服务开发实战(TaoToken 统一 Key 接入篇)

大模型之Spring AI实战系列(二十三):Spring AI + MCP + 自定义MCP服务开发实战(TaoToken 统一 Key 接入篇)
1. 从零开发自定义 MCP 服务Spring AI 工具注册与 TaoToken 统一 Key 接入Spring AI 集成 MCP 协议这件事真正卡住大多数人的不是协议本身而是两件事一是自定义 MCP 服务怎么把普通 Java 方法变成 LLM 能识别的工具二是模型调用通道怎么配才能稳定跑通。这篇就围绕 Spring AI MCP 自定义 MCP 服务开发实战把天气查询服务封装成 LLM 可调用工具并用 TaoToken 统一 Key 完成模型调用配置最后验证自定义工具能被 Spring AI 正确发现并触发。如果你之前跟着系列文章做过 Spring AI 的 Tool Calling会发现 MCP 的思路其实很像都是把方法暴露给模型。区别在于 MCP 把工具能力标准化成了协议服务端和客户端可以跨进程、跨语言通信。自定义 MCP 服务的价值就在这里——你写的天气查询、订单查询、内部 API 封装只要注册成 MCP 工具任何支持 MCP 的客户端都能调用。适合谁看已经会用 Spring Boot 写接口、想把自己的业务能力接进大模型工具链的开发者正在做 Agent 或智能助手、需要动态扩展工具集的团队以及被模型通道配置反复折腾、想用统一 Key 简化接入的人。下面从环境准备开始每一步都给可复制的配置和代码。2. TaoToken 前置准备统一 Key 与 API 通道配置在写 MCP 服务之前先把模型调用通道配好。传统做法是每个项目单独申请模型厂商 Key散落在各个配置文件里换模型就要改代码。TaoToken 的思路是提供一个统一的 API 通道你只需要一个 Key就能在 Spring AI 里通过 OpenAI 兼容协议调用不同模型。先拿到 Key。访问 https://taotoken.net/api-keys 创建 API Key建议按项目命名方便后续排查。创建后在控制台 https://taotoken.net/console 能看到用量和调用记录。如果你还没决定用哪个模型可以先在模型对话 https://taotoken.net/model-chat 里试一下效果确认响应格式符合预期再写进配置。Spring AI 的 OpenAI starter 默认走 OpenAI 官方地址我们要做的是把 base-url 指向 TaoToken 的 API 地址Key 用刚创建的。这样 Spring AI 的 ChatClient、Embedding、Tool Calling 全部走同一条通道MCP 客户端调用模型时也复用这套配置。这里有个容易踩的坑Spring AI 不同版本对 base-url 的拼接方式不一样。1.0.0 版本里spring.ai.openai.base-url需要写到/v1这一层而有些 starter 会自动补/v1。实测下来写成https://taotoken.net/api让 starter 自己拼/v1/chat/completions最稳。如果你遇到 404先检查这个路径。另外MCP 服务端本身不调用模型它只暴露工具真正调用模型的是 MCP 客户端。所以 TaoToken 的配置主要写在客户端项目里。但服务端如果也要做工具内部的自检或补全同样可以复用这套 Key。下面配置片段两个项目都会用到。注意Key 不要硬编码进代码或提交到仓库。用环境变量或配置中心管理本地开发可以用.env或 IDE 的运行配置注入。3. 可复制配置application.yml 与 MCP Server 注册代码这一节给完整可复制的配置。先看 MCP 服务端的application.ymlserver: port: 8080 spring: application: name: my-mcp-weather-server main: banner-mode: off web-application-type: servlet ai: mcp: server: name: my-mcp-weather-server version: 0.0.1 stdio: false logging: level: io.modelcontextprotocol: WARN file: name: ./logs/spring-ai-mcp-weather-server.log关键点spring.ai.mcp.server.name和version是 MCP 协议握手时返回给客户端的标识客户端配置里要对应。stdio: false表示走 SSE 传输服务端以 Web 应用方式启动。如果你要用 STDIO 传输改成true并把web-application-type设为none。Maven 依赖部分服务端需要 MCP server starterdependencyManagement 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-mcp-server-webmvc-spring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies工具注册的核心代码启动类里把 WeatherService 注册为工具提供者SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }WeatherService 用Tool注解暴露方法Service public class WeatherService { private final RestClient restClient; public WeatherService() { this.restClient RestClient.builder() .baseUrl(https://api.weather.gov) .defaultHeader(Accept, application/geojson) .defaultHeader(User-Agent, WeatherApiClient/1.0) .build(); } Tool(description Get weather forecast for a specific latitude/longitude) public String getWeatherForecastByLocation(double latitude, double longitude) { var points restClient.get() .uri(/points/{lat},{lon}, latitude, longitude) .retrieve() .body(Points.class); var forecast restClient.get() .uri(points.properties().forecast()) .retrieve() .body(Forecast.class); return formatForecast(forecast); } Tool(description Get weather alerts for a US state, input is two-letter state code) public String getAlerts(String state) { Alert alert restClient.get() .uri(/alerts/active/area/{state}, state) .retrieve() .body(Alert.class); return formatAlert(alert); } }客户端项目的application.yml要同时配 TaoToken 通道和 MCP 连接server: port: 8001 spring: application: name: mcp-weather-client main: web-application-type: none banner-mode: off ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: gpt-4o-mini mcp: client: toolcallback: enabled: true sse: connections: my-mcp-weather-server: url: http://localhost:8080 ai: user: input: What tools are available?这里spring.ai.openai.api-key用环境变量注入base-url指向 TaoToken API 地址model指定模型 ID。MCP 客户端通过 SSE 连到服务端的 8080 端口。三件套齐了Base URL、Key、Model ID。4. 验证请求启动服务并确认自定义工具被正确发现配置写完先启动 MCP 服务端mvn clean package -DskipTests java -jar target/spring-ai-mcp-weather-server-0.0.1-SNAPSHOT.jar看到Tomcat started on port 8080和 MCP server 注册日志就说明服务端起来了。可以用 curl 探一下 SSE 端点curl -N http://localhost:8080/sse正常会返回event: endpoint和一条带 sessionId 的 data。如果返回 404检查spring-ai-mcp-server-webmvc-spring-boot-starter是否引入以及web-application-type是否为 servlet。接着启动客户端先验证工具发现export TAOTOKEN_API_KEY你的Key java -Dai.user.inputWhat tools are available? \ -jar target/spring-ai-mcp-weather-client-0.0.1-SNAPSHOT.jar控制台会输出类似 QUESTION: What tools are available? ASSISTANT: 当前可用的工具包括 1. getWeatherForecastByLocation - 根据经纬度获取天气预报 2. getAlerts - 获取指定州的天气警报这说明 Spring AI 已经通过 MCP 协议从服务端拉到了工具列表并注入到 ChatClient 的 tool callbacks 里。再测一次真实调用java -Dai.user.input纽约的天气怎么样 \ -jar target/spring-ai-mcp-weather-client-0.0.1-SNAPSHOT.jar模型会先决定调用getWeatherForecastByLocation传入纽约的经纬度MCP 客户端把请求转发给服务端服务端执行 RestClient 调用天气 API结果回传给模型模型再组织成自然语言。控制台能看到完整的天气预报文本。这一步跑通说明自定义 MCP 服务开发链路完整闭环。如果你用 STDIO 传输客户端配置改成spring: ai: mcp: client: stdio: servers-configuration: classpath:/mcp-servers-config.jsonmcp-servers-config.json内容{ mcpServers: { my-mcp-weather-server: { command: java, args: [ -Dspring.ai.mcp.server.stdiotrue, -Dspring.main.web-application-typenone, -Dspring.main.banner-modeoff, -jar, /absolute/path/to/spring-ai-mcp-weather-server-0.0.1-SNAPSHOT.jar ] } } }STDIO 模式下服务端不占端口由客户端拉起子进程通信适合本地工具和 CLI 场景。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth跑 MCP Spring AI 这套组合报错集中在几个地方。下面按真实报错对照排查。401 Unauthorized客户端调用模型时返回 401说明 TaoToken Key 没生效。检查spring.ai.openai.api-key是否读到了环境变量base-url是否写成https://taotoken.net/api。如果 Key 正确但还 401看是不是把 Key 写到了spring.ai.openai.api-key之外的地方或者环境变量名拼错。用echo $TAOTOKEN_API_KEY确认。local proxy failed / Connection refusedMCP 客户端连不上服务端。SSE 模式下检查服务端是否真的在 8080 监听curl http://localhost:8080/sse能不能通。如果服务端日志显示启动成功但客户端报连接失败多半是端口被占或防火墙拦截。STDIO 模式下这个错通常是mcp-servers-config.json里的 jar 路径不对或者 java 命令不在 PATH 里。reading choices / choices 字段为空模型返回体解析失败。常见原因是 base-url 拼接多了或少了/v1导致请求打到了错误端点返回的不是标准 chat completion 格式。把base-url改成https://taotoken.net/api再试。另一个原因是模型 ID 写错比如用了 TaoToken 不支持的模型名返回体里没有 choices 字段。OAuth / 认证失败如果你在 MCP 服务端配了 OAuth 保护客户端连接时需要带 token。Spring AI MCP 客户端目前对 OAuth 的支持需要手动配置spring.ai.mcp.client.sse.connections.name.headers注入 Authorization 头。本地开发建议先关掉服务端鉴权跑通链路再加。工具没被发现客户端日志里没有工具列表。检查服务端Tool注解的方法是否是 publicMethodToolCallbackProvider是否注册了正确的 bean。另外spring.ai.mcp.client.toolcallback.enabledtrue必须显式打开否则客户端不会把 MCP 工具注入 ChatClient。模型不调用工具工具列表有了但模型直接回答不调工具。这通常是模型能力问题换一个 tool calling 支持更好的模型 ID。另外Tool的 description 要写清楚用途和参数格式模型靠这个决定是否调用。6. 语义一致 CTA把自定义 MCP 服务接进你的工具链到这里Spring AI MCP 自定义 MCP 服务的完整链路就跑通了。你手里有一套可复用的模式任何 Spring Boot 服务只要把方法加上Tool注解注册成ToolCallbackProvider就能变成 MCP 工具被大模型调用。天气查询只是示例换成订单查询、库存检查、内部 API 封装代码结构完全一样。模型通道这块TaoToken 的统一 Key 省掉了多厂商配置的麻烦。Spring AI 的 OpenAI starter 直接指向 https://taotoken.net/api一个 Key 跑通 ChatClient、Tool Calling 和 MCP 客户端。如果你要长期做编码类 Agent可以看 Coding Plan https://taotoken.net/coding-plan需要查接入细节看文档 https://taotoken.net/docKey 管理在 https://taotoken.net/api-keys。下一步可以尝试的方向把多个 MCP 服务端注册到同一个客户端让模型在多个工具集之间选择给 MCP 服务端加动态工具注册根据配置决定暴露哪些工具或者把 MCP 服务端部署到内网客户端通过 SSE 跨网络调用。这些都是在今天这套代码基础上扩展核心的注册和调用机制不变。