FileUploadServer 开发记录:MCP 接口、分布式部署与踩坑(2026-08-02)
FileUploadServer 开发记录(2026-08-02)
记录范围:MCP 接口开发、分布式部署(网关 + WS 节点)、公开访问问题排查与屏蔽、部署流程优化
涉及版本:b26d430(feat: MCP接口开发 + 网关更新(屏蔽公开访问)+ 部署skill优化)
一、概述
本次开发围绕三件事:
- 为文件服务器实现 MCP 接口(
FileUploadServer.Mcp),把上传/下载/删除/列表/详情/公开设置暴露为 AI 可调用的 6 个工具 - 分布式部署:网关(Web)更新到最新代码、清理 WS 存储节点重复进程
- 公开访问问题排查:定位
/p/路径 503 根因,最终决策屏蔽公开访问并标注问题待整改
二、MCP 接口开发
2.1 需求与方案
依据 doc/MCP-Development-Guide.md 实现。关键决策(与用户确认):
| 决策点 | 选择 | 理由 |
|---|---|---|
| 协议实现 | 手写 JSON-RPC 2.0 over stdio | 零第三方依赖、完全掌控自定义错误码(-32602/-32xxx)、与项目手写 WS 协议的风格一致 |
| 鉴权 | 启动时环境变量注入 Master Admin Key | 密钥不进入 LLM 上下文,安全 |
| 超时/重试 | 上传/下载 300s,其他 30s;5xx 指数退避重试 2 次 | 符合文档 §3 |
| 错误码 | 400/404→-32602、401/403→-32003、429→-32004、503→-32005、超时→-32000、连接失败→-32001 | 文档 §5 |
2.2 实现结构
FileUploadServer.Mcp/
├── Program.cs # stdio 主循环、配置加载、优雅退出
├── McpServerConfig.cs # 配置(BaseUrl / MasterKey / 超时 / 重试)
├── McpLogger.cs # 结构化日志(写 stderr,不污染 stdout)
├── Protocol/ # JsonRpcRequest/Response/Error、McpError、StdioTransport
├── Server/ # McpServer(生命周期状态机)、ToolDefinition(6工具定义)
├── Services/ # McpHttpClient(鉴权/超时/重试)、FileToolHandlers、ErrorMapper
└── Models/ # FileItemDto、ApiKeyDto
核心设计:
- 双层错误模型:参数校验失败 → JSON-RPC error(-32602);下游 HTTP 失败 → CallToolResult
isError:true+ text 含错误码 - tools/call 永远返回 CallToolResult(content[].text + isError),业务错误不抛 JSON-RPC error
- 状态机:
initialize → notifications/initialized门控,未初始化返回 -32002 - 可测试性:
McpServer.HandleAsync(JsonRpcRequest)可被测试直接驱动,不依赖真实 stdio
2.3 6 个工具
| 工具 | HTTP 端点 | 校验 |
|---|---|---|
file_list |
GET /api/files | — |
file_info |
GET /api/files/ | file_id 必填 int |
file_upload |
POST /api/files(multipart) | local_file_path 必填、文件存在 |
file_download |
GET /api/files/download/ | file_id 必填 int,Base64 返回 |
file_delete |
DELETE /api/files/ | file_id 必填 int |
file_set_public |
PUT /api/admin/files//public | is_public=true 时必须 public_path |
2.4 测试
FileUploadServer.Tests/Mcp/ 下 12 个测试文件 + 3 个助手,52 个测试全部通过,覆盖文档 §8 全部用例(LIFE/LIST/FL/FI/UP/DL/DEL/PUB/AUTH/RETRY/ERR/E2E)。
三、部署记录
3.1 网关更新(111.229.53.125:7000)
- 原部署是 7-11 版本(自包含
./FileUploadServer.Web),Web 项目有一批未提交改动未上线 - 流程:备份配置 → 停旧进程 → 上传 → 恢复配置 → 启动 → 验证
- 关键:
Storage.Mode: WebSocket保持不变,encryption.key绝不能覆盖
3.2 WS 存储节点清理(192.168.1.4)
- 发现 3 个同 client-id 的重复 WsClient 进程(7-11 当天先后启动)→ 全部清理为单实例
- 发现 WS 认证长期失败:旧 client-secret 的 SHA256 与数据库
ClientSecretHash不匹配 → 通过regenerate-secretAPI 生成新密钥 - 单实例 + 正确密钥后稳定连接(
Connected successfully)
3.3 MCP Server 接入
- MCP Server 走 stdio,运行在 Claude Code 本机,不部署到远程
- 配置
.mcp.json:FILE_SERVER_BASE_URL=http://111.229.53.125:7000+ Admin 密钥
四、踩坑汇总(操作踩坑)
4.1 MCP 开发踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| 静态初始化顺序 | ToolDefinitions.All 声明在工具定义前,捕获到 null |
All 移到 6 个定义之后(C# 静态字段按文本顺序初始化) |
| 静态 JsonObject 共享 | 重复序列化报 "The node already has a parent" | ToJson() 中 InputSchema.DeepClone() |
| JsonNode.TryGetValue | TryGetValue<T> 只存在于 JsonValue,JsonNode 调用报错 |
先 node is JsonValue value 再调用 |
| 原始插值字符串 | $$""" 与 JSON 大括号冲突(CS9007) |
改用普通字符串 + 转义拼接 |
| .NET 10 multipart | Content-Disposition: form-data; name=path 不带引号 |
断言改为 name=path |
| 文档自相矛盾 | RETRY-02 期望 503→-32001,ERR-05 期望 503→-32005 | 采用语义正确的 -32005 |
| HttpUtility 不可用 | HttpUtility.ParseQueryString 属 System.Web,.NET Core 无 |
URL 字符串拼接 |
4.2 部署踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| pkill -f 误杀 SSH 会话 | pkill -f 'FileUploadServer.Web' 匹配到 ssh 命令行自身 → 连接断开(exit 255) |
用 pgrep -f '\./FileUploadServer\.Web' 取精确 PID 再 kill |
| text file busy | 运行中的可执行文件无法覆盖(scp 失败) | 先停进程再上传 |
| nohup 挂起 ssh | nohup 启动的子进程持有 ssh 管道 → 命令超时挂起 | 启动命令与验证命令分离执行 |
| WS secret 不匹配 | 认证 token 计算:服务端用 ClientSecretHash,客户端先 SHA256(secret)——必须传数据库哈希值 |
实际是旧 secret 哈希与库不符,用 regenerate-secret 解决 |
| 多进程争抢 | 3 个 WsClient 同 id 反复 Disconnected: Connection lost |
清理为单实例 |
| sshpass 不可用 | 密码认证无 sshpass | SSH_ASKPASS + setsid 方式 |
| 沙箱管道丢环境变量 | Bash 工具管道中内联环境变量丢失 | 用输入重定向 < file 代替管道 |
4.3 公开访问排查踩坑
| 坑 | 现象 | 解决 |
|---|---|---|
| StartsWithSegments("/p/") 不匹配 | PathString.StartsWithSegments("/p/") 对 /p/public/... 返回 False(实测) |
定位到 ApiKeyAuthMiddleware:27 的既有 bug,被 PublicFileMiddleware 屏蔽前掩盖 |
| 老文件 tag mismatch | 新文件公开访问解密成功,老文件(7-11)解密失败 | 老密文与当前密钥不匹配(数据层问题),改代码无法修复,只能重传 |
| 本地解密验证 vs 网关结果矛盾 | 本地用密钥文件解密失败,网关公开访问新文件却成功 | 最终确认网关用密钥文件,本地验证方法正确;老文件确实无法解密 |
五、总结归纳
5.1 架构认知修正
- 本项目的"前后端分离"是**网关(Web)+ WS 存储节点(WsClient)**的分布式架构,不是传统 Web 前后端
- 访问路径只有两条:API 请求(
FileApiController)和公开访问(/p/,本应只读本地文件) PublicFileMiddleware的 WS 直接读取(Step 8.5)是未提交的新增改动,违背"访问统一走 API"的分层架构,且对加密文件解密失败——本次已屏蔽
5.2 设计原则沉淀
- 分层:中间件不绕过 API 层直接操作存储策略
- 不改好的部分:API 下载返回密文(前端解密)是既有设计,未被破坏
- 数据 vs 代码问题:老密文无法解密是数据问题,改代码无效,需重传
- 部署标准化:部署前 git 提交推送 + 敏感信息检查 + 部署后清理(已写入 skill)
六、后续计划
6.1 公开访问整改(高优先)
- 统一封装:公开访问走
FileApiController.Download的共享封装(或公开文件限定本地磁盘) - 修复
ApiKeyAuthMiddleware:27bug:StartsWithSegments("/p/")→StartsWithSegments("/p")或Path.StartsWith("/p/") - 重新启用
PublicFileMiddleware(修复后),删除其 Step 8.5 或改为走 API 封装
6.2 老文件恢复
- p.txt/d.txt/fresh.txt/Markdown入门.md 等老文件密文无法解密 → 如需恢复,由用户提供原文件重新上传
6.3 MCP 增强
- 实现
resources/read(暴露文件元数据为资源,约 20 行) - 可选:编写 OpenAPI → tools/list 的自动生成脚本
6.4 运维
- 网关配置备份(.bak)确认稳定后清理
- 定期检查 WS 节点单实例状态(多进程会导致断连)
- 部署 skill 随实践持续完善
附:本次关键产物
FileUploadServer.Mcp/— MCP Server(手写 JSON-RPC 2.0 over stdio)FileUploadServer.Tests/Mcp/— 52 个单元测试.claude/skills/deploy-file-upload-server/SKILL.md— 优化后的部署 skill(git 提交推送 + 敏感信息检查 + 部署后清理).claude/skills/file-upload-server-mcp/SKILL.md— MCP 接入 skilldoc/MCP-Development-Guide.md、doc/MCPdoc.md— MCP 设计文档