☰
Spring AI集成MCP:从Function Calling到AI工具调用的标准化实践
2026/10/3 4:26:40 网站建设 项目流程

1. MCP到底解决了什么问题:先把它放到AI应用架构里看

我写Spring AI学习笔记,前几篇一直在折腾模型对话、Prompt模板、Function Calling这类基础能力,到第四篇终于轮到MCP了。先说结论:如果你已经用过Spring AI的Function Calling,那MCP理解起来会非常顺——它就是把“给AI接工具”这件事从一个项目内的局部功能,升级成了跨系统、跨语言的标准化协议。

传统的Function Calling做法,是在当前Spring Boot进程内写一个@Bean方法,注册给大模型当工具,模型决定调用时由Spring AI的框架帮你直接执行。这套东西在单应用内确实够用,但一旦拆成微服务、或者要对接团队之外的工具,问题就来了:每个服务都有自己的鉴权方式、参数格式、接口文档,你每接一个外部能力都要写一套适配代码。MCP要治的就是这个“接口方言满天飞”的病。

MCP全称Model Context Protocol,模型上下文协议,最早由Anthropic在2024年底提出。它把“AI应用怎么连接数据源和工具”这个场景抽象成一套统一协议:AI应用作为Host,通过MCP Client连接一个或多个MCP Server,Server负责把本地的工具、资源、提示词暴露出去。这样AI应用不需要关心工具背后是Java、Python、Node还是什么其他语言,也不需要关心它跑在本地进程还是远程服务上,只要双方都遵守MCP协议就能直接对话。

我个人的体会是,MCP最值钱的地方不是“协议本身有多复杂”,而是它把工具接入做成了类似USB-C这种通用接口。以前你给AI应用接一个内部查询接口,要写OpenAPI规范、要处理鉴权、要调SDK;现在只需要把接口包成一个MCP Server,AI应用就能自动发现工具、读取参数Schema、调用并拿到结果。对于Java开发者来说,Spring AI从1.0开始把MCP的Server和Client两端都做成了Starter,开箱即用,这也是我为什么把这篇单独拿出来写。

2. MCP的核心概念:Host、Client、Server与调用链路

2.1 三个角色和三个原语

MCP的架构里有三个角色:

  • MCP Host:指的是AI应用本身,比如Claude Desktop、IDE插件,或者我们的Spring AI应用。Host负责跟用户交互、跟大模型交互,同时管理多个MCP连接。
  • MCP Server:对外暴露工具、资源、提示词的进程。Server可以用任何语言实现,只要遵循MCP协议。
  • MCP Client:Host内部用来连接Server的组件,负责协议通信、消息编解码、会话管理。在Spring AI里,这个Client由spring-ai-starter-mcp-client自动装配。

协议层面有三大原语:

  • Tools(工具):可被大模型调用的函数,比如查询数据库、调用REST接口、执行一段命令。工具是MCP最常用、也是Spring AI集成最核心的一块。
  • Resources(资源):可以被读取的数据源,比如文件内容、数据库记录、API返回结果,通常以URI形式暴露。
  • Prompts(提示词模板):可复用的提示词工程模板,比如“代码评审模板”“SQL生成模板”,Server端定义好后Host可以直接引用。

对大多数业务场景来说,你最需要关心的是Tools。因为Spring AI本身在“模型对话”这个层面已经做得够好,接入MCP主要就是为了让模型拥有调用外部工具的能力,而Tools这层正好覆盖这个需求。

2.2 传输方式:stdio和Streamable HTTP是怎么选的

MCP支持多种传输方式,Spring AI里最常见的两种:

  • stdio:Server作为子进程启动,Host通过标准输入输出流跟它通信。优点是零网络配置、安全隔离好,适合Server与应用同机部署的场景。Spring AI为此提供了spring-ai-mcp-server-stdio这个分包。缺点是不能跨机器,而且Server进程的生命周期要跟随Host。
  • Streamable HTTP(也称WebMVC/WebFlux模式):Server独立部署成一个HTTP服务,Client通过HTTP请求访问。优点是支持远程调用、便于扩缩容,也方便复用已有的Spring Boot基础设施。Spring AI 1.0之后的默认HTTP模式就是这个,再早一点的SSE模式已经逐渐被替换了。

