1. 从三套适配代码到一套协议:Spring AI 2.0 MCP 到底解决什么问题
如果你正在做 AI 应用落地,大概率踩过这个坑:OpenAI 有 function calling,Claude 有 tool use,Gemini 又是另一套 declarative tools。接三个模型,就得写三套工具适配代码。更麻烦的是企业内部那一堆能力孤岛——财务的智能报表、CRM 的客户洞察、知识库的语义检索,每个都是独立服务,想串给大模型调用,全靠手写胶水代码。
Model Context Protocol(MCP)就是冲着这件事来的。它定义了一套标准化协议,让 AI 模型以统一方式与外部工具和资源交互。Spring AI 2.0 把底层 MCP Java SDK 升级到了 0.17.2,并引入McpSyncServerCustomizer、McpAsyncServerCustomizer等接口,同时修复了非 Web 上下文下的自动配置问题。
这篇聚焦一个具体目标:用 Spring AI 2.0 的 Model Context Protocol SDK 搭一套多服务器编排骨架,覆盖服务注册、工具发现与调用路由。我会给出可复制的依赖与配置片段,演示本地启动多个 MCP 服务、验证工具列表与调用链路的完整动作。适合已经写过 Spring Boot、想快速把 MCP 跑起来的同学。
MCP 的核心能力有四类:Tools(模型可调用的函数)、Resources(模型可读取的数据)、Prompts(预定义提示模板)、Sampling(服务器反调模型)。多服务器编排主要围绕 Tools 展开,因为工具发现和调用路由是编排的核心。
MCP Java SDK 分三层:Client/Server 层、Session 层、Transport 层。Transport 层负责 JSON-RPC 消息的序列化与传输,Spring AI 目前支持 Stdio、SSE、Streamable HTTP 三种方式。Stdio 适合本地进程,SSE 适合远程长连接,Streamable HTTP 是 0.17.2 引入的新传输方式,在保持无状态的同时提供流式响应能力。
多服务器编排要解决三个问题:服务注册(每个 MCP Server 怎么被客户端发现)、工具发现(不同 Server 的工具如何统一暴露)、调用路由(LLM 决定调用某个工具时,请求怎么落到正确的 Server)。Spring AI 通过配置化的多连接 + 工具名前缀 + ToolCallback 桥接,把这三件事串了起来。
2. TaoToken 前置:给多服务器编排准备一个稳定的模型入口
多服务器编排跑起来之后,最终还是要让 LLM 来决定调用哪个工具。这时候模型入口的稳定性就很关键——如果模型侧频繁超时或鉴权失败,你根本分不清是 MCP 编排的问题还是模型接入的问题。
我试过用 TaoToken 作为模型接入层,它的 API 地址是https://taotoken.net/api,兼容 OpenAI 风格的调用方式。对于 Spring AI 来说,你只需要把base-url指向它,再配一个 API Key 就能用。这样做的好处是:MCP 编排层和模型接入层解耦,排查问题时可以分别验证。
先拿到 API Key。访问https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai创建密钥,复制出来备用。注意不要把它硬编码进代码,用环境变量注入。
然后在 Spring AI 的配置里指定 base-url 和 api-key。Spring AI 2.0 的 OpenAI starter 支持自定义 base-url,这样就能把请求转发到 TaoToken 的兼容端点。模型 ID 按你实际使用的填写,比如gpt-4o或claude-3-5-sonnet这类。
如果你需要长期跑编码类 Agent 任务,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai。它更适合持续性的编码场景,和本篇的多服务器编排可以配合使用。
这里要强调一点:TaoToken 在这里的角色是模型接入层,不是 MCP Server 本身。MCP Server 是你自己写的、暴露内部能力的服务;TaoToken 提供的是 LLM 的调用入口。两者职责不同,不要混淆。
配置好模型入口后,你就可以专注于 MCP 编排本身了。接下来看具体的依赖和配置。
3. 可复制配置:依赖、application.yml 与多服务器连接片段
这一节给出可以直接复制的配置。先看 Maven 依赖。Spring AI 的 MCP 集成分 Client 和 Server 两个维度,各自有对应的 starter。
<dependencies> <!-- MCP 客户端:连接外部 MCP 服务器 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <!-- MCP 服务器:暴露本应用的工具能力 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> </dependencies>版本管理通过 BOM 引入,避免手动对齐版本号:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.0-M2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>Spring AI 的里程碑版本需要单独添加仓库:
<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>接下来是application.yml。这是多服务器编排的核心,我把它拆成模型接入和 MCP 客户端两部分。模型接入指向 TaoToken:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o temperature: 0.7MCP 客户端部分,同时连接本地 Stdio 服务和远程 SSE 服务,演示混合编排:
spring: ai: mcp: client: enabled: true type: SYNC request-timeout: 30s toolcallback: enabled: true stdio: connections: local-fs: command: npx args: - "-y" - "@modelcontextprotocol/server-filesystem" - "/data/docs" sse: connections: order-service: url: http://localhost:8081 sse-endpoint: /mcp/sse crm-service: url: http://localhost:8082 sse-endpoint: /mcp/sse如果你在 Windows 下跑 Stdio,npx需要用cmd.exe /c包装:
stdio: connections: local-fs: command: cmd.exe args: - "/c" - "npx.cmd" - "-y" - "@modelcontextprotocol/server-filesystem" - "D:\\data\\docs"toolcallback.enabled: true是关键配置。它让所有从 MCP Server 发现的工具自动注册为 Spring AI 的ToolCallback,这样在ChatClient里就能直接调用,不用手动桥接。
如果你需要自定义客户端行为,比如针对某个慢服务单独设超时,可以实现McpSyncClientCustomizer:
@Component public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer { @Override public void customize(String serverConfigurationName, McpClient.SyncSpec spec) { if ("slow-external-api".equals(serverConfigurationName)) { spec.requestTimeout(Duration.ofSeconds(60)); } spec.toolsChangeConsumer(tools -> System.out.println("Tools changed for [" + serverConfigurationName + "]: " + tools.stream().map(McpSchema.Tool::name).toList())); } }工具过滤也是多服务器编排里常用的。当某个 Server 暴露了太多工具,你只想放行一部分,可以实现McpToolFilter:
@Component public class SecurityToolFilter implements McpToolFilter { private static final Set<String> ALLOWED = Set.of( "query_orders", "get_order_detail", "search_products", "read_file"); @Override public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) { if (tool.name().contains("write") || tool.name().contains("delete")) { return false; } return ALLOWED.contains(tool.name()); } }注意McpToolFilter在应用上下文里只能定义一个 Bean,它会作用于所有 MCP 连接。命名冲突由McpToolNamePrefixGenerator处理,默认会给工具名加上服务器前缀,比如order-service__query_orders,这样不同 Server 的同名工具不会打架。
4. 验证请求:本地启动多 MCP 服务并检查工具列表与调用链路
配置写完了,接下来验证。分三步:启动 MCP Server、检查工具注册、发起一次真实调用。
先写一个 MCP Server,暴露订单查询工具。用注解方式最直观:
@Component public class OrderMcpTools { private final OrderService orderService; public OrderMcpTools(OrderService orderService) { this.orderService = orderService; } @McpTool(name = "query_orders", description = "根据客户ID查询订单列表") public List<OrderSummary> queryOrders( @McpToolParam(description = "客户ID", required = true) String customerId, @McpToolParam(description = "订单状态") String status) { return orderService.findByCustomerAndStatus(customerId, status) .stream() .map(o -> new OrderSummary(o.getId(), o.getStatus(), o.getAmount())) .toList(); } @McpTool(name = "get_order_detail", description = "获取订单详细信息") public OrderDetail getOrderDetail( @McpToolParam(description = "订单ID", required = true) String orderId) { Order order = orderService.findById(orderId); return new OrderDetail(order.getId(), order.getCustomerName(), order.getItems(), order.getTotalAmount()); } }Server 侧的配置:
spring: ai: mcp: server: type: SYNC protocol: SSE server-info: name: order-mcp-server version: 1.0.0 annotation-scanner: enabled: true启动这个 Server,它会在 8081 端口暴露 SSE 端点。再启动一个 CRM Server 在 8082,暴露get_customer_info工具。两个 Server 都起来后,回到客户端应用。
客户端启动时,Spring AI 会自动连接所有配置的 MCP Server,并把工具注册到SyncMcpToolCallbackProvider。写一个CommandLineRunner打印注册结果:
@Component public class ToolRegistryInspector implements CommandLineRunner { private final SyncMcpToolCallbackProvider toolProvider; public ToolRegistryInspector(SyncMcpToolCallbackProvider toolProvider) { this.toolProvider = toolProvider; } @Override public void run(String... args) { ToolCallback[] callbacks = toolProvider.getToolCallbacks(); System.out.println("=== Registered MCP Tools ==="); for (ToolCallback cb : callbacks) { System.out.printf(" - %s: %s%n", cb.getName(), cb.getDescription()); } } }预期输出类似:
=== Registered MCP Tools === - local-fs__read_file: Read the contents of a file - local-fs__list_directory: List directory contents - order-service__query_orders: 根据客户ID查询订单列表 - order-service__get_order_detail: 获取订单详细信息 - crm-service__get_customer_info: 获取客户信息看到前缀了吗?local-fs__、order-service__、crm-service__,这是默认前缀生成器自动加的,跨服务器命名冲突就这样解决了。
最后验证调用链路。把工具桥接进ChatClient:
@Configuration public class ChatClientConfig { @Bean public ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) { return ChatClient.builder(chatModel) .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks()) .build(); } }发起一次真实请求:
@Service public class AssistantService { private final ChatClient chatClient; public AssistantService(ChatClient chatClient) { this.chatClient = chatClient; } public String ask(String message) { return chatClient.prompt() .system("你是客服助手,可以使用订单查询和客户信息工具。") .user(message) .call() .content(); } }调用ask("帮我查一下客户 C1001 的订单"),LLM 会自动决定调用order-service__query_orders,请求通过 MCP 协议落到订单 Server,返回结果后再由模型组织成自然语言。整条链路:用户输入 → ChatClient → LLM 决策 → ToolCallback → MCP Client → MCP Server → 业务服务 → 原路返回。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
多服务器编排跑起来的过程中,报错基本集中在几个地方。这一节按真实报错对照排查。
401 Unauthorized。这个最常见,分两种来源。一种是模型侧,TAOTOKEN_API_KEY没注入或写错,检查环境变量是否生效,可以在启动日志里确认 base-url 和 key 是否被正确读取。另一种是 MCP Server 侧,如果 Server 配了鉴权,客户端连接时没带 token,也会 401。排查方法:先用 curl 单独测模型端点,再单独测 MCP Server 的 SSE 端点,定位是哪一层。
local proxy failed。这个报错通常出现在 Stdio 连接场景。原因一般是command或args写错,子进程根本没起来。检查npx是否在 PATH 里,Windows 下是否用了cmd.exe /c包装。还有一个容易忽略的点:args里的路径如果不存在,子进程会启动失败但报错信息不直观。先把命令复制到终端手动跑一遍,确认能起来再放进配置。
reading choices 相关报错。这类报错一般来自模型响应解析阶段,说明请求发出去了但返回体不符合预期。常见原因是 base-url 配错,比如漏了/api或者多了斜杠。TaoToken 的 API 地址是https://taotoken.net/api,Spring AI 会在后面拼接/v1/chat/completions这类路径,所以 base-url 不要自己再加/v1。另外模型 ID 写错也会导致返回体异常,确认你填的模型名在服务端是存在的。
OAuth 相关报错。如果 MCP Server 用了 OAuth 鉴权,客户端需要配置对应的认证信息。Spring AI 的 MCP Client 支持在连接配置里加 header。排查时先确认 token 是否过期,再看 header 名称是否和服务端要求一致。有些服务端要求Authorization: Bearer xxx,有些要求自定义 header,对不上就会报 OAuth 错误。
工具列表为空。配置看起来都对,但getToolCallbacks()返回空数组。检查三点:toolcallback.enabled是否为 true;annotation-scanner.enabled是否为 true;MCP Server 是否真的启动成功。可以在客户端启动日志里搜索连接相关的 INFO 日志,确认每个 Server 的连接状态。
工具名冲突。如果看到两个工具同名,说明前缀生成器没生效。默认情况下 Spring AI 会加前缀,但如果你自定义了McpToolNamePrefixGenerator并返回了空字符串,就会冲突。检查是否有自定义的前缀生成器 Bean。
排查的核心思路是分层验证:模型层、MCP 连接层、工具注册层、调用层,一层一层单独测,不要一上来就端到端跑。
6. 把编排跑稳之后:模型入口与长期编码的配合
多服务器编排骨架搭起来之后,你会发现真正影响体验的往往是模型侧的稳定性。MCP 负责把工具串起来,但决策调用哪个工具的是 LLM。如果模型入口不稳定,编排再优雅也白搭。
TaoToken 在这里的价值是提供一个统一的模型接入点。你可以在application.yml里把 base-url 指向https://taotoken.net/api,用同一个 Key 切换不同模型,不用改 MCP 编排层的代码。模型对话入口在https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai,可以用来快速验证某个模型对工具调用的支持情况。
如果你在搭的是长期运行的编码 Agent,MCP 编排会涉及大量工具调用和上下文管理,这时候 Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai。它针对持续性编码场景做了优化,和本篇的多服务器编排可以配合。
接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai,里面有完整的参数说明和示例。控制台在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_springai,可以查看调用量和余额。
最后给一个实操建议:先把 MCP 编排跑通,用本地 Stdio 服务验证工具发现和调用链路,确认没问题后再接入远程 SSE 服务。模型入口先用一个稳定的端点,等编排稳定了再考虑多模型切换。这样排查问题时变量最少,定位最快。