当前位置: 首页 > news >正文

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;}
}

定义ToolCallbackProviderBean

@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,支持百万级连接)
资源消耗高(每个连接占用一个线程)低(少量线程处理所有连接)
协议支持仅 SSESSE + 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: 以下是一些杭州西湖附近性价比较高的酒店推荐:

  1. 杭州白金汉爵大酒店

    地址:珊瑚沙东路9号

  2. 维也纳国际酒店(杭州西溪灵隐店)

    地址:合贸路33号1号楼(古墩路地铁站C口步行400米)

  3. 锦辰酒店

    地址:文二路268号(文二路学院路交叉口)

  4. 汉庭酒店(杭州西湖保俶路店)

    地址:宝石二路2号

  5. 桔子酒店(杭州西湖区宋城店)

    地址:转塘街道之江长九中心1号楼

  6. 杭州文华景澜大酒店

    地址:文二路38号

  7. 如家商旅酒店(杭州西湖湖滨断桥店)

    地址:保俶路27号

  8. 全季酒店(杭州文二西路西溪湿地店)

    地址:文二西路710号

  9. 格雷斯精选酒店(杭州西溪店)

    地址:留下街道荆山岭路2号汇峰国际B座

  10. 湖滨四季酒店(杭州保俶路下宁桥地铁站店)

    地址:文二路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的请求一共五个,分别是initializenotifications/initializedtools/listresources/listresources/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+SSEStreamable 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 需要具备 namedescriptioninputSchema 三个属性, 其中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

提示词解读:

    1. 角色定义
    • 名称:Cline
    • 身份:高级软件工程师
    • 能力:精通多种编程语言、框架、设计模式和最佳实践
    1. 工具系统
    • 工具调用格式: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(提交最终结果)
    1. 工作模式
    • ACT模式(执行模式):
      • 使用工具完成任务
      • 禁止使用plan_mode_respond
    • PLAN模式(规划模式):
      • 仅能使用plan_mode_respond进行讨论
      • 用于需求分析、方案设计,用户批准后切换回ACT模式执行
    1. 文件编辑策略
    • write_to_file:适用于创建新文件或完全重写
    • replace_in_file:适用于局部修改(精确匹配,支持多段替换)
    • 注意事项:
      • 修改后需考虑IDE自动格式化影响
      • 替换内容必须完整,不能截断
    1. 安全与规范
    • 工作目录限制:固定在/Users/celen/Desktop,不能cd切换
    • 命令执行安全:
      • 危险操作(如删除、安装)需用户明确批准(requires_approval=true)
      • 命令需适配用户系统(macOS/zsh)
    • 沟通风格:
      • 禁止无意义开场白(如"Great"、“Certainly”)
      • 必须直接、技术性表达
      • 禁止开放式提问(除非必要)
    1. 任务执行流程
    • 分析任务:结合environment_details(自动提供的文件结构)
    • 选择工具:在中评估最优工具 逐步执行:一次仅用一个工具,等待用户确认后再继续 提交结果:用attempt_completion交付最终成果(不能含未完成任务)
    1. MCP(Model Context Protocol)扩展 支持连接外部服务(如Git、GitHub、天气API等) 每个MCP服务提供特定工具(如git_commit、get_weather_forecast)
    1. 关键原则
    • 一次一步:工具必须按顺序执行,等待用户反馈
    • 最小交互:尽量用工具获取信息,而非提问
    • 结果导向:任务完成后必须用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配个图!”

相关文章:

  • 聊聊对Mysql的理解
  • 每日c/c++题 备战蓝桥杯(洛谷P1015 [NOIP 1999 普及组] 回文数)
  • 从头训练小模型: 4 lora 微调
  • 性能优化实践:内存优化技巧
  • LeetCode 热题 100 994. 腐烂的橘子
  • 宏任务与微任务
  • 高等数学第三章---微分中值定理与导数的应用(3.4~3.5)
  • 【前端】【总复习】HTML
  • 互联网大厂Java面试:从基础到实战
  • 运算放大器的主要技术指标
  • 33.降速提高EMC能力
  • SpringBoot中接口签名防止接口重放
  • 前端面经-VUE3篇(三)--vue Router(二)导航守卫、路由元信息、路由懒加载、动态路由
  • Java后端开发day40--异常File
  • 【QT】QT中http协议和json数据的解析-http获取天气预报
  • express 怎么搭建 WebSocket 服务器
  • Linux | 了解Linux中的任务调度---at与crontab 命令
  • 调试Cortex-M85 MCU启动汇编和链接命令文件 - 解题一则
  • 基于多策略混合改进哈里斯鹰算法的混合神经网络多输入单输出回归预测模型HPHHO-CNN-LSTM-Attention
  • 【AI提示词】黑天鹅模型专家
  • “五一”假期第四天,全社会跨区域人员流动量预计超2.7亿人次
  • 中国海警局新闻发言人就日民用飞机侵闯我钓鱼岛领空发表谈话
  • 马上评|提供情绪价值,也是文旅经济的软实力
  • 海外考古大家访谈|斯文特·帕波:人类进化遗传学的奠基者
  • 人民日报今日谈:为何重视这个“一体化”
  • AI世界的年轻人|他用影像大模型解决看病难题,“要做的研究还有很多”