我自己的经验是:如果只是自己本机玩,或者在公司内部把AI能力嵌入现有Java服务,用stdio最省事;如果要做成独立的工具服务供多个AI应用调用,那必须走HTTP模式。这也正好回答了“mcp host和mcp server”是什么关系:Host是使用方,Server是提供方,两者可以同机也能跨网络,完全由传输方式决定。

2.3 mcp怎么被调用的:一次完整调用的拆解

很多人第一次看MCP代码会困惑:明明我只写了一个@Tool方法,Spring AI是怎么让大模型调用它的?实际上整条链路是这样的:

  1. AI应用(Host)启动时,MCP Client连接Server,完成initialize握手。
  2. 握手完成后,Client调用tools/list,把Server上所有工具拉回来,并转换成模型能理解的Function Calling格式。
  3. 用户向大模型提问,模型判断“这个问题需要查数据/执行操作”时,在响应里带上一个工具调用请求。
  4. Spring AI把这个请求交给MCP Client,Client按协议打包成JSON-RPC消息,调用tools/call,把参数传给Server。
  5. Server拿到工具名和参数后,反射执行本地方法,把结果返回给Client。
  6. Client把结果回传给大模型,模型基于工具返回内容生成最终回答。

这里最关键的一点是:大模型本身不直接执行任何代码,它只负责“决定调用哪个工具”和“生成参数”。真正执行逻辑的还是你写的Java方法。这也解释了为什么MCP工具的参数描述要写得足够清晰——大模型是靠描述来理解每个参数含义的,描述写得模糊,模型就更容易传错参数。

3. 环境准备:Spring Boot接入MCP的依赖与配置选型

3.1 版本选择与依赖引入

Spring AI对MCP的支持在1.0正式版里已经非常完善,我建议直接使用1.0.0-GA或后续稳定版,不要再碰0.8.x那一批老版本。老版本的包名是spring-ai-mcp-server-webmvc、spring-ai-mcp-client,新版本改成了spring-ai-starter-mcp-server、spring-ai-starter-mcp-client,如果不小心引了旧坐标,会出现类找不到或者自动装配不生效的问题。

以Maven为例,一个最简的MCP Server项目需要这样引入:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

然后是模型侧的依赖。如果你是在同一个项目里既当Server又当Client,还需要引入对应大模型的starter:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>

注意Spring AI的Starter设计是模块化拆分的,模型、MCP Server、MCP Client各自独立,你按需引入就行。如果要在同一个应用内测试“Host直连Server”,可以把Server和Client都引进来,但它们俩同时存在时一定要把Server端口和Client连接地址区分清楚,否则很容易出现Client连了自身端口导致握手失败的情况。

3.2 MCP Server端配置详解

Server端配置在application.yml里,核心参数是工具扫描路径和传输方式。一个典型的HTTP模式配置是这样:

spring: ai: mcp: server: name: my-tool-server version: 1.0.0 enabled: true transport: http

如果你使用stdio模式,配置稍有不同,但大部分时候你甚至不需要写transport,Spring AI会根据classpath里有没有web依赖来决定默认传输方式。我最常遇到的问题是:项目里既引了spring-boot-starter-web又想要stdio,结果服务启动时自动走了HTTP模式。这种情况下要么去掉web依赖,要么显式指定transport: stdio。

Server端暴露工具的方式很简单,在任意被Spring管理的Bean上写@Tool注解即可:

