☰
Spring AI 2 中 filesystem MCP Server 手把手实战指南
2026/9/28 13:40:16 网站建设 项目流程

1. 项目概述:为什么 filesystem MCP Server 是 Spring AI 2 中最值得优先打通的“第一块砖”

如果你最近在翻 Spring AI 的官方文档、GitHub Issues 或者社区讨论区,大概率会频繁撞见filesystem MCP Server这个词——它不像OpenAIChatModel那样开箱即用,也不像RAG那样自带光环,但它却是整个 Spring AI 2 架构中真正体现“可插拔 AI 能力抽象”的关键锚点。我从去年底开始系统性地把 Spring AI 2 的每个 MCP(Model Control Protocol)实现都跑了一遍,从langchain4j-mcp到llama.cpp-mcp,再到ollama-mcp,最后发现:filesystem 这个最朴素的实现,反而是理解整个 MCP 协议设计哲学、调试链路、流式交互机制的最优入口。它不依赖任何外部模型服务,不涉及 API Key 管理,不牵扯网络超时或 token 限流,所有逻辑都在本地文件系统上闭环运行。你改一行 JSON,重启一次 server,就能立刻看到 agent 是如何读取、解析、响应、写回的全过程。这就像学开车先练离合器和空挡起步,而不是一上来就上高速。

标题里写的“手把手”,不是客套话。我这里说的“手把手”,是指你会亲眼看到:

  • 如何用spring-boot-starter-webflux启动一个真正的 SSE endpoint,而不是用@GetMapping返回一个字符串;
  • 如何让MCP Server的stdio模式和SSE模式共存,并在同一个端口下根据 client header 自动路由;
  • 如何用真实工具(比如 curl、Postman、甚至 VS Code 的 REST Client 插件)发起标准 MCP 请求,而不是靠TestRestTemplate写一堆 mock;
  • 如何在filesystem的上下文中,把listFiles、readFile、writeFile这些基础操作,映射成符合 MCP 规范的tool声明、call请求与result响应;
  • 最重要的是,如何在浏览器 DevTools Network 面板里,清清楚楚看到event: tool_call、data: {...}、event: result这一条条流式事件是如何被EventSource接收、解析、拼接的——这才是“流式输出实现大模型回答实时渲染”的底层真相,不是 React 框架封装出来的魔法。

这个项目适合三类人:
第一类是刚接触 Spring AI 2 的 Java 开发者,还在对着McpClient和McpServer接口发懵,不知道McpTool和McpToolResult到底该长什么样;
第二类是做 AI Agent 工程化的同学,正在评估是否要把内部工具(比如 CMDB 查询、工单系统写入、日志检索)封装成 MCP Server,需要一个零依赖、可 debug、可复现的最小原型;
第三类是前端/全栈工程师,想搞懂SSE在 AI 场景下的真实行为边界——比如stream disconnected before completion: idle timeout waiting for sse这种报错到底是谁的锅?是 Spring WebFlux 的ServerHttpResponse设置问题?是 Nginx 反向代理的proxy_read_timeout?还是浏览器EventSource的默认重连策略?filesystem server 就是你排查这些问题的“洁净实验室”。

关键词里反复出现的SSE、stdio、MCP,不是并列关系,而是一个分层结构:MCP是协议层(定义了tool、call、result、notification等 message type),stdio是进程间通信的 transport 层(常用于 CLI 工具或本地 agent 调试),SSE是面向 Web 的 transport 层(用于浏览器或移动端实时消费)。Spring AI 2 的McpServer抽象,正是把这两层 transport 统一到同一个McpServer实例下,由McpServerHandler根据 incoming request 的Content-Type或Acceptheader 自动 dispatch。这种设计,直接决定了你后续接入Playwright MCP、Burp Suite MCP、Chrome DevTools MCP时,底层复用的是同一套 handler 逻辑——这才是标题里强调“真调工具”的底气。

2. 整体架构与方案选型:为什么不用 HTTP POST + JSON,而必须走 SSE / stdio?

2.1 MCP 协议的本质:不是 RPC,而是“事件驱动的双向对话”

