Spring AI + MCP 实战:从原理到 Stdio/SSE 双模式完整指南

📅 2026/7/23 9:36:25 👁️ 阅读次数 📝 编程学习
Spring AI + MCP 实战:从原理到 Stdio/SSE 双模式完整指南

Spring AI + MCP 实战:从原理到 Stdio/SSE 双模式完整指南

1.MCP介绍与原理

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 于 2024 年 11 月推出的开放标准,旨在为大型语言模型(LLMs)提供统一接口,以便连接和调用外部数据源和工具。

目前,各大 LLM 平台(如 Deepseek、ChatGPT、Claude)普遍支持 Function Calling,允许模型在需要时调用特定函数(如访问网络、查询数据库等)来扩展能力。

然而,不同平台的 Function Call API 存在实现差异,导致开发者在切换平台时需要重新适配,增加了开发成本。

MCP的核心是对大模型调用外部工具建立一个标准化流程。MCP基于 Function Calling,进一步定义了从请求构建、发送、执行到结果返回的标准化流程。通过 MCP,模型可以以统一方式与各种外部工具和数据源交互,极大提升了跨平台兼容性和 AI 应用开发效率。

MCP 与 Function Calling 的区别和联系如下:

  • Function Calling :是 LLM 内部定义的一组函数,通过 JSON schema 让 LLM知道有哪些功能能调用。
  • MCP:在 Function Calling 基础上,进一步标准化了函数调用的完整流程,包括请求的构建、发送、执行以及结果的返回。

简单来说,MCP 是对 Function Calling 的扩展与升级,实现了更高层次的抽象和更强的可扩展性。可以将 MCP 理解为 AI 世界里的“USB-C标准”,为模型接入各种数据源和工具提供了统一接口,确保连接便捷且安全。

MCP 遵循客户端-服务器架构,角色主要包含三部分:

  • MCP Host

运行 LLM(如 Claude、ChatGPT、Deepseek)的实体节点,如果使用的LLM为线上模型,可以忽略这部分。

  • MCP Client

运行着与大模型对话的客户端(可能会使用工具)叫做MCP Client。其与 MCP Server 保持 1:1 连接,负责解析模型请求,如果使用工具会将请求转发到对应 MCP Server。

  • MCP Server

实际运行外部工具(如访问文件系统、发送邮件、查询日历)的服务端叫做MCP Server。负责处理请求并将结果返回给 Client。

MCP Cilent与MCP Server之间有两种通信机制:Stdio(标准输入/输出)和SSE(Server-Sent-Event,服务器发送事件),两种机制介绍如下:

  • Stdio(标准输入/输出):当服务器和客户端同时运行在本机时,可以使用Stdio机制。
  • SSE(Server-Sent-Event):当服务器部署在远程服务器上,客户端通过HTTP 请求发送消息使用这种方式。

2.MCP Java SDK 架构

下图是MCP Java SDK 架构示意图:

上图中,McpClient处理客户端操作,McpServer管理服务端操作,两者都使用McpSession进行通信管理。传输层(Mcp Transport)负责处理JSON-RPC 消息的序列化和反序列化,支持三种传输实现:STDIO、Spring MVC SSE、Spring WebFlux SSE,三者区别如下:

  • STDIO:基于进程间的标准输入/输出(STDIO)传输,支持单进程,同步交互处理消息。适用于MCP 服务端和客户端都在同一节点上集成。
  • Spring MVC SSE(HTTP SSE):基于Spring MVC的SSE传输,支持Servlet线程池,阻塞式处理消息。适用于普通的Web应用。
  • Spring WebFlux SSE:官方建议方式。基于Spring WebFlux的反应式SSE,支持高并发、低延迟,响应式处理消息。适用于高并发的web微服务。

对于以上不同的传输方式,Spring AI 提供了多个启动器(starter),简化MCP在SpringBoot中的使用。

  • 客户端Starter:

spring-ai-starter-mcp-client:支持 STDIO 与 HTTP-SSE。

spring-ai-starter-mcp-client-webflux:基于 WebFlux 的 SSE 客户端实现。

  • 服务端Starter:

spring-ai-starter-mcp-server:支持 STDIO 传输。

spring-ai-starter-mcp-server-webmvc:基于 Spring MVC 的 SSE 服务端实现。

spring-ai-starter-mcp-server-webflux:基于 WebFlux 的 SSE 服务端实现。

3.MCP 案例-Stdio传输模式

在该案例中,我们会创建MCP Server 和MCP Client 两个SpringBoot项目,MCP Server项目中会创建一个getWeather工具,该工具通过OpenWeather可以查询某个城市天气情况;MCP Client 项目中创建相应的Controller,根据配置通过STDIO 方式与MCP Server进行通信,实现调用天气工具。

3.1. Mcp Server开发

按照如下步骤创建MCP Server对应的SpringBoot项目。

1) 创建SpringBoot项目

SpringBoot项目命名为SpringAIMCPStdioServer,设置使用的JDK为17版本。


