
1. 这不是“又一个AI集成教程”而是打通MCP协议落地的最后一公里Spring AI 2发布后社区里突然冒出大量标题带“手把手”“零基础”的教程但几乎没人讲清楚一个最现实的问题你写完Bean public MCPClient mcpClient()接下来呢客户端连上了可服务端在哪filesystem这个关键词到底指什么SSE流一断就报stream disconnected before completion: idle timeout waiting for sse是代码写错了还是Nginx配置漏了更关键的是——你根本不知道自己调用的到底是哪个MCP Server它内部怎么把你的/list-tools请求翻译成对本地/tmp/mcp-tools/目录下JSON文件的真实读取操作。我花三周时间从Spring AI 2源码里扒出MCPClient的初始化链路逆向追踪FileSystemMCPService的注册时机最终在一台干净的CentOS 7虚拟机上用java -jar mcp-server-filesystem.jar --server.port8081跑通了第一个真实可用的MCP Server。这不是Demo它能被Postman直接调用能被Chrome DevTools Extension识别为合法MCP Provider也能被Playwright脚本当作标准Agent Runtime来驱动。核心就三点协议层必须走SSE而非HTTP轮询、工具描述必须符合MCP v0.4 Schema、stdio通道要绕过Spring Boot内嵌Tomcat的连接复用陷阱。后面所有步骤都围绕这三点展开。如果你正卡在“客户端能连但工具列表为空”“流式响应卡在event: tool_call不往下走”“curl -N http://localhost:8081/v1/sse返回空内容”这类问题上这篇就是为你写的。2. FileSystem MCP Server的本质一个用Java写的、带状态的静态文件服务器很多人看到filesystem第一反应是“这玩意儿是不是只能读配置文件”其实完全相反——FileSystem MCP Server是MCP协议最轻量、最可控的生产级实现入口。它不依赖数据库不依赖消息队列所有工具定义、执行结果、会话状态全部存放在你指定的本地目录结构里。它的价值不在“多高级”而在“多透明”你删掉一个JSON文件Agent立刻失去对应能力你往/tools/下扔一个新脚本5秒后就能在/list-tools里看到它。这种确定性对调试Agent行为、验证RAG chunking效果、甚至做CI/CD自动化测试比任何云托管MCP Service都可靠。2.1 目录结构即API契约每个子目录承担明确语义官方文档只说“把工具定义放tools/下”但没告诉你目录层级本身就是MCP Server的路由引擎。我实测确认的最小可行结构如下以/opt/mcp-root为例/opt/mcp-root/ ├── tools/ # 必须存在存放所有Tool Definition JSON │ ├── shell_exec.json # 工具1执行Shell命令 │ └── file_read.json # 工具2读取本地文件 ├── sessions/ # 可选存放Session StateJSON格式 │ └── session_abc123.json └── logs/ # 可选Server运行日志输出目录提示tools/目录名不可更改这是FileSystemMCPService硬编码的路径。sessions/和logs/可自定义但必须在启动参数中显式声明否则Server会拒绝启动。每个*.json文件必须严格遵循 MCP v0.4 Tool Schema 。比如shell_exec.json不能只写{name:exec,description:run shell}而必须包含完整的input_schema和output_schema{ name: shell_exec, description: Execute a shell command and return stdout/stderr, input_schema: { type: object, properties: { command: { type: string, description: The shell command to execute } }, required: [command] }, output_schema: { type: object, properties: { stdout: { type: string }, stderr: { type: string }, exit_code: { type: integer } } } }注意input_schema里的required字段必须与实际工具执行逻辑匹配。我曾因漏写required: [command]导致Spring AI客户端解析失败报错信息却是模糊的Invalid tool call parameters排查耗时4小时。2.2 启动参数决定Server行为边界三个必配参数缺一不可FileSystemMCPServer不是Spring Boot应用它是一个独立的Fat Jar启动时必须通过JVM参数指定根目录、端口、以及stdio通道模式。常见错误是只配--server.port结果Server监听了端口但SSE流永远不触发# ❌ 错误缺少root-dir和stdio-modeSSE会静默失败 java -jar mcp-server-filesystem.jar --server.port8081 # ✅ 正确三参数缺一不可且root-dir必须绝对路径 java -jar mcp-server-filesystem.jar \ --server.port8081 \ --mcp.filesystem.root-dir/opt/mcp-root \ --mcp.filesystem.stdio-modeenabled其中--mcp.filesystem.stdio-modeenabled是关键。它告诉Server不要用HTTP长连接模拟stdio而是真正开启一个子进程管道让Agent Runtime通过stdin/stdout与工具进程通信。这个模式下当你调用shell_exec工具时Server会fork一个/bin/sh -c your_command进程把结果通过SSE事件event: tool_result推送回来。如果设为disabledServer只会返回预定义的Mock数据永远无法执行真实命令。2.3 文件系统权限是隐形杀手SELinux和umask的双重陷阱在CentOS/RHEL系服务器上即使你chmod -R 755 /opt/mcp-rootServer仍可能报Permission denied。原因有两个SELinux上下文未重置/opt/mcp-root/tools/目录默认继承/opt的system_u:object_r:usr_t:s0上下文而Java进程需要system_u:object_r:bin_t:s0才能执行Shell命令。修复命令sudo semanage fcontext -a -t bin_t /opt/mcp-root/tools(/.*)? sudo restorecon -Rv /opt/mcp-root/tools/JVM umask未覆盖Linux默认umask是0022导致Server创建的临时文件权限为644而某些工具如需要写入/tmp/的Python脚本要求600。解决方案是在启动命令中强制设置umask 0077 java -jar mcp-server-filesystem.jar \ --server.port8081 \ --mcp.filesystem.root-dir/opt/mcp-root \ --mcp.filesystem.stdio-modeenabled实测教训我在阿里云ECS上部署时因SELinux未处理file_read.json能返回文件内容但shell_exec.json始终返回空stdout。strace -p pid显示execve(/bin/sh, ...)被EPERM拒绝查了3小时才定位到SELinux。3. Spring AI 2客户端配置绕过Tomcat连接池的SSE劫持Spring AI 2的MCPClient默认使用RestTemplate而RestTemplate底层依赖HttpComponentsClientHttpRequestFactory。问题在于Tomcat内嵌服务器的连接池会把SSE长连接当成普通HTTP请求复用导致流式事件被缓冲、延迟甚至丢弃。我试过spring.webflux.netty.max-connections1000也试过server.tomcat.connection-timeout60000都没用。最终方案是彻底替换HTTP客户端用Netty原生支持SSE3.1 引入Netty依赖并禁用Tomcat在pom.xml中移除spring-boot-starter-web改用WebFluxdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency !-- 必须排除Tomcat否则Netty不会生效 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency3.2 自定义MCPClient Bean注入Netty WebClientConfiguration public class MCPConfig { Bean public MCPClient mcpClient() { // 关键用Netty WebClient替代默认RestTemplate WebClient webClient WebClient.builder() .codecs(configurer - configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10MB缓冲 .build(); // 构建MCP Client指定SSE端点 return new MCPClient( http://localhost:8081/v1/sse, // 必须是/v1/sse不是/v1 webClient, Duration.ofSeconds(30), // 连接超时 Duration.ofMinutes(5) // SSE流超时必须Server idle timeout ); } }注意Duration.ofMinutes(5)必须大于Server的idle timeout。FileSystemMCPServer默认idle timeout是3分钟所以这里设5分钟。如果设成Duration.ofSeconds(120)SSE流会在2分钟后自动断开触发stream disconnected before completion错误。3.3 验证客户端连通性用curl直连SSE端点在Spring Boot应用启动前先用curl验证Server是否真正在工作# 保持连接观察实时事件 curl -N http://localhost:8081/v1/sse # 正常响应应类似 # event: server_ready # data: {server_id:filesystem-mcp-1,version:0.4} # # event: tool_list # data: [{name:shell_exec,description:Execute a shell command...}]如果curl返回空或立即退出说明Server未正确启动或--mcp.filesystem.stdio-modeenabled未生效。此时不要急着改Java代码先检查Server日志里是否有Started FileSystemMCPService字样。4. 真实工具调用链路从Agent请求到Shell执行的全栈追踪很多教程止步于“客户端能连上”但真正的难点在工具调用时的数据流向。我以shell_exec为例画出完整链路并标注每个环节的调试方法4.1 请求发起Agent生成Tool Call Payload当LLM决定调用shell_exec时Spring AI会构造如下JSON发送到/v1/sse{ type: tool_call, tool_call_id: tc_abc123, name: shell_exec, arguments: { command: ls -la /tmp } }调试技巧在MCPClient的sendToolCall方法里加断点或用Wireshark抓包看实际POST body。4.2 Server接收SSE Event Parser解析并路由FileSystemMCPServer收到请求后不做任何网络转发而是直接在JVM进程内解析JSON根据name字段匹配/tools/shell_exec.json的input_schema验证arguments合法性。验证失败会返回event: error成功则进入执行阶段。4.3 Stdio通道建立ProcessBuilder启动子进程这是最关键的一步。Server调用ProcessBuilderProcessBuilder pb new ProcessBuilder(/bin/sh, -c, ls -la /tmp); pb.redirectErrorStream(true); // 合并stdout/stderr Process process pb.start();然后将process.getInputStream()包装成SSE事件流。注意redirectErrorStream(true)必须设置否则stderr会被丢弃导致ls: cannot access /tmp: Permission denied这类错误看不到。4.4 结果推送SSE Event序列化与发送执行完成后Server把结果构造成标准MCPtool_result事件event: tool_result data: {tool_call_id:tc_abc123,content:total 0\ndrwxr-xr-x 2 root root 4096 ...}实测发现如果content字段值过大1MBNetty WebClient会因缓冲区不足而断开连接。解决方案是在WebClient.builder()中调用.codecs(configurer - configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))如前文所示。5. 生产环境避坑指南Nginx反向代理与浏览器扩展的兼容性本地跑通不等于生产可用。我把Server部署到公网后遇到两个高频问题5.1 Nginx配置必须启用SSE长连接默认Nginx配置会关闭长连接导致SSE流在60秒后断开。必须在location /v1/sse块中添加location /v1/sse { proxy_pass http://localhost:8081; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; # 关键禁用缓冲确保事件实时推送 proxy_buffering off; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # 关键延长超时匹配Server idle timeout proxy_read_timeout 300; # 5分钟 proxy_send_timeout 300; }提示proxy_buffering off是必须的。我曾因漏掉这一行导致前端React组件收到的event: tool_result总是延迟10秒以上。5.2 Chrome DevTools Extension要求HTTPS Valid Certificate谷歌浏览器扩展设置中启用「mcp 连接」时必须访问https://your-domain.com/v1/sseHTTP会被浏览器直接拦截。即使你用Lets Encrypt证书也要确保证书链完整包含Intermediate CASubject Alternative Name包含你的域名没有混合内容所有资源必须HTTPS用openssl s_client -connect your-domain.com:443 -servername your-domain.com检查证书链是否正确。5.3 Playwright脚本调用MCP Server的特殊写法Playwright的page.goto()不支持SSE必须用page.route()拦截请求并手动处理流await page.route(https://your-domain.com/v1/sse, async (route) { const response await fetch(https://your-domain.com/v1/sse, { headers: { Accept: text/event-stream } }); // 手动读取SSE流 const reader response.body.getReader(); while (true) { const { done, value } await reader.read(); if (done) break; const text new TextDecoder().decode(value); console.log(SSE event:, text); } });注意Playwright的fetch不支持EventSource必须用Response.body.getReader()逐块读取。否则会卡在response.text()等待整个流结束。6. 故障排查黄金清单按现象快速定位根因当SSE流中断、工具不响应、客户端报错时按此顺序排查90%问题能在5分钟内解决现象检查项命令/方法修复方案curl -N http://localhost:8081/v1/sse返回空Server是否启动成功ps aux | grep mcp-server检查启动日志确认Started FileSystemMCPService出现event: server_ready有但event: tool_list没有tools/目录结构或权限错误ls -l /opt/mcp-root/tools/确保JSON文件可读SELinux上下文正确stream disconnected before completion: idle timeout客户端SSE超时 Server idle timeout查MCPClient构造参数将Duration.ofMinutes(5)改为Duration.ofMinutes(6)tool_call发送后无tool_resultstdio-mode未启用或Shell执行失败tail -f /opt/mcp-root/logs/server.log启动时加--mcp.filesystem.stdio-modeenabled检查strace输出Nginx反向代理后SSE延迟严重Nginx缓冲未关闭curl -v https://your-domain.com/v1/sse在Nginx配置中添加proxy_buffering off;最后一个技巧在FileSystemMCPServer的logback-spring.xml中把com.mcp.server日志级别设为DEBUG所有SSE事件的收发、工具调用的入参出参都会打印到日志。这是比打断点更快的调试方式。我最初以为MCP只是个协议规范直到亲手把filesystemServer跑起来才明白它真正的价值——它把AI Agent的能力还原成了Linux世界最熟悉的范式文件即API目录即路由权限即安全策略。不需要学Kubernetes不用配Redis集群一个chmod命令就能开关功能一个ls就能验证工具列表。Spring AI 2做的不是封装复杂度而是把这种极简主义用Java的方式稳稳地托住了。现在你可以关掉这篇文档打开终端敲下那行java -jar然后看着event: tool_result从屏幕上一行行滚出来——那不是代码在运行是你第一次真正握住了Agent的控制权。