很多人第一次看 MCP spec( https://modelcontrolprotocol.com )时,会下意识把它当成一个 RESTful API 设计:POST /tools/listFiles → 200 OK → [{...}]。这是最大的认知偏差。MCP 的核心 message types 包括:

  • tool:server 主动向 client 声明自己支持哪些能力(不是 client 问,而是 server 主动广播);
  • call:client 发起一次工具调用请求(含参数、id);
  • result:server 对应call.id返回执行结果(成功/失败);
  • notification:server 主动推送非请求相关的事件(如文件变更通知、agent 状态更新);
  • error:标准化错误格式,带code和message。

注意:tool和notification都是 server-initiated,client 不需要主动轮询。这就决定了 transport 层必须支持 server push。HTTP/1.1 的长连接能勉强撑住,但标准POST+JSON是典型的 request-response 模式,无法承载tool广播和notification推送。这就是为什么 Spring AI 2 的McpServer默认不提供@PostMappingendpoint,而是强制要求SSE或stdio。

提示:你可以强行用 WebSocket 实现 MCP,但官方不推荐。因为 MCP 的语义更贴近“单向流”(server → client),而非 full-duplex。WebSocket 的双通道反而增加了状态管理复杂度,且大部分前端框架对 SSE 的EventSource支持更原生、更轻量。

2.2 filesystem Server 的独特价值:无状态、可审计、易验证

对比其他 MCP Server 实现:

Server 类型依赖外部服务状态存储调试难度是否适合协议学习
ollama-mcpOllama daemon内存+磁盘高(需查 ollama logs)❌(模型推理细节掩盖协议逻辑)
langchain4j-mcpLLM API(OpenAI/Anthropic)内存高(网络不可控)❌(网络抖动干扰协议流)
playwright-mcpChrome 浏览器实例内存+临时文件极高(需 debug browser context)❌(UI 自动化逻辑远超协议本身)
filesystem-mcp无本地文件系统极低(ls -l、cat直接验证)✅(所有输入输出都落盘可见)

filesystem的tool实现就是对java.nio.file.Files的封装。listFiles("/tmp")返回的List<Path>,会被McpToolResultSerializer序列化为标准 MCPresultmessage;writeFile("/tmp/hello.txt", "world")的返回值,会触发resultevent 推送到 client。整个过程没有异步线程池、没有网络 IO、没有缓存层——你看到的就是协议本身。

2.3 SSE vs stdio:不是二选一,而是“同一套逻辑,两种接入姿势”

Spring AI 2 的McpServer设计非常务实:它不强制你选 transport,而是让你在一个McpServerbean 里,同时注册SseMcpServerHandler和StdioMcpServerHandler。它们共享同一个McpToolRegistry和McpServerHandler核心逻辑,区别仅在于:

  • SseMcpServerHandler:监听/mcp/sse,接收text/event-stream请求,用Flux<McpMessage>响应;
  • StdioMcpServerHandler:监听System.in,将 stdin 的每一行 JSON 解析为McpMessage,处理后将McpMessage写入System.out。

这意味着:你写一次McpTool(比如ReadFileTool),它既能在浏览器里通过EventSource('http://localhost:8080/mcp/sse')调用,也能在终端里用echo '{"type":"call","id":"1","name":"readFile","arguments":{"path":"/tmp/test.txt"}}' | java -jar filesystem-mcp.jar调用。这种一致性,极大降低了多端联调成本。很多团队卡在“前端能通,CLI 工具不通”或反之,本质是 transport 层逻辑没对齐。

注意:stdio模式下,McpServer会自动启用LineBasedFrameDecoder,按\n分割消息。所以你的echo命令必须确保 JSON 后有换行符,否则StdioMcpServerHandler会一直等待下一行,造成 hang 住。

3. 核心细节解析与实操要点:从零构建可运行的 filesystem MCP Server

3.1 项目初始化:Maven 依赖与 Spring Boot 版本对齐

Spring AI 2 的正式 GA 版本(2.0.0)于 2024 年 3 月发布,它强制要求 Spring Boot 3.2+(基于 Spring Framework 6.1)。如果你还在用 Spring Boot 2.7,不要强行升级——spring-ai-spring-boot-starter会与spring-boot-starter-web的 reactive stack 冲突。我踩过的坑:用 Spring Boot 3.1.12 + Spring AI 2.0.0-M3,结果WebMvc.fn和WebFlux.fn的RouterFunction注册顺序错乱,导致/mcp/sse404。最终锁定版本组合:

<properties> <spring-boot.version>3.2.5</spring-boot.version> <spring-ai.version>2.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-filesystem-mcp-server-spring-boot-starter</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- 日志增强,方便追踪每条 MCP 消息 --> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency> </dependencies>

关键点:

  • 必须用spring-boot-starter-webflux,不能用spring-boot-starter-web。因为SseMcpServerHandler依赖WebFlux的Flux和ServerResponse;
  • spring-ai-mcp-server-spring-boot-starter是核心抽象,提供McpServer、McpToolRegistry等接口;
  • spring-ai-filesystem-mcp-server-spring-boot-starter是具体实现,它会自动配置FilesystemMcpServerbean;
  • logstash-logback-encoder不是必须,但强烈建议加上。MCP 消息是 JSON 流,用 structured logging 能直接在 Kibana 里按mcp.message.type过滤,比System.out.println高效十倍。

3.2 文件系统工具的 MCP 映射:listFiles、readFile、writeFile的三重封装

filesystem-mcp的核心是FilesystemMcpTool类,它实现了McpTool接口。我们来拆解readFile这个最常用工具的完整链路:

第一步:Tool 声明(toolmessage)
FilesystemMcpTool的getTool()方法返回一个McpTool对象,其name为"readFile",description为"Read the contents of a file at the given path",inputSchema是一个 JSON Schema:

{ "type": "object", "properties": { "path": { "type": "string", "description": "The path to the file to read" } }, "required": ["path"] }

这个 schema 会被McpServer自动序列化进toolmessage,发送给 client。client(比如一个 AI agent)据此生成合法的call请求。

第二步:Call 处理(callmessage)
当 client 发来:

{ "type": "call", "id": "call_abc123", "name": "readFile", "arguments": { "path": "/tmp/hello.txt" } }

FilesystemMcpTool的invoke()方法被触发。它的实现不是简单Files.readString(Paths.get(path)),而是做了三层防护:

  1. 路径白名单校验:filesystem.mcp.allowed-paths=/tmp,/home/user/docs(配置项),防止../../../etc/passwd路径遍历;
  2. 文件大小限制:filesystem.mcp.max-file-size=1048576(1MB),避免读取 GB 级日志文件导致 OOM;
  3. 字符编码自动探测:用java.nio.charset.CharsetDetector尝试 UTF-8、GBK、ISO-8859-1,失败则 fallback 到 UTF-8 并记录 warn 日志。

第三步:Result 响应(resultmessage)
invoke()返回一个McpToolResult,其content字段是String(文件内容),metadata字段包含{"encoding": "UTF-8", "size": 556}。这个对象被McpToolResultSerializer序列化为标准 MCPresultmessage:

{ "type": "result", "id": "call_abc123", "content": "Hello, world!\nThis is a test file.", "metadata": {"encoding": "UTF-8", "size": 556} }

实操心得:McpToolResult.content必须是String,不能是byte[]或Path。Spring AI 2 的McpToolResultSerializer只认String。如果你要返回二进制(如图片),必须 base64 编码后塞进content,并在metadata里声明"content-type": "image/png;base64"。这是协议硬性规定,绕不开。

3.3 SSE Endpoint 的深度配置:解决stream disconnected before completion的根源

/mcp/sseendpoint 看似简单,但生产环境必调的三个参数,90% 的教程都漏掉:

1.spring.webflux.server.max-header-size
默认值是 8KB。当 client 发送一个带大量tool声明的初始请求(比如 agent 加载了 50 个 tools),header 可能超限,导致 connection reset。建议设为64KB:

spring: webflux: server: max-header-size: 65536

2.server.tomcat.connection-timeout(仅 Tomcat) orserver.reactive.max-idle-time(Netty)
这是stream disconnected before completion: idle timeout waiting for sse的罪魁祸首。Spring Boot 3.2 默认用 Netty,其max-idle-time默认是 30 秒。一旦 client(如浏览器)在 30 秒内没收到任何 event,Netty 就 close connection。解决方案:

server: reactive: max-idle-time: 300s # 5分钟,足够长

3.spring.webflux.server.response-buffer-size
SSE 的data:字段必须以\n\n结尾。如果McpMessage序列化后的 JSON 很长(比如listFiles返回上千个文件),Netty 的 buffer 可能截断,导致data:不完整,EventSource解析失败。增大 buffer:

spring: webflux: server: response-buffer-size: 65536

注意:这三个参数必须同时调整。我曾只调max-idle-time,结果EventSource收到半截 JSON,控制台报SyntaxError: Unexpected end of JSON input,debug 了两天才发现是 buffer 太小。

3.4 stdio 模式的实战技巧:如何用echo和jq构建自动化测试脚本

stdio模式不是玩具,它是 CI/CD 中验证 MCP Server 行为的黄金标准。以下是我每天用的测试流程:

Step 1:启动 server(后台静默)

nohup java -jar filesystem-mcp.jar --spring.profiles.active=stdio > /dev/null 2>&1 & SERVER_PID=$! sleep 2 # 等待 server 启动

Step 2:发送tool请求(获取可用工具列表)

echo '{"type":"tool"}' | java -jar filesystem-mcp.jar 2>/dev/null | jq -r '.name' # 输出:listFiles, readFile, writeFile, deleteFile, createDirectory

Step 3:发送call并验证result

# 创建测试文件 echo "test content" > /tmp/test_sse.txt # 调用 readFile CALL_JSON='{"type":"call","id":"test1","name":"readFile","arguments":{"path":"/tmp/test_sse.txt"}}' RESULT=$(echo "$CALL_JSON" | java -jar filesystem-mcp.jar 2>/dev/null) CONTENT=$(echo "$RESULT" | jq -r '.content') if [ "$CONTENT" = "test content" ]; then echo "✅ readFile test passed" else echo "❌ readFile test failed: $CONTENT" fi

关键技巧:

  • 2>/dev/null是必须的,因为StdioMcpServerHandler会把日志打到 stderr,不屏蔽会导致jq解析失败;
  • jq -r '.content'的-r参数输出 raw string,去掉引号,方便 shell 比较;
  • CALL_JSON必须是单行 JSON,不能有缩进或换行,否则LineBasedFrameDecoder会等下一行。

4. 实操过程与核心环节实现:从启动到真调的完整 walkthrough

4.1 第一步:创建 Spring Boot 项目并添加依赖

打开 start.spring.io ,选择:

  • Project:Maven
  • Language:Java
  • Spring Boot:3.2.5
  • Dependencies:Spring WebFlux(注意不是 Spring Web)

生成后,手动在pom.xml中添加 Spring AI 2 依赖(如前文 3.1 所示)。切记不要用 Spring Initializr 自带的 “Spring AI” 选项——它目前只提供旧版spring-ai-core,不包含 MCP 模块。

4.2 第二步:配置 application.yml,激活 filesystem MCP

# application.yml spring: profiles: active: filesystem # 激活 filesystem MCP Server ai: mcp: server: enabled: true # 同时启用 SSE 和 stdio sse: enabled: true path: /mcp/sse stdio: enabled: true # filesystem-specific config filesystem: mcp: allowed-paths: /tmp,/home/${USER}/mcp-test max-file-size: 1048576 # 可选:设置默认工作目录,避免每次 call 都要写绝对路径 default-directory: /tmp logging: level: org.springframework.ai.mcp: DEBUG com.example.filesystemmcp: DEBUG

spring.profiles.active=filesystem是关键。spring-ai-filesystem-mcp-server-spring-boot-starter的FilesystemMcpServerAutoConfiguration类,只有在这个 profile 下才会@ConditionalOnProperty("spring.profiles.active", havingValue = "filesystem")生效。

4.3 第三步:启动应用,验证 SSE endpoint 基础可用

./mvnw spring-boot:run

启动后,访问http://localhost:8080/mcp/sse。如果看到浏览器显示Loading...且 Network 面板里 status 200、Content-Type: text/event-stream,说明 SSE 通道已建立。此时还没有任何 event,因为 server 还没发送tool广播。

验证tool广播是否发出:
用 curl 模拟 client:

curl -H "Accept: text/event-stream" http://localhost:8080/mcp/sse

你应该立即看到类似输出:

event: tool data: {"type":"tool","name":"listFiles","description":"List files in a directory","inputSchema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}} event: tool data: {"type":"tool","name":"readFile","description":"Read the contents of a file at the given path","inputSchema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}} ...

