1. 先搞清楚 MCP 协议到底在解决什么问题
如果你写过 Spring AI 的工具调用,大概率经历过这种场面:每接一个外部能力,就要手写一个FunctionCallback,参数 schema 自己拼,鉴权逻辑自己塞,工具一多,ChatClient的构造器长得像春运火车票。MCP 协议要干的事,就是把这堆散装工具调用收敛成一套标准接口——你可以把它理解成 AI 世界的 USB-C:模型这头是主机,工具那头是设备,中间插一根标准线,谁也不用为谁改接口。
MCP 全称 Model Context Protocol,核心是客户端-服务器架构。MCP Server 负责把工具、资源、提示词模板暴露出来,MCP Client 负责发现并调用这些能力。通信层走 JSON-RPC 2.0,传输方式支持 Stdio(本地进程)和 SSE(远程 HTTP)。落到 Spring AI 里,你只需要在配置文件里声明「我要连哪个 MCP Server」,启动时框架会自动拉取工具列表并注册成ToolCallback,业务代码里照常chatClient.prompt().user(...).call()就行。
这篇要落地的场景很具体:用 Spring AI 通过 MCP 协议接入高德地图工具链,让模型能查周边 POI、算路线、做地理编码。同时模型侧的凭据不走散装环境变量,统一收口到 TaoToken 的 API 通道管理。适合谁看?已经在写 Spring AI 应用、想把手写工具调用换成标准协议的中级 Java 开发者;以及手上有一堆模型 Key、想统一管起来免得到处漏的团队。
我试过最省事的路径是:高德 MCP Server 用官方 npm 包本地起,Spring AI 侧只写 YAML 和几行装配代码,模型请求全部指向 TaoToken 的兼容端点。下面按「声明 Server → 注册工具 → 配置客户端 → 发一次真实请求验证」的顺序走,每一步都给可复制的片段。
2. TaoToken 前置:把模型侧 Key 收口成一条通道
在动 Spring AI 之前,先把模型凭据这件事理清楚。传统写法是把OPENAI_API_KEY塞进application.yml或者环境变量,一个项目一份,换模型换端点就得改代码。TaoToken 的做法是给你一个统一的 API 入口和一把 Key,模型侧的所有请求都走这条通道,Spring AI 里只需要配base-url和api-key两个值。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册登录,进控制台。左侧菜单找到 API Keys,点新建,起个能认出来的名字比如spring-ai-mcp-dev,生成后立刻复制——页面刷新就看不到了。这把 Key 就是后面application.yml里spring.ai.openai.api-key的值。
接着确认模型 ID。进「模型对话」页面(deep link:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite),能看到当前可用的模型列表,记下你要用的那个 ID,比如claude-sonnet-4-20250514或者gpt-4o。MCP 工具调用对模型的 function calling 能力有要求,纯文本模型跑不通,选的时候留意一下。
API 端点固定是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为base-url写进配置。Spring AI 的 OpenAI starter 会在后面自动拼/v1/chat/completions,所以你别自己补/v1,补了会变成/v1/v1直接 404。
注意:Key 只显示一次,建议生成后立刻写进本地
application-local.yml并加进.gitignore。团队协作时用环境变量注入,别把明文 Key 提交到仓库。
如果你后面要跑长期编码任务或者 Agent 循环,可以看下 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite),按套餐走比按 token 计费更适合高频调用场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,遇到端点或参数疑问先翻这里。
这一步的产出就三样东西:一把 Key、一个模型 ID、一个 base URL。记在便签上,下一节直接往 YAML 里填。
3. 可复制配置:MCP Server 声明 + Spring AI 客户端装配
这一节是全文的核心,配置写对了后面基本一路顺。分三块:Maven 依赖、MCP Server 声明文件、Spring 配置。
先看依赖。Spring AI 的 MCP 客户端 starter 版本迭代很快,包路径偶有变动,建议锁定一个能跑的版本。下面这套在 Spring Boot 3.3.x + Spring AI 1.0.0-M6 下验证过:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency>如果你用的是 Spring AI Alibaba 的 BOM,版本号跟着 BOM 走,别混用。依赖拉不下来先检查仓库配置,M 版本在中央仓库有,不需要额外加 milestone 源。
第二块,MCP Server 声明。在src/main/resources下新建mcp-servers.json,内容如下:
{ "mcpServers": { "amap-maps": { "command": "npx", "args": [ "-y", "@amap/amap-maps-mcp-server" ], "env": { "AMAP_MAPS_API_KEY": "替换成你在高德开放平台申请的 Web 服务 Key" } } } }这里的AMAP_MAPS_API_KEY是高德开放平台控制台里创建的「Web 服务」类型 Key,不是 TaoToken 的 Key,两者别搞混。高德那边创建应用时类型选 Web 服务,勾选协议后复制 Key 填进来。command在 Windows 下要写成npx.cmd,这个坑后面排错章节细说。
第三块,application.yml。模型侧走 TaoToken,MCP 侧走 Stdio 本地进程:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-20250514 temperature: 0.7 mcp: client: enabled: true name: spring-ai-mcp-client version: 1.0.0 type: SYNC request-timeout: 30s stdio: servers-configuration: classpath:/mcp-servers.json几个参数说明一下。type: SYNC表示同步客户端,工具调用走阻塞式,适合大多数 CRUD 场景;要流式就用ASYNC。request-timeout给 30 秒,高德接口偶尔慢,给短了会抛超时。servers-configuration指向刚才那个 JSON 文件,classpath 路径别写错。
装配层只需要一个 Bean,把 MCP 拉回来的工具塞给ChatClient:
@Configuration public class McpClientConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { return builder .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks()) .build(); } }ToolCallbackProvider是 starter 自动注入的,启动时会向 MCP Server 发tools/list请求,把高德那十几个工具(地理编码、周边搜索、路径规划等)全部注册进来。你不需要手写任何一个FunctionCallback,这就是 MCP 省事的地方。
提示:如果你的项目里已经有自定义的
ChatClientBean,注意别重复定义,把defaultToolCallbacks合并到现有构造逻辑里即可。
配置写完,启动应用。控制台如果打印出类似Registered MCP tool: maps_around_search的日志,说明工具注册成功。没有日志也别急,下一节发请求验证。
4. 验证请求:发一次真实的地图工具调用
配置对不对,跑一次就知道。写个测试类,让模型查一个具体的地理问题:
@SpringBootTest class McpAmapIntegrationTest { @Autowired private ChatClient chatClient; @Test void shouldInvokeAmapTool() { String answer = chatClient.prompt() .user("帮我查一下郴州高椅岭附近5公里的酒店,列出前3家") .call() .content(); System.out.println(answer); assertNotNull(answer); } }跑之前确认两件事:高德 Key 有效且有余额,TaoToken Key 没写错。执行后你会在控制台看到模型先输出一段思考,然后触发maps_around_search工具调用,参数里带着经纬度和半径,工具返回 JSON 结果后模型再组织成自然语言。
一次成功的输出大概长这样(内容因实时数据而异):
根据高德地图搜索结果,高椅岭附近5公里内找到以下酒店: 1. 郴州高椅岭民宿 - 距离约1.2公里 2. 东江湖景酒店 - 距离约3.5公里 3. 飞天山度假村 - 距离约4.8公里如果模型直接回「我无法查询实时地理信息」,说明工具没注册上,回去检查mcp-servers.json路径和ToolCallbackProvider是否注入成功。如果报 401,那是 TaoToken Key 的问题,去控制台核对。如果报reading 'choices'之类的解析错误,多半是 base-url 拼错了,确认是https://taotoken.net/api而不是带/v1的版本。
想更直观地看工具调用链路,可以在application.yml里把日志级别调一下:
logging: level: org.springframework.ai.mcp: DEBUG org.springframework.ai.openai: DEBUG这样能看到完整的 JSON-RPC 请求响应,工具名、参数、返回结构一目了然。验证通过后,这套最小样例就可以往业务里搬了。
5. 常见报错排查清单:从 npx 到 ToolContext
这一节按真实踩坑顺序列,遇到问题对号入座。
报错一:Cannot run program "npx": CreateProcess error=2
Windows 环境专属。MCP Server 声明里的command写的是npx,但 Windows 下可执行文件是npx.cmd。改mcp-servers.json:
"command": "npx.cmd"Linux/macOS 保持npx不变。这个错在启动阶段就抛,日志里能看到McpClient初始化失败。
报错二:401 Unauthorized或invalid api key
两个 Key 都可能出问题,分开查。先看请求打到哪:如果错误信息里带taotoken.net,是 TaoToken Key 错了或过期,去 API Keys 页面重新生成;如果带amap字样,是高德 Key 问题,检查是否选了「Web 服务」类型、是否勾了协议。还有一种情况是环境变量${TAOTOKEN_API_KEY}没注入,Spring 启动时占位符没解析,Key 变成字面量字符串,也会 401。
报错三:local proxy failed或连接超时
MCP Stdio 模式下,客户端要拉起本地子进程。如果npx拉包慢或者网络抖动,会报连接失败。先把request-timeout调到60s试试;还不行就手动在终端跑一遍npx -y @amap/amap-maps-mcp-server,看包能不能正常下载。首次运行要下依赖,慢是正常的,下完缓存后就快了。
报错四:ToolContext not supported或call(String, ToolContext)异常
这是 Spring AI M6 版本的一个已知摩擦点:框架内部调用工具时传了ToolContext,但 MCP 类型的ToolCallback默认不认这个参数,直接抛异常。临时方案是在测试里别传 context;根治方案是加一层代理拦截。核心思路是判断回调类名含mcp且方法签名是双参数时,降级调用单参数版本:
public class McpToolCallbackProxy implements InvocationHandler { private final FunctionCallback target; public McpToolCallbackProxy(FunctionCallback target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (target.getClass().getSimpleName().toLowerCase().contains("mcp") && method.getName().equals("call") && args.length == 2 && args[1] != null && args[1].getClass().equals(ToolContext.class)) { return target.call(args[0].toString()); } return method.invoke(target, args); } }用Proxy.newProxyInstance把ToolCallback数组包一层再交给ChatClient。注意method.invoke的第一个参数是target不是proxy,写错了会无限递归。
报错五:reading 'choices'空指针
响应体解析失败,九成是 base-url 不对。TaoToken 的端点是https://taotoken.net/api,Spring AI 会自动补/v1/chat/completions。如果你手贱写成https://taotoken.net/api/v1,最终请求路径变成/api/v1/v1/chat/completions,服务端返回 404 或者非标准 JSON,解析器就炸了。改回来即可。
报错六:OAuth 相关错误
如果你接的是需要 OAuth 的远程 MCP Server(SSE 模式),报OAuth token missing说明认证头没配。Stdio 本地模式不涉及这个,检查一下type是不是误配成了SSE。本篇场景用 Stdio,不需要 OAuth。
排查顺序建议:先看启动日志有没有工具注册成功,再看请求打到哪个域名,最后看 JSON-RPC 层日志。三层定位,基本十分钟内能锁定问题。
6. 把 Key 管理和工具链一起收口
跑通之后回头看,这套集成的价值不只是「能查地图」。MCP 把工具接入标准化了,TaoToken 把模型凭据标准化了,两边一夹,你的 Spring AI 应用里就剩业务逻辑。新增一个 MCP Server 只需要往mcp-servers.json里加一段声明,换模型只需要改application.yml里的model字段,Key 始终是那一把。
日常维护时,API Keys 页面(https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite)可以按项目建多把 Key,方便轮换和审计。接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite)里有各语言 SDK 的端点说明,换框架时对照着改。控制台(https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite)能看调用量和余额,跑批量任务前瞄一眼,免得半夜跑一半欠费停摆。
最后留个实操建议:把mcp-servers.json里的高德 Key 也抽成环境变量${AMAP_MAPS_API_KEY},和 TaoToken Key 一样走注入。这样整个项目的凭据面就干净了,换机器部署只改环境变量,配置文件一个字不用动。