@Service public class StockQueryService { @Tool(description = "根据股票代码查询最近收盘价") public String queryStockPrice(String stockCode) { // 调用数据库或第三方接口 return "股票" + stockCode + "最新收盘价:12.34"; } }

看到这里你可能会发现:这不就是Spring AI之前Function Calling的写法吗?从代码层面确实很像,但区别在于,Function Calling的工具直接注册给当前模型,而MCP Server的工具是注册在独立协议服务里的,任何符合MCP规范的Host都能来调用,不再局限于当前Spring Boot进程内的大模型。

3.3 MCP Client端配置详解

Client端要配置的是连接信息,包括Server地址、超时时间和工具名。以HTTP模式连接本机Server为例:

spring: ai: mcp: client: enabled: true name: my-ai-app transport: http connection: base-url: http://localhost:8080 request-timeout: 30s

在Java代码里注入McpToolSpecification或直接使用McpClientManager,Spring AI会自动把远端工具变成模型可调用的工具。最简单的用法是,在构建ChatClient时把MCP工具塞进去:

@Configuration public class McpConfig { @Bean ChatClient chatClient( ChatClient.Builder builder, McpToolSpecification toolSpec) { return builder .defaultTools(toolSpec) .build(); } }

这里要注意,McpToolSpecification这个Bean通常由Client Starter自动装配,但如果你引入了多个MCP Server,需要自己定义Bean来做筛选和绑定。我早期踩过一次坑:项目里同时配置了两个Server地址,结果Client把所有工具合并在一起注册给模型,导致不同Server下同名的工具互相覆盖,只有先启动的那个生效。

4. 实操:把REST接口发布成MCP Server的完整过程

4.1 改造已有的REST接口

这节专门说“java将rest接口发布为mcp”这个高频需求。假设你有一个传统的Spring Boot项目,里面已经有一个查询订单的REST接口:

@RestController @RequestMapping("/api/order") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @GetMapping("/{orderId}") public OrderVO getOrder(@PathVariable String orderId) { return orderService.queryOrder(orderId); } }

要发布成MCP,最直接的做法是抽取Service层接口加@Tool注解,而不是直接在Controller上加。因为REST开发规范要求Controller只负责协议转换和参数校验,工具调用本身应该是业务能力的暴露,两者职责不同。改造后的Service:

@Service public class OrderService { @Tool(description = "根据订单ID查询订单详情,返回订单号、商品名称、金额和状态") public OrderVO queryOrder(String orderId) { // 原有查询逻辑不变 return doQuery(orderId); } }

这里建议返回VO对象而不是void或者boolean。因为大模型拿到工具返回结果后,还要基于这个结果生成自然语言回答,返回信息越结构化、越完整,模型的回答质量越高。如果只返回一个true/false,模型就只能干巴巴地告诉用户“操作成功”,没法给出任何上下文。

4.2 参数定义与工具描述的最佳实践

MCP工具能否被大模型正确调用,很大程度取决于工具描述和参数Schema。Spring AI会自动把@Tool方法的Javadoc风格的description以及方法参数转换成JSON Schema,但细节处还是有不少讲究。

我整理了几条实践经验:

  • 每个工具都要写description,尤其是参数数量超过两个的时候。
  • 参数类型尽量用简单类型(String、int、double)或结构清晰的record,少用继承体系复杂的实体类。
  • 如果某个参数有可选值,应该在@ToolParam的description里写明白,比如“状态码,可选值:PENDING/SHIPPED/DONE”。
  • 方法名不要用queryOrder信息量弱的命名,模型判断是否调用工具时会结合方法名和描述一起看,方法名本身也尽量语义明确,比如queryOrderById。

参数上可以用@ToolParam细化:

@Tool(description = "查询订单信息") public OrderVO queryOrder( @ToolParam(description = "订单ID,例如ORD202501001") String orderId, @ToolParam(description = "是否包含已删除订单,默认false") boolean includeDeleted) { // ... }

为什么参数描述这么重要?因为大模型本身没有业务系统的数据字典,它能依赖的就是这些描述信息。描述写得足够细,模型就更容易在用户含糊表达“帮我查一下那个订单”时,从上下文里推断出orderId的值。反过来,如果参数描述写的是“订单ID”,模型可能不知道该从什么地方提取,调用成功率会明显下降。

4.3 配置MCP Server并验证调用

按我上面的方式改完后,启动Spring Boot应用,控制台会打印类似这样一条日志:

Registered tool: queryOrder(根据订单ID查询订单详情,返回订单号、商品名称、金额和状态) MCP Server started. Transport: http

如果你想确认协议层是否正常工作,可以直接用curl或Postman调MCP HTTP接口。MCP的HTTP模式不是像普通REST那样一个接口一个操作,而是统一走一个端点,消息用JSON-RPC 2.0格式。手动发一个tools/list请求可以这样:

curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

正常情况下会返回工具列表和参数Schema。等模型接入后,完整的调用就是第2.3节描述的那条链路:Host初始化→拉工具列表→模型决策→tools/call→执行方法→回传结果。这一步做完,你的Spring Boot应用就已经从“只能提供REST接口”升级成了“能向AI应用提供标准化工具”的服务。

4.4 Spring AI Client侧消费MCP Server

服务端发布成功后,再开一个Spring Boot项目专门做AI应用,或者同一个项目引入Client Starter,写一个最简单的调用入口:

@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient = chatClient; } public String ask(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }

注意这里ChatClient在构建时已经把MCP的工具注册进去了。当你问模型“订单ORD202501001现在是什么状态”,模型的推理过程是:

