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 +4 -2
- package/lib/files.js +12 -4
- package/lib/text.js +8 -2
- package/package.json +4 -3
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
}
|