mcp-read-file-server 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  **v1.9.0 可回滚写入与目录级策略**:全部文本修改改为「完整载荷 → 独占暂存 → 独立指纹校验 → 原文件备份 → 提交 → 最终校验」事务流程,失败自动回滚,回滚失败保留 `recoveryPath`;加密策略按「目标目录 × 扩展名」实时探测,人工标注(`mark_extension`)独立持久化到 `.mcp-file-policies/`,刷新探测/重启/TTL 过期均不丢失;safeWrite 失败不再回退直写(保护原文);复制/移动目录逐文件执行相同策略;所有工具返回统一 `structuredContent`(ok/code/changed/data/warnings)。
10
10
 
11
+ **v1.9.1 边界修复**:目录复制/移动拒绝双向祖先重叠,避免覆盖未读取的源文件;回滚失败和部分删源正确保留 `changed` / `partial`;行分页无法容纳首行时明确报错,到达请求结束行即停止解析,避免被范围外长行影响。Hono 间接依赖更新至 4.13.5。
12
+
11
13
  ## 适用场景
12
14
 
13
15
  电脑安装了文件加密软件(如天锐绿盾、IP-Guard、亿赛通、深信服等),磁盘上的文件是密文。AI Agent(Claude Code、Cursor、Windsurf、Cline 等)是独立进程,内置文件工具不在白名单内,只能读到密文。而 Node.js 进程在白名单内,通过 MCP Server 提供的替代工具可以正常读写明文。
@@ -115,7 +117,7 @@ write_file / edit_file / copy_path / move_path 均支持 `writePolicy` 参数:
115
117
  - **safeWrite 失败即中止**:不再回退直写破坏原文(SAFE_WRITE_FAILED,changed=false)
116
118
  - **跨实例锁**:同一路径的并发写入经 `.mcp-file-locks/` 互斥(等待 5 秒超时 FILE_BUSY);`expectedHash` 可检测其他编辑器造成的版本变化(CONFLICT)
117
119
  - **断电/强杀残留**:两次 rename 之间的极端崩溃可能留下 `.mcp-backup-*` 与 `.mcp-stage-*`,先核对内容与时间再人工恢复,禁止直接批量清理
118
- - **复制/移动目录**:逐文件执行相同策略;移动先复制并二次比对指纹后再删除已验证的源文件;失败返回 `partial` 与 `sourceRetained`,不静默回退;符号链接明确拒绝;递归目标(目标在源内)明确拒绝
120
+ - **复制/移动目录**:逐文件执行相同策略;移动先复制并二次比对指纹后再删除已验证的源文件;失败返回 `partial` 与 `sourceRetained`,不静默回退;符号链接明确拒绝;源和最终目标存在任一方向的祖先关系时拒绝,相同路径保持不变。回滚失败时 `changed=true`,`partial` 包含当前失败目标,原备份通过 `recoveryPath` 返回
119
121
  - **diskState 三态**:`plaintext`(独立进程验证磁盘明文)、`preserved`(保持加密直写)、`unknown`(内容已校验但无独立读取器证明磁盘状态——不能当作「已证明明文」)
120
122
 
121
123
  ## 文件结构
@@ -322,7 +324,7 @@ claude mcp list
322
324
  - 读取第 10 行:`mode="lines"`, `startLine=10`
323
325
  - 读取第 5-20 行:`mode="lines"`, `startLine=5`, `endLine=20`
324
326
 
325
- > 返回内容会带文件名、读取范围、总行数的头部信息,行模式下每行带行号前缀。超出文件范围时自动截断并提示;行模式流式扫描返回 `nextLine` 供续页。
327
+ > 行模式文本带行号,结构化结果含 `lines`、`nextLine`、`truncated`、`totalLines`。到达请求结束行即停止;未扫描到 EOF 时 `totalLines=null`,`nextLine` 是待确认的续读起点,可能已超过 EOF(下一次调用会明确报 `LINE_OUT_OF_RANGE`)。首行加上行号开销超过页预算时返回 `LINE_TOO_LONG`,请改用字符分页,不会返回游标不前进的成功空页。
326
328
 
327
329
  ### `edit_file` 换行符自动兼容
328
330
 
package/lib/files.js CHANGED
@@ -140,7 +140,8 @@ function createFiles(encryption, options = {}) {
140
140
  error.changed = false;
141
141
  } catch (rollbackError) {
142
142
  retainBackup = backedUp;
143
- error.changed = installed;
143
+ // 即使新文件尚未安装,原目标已经移入备份也属于状态改变。
144
+ error.changed = installed || backedUp;
144
145
  error.recoveryPath = backedUp ? backup : null;
145
146
  error.rollbackError = rollbackError.message;
146
147
  }