  1. 识别到需要订单信息。
  2. 从可用工具列表里匹配到queryOrder。
  3. 提取参数orderId=ORD202501001。
  4. 发起工具调用。
  5. 拿到结果后组织回答。

最终用户看到的就是一句自然语言回答:订单ORD202501001已发货,商品是XX,金额100元。整个过程你只写了一个Service方法,工具发现、参数解析、协议通信、结果回传全部由MCP和Spring AI框架代劳了。

5. 集成RAG时的协同设计:MCP和检索不是二选一

顺着热搜词里“spring-ai集成rag”这个方向,我也想聊聊MCP和RAG到底怎么配合。很多人第一次接触这两个概念时会困惑:RAG是让模型读文档回答问题,MCP是让模型调工具拿数据,它们是不是重复了?其实不然,它们解决的是两类完全不同的需求。

RAG适合的是“非结构化知识检索”,比如公司内部的Word、PDF、Wiki页面,这些内容没有固定字段,你没法用参数去查询,只能靠向量相似度去召回。MCP工具适合的是“结构化数据获取”,比如订单状态、库存数量、用户信息,这些数据有明确的查询条件,直接调接口拿最新值比建向量索引更准、更快。

我见过一个比较合理的组合场景:一个内部客服AI,先通过RAG把产品的常见问题文档召回回来,再利用MCP工具查询该用户的订单详情和售后记录,最后把两边的信息一并交给大模型生成答复。这种模式下,RAG负责知识面,MCP工具负责数据面,各管一段。

Spring AI里同时启用两者并不复杂,只要把MCP工具和向量库Retriever都注册到同一个ChatClient即可:

ChatClient chatClient = ChatClient.builder(model) .defaultTools(mcpToolSpecification) .defaultComponents(retriever) .build();

需要注意的坑是:工具太多会增大模型的选择难度,响应速度也会变慢。如果你的向量检索和MCP工具加起来超过一二十个,建议做一次工具裁剪,只注册与当前业务强相关的。我自己的做法是,按会话主题动态决定注入哪些工具,避免让模型在无关工具之间反复“选择困难”。

6. 实践过程中的坑与排查技巧

6.1 工具注册不上或互相覆盖

这是接入MCP时最容易遇到的问题。表现是:模型明确应该能调某个工具,但响应里就是没有工具调用,或者调出来的是另一个同名工具。排查方向有三个:

  • 检查MCP Server日志,确认工具是否真的注册成功,注意看方法所在类有没有被Spring扫描到。
  • 多个MCP Server场景下,检查Client配置的server列表里有没有同名工具,MCP协议本身不做去重,同名工具后注册的会把先注册的覆盖掉。
  • 检查工具方法所在Bean是否被AOP代理了,如果方法上既有@Transactional又有@Tool,有时候切面会干扰Spring AI对工具方法的反射处理,我建议把@Tool方法放在独立的Service类里,避免和事务切面混在一起。

另外有个比较隐蔽的问题:Spring AI会按方法名对工具做去重,如果你在同一个Bean里写了两个重载方法且都加了@Tool,后面那个会把前面那个覆盖掉。MCP协议里工具名必须全局唯一,所以我在实际开发中会让方法名尽量体现业务语义,避免重载。

6.2 参数类型不匹配和序列化问题

模型生成的参数本质上是JSON,MCP Client在调用Server时要把JSON转换成Java对象。Spring AI底层用Jackson来做反序列化,所以你的工具参数类必须符合Jackson的默认序列化规则。我在项目中遇到过两次典型的坑:

