☰
Spring AI Alibaba实战训练营-20 基于注解驱动的MCP快速开发入门指南:把MCP Server配置改到TaoToken
2026/10/2 5:58:28 网站建设 项目流程

1. 从零跑通一个注解驱动的 MCP Server 到底卡在哪

如果你已经写过 Spring Boot 的 Controller,那理解 MCP Server 其实不难:它本质上就是把你写好的 Java 方法,通过一套标准协议暴露给 AI 模型去调用。区别在于,Controller 的调用方是前端或者另一个服务,而 MCP Server 的调用方是大模型。模型会根据你写在注解里的 description,自己判断该不该调、调哪个、传什么参数。

Spring AI Alibaba 提供的注解驱动方式,把这件事压缩到了两个注解:@McpTool定义工具,@McpToolParam定义参数。你不用手写 JSON Schema,不用手动注册工具描述,框架会扫描注解自动生成。这对有 Spring Boot 基础的 Java 开发者来说,学习曲线非常平缓。

但真正动手时,卡点往往不在注解本身,而在三个地方:一是依赖版本和启动器选型,选错了启动就报错;二是模型侧的 Base URL 和 Key 怎么填,很多人习惯性去翻各家厂商的文档,结果配置项对不上;三是启动之后怎么验证工具真的被模型识别到了,而不是自己写了个方法却没人调。

这篇就按「能跑起来、能调通、能排错」的顺序走一遍。我会用一个获取指定城市当前时间的工具作为示例,服务端用 WebFlux 启动器,客户端接一个 OpenAI 兼容协议的模型通道。整个流程你可以在本地完整复现,不需要额外的中间件。

先说清楚适合谁看:有 Spring Boot 基础,能看懂 pom 依赖和 application.yml,知道什么是 Bean 和注解,但对 MCP 协议还停留在「听说过」阶段的 Java 开发者。如果你已经写过 MCP Server,这篇的排错部分可能对你更有用。

核心检索词先摆出来:Spring AI Alibaba 注解驱动 MCP 开发,本质是用@McpTool把普通 Spring Bean 方法变成 AI 可调用的工具,适合想快速验证 MCP 链路的 Java 后端。下面从依赖开始,一步步来。

2. TaoToken 前置:统一 Key 与 API 通道的填写位置

在写代码之前,先把模型通道这件事定下来。MCP 客户端需要调用一个大模型来决定是否触发工具调用,这个模型通道需要三个东西:Base URL、API Key、Model ID。很多人在这里会绕弯路,因为不同厂商的兼容端点、鉴权头、模型命名都不一样。

我这边统一走 TaoToken 的 API 通道,它的好处是 OpenAI 兼容协议,配置项和 Spring AI 的spring.ai.openai前缀天然对齐,不需要额外写适配层。你需要提前准备的是一个 API Key,在控制台的 API Keys 页面创建即可。

具体来说,三个参数的填写位置如下:

Base URL 填https://taotoken.net/api,注意这里不加任何路径后缀,Spring AI 的 OpenAI 自动配置会自己拼接/v1/chat/completions这类端点。API Key 通过环境变量注入,不要硬编码在 yml 里。Model ID 填你实际要用的模型名,比如claude-sonnet-4-5或者gpt-4o这类,具体以你账号下可用的模型为准。

这里有个容易踩的坑:Spring AI 的spring.ai.openai.base-url期望的是不带/v1的根地址,如果你填成https://taotoken.net/api/v1,最终请求会变成/api/v1/v1/chat/completions,直接 404。所以记住,Base URL 就到/api为止。

环境变量的设置方式,Linux 和 Mac 下:

export TAOTOKEN_API_KEY=你的Key

Windows PowerShell:

$env:TAOTOKEN_API_KEY="你的Key"

然后在 yml 里用${TAOTOKEN_API_KEY}引用。这样做的好处是,代码和配置可以进版本库,Key 不会泄露。如果你在 IDE 里跑,记得在 Run Configuration 的 Environment variables 里也加上,否则启动时会报占位符解析失败。

另外提醒一点,MCP 客户端本身不产生模型调用费用,费用发生在模型根据工具描述决定调用哪个工具的那一步。所以 Key 的额度是消耗在模型推理上的,工具执行本身是本地 Java 代码,不花钱。这个认知对后面排查「为什么没调用工具」很关键——如果模型压根没返回 tool_calls,那问题在模型侧或者工具描述,不在 MCP 传输层。

准备好 Key 之后,就可以进入依赖配置了。下一节给出完整的 pom 片段和注解代码,你可以直接复制。

3. 可复制配置:pom 依赖、注解工具类与 application.yml

这一节是整篇的核心,所有片段都可以直接复制到你的项目里。我按服务端和客户端两个模块来组织,先服务端。

服务端的 pom 需要两个关键依赖:注解支持模块和 WebFlux 启动器。注意启动器选型,如果你选了spring-ai-starter-mcp-server-webmvc,那传输层走的是 SSE 的另一种实现,配置项会不一样。这里统一用 WebFlux 版本,和后面的客户端配置匹配。

<dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-annotations</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> </dependencies>

工具类就是普通的@Service,方法上挂@McpTool。这里的关键是 description 要写清楚,因为模型就是靠这句话判断要不要调用。我见过有人把 description 写成「获取时间」,结果模型在用户问「现在几点」时反而不调,因为描述太模糊。写成「Get the current time of a specified city by time zone id」这种,命中率会高很多。

@Service public class TimeTool { private static final Logger logger = LoggerFactory.getLogger(TimeTool.class); @McpTool(name = "getCityTime", description = "Get the current time of a specified city by time zone id, such as Asia/Shanghai") public String getCityTime( @McpToolParam(description = "Time zone id, such as Asia/Shanghai", required = true) String timeZoneId) { logger.info("Tool invoked with timeZoneId={}", timeZoneId); ZoneId zid = ZoneId.of(timeZoneId); ZonedDateTime now = ZonedDateTime.now(zid); return now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z")); } }

启动类什么都不用加,一个@SpringBootApplication就够了。框架会自动扫描@McpTool并注册。

@SpringBootApplication public class AnnotationServerApplication { public static void main(String[] args) { SpringApplication.run(AnnotationServerApplication.class, args); } }

服务端默认监听 8080,MCP 的 SSE 端点在/sse。这个路径后面客户端要填。

客户端的 pom 稍微多一点,需要 OpenAI 自动配置、ChatClient、Web 支持,以及 MCP 客户端启动器。

<dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-chat-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-annotations</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> </dependencies>

客户端的 application.yml 是配置的重头戏,Base URL、Key、Model ID 三件套都在这里。注意web-application-type: none,因为客户端是个命令行程序,不需要起 Web 容器。

server: port: 19100 spring: application: name: mcp-annotation-client main: web-application-type: none ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: claude-sonnet-4-5 mcp: client: enabled: true name: my-mcp-client version: 1.0.0 request-timeout: 600s type: SYNC sse: connections: server1: url: http://localhost:8080 annotation-scanner: enabled: true

这里逐项说明。api-key引用环境变量,base-url就是上一节说的https://taotoken.net/api,model填你实际可用的模型 ID。mcp.client.sse.connections.server1.url指向本地服务端的根地址,框架会自动拼/sse。annotation-scanner.enabled: true是让客户端也扫描注解,如果你只在客户端做工具调用而不定义工具,这个可以不开,但开了不影响。

客户端主程序用CommandLineRunner起一个交互循环,把 MCP 工具注册进 ChatClient。

@SpringBootApplication public class AnnotationClientApplication { public static void main(String[] args) { SpringApplication.run(AnnotationClientApplication.class, args); } @Bean public CommandLineRunner chatLoop(ChatClient.Builder builder, ToolCallbackProvider tools, ConfigurableApplicationContext ctx) { return args -> { var chatClient = builder.defaultToolCallbacks(tools.getToolCallbacks()).build(); System.out.println("Available tools:"); for (ToolCallback cb : tools.getToolCallbacks()) { System.out.println(">>> " + cb.getToolDefinition().name()); } Scanner scanner = new Scanner(System.in); while (true) { System.out.print("\n>>> QUESTION: "); String input = scanner.nextLine(); if ("exit".equalsIgnoreCase(input)) break; System.out.println(">>> ASSISTANT: " + chatClient.prompt(input).call().content()); } scanner.close(); ctx.close(); }; } }

到这里,配置片段就齐了。下一节讲怎么启动和验证。

4. 验证请求:启动顺序、工具列表与一次成功调用

启动顺序很重要,先服务端后客户端。服务端起来之后,你可以先用浏览器或者 curl 探一下 SSE 端点是否活着。

cd mcp-annotation-server mvn spring-boot:run

看到Netty started on port 8080之类的日志就说明服务端 OK 了。这时候访问http://localhost:8080/sse,浏览器会挂起一个长连接,这是正常的,SSE 就是长连接。你可以直接 Ctrl+C 掉这个请求,不影响服务端。

然后启动客户端:

cd mcp-annotation-client mvn spring-boot:run

客户端启动后,控制台会先打印可用工具列表。如果你看到:

Available tools: >>> getCityTime

说明 MCP 客户端已经成功连上服务端,并且发现了工具。这一步是整个链路里最关键的验证点。如果这里没有输出工具,后面模型再聪明也没用,因为工具根本没注册进来。

接下来在>>> QUESTION:后面输入:

上海现在几点了?

模型会先做一次推理,判断需要调用getCityTime,参数是Asia/Shanghai。然后 MCP 客户端通过 SSE 把调用请求发给服务端,服务端执行 Java 方法,把结果返回,模型再组织成自然语言输出。你最终会看到类似:

>>> ASSISTANT: 上海现在的时间是 2025-01-15 14:32:08 CST。

同时,服务端的日志里会出现:

INFO TimeTool - Tool invoked with timeZoneId=Asia/Shanghai

这条日志是工具真的被执行了的铁证。如果客户端有回复但服务端没这条日志,说明模型是「编」了一个时间,并没有真正调用工具。这种情况通常是工具描述不够清晰,或者模型本身对 tool calling 的支持不好。

再测一个边界情况,输入一个模型可能不认识的时区:

纽约现在几点?

模型应该会传America/New_York,服务端返回对应时间。如果模型传了个不存在的时区 ID,ZoneId.of会抛异常,这时候你会看到工具调用失败。这正好引出下一节的排错。

5. 本篇常见错排查:401、连接失败与工具未识别

排错这部分我按报错现象来组织,你遇到哪个直接对号入座。

401 Unauthorized 或 invalid api key

这个几乎都是 Key 的问题。先确认环境变量在当前 shell 里真的存在:

echo $TAOTOKEN_API_KEY

如果输出为空,说明没 export 成功,或者你在 IDE 里跑但没配 Environment variables。另一个常见原因是 Key 复制时带了空格或者换行,建议重新复制一次。还有一种情况是 Base URL 填错,比如填成了https://taotoken.net/api/v1,导致请求打到了不存在的路径,有些网关会返回 401 而不是 404,容易误导。

local proxy failed 或 connection refused

这个报错说明客户端连不上服务端。检查三件事:服务端是否真的在 8080 端口监听,spring.ai.mcp.client.sse.connections.server1.url是否写成了http://localhost:8080(不要带/sse,框架会自己拼),以及有没有防火墙拦截本地回环。如果你改了服务端的server.port,客户端这里的 url 也要同步改。

reading choices 相关报错或返回空

这个通常出现在模型响应解析阶段。如果模型返回的 JSON 结构不符合 OpenAI 兼容格式,Spring AI 在解析choices字段时会失败。排查方向是确认你用的模型 ID 确实支持 OpenAI 兼容协议。有些模型只支持原生协议,走兼容端点会返回非标准结构。换一个明确支持兼容协议的模型 ID 再试。

工具未被识别,Available tools 为空

三个检查点:工具类上有没有@Service或@Component,方法上有没有@McpTool,以及客户端的annotation-scanner.enabled是否为 true。还有一个隐蔽的坑:如果服务端和客户端在同一个 JVM 里跑(比如你写了个单模块项目),注解扫描可能会冲突,建议还是分成两个模块。

OAuth 或鉴权头相关报错

如果你用的是需要 OAuth 的通道,Spring AI 的 OpenAI 自动配置默认只发Authorization: Bearer头。TaoToken 的 API 通道用的就是 Bearer 鉴权,所以只要 Key 对,不会出这个问题。如果你看到 OAuth 相关的报错,大概率是 Base URL 指向了需要 OAuth 的端点,换回https://taotoken.net/api即可。

工具调用超时

默认request-timeout是 600s,一般够用。如果你的工具方法里有耗时操作,比如查数据库或者调外部 API,可以适当延长。但更推荐的做法是把耗时逻辑做成异步,避免阻塞 MCP 会话。

排错的核心思路是分层:先确认传输层通不通(工具列表能不能出来),再确认模型层通不通(模型有没有返回 tool_calls),最后确认执行层通不通(服务端日志有没有打印)。这三层任何一层断了,现象都不一样,按层排查比盲目改配置快得多。

6. 语义一致 CTA:把这条链路用到真实编码场景

跑通这个最小示例之后,你手里其实已经有了一个可扩展的 MCP Server 骨架。接下来无非是往里加工具:查数据库的、调内部 API 的、读文件的,每个工具就是一个带@McpTool的 Spring Bean 方法。注解驱动的价值就在这里,加工具的成本极低,不用改任何注册代码。

如果你想把这条链路接到日常编码里,比如让 AI 在写代码时能调用你的内部工具,那重点会从「能不能跑通」转向「怎么稳定地用」。这时候模型通道的稳定性、Key 的额度管理、以及工具描述的准确性就变得更重要。我自己的做法是把模型通道统一到 TaoToken 的 API 上,Base URL 固定https://taotoken.net/api,Key 走环境变量,这样换模型只需要改一个 model 字段,不用动代码。

对于需要长期跑 Agent 或者频繁做工具调用的场景,可以看一下 Coding Plan 这类方案,它在额度上比按次调用更划算。如果你只是想先验证模型对某个工具描述的理解能力,可以直接在模型对话里试,不用每次都起本地服务。

接入文档里有更完整的参数说明和端点列表,遇到配置项对不上的时候翻一下比猜快。API Keys 页面则是你创建和管理 Key 的地方,建议给不同项目建不同的 Key,方便排查问题时定位是哪个项目在消耗额度。

最后留一个实用技巧:工具方法的返回值尽量用字符串或者简单的 JSON 结构,不要返回复杂的嵌套对象。模型对返回值的解析能力有限,结构越简单,它组织自然语言回答时越不容易出错。这个坑我在做数据库查询工具时踩过,返回了一个深层嵌套的 Map,模型直接懵了,改成扁平结构之后就好了。

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

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

立即咨询