@j0hanz/filesystem-mcp 1.11.1 → 1.13.0
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/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +26 -0
- package/dist/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/server/roots-manager.d.ts +1 -1
- package/dist/server/roots-manager.js +8 -13
- package/dist/tools/apply-patch.js +34 -38
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +183 -115
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +266 -217
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- package/dist/tools.js +2 -2
- package/package.json +1 -1
package/dist/server/bootstrap.js
CHANGED
|
@@ -5,7 +5,7 @@ import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextproto
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
|
-
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { isInitializeRequest, LATEST_PROTOCOL_VERSION, SetLevelRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { DEFAULT_LOG_LEVEL, parseEnvInt } from '../lib/constants.js';
|
|
10
10
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
11
11
|
import { withAllowedDirectoriesState } from '../lib/paths.js';
|
|
@@ -243,7 +243,7 @@ async function readRequestBody(req) {
|
|
|
243
243
|
req.on('error', reject);
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
-
async function createHttpSession(options, sessions) {
|
|
246
|
+
async function createHttpSession(options, sessions, negotiatedProtocolVersion) {
|
|
247
247
|
const mcpServer = await createServer(options);
|
|
248
248
|
const rootsManager = getRootsManager(mcpServer);
|
|
249
249
|
rootsManager.registerHandlers(mcpServer);
|
|
@@ -255,6 +255,7 @@ async function createHttpSession(options, sessions) {
|
|
|
255
255
|
server: mcpServer,
|
|
256
256
|
rootsManager,
|
|
257
257
|
transport,
|
|
258
|
+
negotiatedProtocolVersion,
|
|
258
259
|
});
|
|
259
260
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
260
261
|
},
|
|
@@ -270,7 +271,12 @@ async function createHttpSession(options, sessions) {
|
|
|
270
271
|
});
|
|
271
272
|
};
|
|
272
273
|
await mcpServer.connect(transport);
|
|
273
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
server: mcpServer,
|
|
276
|
+
rootsManager,
|
|
277
|
+
transport,
|
|
278
|
+
negotiatedProtocolVersion,
|
|
279
|
+
};
|
|
274
280
|
}
|
|
275
281
|
function sendJsonRpcError(res, status, code, message) {
|
|
276
282
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
@@ -299,6 +305,29 @@ function getSessionId(req) {
|
|
|
299
305
|
? rawSessionId
|
|
300
306
|
: undefined;
|
|
301
307
|
}
|
|
308
|
+
function getProtocolVersionHeader(req) {
|
|
309
|
+
const rawProtocolVersion = req.headers['mcp-protocol-version'];
|
|
310
|
+
return typeof rawProtocolVersion === 'string'
|
|
311
|
+
? rawProtocolVersion
|
|
312
|
+
: undefined;
|
|
313
|
+
}
|
|
314
|
+
function resolveNegotiatedProtocolVersion(requestedVersion) {
|
|
315
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
|
|
316
|
+
? requestedVersion
|
|
317
|
+
: LATEST_PROTOCOL_VERSION;
|
|
318
|
+
}
|
|
319
|
+
function ensureSessionProtocolVersion(req, res, session) {
|
|
320
|
+
const protocolVersion = getProtocolVersionHeader(req);
|
|
321
|
+
if (!protocolVersion) {
|
|
322
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, 'Bad Request: Missing MCP-Protocol-Version header');
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (protocolVersion !== session.negotiatedProtocolVersion) {
|
|
326
|
+
sendJsonRpcError(res, 400, JSON_RPC_SERVER_ERROR, `Bad Request: MCP-Protocol-Version must match negotiated version ${session.negotiatedProtocolVersion}`);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
302
331
|
function isAuthorizedBearer(apiKey, authHeader) {
|
|
303
332
|
const bearerPrefix = 'Bearer ';
|
|
304
333
|
if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
|
|
@@ -385,6 +414,10 @@ export async function startHttpServer(port, options) {
|
|
|
385
414
|
discardRequestBody(req);
|
|
386
415
|
return;
|
|
387
416
|
}
|
|
417
|
+
if (!ensureSessionProtocolVersion(req, res, session)) {
|
|
418
|
+
discardRequestBody(req);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
388
421
|
const body = await readRequestBody(req);
|
|
389
422
|
await handleSessionTransportRequest(session, req, res, body);
|
|
390
423
|
return;
|
|
@@ -396,7 +429,7 @@ export async function startHttpServer(port, options) {
|
|
|
396
429
|
sendJsonRpcError(res, 503, JSON_RPC_SERVER_ERROR, 'Too many sessions');
|
|
397
430
|
return;
|
|
398
431
|
}
|
|
399
|
-
const session = await createHttpSession(options, sessions);
|
|
432
|
+
const session = await createHttpSession(options, sessions, resolveNegotiatedProtocolVersion(body.params.protocolVersion));
|
|
400
433
|
await handleSessionTransportRequest(session, req, res, body);
|
|
401
434
|
return;
|
|
402
435
|
}
|
|
@@ -411,6 +444,8 @@ export async function startHttpServer(port, options) {
|
|
|
411
444
|
const session = getSessionOrRespondNotFound(sessions, sessionId, res);
|
|
412
445
|
if (!session)
|
|
413
446
|
return;
|
|
447
|
+
if (!ensureSessionProtocolVersion(req, res, session))
|
|
448
|
+
return;
|
|
414
449
|
await handleSessionTransportRequest(session, req, res);
|
|
415
450
|
}
|
|
416
451
|
else {
|
|
@@ -3,7 +3,7 @@ import { type AllowedDirectoriesState } from '../lib/paths.js';
|
|
|
3
3
|
import { type LoggingState } from './bootstrap.js';
|
|
4
4
|
import type { ServerOptions } from './bootstrap.js';
|
|
5
5
|
export declare class RootsManager {
|
|
6
|
-
private
|
|
6
|
+
private _debouncedUpdate;
|
|
7
7
|
private rootDirectories;
|
|
8
8
|
private allowedDirectoriesState;
|
|
9
9
|
private clientInitialized;
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
5
5
|
import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
|
|
6
6
|
import { getValidRootDirectories, isPathWithinDirectories, normalizePath, resolveAllowedDirectoriesState, setAllowedDirectoriesStateResolved, } from '../lib/paths.js';
|
|
7
|
-
import { isRecord } from '../lib/utils.js';
|
|
7
|
+
import { debounce, isRecord } from '../lib/utils.js';
|
|
8
8
|
import { logToMcp } from './bootstrap.js';
|
|
9
9
|
const ROOTS_TIMEOUT_MS = 5000;
|
|
10
10
|
const ROOTS_DEBOUNCE_MS = 100;
|
|
@@ -81,7 +81,7 @@ async function filterRootsWithinBaseline(roots, baseline, signal) {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
export class RootsManager {
|
|
84
|
-
|
|
84
|
+
_debouncedUpdate;
|
|
85
85
|
rootDirectories = [];
|
|
86
86
|
allowedDirectoriesState = {
|
|
87
87
|
primary: [],
|
|
@@ -102,9 +102,9 @@ export class RootsManager {
|
|
|
102
102
|
return this.clientInitialized;
|
|
103
103
|
}
|
|
104
104
|
destroy() {
|
|
105
|
-
if (this.
|
|
106
|
-
|
|
107
|
-
this.
|
|
105
|
+
if (this._debouncedUpdate) {
|
|
106
|
+
this._debouncedUpdate.cancel();
|
|
107
|
+
this._debouncedUpdate = undefined;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
getAllowedDirectoriesState() {
|
|
@@ -149,15 +149,10 @@ export class RootsManager {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
scheduleRootsUpdate(server) {
|
|
152
|
-
|
|
153
|
-
this.
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
this.rootsUpdateTimeout = setTimeout(() => {
|
|
157
|
-
this.rootsUpdateTimeout = undefined;
|
|
158
|
-
void this.updateRootsFromClient(server);
|
|
152
|
+
this._debouncedUpdate ??= debounce((s) => {
|
|
153
|
+
void this.updateRootsFromClient(s);
|
|
159
154
|
}, ROOTS_DEBOUNCE_MS);
|
|
160
|
-
this.
|
|
155
|
+
this._debouncedUpdate(server);
|
|
161
156
|
}
|
|
162
157
|
logMissingDirectories(server) {
|
|
163
158
|
if (this.options.allowCwd) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
1
|
import * as path from 'node:path';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
|
-
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
4
|
+
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
|
-
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
6
|
+
import { atomicWriteFile, processInParallel, withAbort, } from '../lib/fs-helpers.js';
|
|
7
7
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
8
8
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -29,15 +29,6 @@ function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
|
|
|
29
29
|
return;
|
|
30
30
|
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for patch: ${filePath} (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
|
|
31
31
|
}
|
|
32
|
-
function assertPatchHasHunks(patch) {
|
|
33
|
-
if (!patch.trim()) {
|
|
34
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch content is empty.');
|
|
35
|
-
}
|
|
36
|
-
const hasHunk = /@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/u.test(patch);
|
|
37
|
-
if (!hasHunk) {
|
|
38
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch must include unified hunk headers (e.g., @@ -1,2 +1,2 @@).');
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
32
|
function countStructuredPatchStats(diff) {
|
|
42
33
|
let linesAdded = 0;
|
|
43
34
|
let linesRemoved = 0;
|
|
@@ -63,12 +54,12 @@ function extractPatchTargetPath(diff) {
|
|
|
63
54
|
return stripGitPrefix(diff.oldFileName);
|
|
64
55
|
return undefined;
|
|
65
56
|
}
|
|
66
|
-
async function
|
|
57
|
+
async function applyDiff(filePath, diff, options, signal) {
|
|
67
58
|
const validPath = await validateExistingPath(filePath, signal);
|
|
68
59
|
assertAllowedFileAccess(filePath, validPath);
|
|
69
|
-
const stats = await withAbort(
|
|
60
|
+
const stats = await withAbort(stat(validPath), signal);
|
|
70
61
|
assertPatchTargetSizeWithinLimit(validPath, stats.size, MAX_TEXT_FILE_SIZE);
|
|
71
|
-
const content = await
|
|
62
|
+
const content = await readFile(validPath, { encoding: 'utf-8', signal });
|
|
72
63
|
const patched = applyPatch(content, diff, {
|
|
73
64
|
fuzzFactor: options.fuzzFactor,
|
|
74
65
|
autoConvertLineEndings: options.autoConvertLineEndings,
|
|
@@ -89,32 +80,33 @@ async function applyPatchToFile(filePath, diff, options, signal) {
|
|
|
89
80
|
}
|
|
90
81
|
return { path: validPath, applied: true, ...patchStats };
|
|
91
82
|
}
|
|
92
|
-
async function
|
|
83
|
+
async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
93
84
|
const validBase = await validateExistingPath(basePath, signal);
|
|
94
|
-
const
|
|
95
|
-
for (const diff of parsed) {
|
|
85
|
+
const promises = parsed.map((diff) => {
|
|
96
86
|
const fileName = extractPatchTargetPath(diff);
|
|
97
87
|
if (!fileName) {
|
|
98
|
-
|
|
88
|
+
return () => Promise.resolve({
|
|
99
89
|
path: '<unknown>',
|
|
100
90
|
applied: false,
|
|
101
91
|
error: 'Missing file name in patch header',
|
|
102
92
|
});
|
|
103
|
-
continue;
|
|
104
93
|
}
|
|
105
94
|
const filePath = path.resolve(validBase, fileName);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
95
|
+
return async () => {
|
|
96
|
+
try {
|
|
97
|
+
const result = await applyDiff(filePath, diff, options, signal);
|
|
98
|
+
return { ...result, path: fileName };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
path: fileName,
|
|
103
|
+
applied: false,
|
|
104
|
+
error: formatUnknownErrorMessage(error),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const { results } = await processInParallel(promises, (task) => task(), PARALLEL_CONCURRENCY, signal);
|
|
118
110
|
const totals = results.reduce((acc, r) => {
|
|
119
111
|
if (r.applied) {
|
|
120
112
|
acc.applied++;
|
|
@@ -137,24 +129,28 @@ async function handleMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
137
129
|
});
|
|
138
130
|
}
|
|
139
131
|
async function handleApplyPatch(args, signal) {
|
|
140
|
-
|
|
132
|
+
if (!args.patch.trim()) {
|
|
133
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch content is empty.');
|
|
134
|
+
}
|
|
141
135
|
const fuzzFactor = args.fuzzFactor ?? 0;
|
|
142
136
|
const parsed = parsePatch(args.patch);
|
|
137
|
+
const hasHunks = parsed.some((p) => p.hunks.length > 0);
|
|
138
|
+
if (!hasHunks) {
|
|
139
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch must include unified hunk headers (e.g., @@ -1,2 +1,2 @@).');
|
|
140
|
+
}
|
|
143
141
|
const options = {
|
|
144
142
|
dryRun: args.dryRun,
|
|
145
143
|
fuzzFactor,
|
|
146
144
|
autoConvertLineEndings: args.autoConvertLineEndings,
|
|
147
145
|
};
|
|
148
|
-
// Multi-file patch: best-effort per file
|
|
149
146
|
if (parsed.length > 1) {
|
|
150
|
-
return
|
|
147
|
+
return processMultiFilePatch(args.path, parsed, options, signal);
|
|
151
148
|
}
|
|
152
|
-
// Single-file patch: delegate to shared helper, then assert success
|
|
153
149
|
const diff = parsed[0];
|
|
154
150
|
if (!diff) {
|
|
155
151
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No patch content found.');
|
|
156
152
|
}
|
|
157
|
-
const result = await
|
|
153
|
+
const result = await applyDiff(args.path, diff, options, signal);
|
|
158
154
|
if (!result.applied) {
|
|
159
155
|
throw new McpError(ErrorCode.E_INVALID_INPUT, result.error === 'Patch had no effect'
|
|
160
156
|
? 'Patch had no effect \u2014 the file content is unchanged after applying. The patch may not match the current file content. Generate a fresh patch via diff_files and retry.'
|
|
@@ -196,7 +192,7 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
196
192
|
return `🛠 patch: ${name} • failed`;
|
|
197
193
|
const added = sc.linesAdded ?? 0;
|
|
198
194
|
const removed = sc.linesRemoved ?? 0;
|
|
199
|
-
const dry = args.dryRun ? 'dry run
|
|
195
|
+
const dry = args.dryRun ? 'dry run ' : '';
|
|
200
196
|
if (added > 0 || removed > 0)
|
|
201
197
|
return `🛠 patch: ${name} • ${dry} +${added} -${removed}`;
|
|
202
198
|
return `🛠 patch: ${name} • ${dry}no changes`;
|
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
6
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
6
7
|
import { ErrorCode } from '../lib/errors.js';
|
|
7
8
|
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
@@ -26,15 +27,9 @@ export const CALCULATE_HASH_TOOL = {
|
|
|
26
27
|
taskSupport: 'optional',
|
|
27
28
|
};
|
|
28
29
|
async function hashFile(filePath, encoding, signal) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for await (const chunk of stream) {
|
|
33
|
-
hashOp.update(chunk);
|
|
34
|
-
assertNotAborted(signal);
|
|
35
|
-
}
|
|
36
|
-
assertNotAborted(signal);
|
|
37
|
-
return encoding === undefined ? hashOp.digest() : hashOp.digest(encoding);
|
|
30
|
+
const hasher = createHash('sha256');
|
|
31
|
+
await pipeline(createReadStream(filePath, { signal, highWaterMark: 64 * 1024 }), hasher, { signal });
|
|
32
|
+
return encoding ? hasher.digest(encoding) : hasher.digest();
|
|
38
33
|
}
|
|
39
34
|
function toStableRelativePath(root, entryPath) {
|
|
40
35
|
const relativePath = path.relative(root, entryPath);
|
|
@@ -84,25 +79,27 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
84
79
|
});
|
|
85
80
|
}
|
|
86
81
|
assertNotAborted(signal);
|
|
87
|
-
// Phase 2: hash files concurrently with bounded pool.
|
|
88
82
|
const concurrency = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
89
83
|
const entries = [];
|
|
90
84
|
let filesHashed = 0;
|
|
91
85
|
const totalFiles = filteredPaths.length;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
const taskQueue = filteredPaths;
|
|
87
|
+
const worker = async () => {
|
|
88
|
+
while (taskQueue.length > 0) {
|
|
89
|
+
const task = taskQueue.pop();
|
|
90
|
+
if (!task)
|
|
91
|
+
break;
|
|
92
|
+
assertNotAborted(signal);
|
|
93
|
+
const fileHash = await hashFile(task.filePath, undefined, signal);
|
|
94
|
+
entries.push({ path: task.relativePath, hash: fileHash });
|
|
95
|
+
filesHashed++;
|
|
96
|
+
reportPeriodicProgress(onProgress, filesHashed, {
|
|
97
|
+
throttleModulo: 25,
|
|
98
|
+
total: totalFiles,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
106
103
|
reportPeriodicProgress(onProgress, filesHashed, {
|
|
107
104
|
throttleModulo: 25,
|
|
108
105
|
total: totalFiles,
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { formatPatch, structuredPatch } from 'diff';
|
|
4
4
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
@@ -20,19 +20,18 @@ export const DIFF_FILES_TOOL = {
|
|
|
20
20
|
gotchas: ['`isIdentical=true` means no hunks (`@@`) and empty diff.'],
|
|
21
21
|
taskSupport: 'forbidden',
|
|
22
22
|
};
|
|
23
|
-
function computeDiffStats(
|
|
23
|
+
function computeDiffStats(hunks) {
|
|
24
24
|
let linesAdded = 0;
|
|
25
25
|
let linesRemoved = 0;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
linesRemoved++;
|
|
26
|
+
for (const hunk of hunks) {
|
|
27
|
+
for (const line of hunk.lines) {
|
|
28
|
+
if (line.startsWith('+') && !line.startsWith('+++'))
|
|
29
|
+
linesAdded++;
|
|
30
|
+
else if (line.startsWith('-') && !line.startsWith('---'))
|
|
31
|
+
linesRemoved++;
|
|
32
|
+
}
|
|
34
33
|
}
|
|
35
|
-
return { linesAdded, linesRemoved, hunksCount };
|
|
34
|
+
return { linesAdded, linesRemoved, hunksCount: hunks.length };
|
|
36
35
|
}
|
|
37
36
|
function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
|
|
38
37
|
if (size <= maxFileSize)
|
|
@@ -55,14 +54,23 @@ async function handleDiffFiles(args, signal, resourceStore) {
|
|
|
55
54
|
fs.readFile(originalPath, { encoding: 'utf-8', signal }),
|
|
56
55
|
fs.readFile(modifiedPath, { encoding: 'utf-8', signal }),
|
|
57
56
|
]);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const patchObj = await new Promise((resolve) => {
|
|
58
|
+
structuredPatch(path.basename(originalPath), path.basename(modifiedPath), originalContent, modifiedContent, undefined, undefined, {
|
|
59
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
60
|
+
ignoreWhitespace: args.ignoreWhitespace,
|
|
61
|
+
stripTrailingCr: args.stripTrailingCr,
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
callback: (res) => {
|
|
64
|
+
resolve(res);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
62
67
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
if (!patchObj) {
|
|
69
|
+
throw new McpError(ErrorCode.E_TIMEOUT, `Diff computation timed out or failed due to complexity.`, originalPath);
|
|
70
|
+
}
|
|
71
|
+
const isIdentical = patchObj.hunks.length === 0;
|
|
72
|
+
const diffText = isIdentical ? '' : formatPatch(patchObj);
|
|
73
|
+
const stats = isIdentical ? undefined : computeDiffStats(patchObj.hunks);
|
|
66
74
|
const externalized = maybeExternalizeTextContent(resourceStore, diffText, {
|
|
67
75
|
name: 'diff:patch',
|
|
68
76
|
mimeType: 'text/x-diff',
|
|
@@ -3,5 +3,8 @@ import type { z } from 'zod';
|
|
|
3
3
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
4
4
|
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
5
5
|
export declare const EDIT_FILE_TOOL: ToolContract;
|
|
6
|
-
|
|
6
|
+
type EditInput = z.infer<typeof EditFileInputSchema>;
|
|
7
|
+
type EditOutput = z.infer<typeof EditFileOutputSchema>;
|
|
8
|
+
export declare function handleEditFile(args: EditInput, signal?: AbortSignal): Promise<ToolResponse<EditOutput>>;
|
|
7
9
|
export declare function registerEditFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
10
|
+
export {};
|