  • 参数里有LocalDateTime类型,模型不知道具体格式,传了个“2025-01-01 10:00:00”进来,Jackson直接报错。解决办法是给参数类加@JsonFormat注解,或者在@ToolParam的description里写清楚格式。
  • 参数是List<Map<String,Object>>这种复杂嵌套结构,模型生成的JSON键名跟实体类字段对不上,导致部分字段丢失。解决办法是尽量把参数收敛成简单的record或者明确的DTO,字段名要跟业务口径一致。

要快速定位这类问题,建议打开Spring AI的调试日志:

logging: level: org.springframework.ai: DEBUG org.springframework.ai.mcp: TRACE

把日志打开后,你能看到MCP Client发出去的完整请求和Server返回的原始响应,序列化问题基本一眼就能看出来。

6.3 超时和连接问题

MCP工具调用跟普通REST请求最大的区别是,它要走完“模型决策→工具调用→结果回传”整条链路,耗时天然比单纯调一次Java方法长。默认的请求超时时间如果设置得太短,大模型还没来得及返回工具调用结果,客户端就判定超时了。

我一般会把MCP Client的request-timeout设置成30秒以上,如果你的工具有些慢查询,甚至设置到60秒。另外,HTTP传输模式下,Server端也要注意网络超时和线程池配置,否则高并发时工具调用直接排队,前端等得心慌。

还有个小问题是端口占用。同一个机器上跑多个MCP Server时,如果每个Server都默认用8080端口,后启动的服务肯定起不来。我给每个Server显式指定端口,并在Client配置里把base-url对应好,这样本地联调多个工具不会互相干扰。

7. 从MCP生态看后续方向:蓝湖、Figma、Codex这些都在做什么

如果你在网上去搜MCP相关的内容,会发现热度最高的不是Spring AI,而是蓝湖MCP、Figma MCP、Codex MCP这些偏设计、偏开发工具的应用。它们本质上做的事情跟我们在Spring Boot里做的一模一样:把某个软件的能力包装成MCP Server,让AI应用能直接操作这个软件。

拿Figma MCP举例,它暴露的工具通常是“获取设计稿信息”“读取图层列表”“修改节点属性”这类能力。AI应用接入后,你可以在对话里说“帮我把设计稿里主色调换成蓝色”,模型就会调用Figma MCP Server里对应的工具,通过Figma的API完成这个操作。蓝湖MCP也是类似,面向产品设计协作场景,把蓝湖上的设计稿、标注、切图信息变成AI可调用的工具。

对我们Java后端开发者来说,这些生态案例的启发在于:MCP不只能接数据库、REST接口,还能接桌面软件、设计工具、IDE、甚至游戏引擎。你只要把能力封装成工具并暴露成MCP Server,任何支持MCP的AI应用都能连进来。如果你所在团队有内部系统,比如用Jadx做逆向分析的、用Blender做建模的、用Godot做游戏开发的,理论上都可以参考同一个套路,把核心能力MCP化,然后交给AI应用统一调度。

这也是我在Spring AI学习到第四篇时最大的感受:MCP的想象空间不在于协议本身,而在于它把“AI接入外部世界”的成本降到了极低。以前每接一个新系统都要从零定制,现在只需要让人家暴露一个MCP Server,AI应用就能自动发现能力、完成调用。

8. 几个提高MCP应用质量的经验总结

写到这里,Spring AI集成MCP的主干内容基本讲完了。最后分享几条我在实际项目里沉淀下来的经验,供大家参考:

第一,工具的数量要克制。MCP Server可以暴露几百个工具,但大模型一次能“看到”的工具数量是有限的,工具太多不仅拖慢响应速度,还会降低模型选对工具的概率。我习惯按业务域拆分MCP Server,比如一个订单域Server、一个用户域Server,Client按需接入。

第二,工具要有可观测性。MCP调用链路变长之后,出了问题很难定位。建议在工具方法里打上操作日志,记录入参、出参、耗时。如果想要更精细的追踪,可以在MCP Client出口加一个过滤器,把tools/list和tools/call的消息都记录下来。

第三,安全和鉴权不能省。MCP Server暴露给AI应用的工具,其实等同于对外暴露了一组高权限接口。你最好在每个工具方法上做一次权限校验,而不是只依赖AI应用侧的对话权限。尤其是那些会执行写操作的工具,比如“删除订单”“修改配置”,必须加上二次确认逻辑。

第四,版本升级要谨慎。Spring AI的MCP模块版本迭代很快,包名和配置项变动也频繁。升级大版本时,重点检查三个地方:transport配置是否还生效、@ToolParam和@Tool注解有没有改动、McpToolSpecification的装配逻辑有没有变化。

MCP这块我目前也是边用边学,Spring AI的官方文档和GitHub仓库更新很频繁,遇到不懂的直接去翻源码比看二手博客更快。如果你正在用Java做AI应用,MCP绝对是值得投入时间研究的方向——它让Java后端沉淀下来的业务能力,第一次能这么顺畅地被AI调用起来。

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

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

立即咨询