@j0hanz/filesystem-mcp 1.10.0 → 1.11.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/dist/completions.js +25 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +135 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +3 -1
- package/dist/schemas.js +2 -0
- package/dist/server/bootstrap.js +2 -1
- package/dist/tools/apply-patch.js +7 -4
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +6 -2
- package/dist/tools/edit-file.js +26 -9
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.js +77 -89
- package/dist/tools/read.js +1 -1
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +67 -48
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +48 -46
- package/dist/tools/shared.js +4 -3
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.js +3 -1
- package/dist/tools/write-file.js +4 -4
- package/package.json +1 -1
package/dist/tools/edit-file.js
CHANGED
|
@@ -20,11 +20,23 @@ export const EDIT_FILE_TOOL = {
|
|
|
20
20
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
21
21
|
nuances: ['Each edit applies to the output of the previous edit.'],
|
|
22
22
|
gotchas: ['Unmatched `oldText` entries listed in `unmatchedEdits`.'],
|
|
23
|
-
taskSupport: '
|
|
23
|
+
taskSupport: 'forbidden',
|
|
24
24
|
};
|
|
25
25
|
function escapeRegExp(string) {
|
|
26
26
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
27
|
}
|
|
28
|
+
function computeDiffStats(original, modified) {
|
|
29
|
+
const patch = createTwoFilesPatch('a', 'b', original, modified);
|
|
30
|
+
let linesAdded = 0;
|
|
31
|
+
let linesRemoved = 0;
|
|
32
|
+
for (const line of patch.split('\n')) {
|
|
33
|
+
if (line.startsWith('+') && !line.startsWith('+++'))
|
|
34
|
+
linesAdded++;
|
|
35
|
+
else if (line.startsWith('-') && !line.startsWith('---'))
|
|
36
|
+
linesRemoved++;
|
|
37
|
+
}
|
|
38
|
+
return { linesAdded, linesRemoved };
|
|
39
|
+
}
|
|
28
40
|
function applyEdits(content, edits, ignoreWhitespace) {
|
|
29
41
|
let newContent = content;
|
|
30
42
|
let appliedEdits = 0;
|
|
@@ -74,10 +86,15 @@ function applyEdits(content, edits, ignoreWhitespace) {
|
|
|
74
86
|
appliedEdits += 1;
|
|
75
87
|
}
|
|
76
88
|
}
|
|
89
|
+
const { linesAdded, linesRemoved } = appliedEdits > 0
|
|
90
|
+
? computeDiffStats(content, newContent)
|
|
91
|
+
: { linesAdded: 0, linesRemoved: 0 };
|
|
77
92
|
const result = {
|
|
78
93
|
content: newContent,
|
|
79
94
|
appliedEdits,
|
|
80
95
|
unmatchedEdits,
|
|
96
|
+
linesAdded,
|
|
97
|
+
linesRemoved,
|
|
81
98
|
};
|
|
82
99
|
if (minLine !== undefined && maxLine !== undefined) {
|
|
83
100
|
result.lineRange = [minLine, maxLine];
|
|
@@ -92,11 +109,12 @@ export async function handleEditFile(args, signal) {
|
|
|
92
109
|
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for edit: ${args.path} (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, args.path, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
|
|
93
110
|
}
|
|
94
111
|
const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
|
|
95
|
-
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
|
|
112
|
+
const { content: newContent, appliedEdits, unmatchedEdits, linesAdded, linesRemoved, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
|
|
96
113
|
const structured = {
|
|
97
114
|
ok: true,
|
|
98
115
|
path: validPath,
|
|
99
116
|
appliedEdits,
|
|
117
|
+
...(appliedEdits > 0 ? { linesAdded, linesRemoved } : {}),
|
|
100
118
|
...(unmatchedEdits.length > 0 ? { unmatchedEdits } : {}),
|
|
101
119
|
...(lineRange ? { lineRange } : {}),
|
|
102
120
|
};
|
|
@@ -132,9 +150,8 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
132
150
|
guard: options.isInitialized,
|
|
133
151
|
progressMessage: (args) => {
|
|
134
152
|
const name = path.basename(args.path);
|
|
135
|
-
const count = args.edits.length;
|
|
136
153
|
const tag = args.dryRun ? ' [dry run]' : '';
|
|
137
|
-
return `🛠 edit: ${name}
|
|
154
|
+
return `🛠 edit: ${name}${tag}`;
|
|
138
155
|
},
|
|
139
156
|
completionMessage: (args, result) => {
|
|
140
157
|
const name = path.basename(args.path);
|
|
@@ -144,12 +161,12 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
144
161
|
if (!sc.ok)
|
|
145
162
|
return `🛠 edit: ${name} • failed`;
|
|
146
163
|
const applied = sc.appliedEdits ?? 0;
|
|
147
|
-
|
|
164
|
+
if (applied === 0)
|
|
165
|
+
return `🛠 edit: ${name} • no changes`;
|
|
166
|
+
const added = sc.linesAdded ?? 0;
|
|
167
|
+
const removed = sc.linesRemoved ?? 0;
|
|
148
168
|
const dry = args.dryRun ? 'dry run — ' : '';
|
|
149
|
-
|
|
150
|
-
return `🛠 edit: ${name} • ${dry}${applied} applied, ${unmatched} unmatched`;
|
|
151
|
-
}
|
|
152
|
-
return `🛠 edit: ${name} • ${dry}${applied} applied`;
|
|
169
|
+
return `🛠 edit: ${name} • ${dry} +${added} -${removed}`;
|
|
153
170
|
},
|
|
154
171
|
});
|
|
155
172
|
const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
|
package/dist/tools/move-file.js
CHANGED
|
@@ -17,7 +17,7 @@ export const MOVE_FILE_TOOL = {
|
|
|
17
17
|
gotchas: [
|
|
18
18
|
'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
|
|
19
19
|
],
|
|
20
|
-
taskSupport: '
|
|
20
|
+
taskSupport: 'forbidden',
|
|
21
21
|
};
|
|
22
22
|
export async function handleMoveFile(args, signal) {
|
|
23
23
|
const sources = args.sources ?? (args.source ? [args.source] : []);
|
|
@@ -158,12 +158,12 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
158
158
|
const src = path.basename(args.source);
|
|
159
159
|
if (result.isError)
|
|
160
160
|
return `🛠 mv: ${src} → ${dest} • failed`;
|
|
161
|
-
return `🛠 mv: ${src} → ${dest}
|
|
161
|
+
return `🛠 mv: ${src} → ${dest}`;
|
|
162
162
|
}
|
|
163
163
|
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
164
164
|
if (result.isError)
|
|
165
165
|
return `🛠 mv: ${count} items → ${dest} • failed`;
|
|
166
|
-
return `🛠 mv: ${count} items → ${dest}
|
|
166
|
+
return `🛠 mv: ${count} items → ${dest}`;
|
|
167
167
|
},
|
|
168
168
|
});
|
|
169
169
|
const validatedHandler = withValidatedArgs(MoveFileInputSchema, wrappedHandler);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/metadata.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
@@ -20,35 +20,81 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
20
20
|
],
|
|
21
21
|
};
|
|
22
22
|
function toStructuredReadManyResult(result) {
|
|
23
|
-
|
|
23
|
+
return {
|
|
24
24
|
path: result.path,
|
|
25
|
+
...(result.content !== undefined ? { content: result.content } : {}),
|
|
26
|
+
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
27
|
+
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
28
|
+
...(result.head !== undefined ? { head: result.head } : {}),
|
|
29
|
+
...(result.tail !== undefined ? { tail: result.tail } : {}),
|
|
30
|
+
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
31
|
+
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
32
|
+
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
33
|
+
...(result.totalLines !== undefined
|
|
34
|
+
? { totalLines: result.totalLines }
|
|
35
|
+
: {}),
|
|
36
|
+
...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
|
|
37
|
+
...(result.truncationReason
|
|
38
|
+
? { truncationReason: result.truncationReason }
|
|
39
|
+
: {}),
|
|
40
|
+
...(result.error ? { error: result.error } : {}),
|
|
25
41
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (result.truncated)
|
|
29
|
-
|
|
30
|
-
if (result.
|
|
31
|
-
|
|
32
|
-
if (result.
|
|
33
|
-
|
|
34
|
-
if (result.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (result.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
}
|
|
43
|
+
function resolveReadManyTruncationReason(result) {
|
|
44
|
+
if (!result.truncated)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (result.readMode === 'head')
|
|
47
|
+
return 'head';
|
|
48
|
+
if (result.readMode === 'tail')
|
|
49
|
+
return 'tail';
|
|
50
|
+
if (result.readMode === 'range')
|
|
51
|
+
return 'range';
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function maybeExternalizeReadManyResult(result, resourceStore) {
|
|
55
|
+
const truncationReason = resolveReadManyTruncationReason(result);
|
|
56
|
+
const baseResult = {
|
|
57
|
+
...result,
|
|
58
|
+
...(truncationReason ? { truncationReason } : {}),
|
|
59
|
+
};
|
|
60
|
+
if (!result.content) {
|
|
61
|
+
return baseResult;
|
|
62
|
+
}
|
|
63
|
+
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(result.path)}`, mimeType: 'text/plain' });
|
|
64
|
+
if (!externalized) {
|
|
65
|
+
return baseResult;
|
|
48
66
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
return {
|
|
68
|
+
...baseResult,
|
|
69
|
+
content: externalized.preview,
|
|
70
|
+
truncated: true,
|
|
71
|
+
resourceUri: externalized.entry.uri,
|
|
72
|
+
truncationReason: 'externalized',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildReadManyResourceLinks(results) {
|
|
76
|
+
return results.flatMap((result) => {
|
|
77
|
+
if (!result.resourceUri)
|
|
78
|
+
return [];
|
|
79
|
+
return [
|
|
80
|
+
buildResourceLink({
|
|
81
|
+
uri: result.resourceUri,
|
|
82
|
+
name: `read:${path.basename(result.path)}`,
|
|
83
|
+
description: 'Full file contents',
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function buildReadManyTextResult(results) {
|
|
89
|
+
return results
|
|
90
|
+
.map((result) => {
|
|
91
|
+
const header = `=== ${result.path} ===`;
|
|
92
|
+
if (result.error) {
|
|
93
|
+
return `${header}\nError: ${result.error}`;
|
|
94
|
+
}
|
|
95
|
+
return `${header}\n${result.content ?? ''}`;
|
|
96
|
+
})
|
|
97
|
+
.join('\n\n');
|
|
52
98
|
}
|
|
53
99
|
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
54
100
|
const options = {
|
|
@@ -60,48 +106,9 @@ async function handleReadMultipleFiles(args, signal, resourceStore, onReadComple
|
|
|
60
106
|
...(onReadComplete ? { onReadComplete } : {}),
|
|
61
107
|
};
|
|
62
108
|
const results = await readMultipleFiles(args.paths, options);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
if (result.truncated && result.readMode === 'head') {
|
|
67
|
-
baseTruncationReason = 'head';
|
|
68
|
-
}
|
|
69
|
-
else if (result.truncated && result.readMode === 'tail') {
|
|
70
|
-
baseTruncationReason = 'tail';
|
|
71
|
-
}
|
|
72
|
-
else if (result.truncated && result.readMode === 'range') {
|
|
73
|
-
baseTruncationReason = 'range';
|
|
74
|
-
}
|
|
75
|
-
const baseResult = {
|
|
76
|
-
...result,
|
|
77
|
-
maxTotalSize,
|
|
78
|
-
...(baseTruncationReason
|
|
79
|
-
? { truncationReason: baseTruncationReason }
|
|
80
|
-
: {}),
|
|
81
|
-
};
|
|
82
|
-
if (!result.content) {
|
|
83
|
-
return baseResult;
|
|
84
|
-
}
|
|
85
|
-
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(result.path)}`, mimeType: 'text/plain' });
|
|
86
|
-
if (!externalized) {
|
|
87
|
-
return baseResult;
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
...baseResult,
|
|
91
|
-
content: externalized.preview,
|
|
92
|
-
truncated: true,
|
|
93
|
-
resourceUri: externalized.entry.uri,
|
|
94
|
-
truncationReason: 'externalized',
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
let succeeded = 0;
|
|
98
|
-
let failed = 0;
|
|
99
|
-
for (const result of mappedResults) {
|
|
100
|
-
if (result.error === undefined)
|
|
101
|
-
succeeded += 1;
|
|
102
|
-
else
|
|
103
|
-
failed += 1;
|
|
104
|
-
}
|
|
109
|
+
const mappedResults = results.map((result) => maybeExternalizeReadManyResult(result, resourceStore));
|
|
110
|
+
const succeeded = mappedResults.filter((r) => r.error === undefined).length;
|
|
111
|
+
const failed = mappedResults.length - succeeded;
|
|
105
112
|
const structured = {
|
|
106
113
|
ok: true,
|
|
107
114
|
results: mappedResults.map((result) => toStructuredReadManyResult(result)),
|
|
@@ -111,26 +118,7 @@ async function handleReadMultipleFiles(args, signal, resourceStore, onReadComple
|
|
|
111
118
|
failed,
|
|
112
119
|
},
|
|
113
120
|
};
|
|
114
|
-
|
|
115
|
-
for (const result of mappedResults) {
|
|
116
|
-
if (!result.resourceUri)
|
|
117
|
-
continue;
|
|
118
|
-
resourceLinks.push(buildResourceLink({
|
|
119
|
-
uri: result.resourceUri,
|
|
120
|
-
name: `read:${path.basename(result.path)}`,
|
|
121
|
-
description: 'Full file contents',
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
124
|
-
const text = mappedResults
|
|
125
|
-
.map((result) => {
|
|
126
|
-
const header = `=== ${result.path} ===`;
|
|
127
|
-
if (result.error) {
|
|
128
|
-
return `${header}\nError: ${result.error}`;
|
|
129
|
-
}
|
|
130
|
-
return `${header}\n${result.content ?? ''}`;
|
|
131
|
-
})
|
|
132
|
-
.join('\n\n');
|
|
133
|
-
return buildToolResponse(text, structured, resourceLinks);
|
|
121
|
+
return buildToolResponse(buildReadManyTextResult(mappedResults), structured, buildReadManyResourceLinks(mappedResults));
|
|
134
122
|
}
|
|
135
123
|
export function registerReadMultipleFilesTool(server, options = {}) {
|
|
136
124
|
const handler = (args, extra) => {
|
package/dist/tools/read.js
CHANGED
|
@@ -18,7 +18,7 @@ export const READ_FILE_TOOL = {
|
|
|
18
18
|
nuances: [
|
|
19
19
|
'Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.',
|
|
20
20
|
],
|
|
21
|
-
taskSupport: '
|
|
21
|
+
taskSupport: 'forbidden',
|
|
22
22
|
};
|
|
23
23
|
async function handleReadFile(args, signal, resourceStore) {
|
|
24
24
|
const options = {
|
|
@@ -3,8 +3,11 @@ import type { z } from 'zod';
|
|
|
3
3
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema } from '../schemas.js';
|
|
4
4
|
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
5
5
|
export declare const SEARCH_AND_REPLACE_TOOL: ToolContract;
|
|
6
|
-
|
|
6
|
+
type SearchAndReplaceArgs = z.infer<typeof SearchAndReplaceInputSchema>;
|
|
7
|
+
type SearchAndReplaceOutput = z.infer<typeof SearchAndReplaceOutputSchema>;
|
|
8
|
+
export declare function handleSearchAndReplace(args: SearchAndReplaceArgs, signal?: AbortSignal, onProgress?: (progress: {
|
|
7
9
|
total?: number;
|
|
8
10
|
current: number;
|
|
9
|
-
}) => void): Promise<ToolResponse<
|
|
11
|
+
}) => void): Promise<ToolResponse<SearchAndReplaceOutput>>;
|
|
10
12
|
export declare function registerSearchAndReplaceTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
13
|
+
export {};
|
|
@@ -94,6 +94,17 @@ function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
|
|
|
94
94
|
const replace = (content, replacement) => content.replaceAll(searchPattern, () => replacement);
|
|
95
95
|
return { count, replace };
|
|
96
96
|
}
|
|
97
|
+
function buildReplacementPlan(content, replacement, matcher) {
|
|
98
|
+
const matchCount = matcher.count(content);
|
|
99
|
+
if (matchCount === 0) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
matchCount,
|
|
104
|
+
originalContent: content,
|
|
105
|
+
updatedContent: matcher.replace(content, replacement),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
97
108
|
function formatFileTooLargeError(filePath, size, maxFileSize) {
|
|
98
109
|
return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
|
|
99
110
|
}
|
|
@@ -112,37 +123,29 @@ async function processEntry(entryPath, context) {
|
|
|
112
123
|
return;
|
|
113
124
|
}
|
|
114
125
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
const plan = await readReplacementPlan(validPath, {
|
|
127
|
+
matcher,
|
|
128
|
+
replacement,
|
|
129
|
+
maxFileSize,
|
|
130
|
+
signal,
|
|
131
|
+
});
|
|
132
|
+
if (!plan) {
|
|
122
133
|
return;
|
|
123
134
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
summary.totalMatches += plan.matchCount;
|
|
136
|
+
summary.filesChanged++;
|
|
137
|
+
recordChangedFile(summary, validPath, plan.matchCount);
|
|
138
|
+
maybeAppendPatchDiff(summary, {
|
|
139
|
+
filePath: validPath,
|
|
140
|
+
originalContent: plan.originalContent,
|
|
141
|
+
updatedContent: plan.updatedContent,
|
|
142
|
+
includeDiff: options.dryRun || options.returnDiff,
|
|
127
143
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
recordChangedFile(summary, validPath, matchCount);
|
|
133
|
-
const newContent = matcher.replace(content, replacement);
|
|
134
|
-
maybeAppendPatchDiff(summary, {
|
|
135
|
-
filePath: validPath,
|
|
136
|
-
originalContent: content,
|
|
137
|
-
updatedContent: newContent,
|
|
138
|
-
includeDiff: options.dryRun || options.returnDiff,
|
|
144
|
+
if (!options.dryRun) {
|
|
145
|
+
await atomicWriteFile(validPath, plan.updatedContent, {
|
|
146
|
+
encoding: 'utf-8',
|
|
147
|
+
signal,
|
|
139
148
|
});
|
|
140
|
-
if (!options.dryRun) {
|
|
141
|
-
await atomicWriteFile(validPath, newContent, {
|
|
142
|
-
encoding: 'utf-8',
|
|
143
|
-
signal,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
catch (error) {
|
|
@@ -153,6 +156,17 @@ async function processEntry(entryPath, context) {
|
|
|
153
156
|
});
|
|
154
157
|
}
|
|
155
158
|
}
|
|
159
|
+
async function readReplacementPlan(validPath, context) {
|
|
160
|
+
const stats = await withAbort(fs.stat(validPath), context.signal);
|
|
161
|
+
if (stats.size > context.maxFileSize) {
|
|
162
|
+
throw new Error(formatFileTooLargeError(validPath, stats.size, context.maxFileSize));
|
|
163
|
+
}
|
|
164
|
+
const content = await fs.readFile(validPath, {
|
|
165
|
+
encoding: 'utf-8',
|
|
166
|
+
signal: context.signal,
|
|
167
|
+
});
|
|
168
|
+
return buildReplacementPlan(content, context.replacement, context.matcher);
|
|
169
|
+
}
|
|
156
170
|
function maybeAppendPatchDiff(summary, params) {
|
|
157
171
|
if (!params.includeDiff)
|
|
158
172
|
return;
|
|
@@ -283,27 +297,7 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
283
297
|
throttleModulo: 25,
|
|
284
298
|
force: true,
|
|
285
299
|
});
|
|
286
|
-
|
|
287
|
-
return buildToolResponse(`Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${args.dryRun ? ' (Dry run)' : ''}`, {
|
|
288
|
-
ok: true,
|
|
289
|
-
matches: summary.totalMatches,
|
|
290
|
-
filesChanged: summary.filesChanged,
|
|
291
|
-
processedFiles: summary.processedFiles,
|
|
292
|
-
...(summary.failedFiles > 0 ? { failedFiles: summary.failedFiles } : {}),
|
|
293
|
-
...(summary.failures.length > 0 ? { failures: summary.failures } : {}),
|
|
294
|
-
...(summary.changedFiles.length > 0
|
|
295
|
-
? { changedFiles: summary.changedFiles }
|
|
296
|
-
: {}),
|
|
297
|
-
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
298
|
-
...((args.dryRun || args.returnDiff) && summary.diff
|
|
299
|
-
? { diff: summary.diff }
|
|
300
|
-
: {}),
|
|
301
|
-
...(summary.diffTruncated ? { diffTruncated: true } : {}),
|
|
302
|
-
...(summary.stoppedReason
|
|
303
|
-
? { stoppedReason: summary.stoppedReason }
|
|
304
|
-
: {}),
|
|
305
|
-
dryRun: args.dryRun,
|
|
306
|
-
});
|
|
300
|
+
return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
|
|
307
301
|
}
|
|
308
302
|
export function registerSearchAndReplaceTool(server, options = {}) {
|
|
309
303
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
@@ -350,3 +344,28 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
350
344
|
return;
|
|
351
345
|
server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), validatedHandler);
|
|
352
346
|
}
|
|
347
|
+
function buildSearchAndReplaceText(summary, dryRun) {
|
|
348
|
+
const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
|
|
349
|
+
const dryRunSuffix = dryRun ? ' (Dry run)' : '';
|
|
350
|
+
return `Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${dryRunSuffix}`;
|
|
351
|
+
}
|
|
352
|
+
function buildSearchAndReplaceStructuredResult(summary, args) {
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
matches: summary.totalMatches,
|
|
356
|
+
filesChanged: summary.filesChanged,
|
|
357
|
+
processedFiles: summary.processedFiles,
|
|
358
|
+
...(summary.failedFiles > 0 ? { failedFiles: summary.failedFiles } : {}),
|
|
359
|
+
...(summary.failures.length > 0 ? { failures: summary.failures } : {}),
|
|
360
|
+
...(summary.changedFiles.length > 0
|
|
361
|
+
? { changedFiles: summary.changedFiles }
|
|
362
|
+
: {}),
|
|
363
|
+
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
364
|
+
...((args.dryRun || args.returnDiff) && summary.diff
|
|
365
|
+
? { diff: summary.diff }
|
|
366
|
+
: {}),
|
|
367
|
+
...(summary.diffTruncated ? { diffTruncated: true } : {}),
|
|
368
|
+
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
369
|
+
dryRun: args.dryRun,
|
|
370
|
+
};
|
|
371
|
+
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -12,7 +12,7 @@ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
|
12
12
|
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
13
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
14
14
|
nuances: ['Returns absolute paths of all allowed directories.'],
|
|
15
|
-
taskSupport: '
|
|
15
|
+
taskSupport: 'forbidden',
|
|
16
16
|
};
|
|
17
17
|
function buildTextRoots(dirs) {
|
|
18
18
|
if (dirs.length === 0) {
|
|
@@ -8,6 +8,11 @@ import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas
|
|
|
8
8
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
10
|
const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50;
|
|
11
|
+
const SEARCH_COMPLETION_REASON_LABELS = {
|
|
12
|
+
timeout: 'timeout',
|
|
13
|
+
maxResults: 'max results',
|
|
14
|
+
maxFiles: 'max files',
|
|
15
|
+
};
|
|
11
16
|
export const SEARCH_CONTENT_TOOL = {
|
|
12
17
|
name: 'grep',
|
|
13
18
|
title: 'Search Content',
|
|
@@ -26,20 +31,11 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
26
31
|
],
|
|
27
32
|
taskSupport: 'optional',
|
|
28
33
|
};
|
|
29
|
-
function
|
|
34
|
+
function findColumnOffset(content, pattern, matcher, caseSensitive) {
|
|
30
35
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function findColumnOffset(content, pattern, isRegex, caseSensitive) {
|
|
38
|
-
try {
|
|
39
|
-
if (isRegex) {
|
|
40
|
-
const flags = caseSensitive ? '' : 'i';
|
|
41
|
-
const regex = new RE2(pattern, flags);
|
|
42
|
-
const match = regex.exec(content);
|
|
36
|
+
if (matcher) {
|
|
37
|
+
matcher.lastIndex = 0;
|
|
38
|
+
const match = matcher.exec(content);
|
|
43
39
|
return match ? match.index : undefined;
|
|
44
40
|
}
|
|
45
41
|
if (caseSensitive) {
|
|
@@ -55,19 +51,21 @@ function findColumnOffset(content, pattern, isRegex, caseSensitive) {
|
|
|
55
51
|
return undefined;
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
|
-
function buildSearchTextResult(
|
|
59
|
-
const { summary } = result;
|
|
54
|
+
function buildSearchTextResult(heading, normalizedMatches, summary) {
|
|
60
55
|
if (normalizedMatches.length === 0)
|
|
61
56
|
return 'No matches';
|
|
62
|
-
|
|
63
|
-
if (summary
|
|
64
|
-
|
|
57
|
+
const text = buildMatchListText(heading, normalizedMatches);
|
|
58
|
+
if (!summary) {
|
|
59
|
+
return text;
|
|
65
60
|
}
|
|
61
|
+
const truncatedReason = summary.truncated
|
|
62
|
+
? resolveTruncatedReason(summary)
|
|
63
|
+
: undefined;
|
|
66
64
|
const summaryOptions = {
|
|
67
65
|
truncated: summary.truncated,
|
|
68
66
|
...(truncatedReason ? { truncatedReason } : {}),
|
|
69
67
|
};
|
|
70
|
-
return
|
|
68
|
+
return text + formatOperationSummary(summaryOptions);
|
|
71
69
|
}
|
|
72
70
|
function resolveTruncatedReason(summary) {
|
|
73
71
|
if (summary.stoppedReason === 'timeout')
|
|
@@ -85,7 +83,7 @@ function buildMatchListText(heading, matches) {
|
|
|
85
83
|
return joinLines(lines);
|
|
86
84
|
}
|
|
87
85
|
function buildSearchMatchPayload(match, context) {
|
|
88
|
-
const column = findColumnOffset(match.content, context.pattern, context.
|
|
86
|
+
const column = findColumnOffset(match.content, context.pattern, context.matcher, context.caseSensitive);
|
|
89
87
|
return {
|
|
90
88
|
file: match.relativeFile,
|
|
91
89
|
line: match.line,
|
|
@@ -100,9 +98,10 @@ function formatSearchMatchLine(match) {
|
|
|
100
98
|
const lineNum = String(match.line).padStart(4);
|
|
101
99
|
return ` ${match.relativeFile}:${lineNum}: ${match.content}`;
|
|
102
100
|
}
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
function buildSearchMatchPayloads(matches, context) {
|
|
102
|
+
return matches.map((match) => buildSearchMatchPayload(match, context));
|
|
103
|
+
}
|
|
104
|
+
function buildStructuredSearchResult(summary, matches, options) {
|
|
106
105
|
return {
|
|
107
106
|
ok: true,
|
|
108
107
|
patternType: options.patternType,
|
|
@@ -155,8 +154,15 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
155
154
|
const basePath = resolvePathOrRoot(args.path);
|
|
156
155
|
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
157
156
|
const patternType = args.isRegex ? 'regex' : 'literal';
|
|
157
|
+
let regexMatcher;
|
|
158
158
|
if (args.isRegex) {
|
|
159
|
-
|
|
159
|
+
try {
|
|
160
|
+
const flags = (args.caseSensitive ? '' : 'i') + (args.multiline ? 'm' : '');
|
|
161
|
+
regexMatcher = new RE2(args.pattern, flags);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
165
|
+
}
|
|
160
166
|
}
|
|
161
167
|
const options = {
|
|
162
168
|
includeHidden: args.includeHidden,
|
|
@@ -188,19 +194,20 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
188
194
|
const normalizedMatches = normalizeSearchMatches(result);
|
|
189
195
|
const searchContext = {
|
|
190
196
|
pattern: args.pattern,
|
|
191
|
-
|
|
197
|
+
matcher: regexMatcher,
|
|
192
198
|
caseSensitive: args.caseSensitive,
|
|
193
199
|
};
|
|
194
|
-
const
|
|
200
|
+
const matchPayloads = buildSearchMatchPayloads(normalizedMatches, searchContext);
|
|
201
|
+
const structuredFull = buildStructuredSearchResult(result.summary, matchPayloads, {
|
|
195
202
|
patternType,
|
|
196
203
|
caseSensitive: args.caseSensitive,
|
|
197
|
-
}
|
|
204
|
+
});
|
|
198
205
|
const needsExternalize = normalizedMatches.length > MAX_INLINE_MATCHES;
|
|
199
206
|
if (!resourceStore || !needsExternalize) {
|
|
200
|
-
return buildToolResponse(buildSearchTextResult(
|
|
207
|
+
return buildToolResponse(buildSearchTextResult(`Found ${normalizedMatches.length}:`, normalizedMatches, result.summary), structuredFull);
|
|
201
208
|
}
|
|
202
209
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
203
|
-
const previewPayload = previewMatches
|
|
210
|
+
const previewPayload = buildSearchMatchPayloads(previewMatches, searchContext);
|
|
204
211
|
const previewStructured = {
|
|
205
212
|
...structuredFull,
|
|
206
213
|
matches: previewPayload,
|
|
@@ -213,7 +220,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
213
220
|
text: JSON.stringify(structuredFull),
|
|
214
221
|
});
|
|
215
222
|
previewStructured.resourceUri = entry.uri;
|
|
216
|
-
const text =
|
|
223
|
+
const text = buildSearchTextResult(`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`, previewMatches);
|
|
217
224
|
return buildToolResponse(text, previewStructured, [
|
|
218
225
|
buildResourceLink({
|
|
219
226
|
uri: entry.uri,
|
|
@@ -263,22 +270,6 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
263
270
|
},
|
|
264
271
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
265
272
|
});
|
|
266
|
-
function buildCompletionSuffix(params) {
|
|
267
|
-
if (params.count === 0) {
|
|
268
|
-
return `No matches in ${params.scope}`;
|
|
269
|
-
}
|
|
270
|
-
const matchWord = params.count === 1 ? 'match' : 'matches';
|
|
271
|
-
const fileWord = params.filesMatched === 1 ? 'file' : 'files';
|
|
272
|
-
const reasonLabels = {
|
|
273
|
-
timeout: 'timeout',
|
|
274
|
-
maxResults: 'max results',
|
|
275
|
-
maxFiles: 'max files',
|
|
276
|
-
};
|
|
277
|
-
const reasonSuffix = params.stoppedReason !== undefined
|
|
278
|
-
? ` [${reasonLabels[params.stoppedReason]}]`
|
|
279
|
-
: '';
|
|
280
|
-
return `${params.count} ${matchWord} in ${params.filesMatched} ${fileWord}${reasonSuffix}`;
|
|
281
|
-
}
|
|
282
273
|
const { isInitialized } = options;
|
|
283
274
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
284
275
|
guard: isInitialized,
|
|
@@ -288,3 +279,14 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
288
279
|
return;
|
|
289
280
|
server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), validatedHandler);
|
|
290
281
|
}
|
|
282
|
+
function buildCompletionSuffix(params) {
|
|
283
|
+
if (params.count === 0) {
|
|
284
|
+
return `No matches in ${params.scope}`;
|
|
285
|
+
}
|
|
286
|
+
const matchWord = params.count === 1 ? 'match' : 'matches';
|
|
287
|
+
const fileWord = params.filesMatched === 1 ? 'file' : 'files';
|
|
288
|
+
const reasonSuffix = params.stoppedReason !== undefined
|
|
289
|
+
? ` [${SEARCH_COMPLETION_REASON_LABELS[params.stoppedReason]}]`
|
|
290
|
+
: '';
|
|
291
|
+
return `${params.count} ${matchWord} in ${params.filesMatched} ${fileWord}${reasonSuffix}`;
|
|
292
|
+
}
|