computer_mcp 1.0.2 → 1.0.4

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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/server.js +64 -24
  3. package/tools/file.js +405 -188
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "computer_mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "MCP Streamable HTTP Server with file and command execution tools",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "computer-mcp": "./server.js"
8
+ "computer-mcp": "server.js"
9
9
  },
10
10
  "files": [
11
11
  "server.js",
package/server.js CHANGED
@@ -9,24 +9,37 @@ import { registerFileTools } from './tools/file.js';
9
9
  import { registerInteractiveBashTool } from './tools/interactive.js';
10
10
  import { loadSkills, getSkillDetails } from './tools/skills.js';
11
11
 
12
- // 创建 MCP 服务器
13
- const server = new McpServer(
14
- {
15
- name: 'computer-mcp-server',
16
- version: '1.0.0'
17
- },
18
- {
19
- capabilities: {
20
- logging: {},
21
- tools: {}
12
+ /** 全局兜底:未捕获的同步异常与未处理的 Promise 拒绝只打日志,不退出进程 */
13
+ function installGlobalProcessErrorHandlers() {
14
+ process.on('uncaughtException', (err, origin) => {
15
+ console.error('[process] uncaughtException', origin || 'sync', err?.stack || String(err));
16
+ });
17
+ process.on('unhandledRejection', (reason) => {
18
+ const msg =
19
+ reason instanceof Error ? reason.stack || String(reason) : String(reason);
20
+ console.error('[process] unhandledRejection', msg);
21
+ });
22
+ }
23
+ installGlobalProcessErrorHandlers();
24
+
25
+ /** 每个 Streamable HTTP 会话需要独立的 McpServer(协议层与 transport 一对一) */
26
+ function createComputerMcpServer() {
27
+ const server = new McpServer(
28
+ {
29
+ name: 'computer-mcp-server',
30
+ version: '1.0.0'
31
+ },
32
+ {
33
+ capabilities: {
34
+ logging: {},
35
+ tools: {}
36
+ }
22
37
  }
23
- }
24
- );
25
-
26
- // 注册工具
27
- registerInteractiveBashTool(server);
28
- // registerBashTool(server);
29
- registerFileTools(server);
38
+ );
39
+ registerInteractiveBashTool(server);
40
+ registerFileTools(server);
41
+ return server;
42
+ }
30
43
 
31
44
  const app = express();
32
45
 
@@ -106,26 +119,40 @@ app.get('/skills/:name', async (req, res) => {
106
119
  }
107
120
  });
108
121
 
109
- // 管理传输层的 Map
110
- const transports = new Map();
122
+ // sessionId -> { transport, mcpServer }
123
+ const sessions = new Map();
111
124
 
112
125
  // MCP 端点处理
113
126
  app.all('/mcp', async (req, res) => {
114
127
  try {
115
- // 重用现有传输层或创建新的
116
- const sessionId = req.headers['mcp-session-id'];
117
- let transport = sessionId ? transports.get(sessionId) : undefined;
128
+ const rawSessionId = req.headers['mcp-session-id'];
129
+ const sessionId = Array.isArray(rawSessionId)
130
+ ? rawSessionId[rawSessionId.length - 1]
131
+ : rawSessionId;
132
+
133
+ let entry = sessionId ? sessions.get(sessionId) : undefined;
134
+ let transport = entry?.transport;
135
+ let mcpServer = entry?.mcpServer;
118
136
 
119
137
  if (!transport) {
138
+ mcpServer = createComputerMcpServer();
120
139
  transport = new StreamableHTTPServerTransport({
121
140
  sessionIdGenerator: () => randomUUID(),
122
141
  retryInterval: 2000, // 默认重试间隔用于 priming events
123
142
  onsessioninitialized: id => {
124
143
  console.log(`[${id}] Session initialized`);
125
- transports.set(id, transport);
144
+ sessions.set(id, { transport, mcpServer });
145
+ },
146
+ onsessionclosed: async id => {
147
+ sessions.delete(id);
148
+ try {
149
+ await mcpServer.close();
150
+ } catch (e) {
151
+ console.error('Error closing MCP server:', e);
152
+ }
126
153
  }
127
154
  });
128
- await server.connect(transport);
155
+ await mcpServer.connect(transport);
129
156
  }
130
157
 
131
158
  await transport.handleRequest(req, res, req.body);
@@ -160,6 +187,19 @@ app.listen(PORT, error => {
160
187
  // 处理服务器关闭
161
188
  process.on('SIGINT', async () => {
162
189
  console.log('Shutting down server...');
190
+ for (const [, { transport, mcpServer }] of sessions) {
191
+ try {
192
+ await mcpServer.close();
193
+ } catch (e) {
194
+ console.error(e);
195
+ }
196
+ try {
197
+ await transport.close();
198
+ } catch (e) {
199
+ console.error(e);
200
+ }
201
+ }
202
+ sessions.clear();
163
203
  process.exit(0);
164
204
  });
165
205
 
package/tools/file.js CHANGED
@@ -1,188 +1,405 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import * as z from 'zod/v4';
4
-
5
- // 文件大小限制:100万字符
6
- const MAX_FILE_SIZE = 1000000;
7
-
8
- /**
9
- * 检查文件大小是否超过限制
10
- * @param {string} filePath - 文件路径
11
- * @param {string} encoding - 文件编码
12
- * @returns {Promise<{valid: boolean, size: number, message?: string}>}
13
- */
14
- async function checkFileSize(filePath, encoding = 'utf-8') {
15
- try {
16
- const stats = await fs.stat(filePath);
17
- const sizeInBytes = stats.size;
18
-
19
- // 粗略估算字符数(根据编码)
20
- // UTF-8: 平均每个字符约1-3字节,这里使用保守估计
21
- let estimatedChars;
22
- if (encoding === 'utf-8') {
23
- estimatedChars = sizeInBytes; // 最坏情况:每字节一个字符
24
- } else if (encoding === 'utf16le') {
25
- estimatedChars = sizeInBytes / 2;
26
- } else {
27
- estimatedChars = sizeInBytes;
28
- }
29
-
30
- if (estimatedChars > MAX_FILE_SIZE) {
31
- return {
32
- valid: false,
33
- size: Math.round(estimatedChars),
34
- message: `文件太大,不能处理。文件大小约为 ${Math.round(estimatedChars).toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`
35
- };
36
- }
37
-
38
- return {
39
- valid: true,
40
- size: Math.round(estimatedChars)
41
- };
42
- } catch (error) {
43
- throw new Error(`检查文件大小失败: ${error.message}`);
44
- }
45
- }
46
-
47
- /**
48
- * 注册文件操作工具
49
- * @param {McpServer} server - MCP 服务器实例
50
- */
51
- export function registerFileTools(server) {
52
- // 注册读取文件工具
53
- server.tool(
54
- 'read_file',
55
- '读取文本文件的内容',
56
- {
57
- path: z.string().describe('文件路径'),
58
- encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8')
59
- },
60
- async ({ path: filePath, encoding = 'utf-8' }) => {
61
- try {
62
- // 先检查文件大小
63
- const sizeCheck = await checkFileSize(filePath, encoding);
64
- if (!sizeCheck.valid) {
65
- throw new Error(sizeCheck.message);
66
- }
67
-
68
- const content = await fs.readFile(filePath, encoding);
69
-
70
- // 再次检查实际字符数
71
- if (content.length > MAX_FILE_SIZE) {
72
- throw new Error(`文件太大,不能处理。文件包含 ${content.length.toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`);
73
- }
74
-
75
- return {
76
- content: [
77
- {
78
- type: 'text',
79
- text: content,
80
- },
81
- ],
82
- };
83
- } catch (error) {
84
- throw new Error(`读取文件失败: ${error.message}`);
85
- }
86
- }
87
- );
88
-
89
- // 注册写入文件工具
90
- server.tool(
91
- 'write_file',
92
- '写入内容到文本文件。如果文件已存在会被覆盖。',
93
- {
94
- path: z.string().describe('文件路径'),
95
- content: z.string().describe('要写入的内容'),
96
- encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8'),
97
- create_dirs: z.boolean().describe('如果目录不存在是否创建(可选,默认 false)').optional().default(false)
98
- },
99
- async ({ path: filePath, content, encoding = 'utf-8', create_dirs = false }) => {
100
- try {
101
- if (create_dirs) {
102
- const dir = path.dirname(filePath);
103
- await fs.mkdir(dir, { recursive: true });
104
- }
105
-
106
- await fs.writeFile(filePath, content, encoding);
107
-
108
- return {
109
- content: [
110
- {
111
- type: 'text',
112
- text: `文件写入成功: ${filePath}\n写入字节数: ${Buffer.byteLength(content, encoding)}`,
113
- },
114
- ],
115
- };
116
- } catch (error) {
117
- throw new Error(`写入文件失败: ${error.message}`);
118
- }
119
- }
120
- );
121
-
122
- // 注册修改文件工具
123
- server.tool(
124
- 'edit_file',
125
- '修改文本文件的内容。可以进行搜索替换操作。',
126
- {
127
- path: z.string().describe('文件路径'),
128
- search: z.string().describe('要搜索的文本(支持正则表达式)'),
129
- replace: z.string().describe('替换后的文本'),
130
- regex: z.boolean().describe('是否使用正则表达式(可选,默认 false)').optional().default(false),
131
- all: z.boolean().describe('是否替换所有匹配项(可选,默认 true)').optional().default(true),
132
- encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8')
133
- },
134
- async ({ path: filePath, search, replace, regex = false, all = true, encoding = 'utf-8' }) => {
135
- try {
136
- // 先检查文件大小
137
- const sizeCheck = await checkFileSize(filePath, encoding);
138
- if (!sizeCheck.valid) {
139
- throw new Error(sizeCheck.message);
140
- }
141
-
142
- let content = await fs.readFile(filePath, encoding);
143
-
144
- // 再次检查实际字符数
145
- if (content.length > MAX_FILE_SIZE) {
146
- throw new Error(`文件太大,不能处理。文件包含 ${content.length.toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`);
147
- }
148
-
149
- let searchPattern;
150
- if (regex) {
151
- searchPattern = new RegExp(search, all ? 'g' : '');
152
- } else {
153
- const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
154
- searchPattern = new RegExp(escapedSearch, all ? 'g' : '');
155
- }
156
-
157
- const matches = content.match(searchPattern);
158
- const matchCount = matches ? matches.length : 0;
159
-
160
- if (matchCount === 0) {
161
- return {
162
- content: [
163
- {
164
- type: 'text',
165
- text: `未找到匹配的内容: "${search}"`,
166
- },
167
- ],
168
- };
169
- }
170
-
171
- const newContent = content.replace(searchPattern, replace);
172
- await fs.writeFile(filePath, newContent, encoding);
173
-
174
- return {
175
- content: [
176
- {
177
- type: 'text',
178
- text: `文件修改成功: ${filePath}\n替换次数: ${matchCount}`,
179
- },
180
- ],
181
- };
182
- } catch (error) {
183
- throw new Error(`修改文件失败: ${error.message}`);
184
- }
185
- }
186
- );
187
- }
188
-
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import * as z from 'zod/v4';
4
+
5
+ // 文件大小限制:100万字符
6
+ const MAX_FILE_SIZE = 1000000;
7
+
8
+ function parseUnifiedDiff(diffText) {
9
+ const normalized = diffText.replace(/\r\n/g, '\n');
10
+ const lines = normalized.split('\n');
11
+
12
+ let idx = 0;
13
+ while (idx < lines.length && lines[idx].trim() === '') idx++;
14
+
15
+ if (idx >= lines.length || !lines[idx].startsWith('--- ')) {
16
+ throw new Error('无效的 unified diff:缺少 "--- " 头部');
17
+ }
18
+ const oldFileHeader = lines[idx].slice(4).trim();
19
+ idx++;
20
+
21
+ if (idx >= lines.length || !lines[idx].startsWith('+++ ')) {
22
+ throw new Error('无效的 unified diff:缺少 "+++ " 头部');
23
+ }
24
+ const newFileHeader = lines[idx].slice(4).trim();
25
+ idx++;
26
+
27
+ const hunks = [];
28
+ const hunkHeaderRegex = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/;
29
+
30
+ while (idx < lines.length) {
31
+ if (lines[idx].trim() === '') {
32
+ idx++;
33
+ continue;
34
+ }
35
+
36
+ const headerMatch = lines[idx].match(hunkHeaderRegex);
37
+ if (!headerMatch) {
38
+ throw new Error(`无效的 hunk 头: ${lines[idx]}`);
39
+ }
40
+
41
+ const oldStart = Number.parseInt(headerMatch[1], 10);
42
+ const oldCount = headerMatch[2] ? Number.parseInt(headerMatch[2], 10) : 1;
43
+ const newStart = Number.parseInt(headerMatch[3], 10);
44
+ const newCount = headerMatch[4] ? Number.parseInt(headerMatch[4], 10) : 1;
45
+ idx++;
46
+
47
+ const hunkLines = [];
48
+ while (idx < lines.length && !lines[idx].startsWith('@@ ')) {
49
+ if (lines[idx].startsWith('\')) {
50
+ idx++;
51
+ continue;
52
+ }
53
+ if (!/^[ +\-]/.test(lines[idx])) {
54
+ throw new Error(`无效的 hunk 行: ${lines[idx]}`);
55
+ }
56
+ hunkLines.push(lines[idx]);
57
+ idx++;
58
+ }
59
+
60
+ hunks.push({
61
+ oldStart,
62
+ oldCount,
63
+ newStart,
64
+ newCount,
65
+ lines: hunkLines
66
+ });
67
+ }
68
+
69
+ return { oldFileHeader, newFileHeader, hunks };
70
+ }
71
+
72
+ function extractOldSideLines(hunkLines) {
73
+ const oldSide = [];
74
+ for (const rawLine of hunkLines) {
75
+ const marker = rawLine[0];
76
+ if (marker === ' ' || marker === '-') {
77
+ oldSide.push(rawLine.slice(1));
78
+ }
79
+ }
80
+ return oldSide;
81
+ }
82
+
83
+ function canMatchOldSideAt(sourceLines, oldSideLines, startIndex) {
84
+ if (startIndex < 0 || startIndex + oldSideLines.length > sourceLines.length) {
85
+ return false;
86
+ }
87
+ for (let i = 0; i < oldSideLines.length; i++) {
88
+ if (sourceLines[startIndex + i] !== oldSideLines[i]) {
89
+ return false;
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+
95
+ function findHunkStartIndex(sourceLines, sourceIndex, hunk, maxOffset) {
96
+ const expected = hunk.oldStart - 1;
97
+ const minIndex = Math.max(sourceIndex, expected - maxOffset);
98
+ const maxIndex = Math.min(sourceLines.length, expected + maxOffset);
99
+ const oldSideLines = extractOldSideLines(hunk.lines);
100
+
101
+ // 优先在期望行号附近查找,兼容少量偏移
102
+ for (let candidate = minIndex; candidate <= maxIndex; candidate++) {
103
+ if (canMatchOldSideAt(sourceLines, oldSideLines, candidate)) {
104
+ return candidate;
105
+ }
106
+ }
107
+
108
+ // 兜底:从当前处理位置向后全量搜索,尽量提高补丁命中率
109
+ for (let candidate = sourceIndex; candidate <= sourceLines.length - oldSideLines.length; candidate++) {
110
+ if (canMatchOldSideAt(sourceLines, oldSideLines, candidate)) {
111
+ return candidate;
112
+ }
113
+ }
114
+
115
+ return -1;
116
+ }
117
+
118
+ function applyUnifiedDiffToContent(content, diffText, options = {}) {
119
+ const strict = options.strict ?? false;
120
+ const maxOffset = options.maxOffset ?? 8;
121
+ const { hunks } = parseUnifiedDiff(diffText);
122
+ if (hunks.length === 0) {
123
+ throw new Error('无效的 unified diff:未包含任何 hunk');
124
+ }
125
+
126
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
127
+ const normalizedContent = content.replace(/\r\n/g, '\n');
128
+ const sourceLines = normalizedContent.split('\n');
129
+ const result = [];
130
+ let sourceIndex = 0;
131
+ let additions = 0;
132
+ let deletions = 0;
133
+
134
+ for (const hunk of hunks) {
135
+ let targetSourceIndex = hunk.oldStart - 1;
136
+ if (!strict) {
137
+ targetSourceIndex = findHunkStartIndex(sourceLines, sourceIndex, hunk, maxOffset);
138
+ if (targetSourceIndex < 0) {
139
+ throw new Error(`无法定位 hunk(起始行 ${hunk.oldStart},允许偏移 ±${maxOffset})`);
140
+ }
141
+ }
142
+
143
+ if (targetSourceIndex < sourceIndex) {
144
+ throw new Error('hunk 顺序错误或存在重叠');
145
+ }
146
+
147
+ while (sourceIndex < targetSourceIndex) {
148
+ result.push(sourceLines[sourceIndex]);
149
+ sourceIndex++;
150
+ }
151
+
152
+ let seenOld = 0;
153
+ let seenNew = 0;
154
+
155
+ for (const rawLine of hunk.lines) {
156
+ const marker = rawLine[0];
157
+ const lineText = rawLine.slice(1);
158
+
159
+ if (marker === ' ') {
160
+ if (sourceIndex >= sourceLines.length || sourceLines[sourceIndex] !== lineText) {
161
+ throw new Error(`上下文不匹配,期望 "${lineText}"`);
162
+ }
163
+ result.push(sourceLines[sourceIndex]);
164
+ sourceIndex++;
165
+ seenOld++;
166
+ seenNew++;
167
+ } else if (marker === '-') {
168
+ if (sourceIndex >= sourceLines.length || sourceLines[sourceIndex] !== lineText) {
169
+ throw new Error(`删除行不匹配,期望 "${lineText}"`);
170
+ }
171
+ sourceIndex++;
172
+ seenOld++;
173
+ deletions++;
174
+ } else if (marker === '+') {
175
+ result.push(lineText);
176
+ seenNew++;
177
+ additions++;
178
+ } else {
179
+ throw new Error(`不支持的 hunk 标记: ${marker}`);
180
+ }
181
+ }
182
+
183
+ if (seenOld !== hunk.oldCount) {
184
+ throw new Error(`hunk oldCount 不匹配:声明 ${hunk.oldCount},实际 ${seenOld}`);
185
+ }
186
+ if (seenNew !== hunk.newCount) {
187
+ throw new Error(`hunk newCount 不匹配:声明 ${hunk.newCount},实际 ${seenNew}`);
188
+ }
189
+ }
190
+
191
+ while (sourceIndex < sourceLines.length) {
192
+ result.push(sourceLines[sourceIndex]);
193
+ sourceIndex++;
194
+ }
195
+
196
+ return {
197
+ content: result.join(eol),
198
+ additions,
199
+ deletions,
200
+ hunkCount: hunks.length
201
+ };
202
+ }
203
+
204
+ /**
205
+ * 检查文件大小是否超过限制
206
+ * @param {string} filePath - 文件路径
207
+ * @param {string} encoding - 文件编码
208
+ * @returns {Promise<{valid: boolean, size: number, message?: string}>}
209
+ */
210
+ async function checkFileSize(filePath, encoding = 'utf-8') {
211
+ try {
212
+ const stats = await fs.stat(filePath);
213
+ const sizeInBytes = stats.size;
214
+
215
+ // 粗略估算字符数(根据编码)
216
+ // UTF-8: 平均每个字符约1-3字节,这里使用保守估计
217
+ let estimatedChars;
218
+ if (encoding === 'utf-8') {
219
+ estimatedChars = sizeInBytes; // 最坏情况:每字节一个字符
220
+ } else if (encoding === 'utf16le') {
221
+ estimatedChars = sizeInBytes / 2;
222
+ } else {
223
+ estimatedChars = sizeInBytes;
224
+ }
225
+
226
+ if (estimatedChars > MAX_FILE_SIZE) {
227
+ return {
228
+ valid: false,
229
+ size: Math.round(estimatedChars),
230
+ message: `文件太大,不能处理。文件大小约为 ${Math.round(estimatedChars).toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`
231
+ };
232
+ }
233
+
234
+ return {
235
+ valid: true,
236
+ size: Math.round(estimatedChars)
237
+ };
238
+ } catch (error) {
239
+ throw new Error(`检查文件大小失败: ${error.message}`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * 注册文件操作工具
245
+ * @param {McpServer} server - MCP 服务器实例
246
+ */
247
+ export function registerFileTools(server) {
248
+ // 注册读取文件工具
249
+ server.tool(
250
+ 'read_file',
251
+ '读取文本文件的内容',
252
+ {
253
+ path: z.string().describe('文件路径'),
254
+ encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8')
255
+ },
256
+ async ({ path: filePath, encoding = 'utf-8' }) => {
257
+ try {
258
+ // 先检查文件大小
259
+ const sizeCheck = await checkFileSize(filePath, encoding);
260
+ if (!sizeCheck.valid) {
261
+ throw new Error(sizeCheck.message);
262
+ }
263
+
264
+ const content = await fs.readFile(filePath, encoding);
265
+
266
+ // 再次检查实际字符数
267
+ if (content.length > MAX_FILE_SIZE) {
268
+ throw new Error(`文件太大,不能处理。文件包含 ${content.length.toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`);
269
+ }
270
+
271
+ return {
272
+ content: [
273
+ {
274
+ type: 'text',
275
+ text: content,
276
+ },
277
+ ],
278
+ };
279
+ } catch (error) {
280
+ throw new Error(`读取文件失败: ${error.message}`);
281
+ }
282
+ }
283
+ );
284
+
285
+ // 注册带行号读取工具
286
+ server.tool(
287
+ 'read_file_numbered',
288
+ '读取文本文件内容,并在每行前加行号(格式:行号|内容)',
289
+ {
290
+ path: z.string().describe('文件路径'),
291
+ encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8')
292
+ },
293
+ async ({ path: filePath, encoding = 'utf-8' }) => {
294
+ try {
295
+ const sizeCheck = await checkFileSize(filePath, encoding);
296
+ if (!sizeCheck.valid) {
297
+ throw new Error(sizeCheck.message);
298
+ }
299
+
300
+ const content = await fs.readFile(filePath, encoding);
301
+ if (content.length > MAX_FILE_SIZE) {
302
+ throw new Error(`文件太大,不能处理。文件包含 ${content.length.toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`);
303
+ }
304
+
305
+ const normalized = content.replace(/\r\n/g, '\n');
306
+ const lines = normalized.split('\n');
307
+ const width = String(lines.length).length;
308
+ const numbered = lines
309
+ .map((line, index) => `${String(index + 1).padStart(width, '0')}|${line}`)
310
+ .join('\n');
311
+
312
+ return {
313
+ content: [
314
+ {
315
+ type: 'text',
316
+ text: numbered,
317
+ },
318
+ ],
319
+ };
320
+ } catch (error) {
321
+ throw new Error(`读取文件失败: ${error.message}`);
322
+ }
323
+ }
324
+ );
325
+
326
+ // 注册写入文件工具
327
+ server.tool(
328
+ 'write_file',
329
+ '写入内容到文本文件。如果文件已存在会被覆盖。',
330
+ {
331
+ path: z.string().describe('文件路径'),
332
+ content: z.string().describe('要写入的内容'),
333
+ encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8'),
334
+ create_dirs: z.boolean().describe('如果目录不存在是否创建(可选,默认 false)').optional().default(false)
335
+ },
336
+ async ({ path: filePath, content, encoding = 'utf-8', create_dirs = false }) => {
337
+ try {
338
+ if (create_dirs) {
339
+ const dir = path.dirname(filePath);
340
+ await fs.mkdir(dir, { recursive: true });
341
+ }
342
+
343
+ await fs.writeFile(filePath, content, encoding);
344
+
345
+ return {
346
+ content: [
347
+ {
348
+ type: 'text',
349
+ text: `文件写入成功: ${filePath}\n写入字节数: ${Buffer.byteLength(content, encoding)}`,
350
+ },
351
+ ],
352
+ };
353
+ } catch (error) {
354
+ throw new Error(`写入文件失败: ${error.message}`);
355
+ }
356
+ }
357
+ );
358
+
359
+ // 注册修改文件工具
360
+ server.tool(
361
+ 'edit_file',
362
+ '使用标准 unified diff 格式修改文本文件内容。',
363
+ {
364
+ path: z.string().describe('文件路径'),
365
+ diff: z.string().describe('标准 unified diff 文本(需包含 --- / +++ / @@ hunk)'),
366
+ strict: z.boolean().describe('是否严格按 hunk 行号匹配(可选,默认 false)').optional().default(false),
367
+ max_offset: z.number().int().min(0).max(200).describe('宽松模式下允许的最大行号偏移(可选,默认 8)').optional().default(8),
368
+ encoding: z.enum(['utf-8', 'ascii', 'utf16le', 'latin1']).describe('文件编码(可选,默认 utf-8)').optional().default('utf-8')
369
+ },
370
+ async ({ path: filePath, diff, strict = false, max_offset = 8, encoding = 'utf-8' }) => {
371
+ try {
372
+ // 先检查文件大小
373
+ const sizeCheck = await checkFileSize(filePath, encoding);
374
+ if (!sizeCheck.valid) {
375
+ throw new Error(sizeCheck.message);
376
+ }
377
+
378
+ const content = await fs.readFile(filePath, encoding);
379
+
380
+ // 再次检查实际字符数
381
+ if (content.length > MAX_FILE_SIZE) {
382
+ throw new Error(`文件太大,不能处理。文件包含 ${content.length.toLocaleString()} 字符,超过了 ${MAX_FILE_SIZE.toLocaleString()} 字符的限制。`);
383
+ }
384
+
385
+ const patched = applyUnifiedDiffToContent(content, diff, {
386
+ strict,
387
+ maxOffset: max_offset
388
+ });
389
+ await fs.writeFile(filePath, patched.content, encoding);
390
+
391
+ return {
392
+ content: [
393
+ {
394
+ type: 'text',
395
+ text: `文件修改成功: ${filePath}\n模式: ${strict ? '严格' : '宽松'}\n最大偏移: ${max_offset}\n应用 hunk 数: ${patched.hunkCount}\n新增行: ${patched.additions}\n删除行: ${patched.deletions}`,
396
+ },
397
+ ],
398
+ };
399
+ } catch (error) {
400
+ throw new Error(`修改文件失败: ${error.message}`);
401
+ }
402
+ }
403
+ );
404
+ }
405
+