# FileUploadServer 开发记录：MCP 接口、分布式部署与踩坑（2026-08-02）

> 发布时间: 2026-08-02 16:20

# FileUploadServer 开发记录（2026-08-02）

**记录范围**：MCP 接口开发、分布式部署（网关 + WS 节点）、公开访问问题排查与屏蔽、部署流程优化
**涉及版本**：`b26d430`（feat: MCP接口开发 + 网关更新（屏蔽公开访问）+ 部署skill优化）

---

## 一、概述

本次开发围绕三件事：
1. **为文件服务器实现 MCP 接口**（`FileUploadServer.Mcp`），把上传/下载/删除/列表/详情/公开设置暴露为 AI 可调用的 6 个工具
2. **分布式部署**：网关（Web）更新到最新代码、清理 WS 存储节点重复进程
3. **公开访问问题排查**：定位 `/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/{id} | file_id 必填 int |
| `file_upload` | POST /api/files（multipart） | local_file_path 必填、文件存在 |
| `file_download` | GET /api/files/download/{id} | file_id 必填 int，Base64 返回 |
| `file_delete` | DELETE /api/files/{id} | file_id 必填 int |
| `file_set_public` | PUT /api/admin/files/{id}/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-secret` API 生成新密钥
- 单实例 + 正确密钥后稳定连接（`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 设计原则沉淀

1. **分层**：中间件不绕过 API 层直接操作存储策略
2. **不改好的部分**：API 下载返回密文（前端解密）是既有设计，未被破坏
3. **数据 vs 代码问题**：老密文无法解密是数据问题，改代码无效，需重传
4. **部署标准化**：部署前 git 提交推送 + 敏感信息检查 + 部署后清理（已写入 skill）

---

## 六、后续计划

### 6.1 公开访问整改（高优先）

1. **统一封装**：公开访问走 `FileApiController.Download` 的共享封装（或公开文件限定本地磁盘）
2. **修复 `ApiKeyAuthMiddleware:27` bug**：`StartsWithSegments("/p/")` → `StartsWithSegments("/p")` 或 `Path.StartsWith("/p/")`
3. **重新启用 `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 接入 skill
- `doc/MCP-Development-Guide.md`、`doc/MCPdoc.md` — MCP 设计文档