@@ -178,6 +179,7 @@ function createFiles(encryption, options = {}) {
178
179
  destination = await resolve(destination, { write: true });
179
180
  if (pathKey(source) === pathKey(destination)) return { changed: false, sameFile: true, source, destination };
180
181
  if (sourceStat.isDirectory() && inside(source, destination)) throw fault('RECURSIVE_TARGET', '目标不能位于源目录内部');
182
+ if (sourceStat.isDirectory() && inside(destination, source)) throw fault('OVERLAPPING_PATHS', '最终目标不能是源目录的祖先;请先复制到独立目录');
181
183
  return locked([source, destination], async () => {
182
184
  const completed = [];
183
185
  const createdDirectories = [];
@@ -198,7 +200,13 @@ function createFiles(encryption, options = {}) {
198
200
  if (!old) { await fs.mkdir(dst, { recursive: true }); createdDirectories.push(dst); }
199
201
  for (const entry of await fs.readdir(src)) await visit(path.join(src, entry), path.join(dst, entry));
200
202
  } else if (st.isFile()) {
201
- const result = await copyOne(src, dst, settings);
203
+ let result;
204
+ try { result = await copyOne(src, dst, settings); }
205
+ catch (error) {
206
+ // 当前失败文件尚未加入completed;保留它的恢复状态和实际目标路径。
207
+ if (error.changed || error.recoveryPath) error.partial = [...(error.partial || []), dst];
208
+ throw error;
209
+ }
202
210
  if (move && result.sameFile) throw fault('SAME_FILE_ALIAS', '源与目标为同一文件的别名,拒绝删源');
203
211
  completed.push({ source: src, destination: dst, ...result });
204
212
  } else throw fault('SPECIAL_FILE', '不支持复制特殊设备文件');
@@ -221,8 +229,8 @@ function createFiles(encryption, options = {}) {
221
229
  }
222
230
  return { changed: move || completed.some(x => x.changed) || createdDirectories.length > 0, source, destination, files: completed.length, moved: move, warnings: completed.flatMap(x => x.warnings || []).slice(0, 10) };
223
231
  } catch (error) {
224
- error.changed = completed.some(x => x.changed) || createdDirectories.length > 0;
225
- error.partial = completed.map(x => x.destination).slice(0, 100);
232
+ error.changed = !!error.changed || removedSources > 0 || completed.some(x => x.changed) || createdDirectories.length > 0;
233
+ error.partial = [...new Set([...(error.partial || []), ...completed.map(x => x.destination)])].slice(0, 100);
226
234
  error.sourceRetained = removedSources === 0;
227
235
  throw error;
228
236
  }
package/lib/text.js CHANGED
@@ -117,7 +117,7 @@ async function* textLines(file, options = {}) {
117
117
  else if (pending) yield pending;
118
118
  }
119
119
 
120
- /** 按行分页,达到输出预算即停,不为统计总行数读取整文件。 */
120
+ /** 按行分页,到达请求结束行即停;未探测EOF时nextLine为待确认的续读位置。 */
121
121
  async function readLines(file, start, end, options = {}) {
122
122
  const lines = [];
123
123
  let lineNo = 0;
@@ -126,9 +126,15 @@ async function readLines(file, start, end, options = {}) {
126
126
  for await (const line of textLines(file, options)) {
127
127
  lineNo++;
128
128
  if (lineNo < start) continue;
129
- if (lineNo > end || chars + line.length + 16 > READ_LIMIT) { nextLine = lineNo; break; }
129
+ if (chars + line.length + 16 > READ_LIMIT) {
130
+ if (!lines.length) throw fault('LINE_TOO_LONG', '该行加上行号后超过单页预算,请使用字符分页');
131
+ nextLine = lineNo;
132
+ break;
133
+ }
130
134
  lines.push({ line: lineNo, text: line });
131
135
  chars += line.length + 16;
136
+ // 不请求迭代器生成下一整行,否则范围外的长行也会让当前页失败。
137
+ if (lineNo === end) { nextLine = lineNo + 1; break; }
132
138
  }
133
139
  if (lineNo === 0 && start === 1) { lines.push({ line: 1, text: '' }); lineNo = 1; }
134
140
  if (!lines.length && start > lineNo) throw fault('LINE_OUT_OF_RANGE', '起始行超出已读取文件的总行数');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-read-file-server",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "加密环境文件操作 MCP 工具。当 Node.js 是加密软件白名单进程时,通过 fs 模块自动解密读写文件明文,替代 AI Agent 内置文件工具,适用于任何支持 MCP 协议的 Agent。",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "start": "node index.js",
19
- "test": "node --test --test-reporter=tap --test-concurrency=1 test/regression.test.js test/protocol.test.js test/adapters.test.js",
19
+ "test": "node --test --test-reporter=tap --test-concurrency=1 test/regression.test.js test/protocol.test.js test/adapters.test.js test/review-fixes.test.js",
20
20
  "check": "node scripts/check.js"
21
21
  },
22
22
  "keywords": [
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "overrides": {
54
54
  "fast-uri": "^3.1.6",
55
- "qs": "^6.16.0"
55
+ "qs": "^6.16.0",
56
+ "hono": "4.13.5"
56
57
  }
57
58
  }