引言: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
java
@Service
public class WeatherService {
@Tool(description = "获取指定经纬度地点的天气预报")
public String getWeatherForecastByLocation(double latitude, // Latitude coordinate
double longitude // Longitude coordinate
) {
// Implementation
return "天气一片晴朗V2 " + System.currentTimeMillis() + "," + latitude + "," + longitude;
}
@Tool(description = "获取指定地域的天气预警")
public String getAlerts(String state // Two-letter US state code (e.g., CA, NY)
) {
// Implementation
return "快跑,有毒V2," + System.currentTimeMillis() + "," + state;
}
}
定义ToolCallbackProvider
Bean
java
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
在application.properties
中配置
java
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
java
"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 应用
局限性:
- 线程阻塞:大量并发连接时,线程池可能耗尽
- 扩展性差:不适合高并发场景(如万级连接)
java
@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 等响应式组件
java
@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
添加依赖
java
<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
java
@Configuration
public class McpConfig implements WebMvcConfigurer {
@Bean
public WebMvcSseServerTransportProvider transportProvider(ObjectMapper mapper) {
return new WebMvcSseServerTransportProvider(mapper, "/mcp"); // 基础路径设为/mcp
}
@Bean
public RouterFunction<ServerResponse> mcpRouterFunction(
WebMvcSseServerTransportProvider transportProvider) {
return transportProvider.getRouterFunction();
}
@Bean
public 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
添加依赖
java
<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
来集成客户端能力

添加依赖:
xml
<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:启用工具回调功能
xml
spring.ai.openai.api-key=sk-***
spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.chat.options.model=deepseek-chat
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
spring.ai.mcp.client.toolcallback.enabled=true
mcp-servers-config.json:
- 类似在Cline中的配置
json
{
"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代码:
java
@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
类似
引入依赖:
xml
<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地址
java
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
java
{
"mcpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=您在高德官网上申请的key"
}
}
}
配置问题:
通过上文知道在Spring AI中添加SSE Server的URL格式,不需要添加sse路径,
java
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,无法配置更多参数;
java
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。
java
@Configuration
public class WebClientConfig {
@Bean
public 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);
});
}
}
测试结果: 提问"明天要去杭州西湖出差,有什么推荐的性价比较高的酒店吗?"
java
@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: 以下是一些杭州西湖附近性价比较高的酒店推荐:
杭州白金汉爵大酒店
地址:珊瑚沙东路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
静态资源
java
// 静态资源:产品手册
@Bean
public 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);
}
动态资源
java
// 动态资源:实时系统指标
@Bean
public 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内容
java
vim /etc/hosts
localhost http://localhost.charlesproxy.com/
在cline_mcp_settings.json
中修改url
java
"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
会发送客户端相关信息,生产环境下可基于此做权限、流量等管控
json
{
"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也可以看到类似信息

json
{
"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
(查询工具列表)为例子
json
"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
json
"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字符串
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请求
java
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
示例
java
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 event
try {
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
触发服务端向客户端推送消息
java
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 method
session.handle(message).block(); // Block for WebMVC compatibility
return 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
java
public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
return Mono.defer(() -> {
// TODO handle errors for communication to without initialization happening
// first
if (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 first
logger.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
java
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 first
var 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后推送给客户端
java
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
- 身份:高级软件工程师
- 能力:精通多种编程语言、框架、设计模式和最佳实践
-
- 工具系统
- 工具调用格式: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类似的模式,用代码构造请求
java
@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配个图!"