2) 在项目中加入如下Maven依赖

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>SpringAIMCPStdioServer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringAIMCPStdioServer</name> <description>SpringAIMCPStdioServer</description> <properties> <java.version>17</java.version> </properties> <!-- 导入 Spring AI BOM,用于统一管理 Spring AI 依赖的版本, 引用每个 Spring AI 模块时不用再写 <version>,只要依赖什么模块 Mavens 自动使用 BOM 推荐的版本 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 依赖的MCP 包 ,只支持 STDIO 传输--> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <!-- 依赖的json 包--> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20210307</version> </dependency> </dependencies> <!-- 打包插件 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.4.4</version> <configuration> <mainClass>com.example.springaimcpstdioserver.SpringAimcpStdioServerApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <!-- 声明仓库, 用于获取 Spring AI 以及相关预发布版本--> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> <repository> <name>Central Portal Snapshots</name> <id>central-portal-snapshots</id> <url>https://central.sonatype.com/repository/maven-snapshots/</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </project>

注意:在该pom.xml中引入了“spring-ai-starter-mcp-server”MCP 依赖包,该包只支持STDIO 传输。

3) 配置resources/application.properties

spring.application.name=SpringAIMCPStdioServer #指定 MCP 服务器的名称为 spring-ai-mcp-weather spring.ai.mcp.server.name=spring-ai-mcp-weather #配置应用监听的端口为 8080 server.port=8086 #禁用 Spring Boot 启动时的横幅(Banner)显示,对于使用 STDIO 传输的 MCP 服务器,禁用横幅有助于避免输出干扰。 spring.main.banner-mode=off #如下参数启用并设置为空,将禁用控制台日志输出格式,减少输出干扰 logging.pattern.console= #配置日志文件的输出路径,将日志写入指定的文件中 logging.file.name=D:/idea_space/SpringAICode/SpringAIMCPStdioServer/model-context-protocol/mcp-weather-stdio-server.log #访问 OpenWeather API 的密钥 OPEN_WEATHER_API_KEY=f0...8

特别注意:以上配置中logging.file.name指定了MCP Server运行过程中日志输出的位置,可以通过该日志查看Server端运行情况(如:工具是否被调用)。

4) 创建 WeatherService.java构建查询天气工具

在项目中创建service包,在该包中创建WeatherService.java类,构建查询天气工具:

packagecom.example.springaimcpstdioserver.service;importjava.io.BufferedReader;importjava.io.InputStreamReader;importjava.net.HttpURLConnection;importjava.net.URL;importjava.net.URLEncoder;importorg.json.JSONArray;importorg.json.JSONObject;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.ai.tool.annotation.ToolParam;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Service;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;/** * 天气服务类,用于获取指定城市的天气信息 * @Service 标记为 Spring 服务层组件 */@ServicepublicclassWeatherService{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(WeatherService.class);privatestaticfinalStringBASE_URL="http://api.openweathermap.org/data/2.5/weather";@Value("${OPEN_WEATHER_API_KEY}")privateStringOPEN_WEATHER_API_KEY;/** * 根据城市名称获取天气信息(使用 OpenWeatherMap) * @param city 城市名称,如 "Beijing" * @return 天气信息文本 */@Tool(description="获取指定城市的当前天气情况,格式化后的天气报告字符串。")publicStringgetWeather(@ToolParam(description="城市名称,必须是英文格式,比如 London 或 Beijing")Stringcity){logger.info("====== 调用了getWeather工具 ======");try{Stringcharset="UTF-8";Stringquery=String.format("q=%s&appid=%s&units=metric&lang=zh_cn",URLEncoder.encode(city,charset),URLEncoder.encode(OPEN_WEATHER_API_KEY,charset));URLurl=newURL(BASE_URL+"?"+query);logger.info("====== 访问URL: ======"+url.toString());HttpURLConnectionconnection=(HttpURLConnection)url.openConnection();connection.setRequestMethod("GET");BufferedReaderreader=newBufferedReader(newInputStreamReader(connection.getInputStream(),charset));StringBuilderresponse=newStringBuilder();Stringline;while((line=reader.readLine())!=null){response.append(line);}reader.close();JSONObjectdata=newJSONObject(response.toString());if(data.getInt("cod")==404){return"未找到该城市的天气信息。";}JSONObjectmain=data.getJSONObject("main");JSONArrayweatherArray=data.getJSONArray("weather");JSONObjectweather=weatherArray.getJSONObject(0);JSONObjectwind=data.getJSONObject("wind");StringweatherDescription=weather.optString("description","无描述");doubletemperature=main.optDouble("temp",Double.NaN);doublefeelsLike=main.optDouble("feels_like",Double.NaN);doubletempMin=main.optDouble("temp_min",Double.NaN);doubletempMax=main.optDouble("temp_max",Double.NaN);intpressure=main.optInt("pressure",0);inthumidity=main.optInt("humidity",0);doublewindSpeed=wind.optDouble("speed",Double.NaN);returnString.format(""" 城市: %s 天气描述: %s 当前温度: %.1f°C 体感温度: %.1f°C 最低温度: %.1f°C 最高温度: %.1f°C 气压: %d hPa 湿度: %d%% 风速: %.1f m/s """,data.optString("name",city),weatherDescription,temperature,feelsLike,tempMin,tempMax,pressure,humidity,windSpeed);}catch(Exceptione){return"获取天气信息时出错: "+e.getMessage();}}publicstaticvoidmain(String[]