Spring AI 实战:第九章、Spring AI MCP之万站直通
引言:AI模型们的"巴别塔困境"
在人工智能的江湖里,各大门派(模型)各怀绝技:GPT-4的"舌灿莲花"、Stable Diffusion的"妙笔生花"、Claude的"逻辑鬼才"、DeepSeek的"庖丁解牛"、通义千问的"八卦推演"。但要让这些"武林高手"同台竞技,就像让李白、达芬奇和爱因斯坦开圆桌会议,专业术语满天飞,协议标准各不同,最后可能演变成"鸡同鸭讲"的惨剧。
2024年11月,Anthropic正式发布模型上下文协议MCP(Model Context Protocol)试图尝试解决这种困境,虽然该协议没有成为行业规范,但在国内外已有较强的共识,各大厂商陆续都开始支持MCP。
一、认识MCP
1.1 基本概念
1.1.1 定义
MCP 是一种开放协议,旨在标准化应用程序向大语言模型(LLM)提供上下文的交互方式。我们可以把 MCP 比作AI 应用的USB-C通用接口,正如USB-C为各类设备提供标准化连接方案,MCP为AI模型与不同数据源/工具建立了统一对接规范
1.1.2 作用
该协议助力开发者在 LLM 之上构建智能体和复杂工作流。由于 LLM 常需整合多方数据与工具,MCP 提供:
- 即插即用集成库:持续扩展的预构建集成方案
- 供应商灵活切换:支持不同 LLM 供应商的无缝迁移
- 数据安全最佳实践:基于基础设施的数据防护机制
1.1.3 架构
采用经典的客户端-服务器架构,支持宿主应用连接多个服务节点:
- MCP宿主:如Claude桌面版、IDE或AI 工具,通过MCP获取数据的应用程序
- MCP客户端:协议客户端,维护与服务端1:1连接的通信管道
- MCP 服务器:轻量化服务程序,通过标准化协议暴露特定能力
- 本地数据源:计算机本地的文件/数据库/服务,MCP 服务器安全访问的内部资源
- 远程服务:通过 API 连接的互联网外部系统(如云服务)
通过这种架构设计,MCP 既保证了本地数据的安全性(敏感信息不出域),又实现了云端服务的扩展性,堪称 AI 时代的"数据外交官协议"。
1.2 MCP小试
说概念总是生涩难懂, 直接看示例
1.2.1 安装Cline
下载Visual Studio Code,https://code.visualstudio.com/后安装Cline,配置大模型的API-KEY,Cline作为集成MCP Client的宿主机,可以安装MCP Server
1.2.2 Mcp Server安装
选择右上角的图标,进入公开可访问的MCP Server MarketPlace,选择Github Starts
可以看到按热门的MCP
选择File System
,它是一个Node.js(本机需要提前完成Node.js的安装)实现的电脑文件操作服务,点Install进行安装,如果点文件名称可直接跳转到对应github地址
安装过程比较简单,按照提示操作点确定即可,最后配置可访问的文件目录/Users/celen/Desktop
然后输入桌面上有几个文件
和在桌面创建一个hello.txt的文件
会自动执行文件的读或写
1.2.3 配置项
在Installed列出已安装的MCP Servers列表,其中Configure MCP Servers
(文件名cline_mcp_settings.json)为对应配置文件
{
“mcpServers”: {
"github.com/modelcontextprotocol/servers/tree/main/src/filesystem": {"autoApprove": [],"disabled": false,"timeout": 60,
** “command”: “npx”,**
"args": ["-y","@modelcontextprotocol/server-filesystem",
** “/Users/celen/Desktop”**
],"transportType": "stdio"}
}
}
mcpServers
定义多个MCP Server的配置(包含运行的命令、参数等),该配置可以手动修改或者走可视化安装时自动写入
- github.com/…/filesystem: 标识该 Server 的类型或来源
- autoApprove:定义需要**自动批准**的权限或操作列表(例如文件读写、网络访问等),空数组
[]
表示不自动批准任何额外权限,需手动授权 - disabled:是否禁用该 MCP Server 实例,false表示启用该服务
- timeout:设置 Server 的超时时间(单位:秒)60 表示如果 Server 在 60 秒内无响应,则认为操作超时。
- command:指定启动该 MCP Server 的命令行工具,
npx
表示使用 Node.js 的包执行工具,python
执行Python包,<font style="color:#000000;">java
执行Java包 - args:传递给
command
的参数列表,"-y"
:可能表示自动确认(类似--yes
),避免交互式提示"@modelcontextprotocol/server-filesystem"
:指定要运行的 npm 包名称"/Users/celen/Desktop"
:文件系统 Server 的根目录路径(服务将监控或操作此目录)
- transportType:定义客户端与 Server 的通信方式,
"stdio"
表示通过标准输入输出(stdin/stdout)进行通信(常见于本地进程间通信)
二、自定义MCP Server
前面安装的MCP Server是由第三方开发完成,那通过Spring AI如何开发一个呢?在Spring AI中支持两类(三种)传输机制,标准输入/输出(STDIO)、服务端主动向客户端发送事件流SSE(Server-Sent Events),其中SSE可以拆分为基于Spring MVC框架实现的SSE和基于Spring WebFlux响应式编程模型实现的 SSE。
基于STDIO形式是将MCP Server当做一个本地的子进程,基于SSE可将MCP Server部署在远端,各有千秋
2.1 STDIO
2.1.1 应用开发
创建工程,选择依赖**Model Context Protocol Server**
开发一个天气服务,定义两个工具,具体实现直接mock
@Service
public class WeatherService {@Tool(description = "获取指定经纬度地点的天气预报")public String getWeatherForecastByLocation(double latitude, // Latitude coordinatedouble longitude // Longitude coordinate) {// Implementationreturn "天气一片晴朗V2 " + System.currentTimeMillis() + "," + latitude + "," + longitude;}@Tool(description = "获取指定地域的天气预警")public String getAlerts(String state // Two-letter US state code (e.g., CA, NY)) {// Implementationreturn "快跑,有毒V2," + System.currentTimeMillis() + "," + state;}
}
定义ToolCallbackProvider
Bean
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
在application.properties
中配置
spring.application.name=mcp-stdio
spring.ai.mcp.server.name=mcp-stdio-weather
spring.ai.mcp.server.version=0.0.1
spring.ai.mcp.server.stdio=true
spring.main.banner-mode=off
logging.file.name=/Users/celen/Desktop/log/mcp-stdio.log
./mvnw clean install
打包,在target目录下找到demo-mcp-server-0.0.1-SNAPSHOT.jar
2.1.2 配置运行
打开cline_mcp_settings.json
文件添加如下配置,左侧就展示成功安装对应MCP Server
"mcp-stdio-weather":{"command": "java","args":["-Dspring.ai.mcp.server.stdio=true","-Dspring.main.web-application-type=none","-Dlogging.pattern.console=","-jar","/Users/celen/Documents/code/spring-ai-action/mcp-stdio/target/mcp-stdio-0.0.1-SNAPSHOT.jar"]}
- command:“java”,指定使用 Java 运行时来执行 JAR 文件。
- args:列表中的参数
- -Dspring.ai.mcp.server.stdio=true :启用 MCP 服务器的 STDIO(标准输入输出)传输模式。服务器将通过 stdin/stdout 与客户端通信(无需 HTTP 端口)。
- -Dspring.main.web-application-type=none:强制禁用 Spring Boot 的 Web 容器(如 Tomcat、Netty),因为使用了 STDIO 模式,不需要启动 HTTP 服务,避免不必要的资源占用(如端口冲突)。
- Dlogging.pattern.console= : 清空控制台日志的输出格式,默认情况下,Spring Boot 会输出带颜色和格式的日志,设为空字符串可减少日志干扰(适合作为子进程运行时)。
- -jar + JAR 文件路径:指定要运行的 Spring Boot 打包的 JAR 文件,/Users/celen/…/mcp-stdio-0.0.1-SNAPSHOT.jar 是本地构建的 Spring Boot 可执行 JAR。
展开mcp-stdio-weather
可看到该服务提供的工具清单
2.1.3 测试
查看杭州天气
在配置项中设置autoApprove
就可以自动执行工具调用,避免手动确认(也可以走可视化界面设置)
杭州有天气预警吗
系统模拟返回杭州当前有天气预警:有毒物质警告(建议迅速采取防护措施)
,经过加工后大模型还做了友善的提醒
2.2 SSE
2.2.1 基本概念
SSE(Server-Sent Events)是一种基于HTTP的服务器推送技术,允许服务端主动向客户端发送事件流(如实时数据更新)。
特点:
- 单向通信:服务端 → 客户端(客户端通过普通HTTP请求交互)。
- 文本协议:基于纯文本,默认使用 text/event-stream 格式。
- 自动重连:客户端内置断线重试机制。
- 轻量级:相比 WebSocket,SSE更简单,适合单向数据推送场景(如股票行情、实时日志)。
Spring MVC实现的SSE
Spring MVC提供的SSE实现,基于Servlet异步处理
特点:
- 阻塞 IO:底层依赖 Servlet 线程模型,每个 SseEmitter 占用一个线程
- 同步编程模型:需手动管理线程(如 ExecutorService)
- 兼容性:适用于传统 Spring MVC 应用
局限性:
- 线程阻塞:大量并发连接时,线程池可能耗尽
- 扩展性差:不适合高并发场景(如万级连接)
@RestController
public class HelloController {@GetMapping("/sse-mvc")public SseEmitter handleSse() {SseEmitter emitter = new SseEmitter(30000L); // 超时时间 30 秒// 模拟推送事件new Thread(() -> {try {for (int i = 0; i < 100; i++) {emitter.send("Event " + i);Thread.sleep(100);}emitter.complete();} catch (Exception e) {emitter.completeWithError(e);}}).start();return emitter;}
}
Spring WebFlux实现的SSE
Spring WebFlux实现的SSE,基于Reactive Streams(响应式流),使用Reactor的Flux推送事件
特点:
- 非阻塞 IO:基于 Netty 或 Reactor-Netty,支持高并发(如 10K+ 连接)
- 函数式编程:通过 Flux/Mono 声明式组合事件流
- 资源高效:占用少量线程(EventLoop 线程池)
优势:
- 背压支持:客户端可控制数据流速
- 集成 Reactive 生态:无缝对接 R2DBC、WebClient 等响应式组件
@RestController
public class HelloController {@GetMapping("/sse-flux")public Flux<ServerSentEvent<String>> handleSseFlux() {return Flux.interval(Duration.ofMillis(100)).map(sequence -> ServerSentEvent.<String>builder().id(String.valueOf(sequence)).event("事件").data("SSE in WebFlux - " + sequence).build());}
}
差异对比
特性 | Spring MVC (SseEmitter) | Spring WebFlux (Flux) |
---|---|---|
底层技术 | Servlet 异步(阻塞 IO) | Reactor-Netty(非阻塞 IO) |
编程模型 | 同步(需手动管理线程) | 响应式(声明式流处理) |
并发能力 | 低(受限于线程池大小) | 高(基于 EventLoop,支持百万级连接) |
资源消耗 | 高(每个连接占用一个线程) | 低(少量线程处理所有连接) |
协议支持 | 仅 SSE | SSE + WebSocket + 其他响应式协议 |
适用场景 | 传统 MVC 应用,低并发需求 | 高并发、实时性要求高的场景(如 IoT、聊天) |
2.2.2 基于SpringMVC的MCP Server
添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webmvc</artifactId></dependency>
代码开发
WeatherService
该服务类和前面保持一致,额外增加McpConfig
- 通过
WebMvcSseServerTransportProvider
构建信息传输Provider,在Endpoint为sse
时处理get
请求,mcp
处理post
请求 - 访问 http://localhost:8080/sse 得到如下信息标识服务启动成功
id:107455ca-e6c4-4802-be43-7162ef884c7f
event:endpoint
data:/mcp?sessionId=107455ca-e6c4-4802-be43-7162ef884c7f
@Configuration
public class McpConfig implements WebMvcConfigurer {@Beanpublic WebMvcSseServerTransportProvider transportProvider(ObjectMapper mapper) {return new WebMvcSseServerTransportProvider(mapper, "/mcp"); // 基础路径设为/mcp}@Beanpublic RouterFunction<ServerResponse> mcpRouterFunction(WebMvcSseServerTransportProvider transportProvider) {return transportProvider.getRouterFunction();}@Beanpublic ToolCallbackProvider weatherTools(WeatherService weatherService) {return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();}}
测试
在Cline中添加Remote Servers,Server URL=http://localhost:8080/sse,点击添加后可在已安装的Server中看到对应信息(注意提前把基于stdio的配置给删除,避免冲突)
测试可发现会正常运行
2.2.3 基于WebFlux的MCP Server
基于webFlux
的流程基本和webMVC
雷同,但注意不需要添加spring-boot-starter-web
,不用创建McpConfig
(自动配置已完成),启用NettyWebServer
添加依赖
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webflux</artifactId></dependency>
三、自定义MCP Client
自定义MCP Server的测试上面都是基于Cline完成,它作为MCP中的宿主机,内部已集成MCP Client能力,抛开Cline,可以自定义MCP Client(生产环节可在集成大模型应用中添加MCP Client,一个MCP HOST就构建成功)。
3.1 STDIO
在Spring AI工程中依赖spring-ai-starter-mcp-client
来集成客户端能力
添加依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
application.properties:
- spring.ai.mcp.client.stdio.servers-configuration : 配置MCP Server相关服务信息
- spring.ai.mcp.client.toolcallback.enabled:启用工具回调功能
spring.ai.openai.api-key=sk-***spring.ai.openai.base-url=https://api.deepseek.comspring.ai.openai.chat.options.model=deepseek-chatspring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
spring.ai.mcp.client.toolcallback.enabled=true
mcp-servers-config.json:
- 类似在Cline中的配置
{"mcpServers": {"mcp-stdio-weather":{"command": "java","args":["-Dspring.ai.mcp.server.stdio=true","-Dspring.main.web-application-type=none","-Dlogging.pattern.console=","-jar","/Users/celen/Documents/code/spring-ai-action/mcp-stdio/target/mcp-stdio-0.0.1-SNAPSHOT.jar"]}}
}
调用Server代码:
@Bean
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) {String userInput = "查询杭州天气预警";return args -> {var chatClient = chatClientBuilder.defaultTools(tools).build();System.out.println("\n>>> QUESTION: " + userInput);System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content());context.close();};
}
输出:
QUESTION: 查询杭州天气预警
ASSISTANT: 杭州的天气预警信息如下:
- 预警内容: 快跑,有毒
请注意安全,并关注相关部门的最新通知!
3.2 SSE
以webflux的形式来演示SSE
,流程和STDIO
类似
引入依赖:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
application.properties:
- spring.ai.mcp.client.sse.connections.自定义的服务名.url : 配置MCP Server地址
spring.ai.mcp.client.sse.connections.weather.url=http://localhost:8080
其他逻辑保持一致可成功调用Server。
3.3 集成高德地图
目前国内阿里云百炼 https://bailian.console.aliyun.com/?tab=mcp#/mcp-market、 支付宝开放平台 https://opendocs.alipay.com/open/0go80l 等公司都开始支持MCP,通过代码Client的集成高德地图服务 https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/amap-maps ,请参考文档完成key的注册
官方示例为:要求在sse后面追加key
{"mcpServers": {"amap-amap-sse": {"url": "https://mcp.amap.com/sse?key=您在高德官网上申请的key"}}
}
配置问题:
通过上文知道在Spring AI中添加SSE Server的URL格式,不需要添加sse路径,
spring.ai.mcp.client.sse.connections.服务名称.url=http://localhost:8080(访问URL)
如果按照高德的示例直接配置spring.ai.mcp.client.sse.connections.map.url=https://mcp.amap.com/sse?key=您在高德官网上申请的key
,启动应用直接抛错;因为代码会自动在URL后追加sse
路径,理论应配置为spring.ai.mcp.client.sse.connections.map.url=https://mcp.amap.com
,但这样就丢弃了key,高德服务端会直接拦截请求,陷入了一个尴尬的局面
在McpSseClientProperties
中配置参数为Map集合对应SseParameters
,该实体只有一个属性url,无法配置更多参数;
public record SseParameters(String url) {}/*** Map of named SSE connection configurations.* <p>* The key represents the connection name, and the value contains the SSE parameters* for that connection.*/private final Map<String, SseParameters> connections = new HashMap<>();
看SseWebFluxTransportAutoConfiguration
中代码是直接使用url,也没有考虑用户配置路径中存在url情况(或者可配置额外可附加的参数),这块从能力上还是有所欠缺,希望后续会升级优化下
解决问题:
如果能自定义List<NamedClientMcpTransport>
会更优雅,但该Bean会自动执行,目前采用一种比较粗暴的做法,拦截url的请求,在包含sse的url后追加参数key。
@Configuration
public class WebClientConfig {@Beanpublic WebClient.Builder webClientBuilder() {return WebClient.builder().filter((request, next) -> {// 拦截 SSE 请求if (request.url().toString().contains("/sse")) {// 在原始 URL 后追加 key 参数URI newUri = UriComponentsBuilder.fromUri(request.url()).queryParam("key", "您在高德官网上申请的key") // 自动处理编码.build().toUri();// 保留原始请求头(关键!)ClientRequest mutatedRequest = ClientRequest.from(request).url(newUri).build();return next.exchange(mutatedRequest);}return next.exchange(request);});}
}
测试结果: 提问"明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?"
@Beanpublic CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) {String userInput = "明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?";return args -> {var chatClient = chatClientBuilder.defaultTools(tools).build();System.out.println("\n>>> QUESTION: " + userInput);System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content());context.close();};}
QUESTION: 明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?
ASSISTANT: 以下是一些杭州西湖附近性价比较高的酒店推荐:
杭州白金汉爵大酒店
地址:珊瑚沙东路9号
维也纳国际酒店(杭州西溪灵隐店)
地址:合贸路33号1号楼(古墩路地铁站C口步行400米)
锦辰酒店
地址:文二路268号(文二路学院路交叉口)
汉庭酒店(杭州西湖保俶路店)
地址:宝石二路2号
桔子酒店(杭州西湖区宋城店)
地址:转塘街道之江长九中心1号楼
杭州文华景澜大酒店
地址:文二路38号
如家商旅酒店(杭州西湖湖滨断桥店)
地址:保俶路27号
全季酒店(杭州文二西路西溪湿地店)
地址:文二西路710号
格雷斯精选酒店(杭州西溪店)
地址:留下街道荆山岭路2号汇峰国际B座
湖滨四季酒店(杭州保俶路下宁桥地铁站店)
地址:文二路125号4号楼(下宁桥地铁站B口步行210米)
如果需要更详细的信息(如价格、评分等),可以告诉我,我可以进一步查询!
四、资源暴露
MCP除了支持Server暴露工具能力(Tools)外,还支持Resources、Prompts、Sampling、Roots,目前就Tools被广泛使用,本小结只演示Resources的能力
功能模块 | 技术定位 | 交互方向 | 关键特性 |
---|---|---|---|
Tools | 服务端暴露的原子化API能力 | LLM ⇄ 外部系统 | • 安全沙箱执行 • 自动OpenAPI描述生成 • 多步骤事务支持 |
Resources | 结构化数据供给层 | 客户端/LLM ← 服务端 | • 动态权限控制 • 版本化数据快照 • 多模态内容支持(文本/图像/音频) |
Prompts | 预置提示工程模板 | 服务端 → LLM | • 参数化模板引擎 • A/B测试版本控制 • 跨模型兼容性适配 |
Sampling | 服务端驱动的智能请求编排 | 服务端 → 客户端 → LLM | • 动态参数调控(temperature/top_p) • 响应过滤 • 计费计量 |
Roots | 分布式资源寻址系统 | 客户端 → 服务端 | • 智能缓存路由 • 故障转移配置 • 混合云资源定位 |
4.1 定义Resource
静态资源
// 静态资源:产品手册@Beanpublic List<McpServerFeatures.SyncResourceSpecification> staticResources() {// 1. 创建资源定义(符合最新API)McpSchema.Resource productManual = new McpSchema.Resource("/resources/product-manual", // URI"产品功能约束", // 名称"详细描述天气查询工具的限制", // 描述"text/html", // MIME类型new McpSchema.Annotations(List.of(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), // 允许访问的角色0.8 // 优先级(0.0-1.0)));// 2. 定义资源内容处理器McpServerFeatures.SyncResourceSpecification spec = new McpServerFeatures.SyncResourceSpecification(productManual, (exchange, request) -> {try {String htmlContent = """<html><body><h1>产品功能约束</h1>* 每个用户每天最多调用10次,超过10次则会收费* 目前所有的数据都是mock的,可靠度为0</body></html>""";return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents(request.uri(), "text/html", htmlContent)));} catch (Exception e) {throw new RuntimeException("Failed to load resource", e);}});return List.of(spec);}
动态资源
// 动态资源:实时系统指标@Beanpublic List<McpServerFeatures.SyncResourceSpecification> dynamicResources(ObjectMapper objectMapper) {McpSchema.Resource systemMetrics = new McpSchema.Resource("/monitoring/system-metrics", "System Metrics", "Real-time CPU/Memory/Disk metrics", "application/json", new McpSchema.Annotations(List.of(McpSchema.Role.USER), 0.9 // 高优先级));McpServerFeatures.SyncResourceSpecification spec = new McpServerFeatures.SyncResourceSpecification(systemMetrics, (exchange, request) -> {try {Map<String, Object> metrics = Map.of("cpu", Map.of("usage", Math.random() * 100, "cores", Runtime.getRuntime().availableProcessors()), "memory", Map.of("free", Runtime.getRuntime().freeMemory(), "max", Runtime.getRuntime().maxMemory()), "timestamp", System.currentTimeMillis());return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", objectMapper.writeValueAsString(metrics))));} catch (Exception e) {throw new RuntimeException("Metrics collection failed", e);}});return List.of(spec);}
4.2 访问Resource
基于Cline测试
查看到增加两个Resources
查询静态资源信息:
查询动态资源:
五、通信浅析
前面的部分已完成Server与Client的自定义开发,接下来一起看下Server与Client的通信,以及如何实现类似Cline的宿主机。
5.1 Charles抓包
Charles配置
通过Charles进行本地网络抓包, 下载安装完成后进行证书的安装、端口设置、启动代理
Host修改
以Cline+Webmvc的组合做示例,配置的url为http://localhost:8080/sse ,要抓包localhost需要修改host内容
vim /etc/hosts
localhost http://localhost.charlesproxy.com/
在cline_mcp_settings.json
中修改url
"test-weather2": {"url": "http://localhost.charlesproxy.com:8080/sse","disabled": false,"autoApprove": []}
抓包
点击server右侧的小圈圈(Restart Server)重新发起请求,Charles中已成功抓到请求
http://localhost.charlesproxy.com:8080/sse** **
- 获取mcp请求的sessionId信息,Mime_Type=text/event-stream
http://localhost.charlesproxy.com:8080/mcp?sessionId=xxx
- 包含mcp的请求一共五个,分别是
initialize
、notifications/initialized
、tools/list
、resources/list
、resources/templates/list
, 其中initialize
会发送客户端相关信息,生产环境下可基于此做权限、流量等管控
{"method": "initialize","params": {"protocolVersion": "2024-11-05","capabilities": {},"clientInfo": {"name": "Cline","version": "3.13.2"}},"jsonrpc": "2.0","id": 0
}
- 以
tools/list
的请求为例,发现请求是遵从JSON-RPC 2.0
协议,但细心的同学发现请求完成后没有直观的看到工具列表信息,是因为整个通信是基于SSE的,要回到sse那个请求下才看到服务端推送的数据
至此,可以理解为基于sse端口获取Seesion信息并建立了服务端到客户端单向实时通信能力,在mcp端口遵守JSON-RPC 2.0
协议,基于不同的method
触发服务端向客户端信息的推送
5.2 inspector工具
除了Charles抓包以外,还有另外一种方式就是利用官方inspector工具。
浏览器请求
打开命令行终端执行安装与启动 <font style="color:#000000;">npx @modelcontextprotocol/inspector
,打开http://127.0.0.1:6274访问,并利用浏览器的检查工具看网络请求
查看EventStream也可以看到类似信息
{"jsonrpc": "2.0","id": 1,"result":{"tools":[{"name": "getAlerts","description": "获取指定地域的天气预警","inputSchema":{"type": "object","properties":{"state":{"type": "string"}},"required":["state"],"additionalProperties": false}},{"name": "getWeatherForecastByLocation","description": "获取指定经纬度地点的天气预报","inputSchema":{"type": "object","properties":{"latitude":{"type": "number","format": "double"},"longitude":{"type": "number","format": "double"}},"required":["latitude","longitude"],"additionalProperties": false}}]}
}
工具测试
<font style="color:#000000;">inspector
提供可视化的操作后天,平时在自定义MCP时可快速发起测试验证
5.3 协议实现
5.3.1 协议内容
在https://github.com/modelcontextprotocol/modelcontextprotocol 可以看到通信协议的schema,看到两个2024-11-05 和 2025-03-26 版本,协议
在https://modelcontextprotocol.io/specification/2025-03-26/basic/transports 这介绍了新版本的特性
重点关注新版本中对服务端与客户端的通信协议做了升级,使用更灵活的Streamable HTTP传输协议替代原有的HTTP+SSE传输方案,差异对比如下(新版本优势明显):
对比维度 | 旧版 HTTP+SSE (2024-11-05) | 新版 Streamable HTTP (2025-03-26) | 新版优势 |
---|---|---|---|
协议名称 | HTTP+SSE | Streamable HTTP | 统一命名,明确功能扩展 |
通信模式 | 单向(服务器 ->客户端推送) | 双向(客户端<->服务器全交互) | 支持复杂交互场景 |
端点设计 | 独立 SSE 端点 | 单一 /mcp 端点(兼容 POST/GET) | 简化部署与维护 |
客户端请求方式 | 仅 GET 请求启动 SSE 流 | 新增 POST 请求:携带 JSON-RPC 请求体 | 支持主动发起带参请求 |
服务器响应形式 | 仅能推送通知 | 可返回: • application/json (单次) • text/event-stream (持续流) | 灵活适配不同场景 |
消息批处理 | 不支持 | 支持 JSON-RPC 批量消息(数组格式) | 提升传输效率 |
多路复用 | 单一连接 | 多流并行:客户端可维护多个独立 SSE 流 | 避免消息阻塞 |
断线恢复 | 无规范 | 强制 Last-Event-ID + 全局唯一事件 ID | 保障消息可靠性 |
会话管理 | 无状态 | Session ID 机制: • 初始化分配 • 显式终止(DELETE) | 支持有状态交互 |
安全增强 | 无强制要求 | 必须: • OAuth 2.0/JWT • 验证 Origin 头 • 本地绑定 localhost | 防御劫持与越权 |
兼容性策略 | 无 | 自动降级: • 新版客户端可回退旧版协议 | 平滑迁移过渡 |
由于本篇文章的示例都是基于2024-11-05版本协议实现的SDK,后续的解读也是基于此版本的(方法掌握才是根本)
ServerResult
约束服务端的返回数据类型,以ListToolsResult
(查询工具列表)为例子
"ServerResult": {"anyOf": [{"$ref": "#/definitions/Result"},{"$ref": "#/definitions/InitializeResult"},{"$ref": "#/definitions/ListResourcesResult"},{"$ref": "#/definitions/ListResourceTemplatesResult"},{"$ref": "#/definitions/ReadResourceResult"},{"$ref": "#/definitions/ListPromptsResult"},{"$ref": "#/definitions/GetPromptResult"},{"$ref": "#/definitions/ListToolsResult"},{"$ref": "#/definitions/CallToolResult"},{"$ref": "#/definitions/CompleteResult"}]},
ListToolsResult
要求返回tools数组,类型为Tool
"ListToolsResult": {"description": "The server's response to a tools/list request from the client.","properties": {"_meta": {"additionalProperties": {},"description": "This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses.","type": "object"},"nextCursor": {"description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.","type": "string"},"tools": {"items": {"$ref": "#/definitions/Tool"},"type": "array"}},"required": ["tools"],"type": "object"}
Tool
需要具备 name
、description
、inputSchema
三个属性, 其中inputSchema
是一个调用工具参数的JSON字符串
"Tool": {"description": "Definition for a tool the client can call.","properties": {"description": {"description": "A human-readable description of the tool.","type": "string"},"inputSchema": {"description": "A JSON Schema object defining the expected parameters for the tool.","properties": {"properties": {"additionalProperties": {"additionalProperties": true,"properties": {},"type": "object"},"type": "object"},"required": {"items": {"type": "string"},"type": "array"},"type": {"const": "object","type": "string"}},"required": ["type"],"type": "object"},"name": {"description": "The name of the tool.","type": "string"}},"required": ["inputSchema","name"],"type": "object"},
5.3.2 Java实现
在Java SDKMcpSchema
类基于schema
完成协议的实现
WebMvcSseServerTransportProvider
在WebMvcSseServerTransportProvider
的构造方法中实例化<font style="color:rgba(0, 0, 0, 0.9);">org.springframework.web.servlet.function.RouterFunction
示例(this.routerFunction),通过<font style="color:rgba(0, 0, 0, 0.9);">handleSseConnection
处理GET请求,<font style="color:rgba(0, 0, 0, 0.9);">handleMessage
处理POST请求
public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUrl, String messageEndpoint,String sseEndpoint) {Assert.notNull(objectMapper, "ObjectMapper must not be null");Assert.notNull(baseUrl, "Message base URL must not be null");Assert.notNull(messageEndpoint, "Message endpoint must not be null");Assert.notNull(sseEndpoint, "SSE endpoint must not be null");this.objectMapper = objectMapper;this.baseUrl = baseUrl;this.messageEndpoint = messageEndpoint;this.sseEndpoint = sseEndpoint;this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
}
handleSseConnection
handleSseConnection
通过函数式ServerResponse.sse
构建响应,生成session并管理生命周期,在全局的map中存储McpServerSession
示例
private ServerResponse handleSseConnection(ServerRequest request) {if (this.isClosing) {return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");}String sessionId = UUID.randomUUID().toString();logger.debug("Creating new SSE connection for session: {}", sessionId);// Send initial endpoint eventtry {return ServerResponse.sse(sseBuilder -> {sseBuilder.onComplete(() -> {logger.debug("SSE connection completed for session: {}", sessionId);sessions.remove(sessionId);});sseBuilder.onTimeout(() -> {logger.debug("SSE connection timed out for session: {}", sessionId);sessions.remove(sessionId);});WebMvcMcpSessionTransport sessionTransport = new WebMvcMcpSessionTransport(sessionId, sseBuilder);McpServerSession session = sessionFactory.create(sessionTransport);this.sessions.put(sessionId, session);try {sseBuilder.id(sessionId).event(ENDPOINT_EVENT_TYPE).data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);}catch (Exception e) {logger.error("Failed to send initial endpoint event: {}", e.getMessage());sseBuilder.error(e);}}, Duration.ZERO);}catch (Exception e) {logger.error("Failed to send initial endpoint event to session {}: {}", sessionId, e.getMessage());sessions.remove(sessionId);return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}
handleMessage
handleMessage
处理客户端post请求,先解析出sessionId
,基于sessionId
获取已缓存的McpServerSession
对象,由于请求是遵循JSON-RPC规范的,可以得到McpSchema.JSONRPCMessage
后调用handle
触发服务端向客户端推送消息
private ServerResponse handleMessage(ServerRequest request) {if (this.isClosing) {return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");}if (!request.param("sessionId").isPresent()) {return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));}String sessionId = request.param("sessionId").get();McpServerSession session = sessions.get(sessionId);if (session == null) {return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));}try {String body = request.body(String.class);McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);// Process the message through the session's handle methodsession.handle(message).block(); // Block for WebMVC compatibilityreturn ServerResponse.ok().build();}catch (IllegalArgumentException | IOException e) {logger.error("Failed to deserialize message: {}", e.getMessage());return ServerResponse.badRequest().body(new McpError("Invalid message format"));}catch (Exception e) {logger.error("Error handling message: {}", e.getMessage());return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));}}
McpServerSession#handle
当message instanceof McpSchema.JSONRPCRequest request
消息类型是JSONRPCRequest
时执行handleIncomingRequest
public Mono<Void> handle(McpSchema.JSONRPCMessage message) {return Mono.defer(() -> {// TODO handle errors for communication to without initialization happening// firstif (message instanceof McpSchema.JSONRPCResponse response) {logger.debug("Received Response: {}", response);var sink = pendingResponses.remove(response.id());if (sink == null) {logger.warn("Unexpected response for unknown id {}", response.id());}else {sink.success(response);}return Mono.empty();}else if (message instanceof McpSchema.JSONRPCRequest request) {logger.debug("Received request: {}", request);return handleIncomingRequest(request).onErrorResume(error -> {var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,error.getMessage(), null));// TODO: Should the error go to SSE or back as POST return?return this.transport.sendMessage(errorResponse).then(Mono.empty());}).flatMap(this.transport::sendMessage);}else if (message instanceof McpSchema.JSONRPCNotification notification) {// TODO handle errors for communication to without initialization// happening firstlogger.debug("Received notification: {}", notification);// TODO: in case of error, should the POST request be signalled?return handleIncomingNotification(notification).doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()));}else {logger.warn("Received unknown message type: {}", message);return Mono.empty();}});}
handleIncomingRequest
handleIncomingRequest
通过匹配不同请求method
找对应处理逻辑,在创建McpServerSession
就初始化好requestHandlers
private Mono<McpSchema.JSONRPCResponse> handleIncomingRequest(McpSchema.JSONRPCRequest request) {return Mono.defer(() -> {Mono<?> resultMono;if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {// TODO handle situation where already initialized!McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(request.params(),new TypeReference<McpSchema.InitializeRequest>() {});this.state.lazySet(STATE_INITIALIZING);this.init(initializeRequest.capabilities(), initializeRequest.clientInfo());resultMono = this.initRequestHandler.handle(initializeRequest);}else {// TODO handle errors for communication to this session without// initialization happening firstvar handler = this.requestHandlers.get(request.method());if (handler == null) {MethodNotFoundError error = getMethodNotFoundError(request.method());return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,error.message(), error.data())));}resultMono = this.exchangeSink.asMono().flatMap(exchange -> handler.handle(exchange, request.params()));}return resultMono.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)).onErrorResume(error -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,error.getMessage(), null)))); // TODO: add error message// through the data field});}
以查询工具列表为例会执行McpAsyncServer.AsyncServerImpl#toolsListRequestHandler
,获取到tools后推送给客户端
private McpServerSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {return (exchange, params) -> {List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();return Mono.just(new McpSchema.ListToolsResult(tools, null));};}
至此,一次交互过程完成。
5.4 Cline与大模型交互
发起请求: 杭州天气有预警吗?
在Charles抓到**api.deepseek.com**
请求(是因为Cline配置的大模型为DeepSeek)
在请求中(temperature=0),messages存在三条消息,
- role=system:设置系统角色,提示词内容很长,直接以文件保存在cline_prompt.txt中
- role=user[0] : 用户输入的任务信息
- role=user[1]:以格式约定的调用方的环境以及时间信息
cline_prompt.txt
提示词解读:
-
- 角色定义
- 名称:Cline
- 身份:高级软件工程师
- 能力:精通多种编程语言、框架、设计模式和最佳实践
-
- 工具系统
- 工具调用格式:XML风格标签(value)
- 核心工具:
- 文件操作:read_file、write_to_file、replace_in_file
- 代码分析:search_files、list_code_definition_names
- 命令执行:execute_command(需用户批准危险操作)
- MCP集成:use_mcp_tool、access_mcp_resource(连接外部服务)
- 交互工具:ask_followup_question(仅必要时使用)
- 任务管理:new_task(保存上下文)、attempt_completion(提交最终结果)
-
- 工作模式
- ACT模式(执行模式):
- 使用工具完成任务
- 禁止使用plan_mode_respond
- PLAN模式(规划模式):
- 仅能使用plan_mode_respond进行讨论
- 用于需求分析、方案设计,用户批准后切换回ACT模式执行
-
- 文件编辑策略
- write_to_file:适用于创建新文件或完全重写
- replace_in_file:适用于局部修改(精确匹配,支持多段替换)
- 注意事项:
- 修改后需考虑IDE自动格式化影响
- 替换内容必须完整,不能截断
-
- 安全与规范
- 工作目录限制:固定在/Users/celen/Desktop,不能cd切换
- 命令执行安全:
- 危险操作(如删除、安装)需用户明确批准(requires_approval=true)
- 命令需适配用户系统(macOS/zsh)
- 沟通风格:
- 禁止无意义开场白(如"Great"、“Certainly”)
- 必须直接、技术性表达
- 禁止开放式提问(除非必要)
-
- 任务执行流程
- 分析任务:结合environment_details(自动提供的文件结构)
- 选择工具:在中评估最优工具 逐步执行:一次仅用一个工具,等待用户确认后再继续 提交结果:用attempt_completion交付最终成果(不能含未完成任务)
-
- MCP(Model Context Protocol)扩展 支持连接外部服务(如Git、GitHub、天气API等) 每个MCP服务提供特定工具(如git_commit、get_weather_forecast)
-
- 关键原则
- 一次一步:工具必须按顺序执行,等待用户反馈
- 最小交互:尽量用工具获取信息,而非提问
- 结果导向:任务完成后必须用attempt_completion明确结束
- 禁止:
- 假设工具执行成功(必须等用户确认)
- 修改文件时截断或不完整替换
- 无意义的客套话
借鉴经验:
上述提示词的解读来自大模型的理解,仅供参考,但特别值得借鉴的地方在于它建立了一个高度结构化、安全可控的AI操作体系,同时保持了足够的灵活性来处理各种软件开发任务。它通过明确的规范、严谨的工作流程和丰富的工具集,使AI能够在软件工程领域进行专业、可靠的操作
- 明确的角色定义:清晰界定了AI的专业领域和能力范围(软件工程相关),设定了专业身份(高级软件工程师)
- 工具化操作体系:采用XML格式的工具调用规范,结构清晰,每个工具都有详细的参数说明和使用示例,工具分类明确(文件操作、命令执行、代码分析等)
- 严谨的工作流程:强调迭代式工作方法,一次只使用一个工具
- 双模式设计:ACT模式(执行模式)用于实际操作,PLAN模式(规划模式)用于方案设计,两种模式有明确的切换规则和使用限制
- 文件编辑规范:区分write_to_file和replace_in_file的使用场景,提供详细的文件修改指南和最佳实践
- 安全控制机制:危险操作需要用户明确批准(requires_approval参数),限制工作目录,防止越权操作,命令执行前需考虑系统环境
- 沟通规范:禁止无意义的寒暄用语,要求直接、技术性的表达方式,限制不必要的问题询问
- 结果交付规范:使用attempt_completion工具明确标记任务完成,禁止开放式结尾,要求提供确定性结果
不足之处是每次发起请求都会将所有的工具scheme传输给大模型,导致提示词内容过长
代码执行:
按照Cline类似的模式,用代码构造请求
@GetMapping(value = "/cline", produces = "text/html;charset=UTF-8")public Flux<String> cline(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {SystemMessage systemMessage = new SystemMessage("""You are Cline, a highly skilled software engineer... 省略""");UserMessage userMessage = new UserMessage("""<task>\\n杭州天气有预警吗?\\n</task>""");UserMessage env =new UserMessage("""<environment_details>\\n# VSCode Visible Files\\n../Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\\n\\n# VSCode Open Tabs\\n../Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\\n\\n# Current Time\\n2025/4/28 下午10:23:28 (Asia/Shanghai, UTC+8:00)\\n\\n# Current Working Directory (/Users/celen/Desktop) Files\\n(Desktop files not shown automatically. Use list_files to explore if needed.)\\n\\n# Context Window Usage\\n0 / 64K tokens used (0%)\\n\\n# Current Mode\\nACT MODE\\n</environment_details>""");Prompt p = new Prompt(List.of(systemMessage,userMessage,env));return chatClient.prompt(p).stream().content();}
得到结果:
1. The user is asking about weather alerts in Hangzhou, China.2. Looking at the connected MCP servers, there is a 'test-weather2' server that provides weather-related tools.3. The server has a 'getAlerts' tool that can get weather alerts for a specified region.4. The tool requires a 'state' parameter - for Hangzhou, we should use 'Zhejiang' as the province/state.5. All required parameters are available or can be reasonably inferred.
<use_mcp_tool>
<server_name>test-weather2</server_name><tool_name>getAlerts</tool_name><arguments>{"state": "Zhejiang"}</arguments>
</use_mcp_tool>
Response file saved.
得到的内容和Cline返回几乎一致(可以对比下图); 返回的结果中提示需要使用mcp工具;如果再开发一个界面来授权调用工具,把工具执行结果再发给大模型,就完成类似效果
点击确认使用工具后,会触发localhost工具的调用,然后把工具执行结果返回给大模型
结语:本来没有路,走的人多了就有了
MCP或许还不是行业标准,但已展现出巨大价值:
- 拆巴别塔:用统一协议终结AI服务的"方言割据"
- 修高速路:让数据、工具、提示词在标准化通道上飞驰
- 建服务区:开发者再不用自己"铺路搭桥",专注业务创新
正如USB-C统一了充电接口,MCP正在成为AI服务的"万能插头"。很快,或许只需一句:“嘿~MCP,帮我调天气AI写首诗,再用Stable Diffusion配个图!”