1. 为什么 Java 开发者需要认真对待 MCP
如果你正在用 Spring AI 做大模型开发,大概率已经踩过这样的坑:模型想查一下订单状态,你得在代码里写死一个@Tool方法;模型想读一份本地文档,你又得手动塞进 Prompt 里。工具越接越多,代码越来越乱,换一个模型厂商还得重写一遍适配层。Model Context Protocol(MCP)就是为了解决这个问题出现的——它把「模型调用外部能力」这件事从业务代码里抽出来,变成一套标准协议。
MCP 全称 Model Context Protocol,直译是「模型上下文协议」,你可以把它理解成大模型世界里的 USB-C 接口。以前每个工具都要给模型单独做一根线,现在统一成一个插口,谁实现了 MCP,谁就能被模型发现和调用。它基于 JSON-RPC 2.0,支持 stdio、SSE、WebSocket 等多种传输方式,核心能力包括工具发现、资源读取、提示模板管理以及能力协商。
这套东西适合谁?我认为有三类 Java 开发者值得花时间:第一类是做企业级 AI 应用的,需要把内部系统安全地暴露给模型;第二类是做 Agent 的,工具数量多到@Tool注解已经管不过来;第三类是想让自己的 Java 服务被 Claude Desktop、Cline 这类客户端直接调用的。Spring AI 从 1.0.0-M5 开始提供了 MCP 的 Boot Starter,客户端和服务端都能开箱即用,这也是我下面要带你跑通的最小实践路径。
需要提前说明的是,MCP 本身只解决「协议怎么通信」,不解决「模型从哪来」。你仍然需要一个兼容 OpenAI 协议的大模型服务端点。我这边统一用 TaoToken 的 API 作为模型入口,它的 Base URL 是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions格式,Spring AI 的 OpenAI Starter 可以直接对接。这样你的 MCP 工具链路和模型调用链路就能在同一个工程里跑通,排查问题时也不会互相干扰。
2. TaoToken 前置准备:Key、Base URL 与模型 ID
在动手写 MCP 代码之前,先把模型这一侧的凭证准备好。很多同学卡在第一步不是因为 MCP 难,而是因为 Key 没配对、Base URL 写错、模型 ID 不存在,结果报了一堆看不懂的错。我建议你按下面的顺序来。
首先登录 TaoToken 控制台创建 API Key。地址是https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai,进去之后在 API Keys 页面新建一个 Key,复制出来保存好。这个 Key 只会完整显示一次,丢了就只能重建。如果你还没决定用哪个模型,可以先去模型对话页面试一下https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai,看看当前可用的模型列表,记下你打算用的 Model ID,比如gpt-4o-mini或者claude-3-5-sonnet这类。
这里有个关键点:TaoToken 的 API 地址是https://taotoken.net/api,注意不要加 UTM 参数,也不要加/v1后缀,Spring AI 的 OpenAI Starter 会自动拼接/v1/chat/completions。如果你手动写 HTTP 请求,那完整路径就是https://taotoken.net/api/v1/chat/completions。这个细节我在第一次接入时踩过坑,多写了一个/v1导致 404,排查了半小时。
环境变量建议这样设置,避免把 Key 硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用的是 Windows PowerShell,对应命令是:
$env:TAOTOKEN_API_KEY="sk-你的实际Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"接下来确认你的 Spring AI 版本。MCP 的 Starter 在 1.0.0-M5 之后才比较稳定,我建议直接用 1.0.0-M6 或更高。在pom.xml里通过 BOM 统一管理版本:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>同时确认你的 Spring Boot 版本在 3.2 以上,JDK 17 以上。MCP Java SDK 0.8.0 之后引入了 Session 会话模型,所有通信都要经过McpSession,不再直接操作 Transport,这个变更在写代码时要注意,构造McpClient和McpServer都要用 Builder 模式。
如果你打算长期跑 Agent 类任务,比如让模型连续调用多个工具完成一个复杂流程,可以考虑 TaoToken 的 Coding Plan,地址是https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai,它在长上下文和连续调用场景下更划算。不过对于下面这个最小实践,普通 API Key 就够了。
3. 可复制配置:application.yml 与 Java SDK 片段
这一节是整篇文章的核心,我会给出完整的application.yml、Maven 依赖和 Java 代码,你直接复制到工程里就能跑。先说明整体结构:我们做一个 MCP 服务端,暴露一个「查询天气」的工具;再做一个 MCP 客户端,通过 Spring AI 的 ChatClient 调用这个工具,而 ChatClient 背后的模型走 TaoToken。
先看服务端的依赖。如果你用 WebFlux 做 SSE 传输,加这个:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency>服务端的application.yml配置如下:
server: port: 8080 spring: ai: mcp: server: name: weather-mcp-server version: 1.0.0 protocol: SSE sse-endpoint: /mcp capabilities: tool: true resource: false prompt: false logging: level: org.springframework.ai.mcp: DEBUG这里sse-endpoint: /mcp表示客户端通过http://localhost:8080/mcp建立 SSE 连接。capabilities.tool: true表示这个服务端只暴露工具能力,不暴露资源和提示模板,按需开启即可。
服务端的工具注册代码,注意 MCP Java SDK 0.8.0 之后工具要通过ToolProvider接口注册:
package com.example.mcp.server; import org.springframework.ai.mcp.server.ToolProvider; import org.springframework.ai.mcp.server.annotation.ToolMethod; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component public class WeatherToolProvider implements ToolProvider { @Override public List<Map<String, Object>> getTools() { return List.of( Map.of( "name", "getWeather", "description", "获取指定城市的当前天气", "inputSchema", Map.of( "type", "object", "properties", Map.of( "city", Map.of( "type", "string", "description", "城市名称,例如 北京" ) ), "required", List.of("city") ) ) ); } @ToolMethod("getWeather") public String getWeather(String city) { // 真实场景这里调用天气 API,这里用模拟数据 return "当前 " + city + " 的天气是晴,气温 25°C,湿度 40%。"; } }启动类就是标准的 Spring Boot 启动类:
package com.example.mcp.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class McpWeatherServerApplication { public static void main(String[] args) { SpringApplication.run(McpWeatherServerApplication.class, args); } }启动后你会看到日志里输出 SSE 端点注册成功的信息。用浏览器访问http://localhost:8080/mcp会保持连接不返回,这是正常的,SSE 是长连接。
再看客户端。客户端依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>客户端的application.yml是重点,这里同时配置了 MCP 连接和 TaoToken 模型入口:
server: port: 8081 spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: ${TAOTOKEN_BASE_URL} chat: options: model: gpt-4o-mini temperature: 0.7 mcp: client: name: weather-mcp-client version: 1.0.0 transport: type: SSE sse: uri: http://localhost:8080/mcp toolcallback: enabled: true logging: level: org.springframework.ai.mcp: DEBUG org.springframework.ai.openai: DEBUG注意base-url写的是https://taotoken.net/api,不要带/v1。toolcallback.enabled: true让 Spring AI 自动把 MCP 发现的工具注册成 ChatClient 可用的 ToolCallback,这样你就不用手动写@Tool了。
客户端的调用代码:
package com.example.mcp.client; import org.springframework.ai.chat.client.ChatClient; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class WeatherRunner implements CommandLineRunner { private final ChatClient chatClient; public WeatherRunner(ChatClient.Builder builder) { this.chatClient = builder.build(); } @Override public void run(String... args) { String answer = chatClient.prompt() .user("帮我查一下北京现在的天气,用工具查") .call() .content(); System.out.println("模型回答: " + answer); } }这段代码里,模型会先判断需要调用getWeather工具,Spring AI 通过 MCP 客户端向服务端发起tools/call请求,拿到结果后再交给模型生成自然语言回答。整条链路是:ChatClient → TaoToken 模型 → 工具调用决策 → MCP Client → MCP Server → 工具执行 → 结果回传 → 模型总结。
4. 验证请求:从启动日志到工具调用链路连通
配置写完之后,怎么确认整条链路真的通了?我一般分三步验证,每一步都有明确的成功标志。
第一步,单独启动 MCP 服务端,观察日志。成功的话你会看到类似这样的输出:
Registered SSE endpoint at /mcp MCP Server initialized: weather-mcp-server v1.0.0 Capabilities: tools=true, resources=false, prompts=false如果看到Capabilities: tools=true,说明工具能力已经暴露。这时候可以用 curl 手动发一个 JSON-RPC 请求测试,MCP 的 SSE 端点需要先建立连接再发消息,用 curl 不太方便,我建议直接用客户端验证。
第二步,启动客户端,观察 MCP 连接日志。成功标志是看到工具发现的结果:
MCP Client connected to http://localhost:8080/mcp Discovered tools: [getWeather] Registered ToolCallback: getWeather看到Discovered tools: [getWeather]就说明协议握手、能力协商、工具发现都成功了。这一步如果卡住,通常是服务端没启动或者端口不对。
第三步,看模型调用结果。客户端启动后会执行WeatherRunner,控制台应该输出:
模型回答: 北京当前天气是晴,气温 25°C,湿度 40%。同时服务端日志里会出现tools/call的请求记录,客户端日志里会出现对 TaoToken 的/v1/chat/completions请求。如果你在客户端日志里看到两次模型请求,那是正常的:第一次模型决定调用工具,第二次模型根据工具结果生成最终回答。
为了更直观地验证,你可以把WeatherRunner改成多轮对话,连续问两个城市:
String answer1 = chatClient.prompt() .user("查一下上海天气") .call() .content(); System.out.println("上海: " + answer1); String answer2 = chatClient.prompt() .user("再查一下广州天气") .call() .content(); System.out.println("广州: " + answer2);如果两次都能正确返回,说明 MCP 会话保持正常,工具可以重复调用。这时候你可以打开 TaoToken 的模型对话页面https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai对比一下,同样的 Prompt 在纯对话模式下模型是没法查天气的,只有接了 MCP 工具才能拿到实时数据,这个对比能帮你确认工具调用确实生效了。
还有一个验证技巧:把服务端的getWeather方法改成返回一个随机数,比如"当前 " + city + " 温度 " + (20 + new Random().nextInt(10)) + "°C",然后连续调用几次,如果每次温度不同,说明工具是真的被执行了,而不是模型在编答案。
5. 本篇常见错排查:401、local proxy failed 与 choices 解析异常
这一节我整理了几个真实遇到的报错,基本都是配置问题,对照着改就能解决。
报错一:401 Unauthorized
org.springframework.web.reactive.function.client.WebClientResponseException$Unauthorized: 401 Unauthorized这个几乎都是 Key 的问题。检查三件事:TAOTOKEN_API_KEY环境变量有没有生效,可以在代码里打印System.getenv("TAOTOKEN_API_KEY")确认;Key 有没有多余空格,复制的时候容易带上换行;Key 是不是已经被删除或过期。另外确认base-url写的是https://taotoken.net/api,如果误写成https://taotoken.net/api/v1,请求路径会变成/api/v1/v1/chat/completions,虽然可能返回 404 而不是 401,但也一并检查。
报错二:local proxy failed 或 Connection refused
java.net.ConnectException: Connection refused: localhost:8080这个通常是 MCP 服务端没启动,或者端口被占用。先确认服务端进程在跑,netstat -an | grep 8080看一下端口监听状态。如果服务端启动失败,检查spring-ai-starter-mcp-server-webflux依赖有没有加对,WebFlux 和 WebMVC 的 Starter 不能混用,混用会导致端点注册冲突。还有一种情况是客户端配置的uri写成了http://localhost:8080,少了/mcp路径,SSE 握手会失败。
报错三:Error reading choices 或 choices 为空
java.lang.NullPointerException: Cannot invoke "java.util.List.get(int)" because "choices" is null这个报错说明模型返回的 JSON 结构不符合 OpenAI 格式,Spring AI 解析choices数组时拿到 null。常见原因有两个:一是base-url配错,请求打到了非 OpenAI 兼容的端点;二是模型 ID 写错,服务端返回了错误信息而不是正常的 chat completion。解决办法是打开 DEBUG 日志,看原始响应体:
logging: level: org.springframework.web.reactive.function.client: DEBUG如果响应体里是{"error": "model not found"}这类信息,那就是 Model ID 的问题,去 TaoToken 模型列表确认可用模型名。如果响应体是 HTML,说明 URL 打到了网页而不是 API,检查base-url有没有多写路径。
报错四:OAuth 或认证方式不匹配
OAuth2 authentication failed / invalid_clientMCP 的 SSE 传输在某些实现里会走 OAuth 流程,但 Spring AI 的 Starter 默认用简单连接。如果你在服务端配置了额外的安全拦截,客户端又没带对应凭证,就会报这个。最小实践阶段建议先关掉服务端的 Spring Security,或者放行/mcp路径:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/mcp/**").permitAll() .anyRequest().authenticated()); return http.build(); }报错五:工具被发现但模型不调用
日志里显示Discovered tools: [getWeather],但模型回答里没有调用工具,直接编了一个天气。这种情况通常是 Prompt 不够明确,模型觉得不需要工具。把用户输入改成「必须使用 getWeather 工具查询北京天气」,或者在系统提示里强调「所有天气问题必须调用工具」。另外确认toolcallback.enabled: true生效了,如果这个开关没开,工具虽然被发现但不会注册给 ChatClient。
排查的时候记住一个原则:先看 MCP 层日志,再看模型层日志。MCP 层确认工具发现和调用是否正常,模型层确认请求是否到达 TaoToken 以及返回结构是否正确。两层分开看,问题定位会快很多。
6. 把 MCP 工具链路接到 TaoToken 的长期实践建议
跑通最小实践之后,你可能会想把它用到真实项目里。我分享几个实际落地时的经验。
第一,工具粒度要控制。MCP 工具不是越多越好,一个服务端暴露 5 到 10 个高内聚的工具比较合适。工具太多会导致模型选择困难,也会让tools/list的响应变大,每次对话都要传输一遍。我一般按业务域拆分服务端,比如订单服务端、用户服务端、文档服务端,客户端按需连接。
第二,传输方式按场景选。本地 CLI 工具用 stdio,Web 应用之间用 SSE,高并发场景用 WebFlux 的 SSE。stdio 的好处是不占端口、进程隔离,适合把 Python 脚本包装成 MCP 工具;SSE 的好处是跨网络、支持多客户端,适合微服务架构。如果你不确定,先用 SSE,调试方便。
第三,模型入口统一管理。MCP 解决的是工具协议,模型调用仍然需要一个稳定的端点。我建议把 TaoToken 的 Base URL 和 Key 放在配置中心或环境变量里,不要散落在各个服务的application.yml中。这样换模型、换 Key 的时候只改一处。如果你同时跑多个 Agent 任务,Coding Plan 的额度管理会比按量计费更清晰,具体可以看https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai。
第四,日志要分级。开发阶段把org.springframework.ai.mcp开到 DEBUG,能看到完整的 JSON-RPC 请求响应;生产环境调到 INFO,只记录工具调用次数和耗时。MCP 的结构化日志里会带sessionId,排查多客户端并发问题时很有用。
第五,注意 SDK 版本升级。MCP Java SDK 从 0.7.0 到 0.8.0 引入了 Session 模型,工具注册从直接实现接口改成了ToolProvider,资源访问从ResourceHandler改成了ResourceProvider。升级前一定要看 Migration Guide,否则编译都过不了。Spring AI 的 Starter 版本要和 SDK 版本对齐,BOM 里统一管理最省心。
最后说一个我自己的用法:把 MCP 服务端做成独立的 Spring Boot 应用,部署在内网,客户端通过 SSE 连接。这样工具的执行环境是隔离的,模型只能通过协议调用,不能直接访问数据库或文件系统。安全边界清晰,审计也方便。客户端这边只负责模型交互和工具编排,不碰具体业务逻辑。这套结构跑下来,新增一个工具只需要在服务端加一个@ToolMethod,客户端不用改代码,重启服务端就能被发现。
如果你还没创建 Key,现在可以去https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai建一个,然后按上面的配置把服务端和客户端跑起来。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_spring_ai,里面有完整的 API 说明和示例。遇到问题先看 DEBUG 日志,大部分配置错误在日志里都能直接看到原因。