每条event: tool后跟一个data:行,且以\n\n结尾。这是 SSE 标准格式。如果看不到,检查logging.level.org.springframework.ai.mcp是否为DEBUG,日志里会有Sending tool: readFile这样的 trace。

4.4 第四步:用 Postman 发起真实call,观察流式响应

Postman 8.12+ 原生支持 SSE。新建一个GET请求,URL 填http://localhost:8080/mcp/sse,在Headers里加:

  • Accept: text/event-stream
  • Cache-Control: no-cache

点击 Send,Postman 会保持连接,并实时显示收到的 events。

现在,我们需要让 server 发送call响应。但filesystem-mcp默认不会主动发call,它只响应 client 的call。所以我们得用另一个工具——curl发送call。

关键:SSE 是 server push,call必须由 client 发起,但 client 怎么发?
答案是:用POST到一个独立的 endpoint。Spring AI 2 的McpServer默认不提供POST /mcp/call,但我们可以轻松加一个:

@RestController public class McpCallController { private final McpServer mcpServer; public McpCallController(McpServer mcpServer) { this.mcpServer = mcpServer; } @PostMapping("/mcp/call") public ResponseEntity<String> handleCall(@RequestBody String callJson) { try { // 解析 JSON 为 McpMessage McpMessage callMessage = new ObjectMapper().readValue(callJson, McpMessage.class); // 调用 server 处理 McpMessage resultMessage = mcpServer.handle(callMessage).block(); // 返回纯 JSON,供 curl 直接消费 return ResponseEntity.ok(new ObjectMapper().writeValueAsString(resultMessage)); } catch (Exception e) { return ResponseEntity.status(400).body("{\"error\":\"" + e.getMessage() + "\"}"); } } }

然后用 curl 发送:

curl -X POST http://localhost:8080/mcp/call \ -H "Content-Type: application/json" \ -d '{"type":"call","id":"test2","name":"listFiles","arguments":{"path":"/tmp"}}'

你会得到一个resultmessage 的 JSON。把它复制,粘贴到 Postman 的 SSE tab 里(Postman 会自动识别并显示为 event)。这就是“真调”的起点:你控制 client 发什么,server 回什么,全程可见。

4.5 第五步:在浏览器中用 EventSource 实现实时渲染

新建一个index.html:

<!DOCTYPE html> <html> <head><title>Filesystem MCP Demo</title></head> <body> <h2>Filesystem MCP Server Demo</h2> <button onclick="listFiles()">List /tmp Files</button> <button onclick="readFile()">Read /tmp/test.txt</button> <div id="output"></div> <script> let eventSource = null; function connect() { if (eventSource) eventSource.close(); eventSource = new EventSource("http://localhost:8080/mcp/sse"); eventSource.onmessage = function(event) { document.getElementById('output').innerHTML += '<p>[message] ' + event.data + '</p>'; }; eventSource.addEventListener('tool', function(event) { document.getElementById('output').innerHTML += '<p>[tool] ' + event.data + '</p>'; }); eventSource.addEventListener('result', function(event) { const data = JSON.parse(event.data); document.getElementById('output').innerHTML += '<p>[result id=' + data.id + '] ' + (data.content ? data.content.substring(0, 100) + '...' : 'no content') + '</p>'; }); eventSource.onerror = function(err) { console.error("EventSource error:", err); document.getElementById('output').innerHTML += '<p style="color:red">[ERROR] Connection lost</p>'; }; } function listFiles() { // 发送 call 到我们的 /mcp/call endpoint fetch('/mcp/call', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ "type": "call", "id": "list_" + Date.now(), "name": "listFiles", "arguments": {"path": "/tmp"} }) }).then(r => r.json()).then(data => { console.log("Got result:", data); }); } function readFile() { fetch('/mcp/call', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ "type": "call", "id": "read_" + Date.now(), "name": "readFile", "arguments": {"path": "/tmp/test.txt"} }) }); } connect(); // 页面加载时自动连接 </script> </body> </html>

用python3 -m http.server 8000启动一个静态服务器,访问http://localhost:8000。点击按钮,你会看到output区域实时追加[result]内容——这就是“通过 SSE 流式输出实现大模型回答实时渲染”的最简实现。没有 React,没有 Vue,只有原生 JS 的EventSource。

实操心得:EventSource默认每 3 秒重连一次。如果你在onerror里看到readyState: 0,别急着 reload,等几秒它会自动恢复。这是浏览器的健壮性设计,不是 bug。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 问题速查表:高频报错与根因定位

报错信息可能根因排查命令/步骤解决方案
404 Not Foundfor/mcp/ssespring-boot-starter-webflux未引入,或@EnableWebFlux冲突curl -I http://localhost:8080/actuator/health看是否是 WebFlux 健康检查确保pom.xml只有spring-boot-starter-webflux,删除spring-boot-starter-web
stream disconnected before completion: idle timeout waiting for sseNettymax-idle-time过短curl -v http://localhost:8080/mcp/sse,观察 connection 是否在 30 秒后关闭在application.yml中设置server.reactive.max-idle-time: 300s
SyntaxError: Unexpected end of JSON inputin browser consoleresponse-buffer-size过小,JSON 被截断curl -H "Accept: text/event-stream" http://localhost:8080/mcp/sse | head -n 5,看data:行是否完整增大spring.webflux.server.response-buffer-size: 65536
java.nio.file.AccessDeniedExceptionfilesystem.mcp.allowed-paths未配置或路径不匹配ls -ld /tmp看权限,grep allowed-paths application.yml在application.yml中明确配置filesystem.mcp.allowed-paths: /tmp
EventSource收不到tool事件McpServer未正确初始化,或McpToolRegistry为空查看启动日志,搜索Registered 4 MCP tools确保spring.profiles.active=filesystem,且spring-ai-filesystem-mcp-server-spring-boot-starter在 classpath

5.2 独家避坑技巧:来自 37 次重启的经验

技巧 1:用curl -N替代浏览器测试 SSE
浏览器的EventSource有缓存和重连策略,有时会掩盖问题。curl -N http://localhost:8080/mcp/sse的-N参数禁用 buffering,能最真实地看到 server 发送的原始 bytes。如果curl -N能收到tool,但浏览器收不到,100% 是浏览器 CORS 或 cache 问题。

技巧 2:McpMessage的id字段必须全局唯一,且不能重复使用
call.id和result.id必须严格匹配。我曾把call.id写死为"1",连续点击两次按钮,server 收到两个id="1"的call,但只返回一个result.id="1",导致第二个 client 永远等不到响应。正确做法:id: "call_" + UUID.randomUUID().toString()。

技巧 3:stdion模式下,System.in的 EOF 会 kill server
StdioMcpServerHandler在读到EOF(Ctrl+D)时,会认为 client 退出,从而 shutdown server。这在脚本中很危险。解决方案:用cat命令代替echo,并用&后台运行:

# 错误:echo 会发送 EOF echo '{"type":"tool"}' | java -jar app.jar # 正确:cat 不会轻易 EOF,且可管道多条 { echo '{"type":"tool"}'; echo '{"type":"call","id":"1","name":"listFiles","arguments":{"path":"/tmp"}}'; } | java -jar app.jar

技巧 4:McpTool的inputSchema必须是 valid JSON Schema,不能是简化版
"properties": {"path": "string"}是错的,必须是"properties": {"path": {"type": "string"}}。Spring AI 2 的JsonSchemaValidator会严格校验。错误的 schema 会导致call被静默拒绝,server 日志里只有WARN,没有 ERROR。用 https://json-schema-validator.herokuapp.com 提前验证。

5.3 进阶调试:用 Wireshark 抓包分析 SSE 流

当一切看起来都对,但EventSource就是不触发onmessage,可能是底层 TCP 问题。用 Wireshark 抓包:

  1. 启动 Wireshark,过滤tcp.port == 8080;
  2. 在浏览器打开http://localhost:8080/mcp/sse;
  3. 观察 TCP stream,确认 server 是否真的发送了event: tool\n\ndata: {...}\n\n;
  4. 关键看:data:行后是否有两个\n(即\n\n),以及Content-Typeheader 是否为text/event-stream。

我遇到过一次:Nginx 反向代理配置了proxy_buffering on,它把多个data:行合并成一个大 chunk 发送给浏览器,导致EventSource无法按行解析。解决方案:proxy_buffering off;。

5.4 生产就绪 checklist:从 demo 到上线的 5 个动作

  1. HTTPS 强制:SSE 在 HTTP 下会被现代浏览器降级或阻止。用server.ssl.*配置 keystore,或前置 Nginx 做 TLS termination;
  2. CORS 配置:spring.webflux.cors.allowed-origins=https://your-frontend.com,避免EventSource被跨域拦截;
  3. Rate Limiting:用Resilience4j或 Spring Cloud Gateway 限制/mcp/sse的连接数,防 DDOS;
  4. Health Check Endpoint:暴露/actuator/mcp,返回{"status":"UP","tools":["listFiles","readFile"]},供 k8s liveness probe 使用;
  5. Structured Logging:用logstash-logback-encoder,字段包括mcp.message.type、mcp.message.id、mcp.tool.name、duration_ms,便于 APM 追踪。

我个人在实际操作中的体会是:filesystem MCP Server 的价值,从来不在它能做什么(读写文件),而在于它是一面镜子,照出你对整个 MCP 协议

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

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

立即咨询