@j0hanz/filesystem-mcp 1.7.2 → 1.8.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/file-operations/common.d.ts +42 -0
- package/dist/lib/file-operations/common.js +87 -0
- package/dist/lib/file-operations/file-info.js +13 -19
- package/dist/lib/file-operations/glob-engine.d.ts +1 -6
- package/dist/lib/file-operations/glob-engine.js +0 -9
- package/dist/lib/file-operations/glob-helpers.d.ts +18 -0
- package/dist/lib/file-operations/glob-helpers.js +23 -0
- package/dist/lib/file-operations/list-directory.js +22 -46
- package/dist/lib/file-operations/read-multiple-files.js +11 -19
- package/dist/lib/file-operations/search-content.d.ts +1 -8
- package/dist/lib/file-operations/search-content.js +126 -204
- package/dist/lib/file-operations/search-files.js +48 -94
- package/dist/lib/file-operations/search-matcher.d.ts +10 -0
- package/dist/lib/file-operations/search-matcher.js +72 -0
- package/dist/lib/file-operations/search-worker.js +3 -1
- package/dist/lib/file-operations/tree.d.ts +2 -2
- package/dist/lib/file-operations/tree.js +26 -42
- package/dist/lib/fs-helpers.d.ts +1 -0
- package/dist/lib/fs-helpers.js +9 -0
- package/dist/lib/option-utils.d.ts +3 -0
- package/dist/lib/option-utils.js +15 -0
- package/dist/lib/path-validation.d.ts +1 -0
- package/dist/lib/path-validation.js +7 -0
- package/dist/lib/progress-reporting.d.ts +11 -0
- package/dist/lib/progress-reporting.js +13 -0
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +14 -14
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +5 -5
- package/dist/resources/workflows.js +17 -17
- package/dist/schemas.js +21 -90
- package/dist/server/bootstrap.js +2 -2
- package/dist/tools/apply-patch.js +3 -0
- package/dist/tools/calculate-hash.js +12 -12
- package/dist/tools/delete-file.js +5 -2
- package/dist/tools/list-directory.js +3 -21
- package/dist/tools/read-multiple.js +11 -21
- package/dist/tools/replace-in-files.js +10 -11
- package/dist/tools/search-content.js +2 -2
- package/dist/tools/search-files.js +3 -21
- package/dist/tools/shared.d.ts +19 -0
- package/dist/tools/shared.js +64 -18
- package/dist/tools/stat-many.js +11 -22
- package/package.json +6 -2
package/dist/schemas.js
CHANGED
|
@@ -18,6 +18,9 @@ function isSafeGlobPattern(value) {
|
|
|
18
18
|
const MAX_PATH_LENGTH = 4096;
|
|
19
19
|
const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots exist. Examples: "src", "src/components"';
|
|
20
20
|
const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
|
|
21
|
+
function defaultFalseBoolean(description) {
|
|
22
|
+
return z.boolean().optional().default(false).describe(description);
|
|
23
|
+
}
|
|
21
24
|
const PathSchemaBase = z
|
|
22
25
|
.string()
|
|
23
26
|
.max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
|
|
@@ -109,16 +112,8 @@ const OperationSummarySchema = z.strictObject({
|
|
|
109
112
|
});
|
|
110
113
|
export const ListDirectoryInputSchema = z.strictObject({
|
|
111
114
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
112
|
-
includeHidden:
|
|
113
|
-
|
|
114
|
-
.optional()
|
|
115
|
-
.default(false)
|
|
116
|
-
.describe('Include hidden items (starting with .)'),
|
|
117
|
-
includeIgnored: z
|
|
118
|
-
.boolean()
|
|
119
|
-
.optional()
|
|
120
|
-
.default(false)
|
|
121
|
-
.describe('Include ignored items (node_modules, .git, etc).'),
|
|
115
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
116
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, .git, etc).'),
|
|
122
117
|
maxDepth: z
|
|
123
118
|
.number()
|
|
124
119
|
.int({ error: 'Must be integer' })
|
|
@@ -143,11 +138,7 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
143
138
|
.max(1000, 'Max 1000 chars')
|
|
144
139
|
.optional()
|
|
145
140
|
.describe('Optional glob pattern filter (e.g. "**/*.ts")'),
|
|
146
|
-
includeSymlinkTargets:
|
|
147
|
-
.boolean()
|
|
148
|
-
.optional()
|
|
149
|
-
.default(false)
|
|
150
|
-
.describe('Resolve and include symlink targets in results'),
|
|
141
|
+
includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
|
|
151
142
|
cursor: z
|
|
152
143
|
.string()
|
|
153
144
|
.optional()
|
|
@@ -174,16 +165,8 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
174
165
|
.optional()
|
|
175
166
|
.default(DEFAULT_SEARCH_RESULTS)
|
|
176
167
|
.describe(`Max results (1-${MAX_SEARCH_RESULTS}). Default: ${DEFAULT_SEARCH_RESULTS}`),
|
|
177
|
-
includeIgnored:
|
|
178
|
-
|
|
179
|
-
.optional()
|
|
180
|
-
.default(false)
|
|
181
|
-
.describe('Include ignored items (node_modules, etc).'),
|
|
182
|
-
includeHidden: z
|
|
183
|
-
.boolean()
|
|
184
|
-
.optional()
|
|
185
|
-
.default(false)
|
|
186
|
-
.describe('Include hidden items (starting with .)'),
|
|
168
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
|
|
169
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
187
170
|
sortBy: SearchFilesSortSchema.optional()
|
|
188
171
|
.default('path')
|
|
189
172
|
.describe('Sort by path, name, size, or modified'),
|
|
@@ -217,16 +200,8 @@ export const TreeInputSchema = z.strictObject({
|
|
|
217
200
|
.optional()
|
|
218
201
|
.default(DEFAULT_TREE_ENTRIES)
|
|
219
202
|
.describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
|
|
220
|
-
includeHidden:
|
|
221
|
-
|
|
222
|
-
.optional()
|
|
223
|
-
.default(false)
|
|
224
|
-
.describe('Include hidden items (starting with .)'),
|
|
225
|
-
includeIgnored: z
|
|
226
|
-
.boolean()
|
|
227
|
-
.optional()
|
|
228
|
-
.default(false)
|
|
229
|
-
.describe('Include ignored items. Disables .gitignore.'),
|
|
203
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
204
|
+
includeIgnored: defaultFalseBoolean('Include ignored items. Disables .gitignore.'),
|
|
230
205
|
});
|
|
231
206
|
export const SearchContentInputSchema = z.strictObject({
|
|
232
207
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
@@ -235,21 +210,9 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
235
210
|
.min(1, 'Pattern required')
|
|
236
211
|
.max(1000, 'Max 1000 chars')
|
|
237
212
|
.describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
|
|
238
|
-
isRegex:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.default(false)
|
|
242
|
-
.describe('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
|
|
243
|
-
caseSensitive: z
|
|
244
|
-
.boolean()
|
|
245
|
-
.optional()
|
|
246
|
-
.default(false)
|
|
247
|
-
.describe('Case-sensitive matching (default: false — searches are case-insensitive).'),
|
|
248
|
-
wholeWord: z
|
|
249
|
-
.boolean()
|
|
250
|
-
.optional()
|
|
251
|
-
.default(false)
|
|
252
|
-
.describe('Match whole words only'),
|
|
213
|
+
isRegex: defaultFalseBoolean('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
|
|
214
|
+
caseSensitive: defaultFalseBoolean('Case-sensitive matching (default: false — searches are case-insensitive).'),
|
|
215
|
+
wholeWord: defaultFalseBoolean('Match whole words only'),
|
|
253
216
|
contextLines: z
|
|
254
217
|
.number()
|
|
255
218
|
.int({ error: 'Must be integer' })
|
|
@@ -273,16 +236,8 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
273
236
|
.optional()
|
|
274
237
|
.default('**/*')
|
|
275
238
|
.describe('Glob for candidate files (e.g. "**/*.ts")'),
|
|
276
|
-
includeHidden:
|
|
277
|
-
|
|
278
|
-
.optional()
|
|
279
|
-
.default(false)
|
|
280
|
-
.describe('Include hidden items (starting with .)'),
|
|
281
|
-
includeIgnored: z
|
|
282
|
-
.boolean()
|
|
283
|
-
.optional()
|
|
284
|
-
.default(false)
|
|
285
|
-
.describe('Include ignored items (node_modules, etc).'),
|
|
239
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
240
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
|
|
286
241
|
});
|
|
287
242
|
export const ReadFileInputSchema = z
|
|
288
243
|
.strictObject({
|
|
@@ -511,16 +466,8 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
511
466
|
}))
|
|
512
467
|
.min(1, 'Min 1 edit required')
|
|
513
468
|
.describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
|
|
514
|
-
dryRun:
|
|
515
|
-
|
|
516
|
-
.optional()
|
|
517
|
-
.default(false)
|
|
518
|
-
.describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
519
|
-
ignoreWhitespace: z
|
|
520
|
-
.boolean()
|
|
521
|
-
.optional()
|
|
522
|
-
.default(false)
|
|
523
|
-
.describe('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
469
|
+
dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
470
|
+
ignoreWhitespace: defaultFalseBoolean('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
524
471
|
});
|
|
525
472
|
export const EditFileOutputSchema = z.strictObject({
|
|
526
473
|
ok: z.boolean(),
|
|
@@ -562,16 +509,8 @@ export const MoveFileOutputSchema = z.strictObject({
|
|
|
562
509
|
});
|
|
563
510
|
export const DeleteFileInputSchema = z.strictObject({
|
|
564
511
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
565
|
-
recursive:
|
|
566
|
-
|
|
567
|
-
.optional()
|
|
568
|
-
.default(false)
|
|
569
|
-
.describe('Delete non-empty directories'),
|
|
570
|
-
ignoreIfNotExists: z
|
|
571
|
-
.boolean()
|
|
572
|
-
.optional()
|
|
573
|
-
.default(false)
|
|
574
|
-
.describe('No error if missing'),
|
|
512
|
+
recursive: defaultFalseBoolean('Delete non-empty directories'),
|
|
513
|
+
ignoreIfNotExists: defaultFalseBoolean('No error if missing'),
|
|
575
514
|
});
|
|
576
515
|
export const DeleteFileOutputSchema = z.strictObject({
|
|
577
516
|
ok: z.boolean(),
|
|
@@ -665,16 +604,8 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
665
604
|
.min(1, 'Search pattern required')
|
|
666
605
|
.describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
|
|
667
606
|
replacement: z.string().describe('Replacement text'),
|
|
668
|
-
isRegex:
|
|
669
|
-
|
|
670
|
-
.optional()
|
|
671
|
-
.default(false)
|
|
672
|
-
.describe('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
|
|
673
|
-
dryRun: z
|
|
674
|
-
.boolean()
|
|
675
|
-
.optional()
|
|
676
|
-
.default(false)
|
|
677
|
-
.describe('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
607
|
+
isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
|
|
608
|
+
dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
678
609
|
includeHidden: z
|
|
679
610
|
.boolean()
|
|
680
611
|
.optional()
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -66,8 +66,8 @@ export async function createServer(options = {}) {
|
|
|
66
66
|
if (serverInstructions) {
|
|
67
67
|
serverConfig.instructions =
|
|
68
68
|
'filesystem-mcp: Secure local filesystem MCP server. ' +
|
|
69
|
-
'
|
|
70
|
-
'
|
|
69
|
+
'Start with: roots -> ls/find -> stat -> read. Never guess paths. ' +
|
|
70
|
+
'For full guidance, read internal://instructions or run the get-help prompt.';
|
|
71
71
|
}
|
|
72
72
|
const server = new McpServer(withDefaultIcons({
|
|
73
73
|
name: 'filesystem-mcp',
|
|
@@ -48,6 +48,9 @@ async function handleApplyPatch(args, signal) {
|
|
|
48
48
|
if (patched === false) {
|
|
49
49
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching with the fuzzFactor parameter.');
|
|
50
50
|
}
|
|
51
|
+
if (patched === content) {
|
|
52
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch had no effect — 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.');
|
|
53
|
+
}
|
|
51
54
|
if (args.dryRun) {
|
|
52
55
|
return buildToolResponse('Dry run successful. Patch can be applied.', {
|
|
53
56
|
ok: true,
|
|
@@ -8,8 +8,9 @@ import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations
|
|
|
8
8
|
import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
9
9
|
import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
|
|
10
10
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
11
|
+
import { reportPeriodicProgress } from '../lib/progress-reporting.js';
|
|
11
12
|
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
15
|
const WINDOWS_PATH_SEPARATOR = /\\/gu;
|
|
15
16
|
export const CALCULATE_HASH_TOOL = {
|
|
@@ -54,13 +55,6 @@ function updateCompositeHash(hasher, pathLengthBytes, relativePath, fileHash) {
|
|
|
54
55
|
hasher.update(relativePathBytes);
|
|
55
56
|
hasher.update(fileHash);
|
|
56
57
|
}
|
|
57
|
-
function reportHashProgress(onProgress, current, force = false) {
|
|
58
|
-
if (!onProgress || current === 0)
|
|
59
|
-
return;
|
|
60
|
-
if (!force && current % 25 !== 0)
|
|
61
|
-
return;
|
|
62
|
-
onProgress({ current });
|
|
63
|
-
}
|
|
64
58
|
async function hashDirectory(dirPath, options = {}) {
|
|
65
59
|
const { signal, onProgress } = options;
|
|
66
60
|
const gitignoreMatcher = await loadRootGitignore(dirPath, signal);
|
|
@@ -102,9 +96,12 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
102
96
|
}));
|
|
103
97
|
entries.push(...batchResults);
|
|
104
98
|
filesHashed += batchResults.length;
|
|
105
|
-
|
|
99
|
+
reportPeriodicProgress(onProgress, filesHashed, { throttleModulo: 25 });
|
|
106
100
|
}
|
|
107
|
-
|
|
101
|
+
reportPeriodicProgress(onProgress, filesHashed, {
|
|
102
|
+
throttleModulo: 25,
|
|
103
|
+
force: true,
|
|
104
|
+
});
|
|
108
105
|
assertNotAborted(signal);
|
|
109
106
|
// Sort by path with byte-wise semantics for deterministic ordering.
|
|
110
107
|
entries.sort(comparePaths);
|
|
@@ -141,7 +138,10 @@ async function handleCalculateHash(args, signal, onProgress) {
|
|
|
141
138
|
else {
|
|
142
139
|
// Hash single file
|
|
143
140
|
const hash = await hashFile(validPath, 'hex', signal);
|
|
144
|
-
|
|
141
|
+
reportPeriodicProgress(onProgress, 1, {
|
|
142
|
+
throttleModulo: 25,
|
|
143
|
+
force: true,
|
|
144
|
+
});
|
|
145
145
|
return buildToolResponse(hash, {
|
|
146
146
|
ok: true,
|
|
147
147
|
path: validPath,
|
|
@@ -171,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
171
171
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
172
172
|
const sc = result.structuredContent;
|
|
173
173
|
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
174
|
-
const finalCurrent =
|
|
174
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, totalFiles + 1);
|
|
175
175
|
let suffix;
|
|
176
176
|
if (!sc.ok) {
|
|
177
177
|
suffix = 'failed';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
3
|
+
import { ErrorCode, isNodeError, McpError } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
|
-
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
5
|
+
import { isAllowedDirectoryRoot, validatePathForWrite, } from '../lib/path-validation.js';
|
|
6
6
|
import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -20,6 +20,9 @@ export const DELETE_FILE_TOOL = {
|
|
|
20
20
|
};
|
|
21
21
|
async function handleDeleteFile(args, signal) {
|
|
22
22
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
23
|
+
if (isAllowedDirectoryRoot(validPath)) {
|
|
24
|
+
throw new McpError(ErrorCode.E_ACCESS_DENIED, `Deleting a workspace root directory is not allowed: ${args.path}`);
|
|
25
|
+
}
|
|
23
26
|
let stats;
|
|
24
27
|
try {
|
|
25
28
|
stats = await withAbort(fs.lstat(validPath), signal);
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { listDirectory } from '../lib/file-operations/list-directory.js';
|
|
6
6
|
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const LIST_DIRECTORY_TOOL = {
|
|
10
10
|
name: 'ls',
|
|
@@ -82,27 +82,9 @@ function buildStructuredListResult(result, nextCursor) {
|
|
|
82
82
|
...(nextCursor !== undefined ? { nextCursor } : {}),
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
function encodeCursor(offset) {
|
|
86
|
-
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
87
|
-
}
|
|
88
|
-
function decodeCursor(cursor) {
|
|
89
|
-
try {
|
|
90
|
-
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
91
|
-
if (typeof parsed === 'object' &&
|
|
92
|
-
parsed !== null &&
|
|
93
|
-
typeof parsed.offset === 'number') {
|
|
94
|
-
const { offset } = parsed;
|
|
95
|
-
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// ignore malformed cursor
|
|
100
|
-
}
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
85
|
async function handleListDirectory(args, signal) {
|
|
104
86
|
const dirPath = resolvePathOrRoot(args.path);
|
|
105
|
-
const cursorOffset = args.cursor !== undefined ?
|
|
87
|
+
const cursorOffset = args.cursor !== undefined ? decodeOffsetCursor(args.cursor) : 0;
|
|
106
88
|
const pageSize = args.maxEntries;
|
|
107
89
|
const options = {
|
|
108
90
|
includeHidden: args.includeHidden,
|
|
@@ -117,7 +99,7 @@ async function handleListDirectory(args, signal) {
|
|
|
117
99
|
const result = await listDirectory(dirPath, options);
|
|
118
100
|
const displayEntries = cursorOffset > 0 ? result.entries.slice(cursorOffset) : result.entries;
|
|
119
101
|
const nextCursor = result.summary.truncated && displayEntries.length > 0
|
|
120
|
-
?
|
|
102
|
+
? encodeOffsetCursor(cursorOffset + displayEntries.length)
|
|
121
103
|
: undefined;
|
|
122
104
|
const displayResult = { ...result, entries: displayEntries };
|
|
123
105
|
return buildToolResponse(buildListTextResult(displayResult, nextCursor), buildStructuredListResult(displayResult, nextCursor));
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '..
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse,
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const READ_MULTIPLE_FILES_TOOL = {
|
|
9
9
|
name: 'read_many',
|
|
@@ -134,29 +134,19 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
134
134
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
135
135
|
context: { path: primaryPath },
|
|
136
136
|
run: async (signal) => {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
progress.increment((current) => `🕮 read_many: ${context} [${current}/${args.paths.length} read]`);
|
|
145
|
-
};
|
|
137
|
+
const context = buildBatchPathContext(args.paths, 'files');
|
|
138
|
+
const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
|
|
139
|
+
toolLabel: '🕮 read_many',
|
|
140
|
+
context,
|
|
141
|
+
totalItems: args.paths.length,
|
|
142
|
+
itemVerb: 'read',
|
|
143
|
+
});
|
|
146
144
|
try {
|
|
147
|
-
const result = await handleReadMultipleFiles(args, signal, options.resourceStore,
|
|
145
|
+
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onItemComplete);
|
|
148
146
|
const sc = result.structuredContent;
|
|
147
|
+
const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
|
|
149
148
|
const total = sc.summary?.total ?? 0;
|
|
150
|
-
const
|
|
151
|
-
const succeeded = sc.summary?.succeeded ?? 0;
|
|
152
|
-
let suffix;
|
|
153
|
-
if (failed) {
|
|
154
|
-
suffix = `${succeeded}/${total} read, ${failed} failed`;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
suffix = `${total} files read`;
|
|
158
|
-
}
|
|
159
|
-
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
149
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, total);
|
|
160
150
|
progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
|
|
161
151
|
return result;
|
|
162
152
|
}
|
|
@@ -8,8 +8,9 @@ import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.j
|
|
|
8
8
|
import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
9
9
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
10
10
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
11
|
+
import { reportPeriodicProgress } from '../lib/progress-reporting.js';
|
|
11
12
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
15
|
export const SEARCH_AND_REPLACE_TOOL = {
|
|
15
16
|
name: 'search_and_replace',
|
|
@@ -204,13 +205,6 @@ function createReplacementMatcher(args) {
|
|
|
204
205
|
}
|
|
205
206
|
return createLiteralReplacementMatcher(args.searchPattern);
|
|
206
207
|
}
|
|
207
|
-
function reportReplaceProgress(onProgress, current, force = false) {
|
|
208
|
-
if (current === 0)
|
|
209
|
-
return;
|
|
210
|
-
if (!force && current % 25 !== 0)
|
|
211
|
-
return;
|
|
212
|
-
onProgress({ current });
|
|
213
|
-
}
|
|
214
208
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
215
209
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
216
210
|
const root = await resolveSearchRoot(args.path, signal);
|
|
@@ -233,14 +227,19 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
233
227
|
concurrency: REPLACE_CONCURRENCY,
|
|
234
228
|
onEntry: () => {
|
|
235
229
|
summary.processedFiles++;
|
|
236
|
-
|
|
230
|
+
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
231
|
+
throttleModulo: 25,
|
|
232
|
+
});
|
|
237
233
|
},
|
|
238
234
|
runEntry: async (entryPath) => processEntry(entryPath, {
|
|
239
235
|
dryRun: args.dryRun,
|
|
240
236
|
returnDiff: args.returnDiff ?? false,
|
|
241
237
|
}, args.replacement, matcher, maxFileSize, signal, summary),
|
|
242
238
|
});
|
|
243
|
-
|
|
239
|
+
reportPeriodicProgress(onProgress, summary.processedFiles, {
|
|
240
|
+
throttleModulo: 25,
|
|
241
|
+
force: true,
|
|
242
|
+
});
|
|
244
243
|
const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
|
|
245
244
|
return buildToolResponse(`Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${args.dryRun ? ' (Dry run)' : ''}`, {
|
|
246
245
|
ok: true,
|
|
@@ -279,7 +278,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
279
278
|
try {
|
|
280
279
|
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
281
280
|
const sc = result.structuredContent;
|
|
282
|
-
const finalCurrent =
|
|
281
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.processedFiles ?? 0) + 1);
|
|
283
282
|
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
284
283
|
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
285
284
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
@@ -5,7 +5,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
6
|
import { searchContent } from '../lib/file-operations/search-content.js';
|
|
7
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
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
11
|
export const SEARCH_CONTENT_TOOL = {
|
|
@@ -242,7 +242,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
242
242
|
suffix += ' [truncated — max files]';
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
const finalCurrent =
|
|
245
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
246
246
|
progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
|
|
247
247
|
return result;
|
|
248
248
|
}
|
|
@@ -4,26 +4,8 @@ import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/con
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { searchFiles } from '../lib/file-operations/search-files.js';
|
|
6
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
|
-
function encodeCursor(offset) {
|
|
10
|
-
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
11
|
-
}
|
|
12
|
-
function decodeCursor(cursor) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
15
|
-
if (typeof parsed === 'object' &&
|
|
16
|
-
parsed !== null &&
|
|
17
|
-
typeof parsed.offset === 'number') {
|
|
18
|
-
const { offset } = parsed;
|
|
19
|
-
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// ignore malformed cursor
|
|
24
|
-
}
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
9
|
export const SEARCH_FILES_TOOL = {
|
|
28
10
|
name: 'find',
|
|
29
11
|
title: 'Find Files',
|
|
@@ -43,7 +25,7 @@ export const SEARCH_FILES_TOOL = {
|
|
|
43
25
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
44
26
|
const basePath = resolvePathOrRoot(args.path);
|
|
45
27
|
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
46
|
-
const cursorOffset = args.cursor !== undefined ?
|
|
28
|
+
const cursorOffset = args.cursor !== undefined ? decodeOffsetCursor(args.cursor) : 0;
|
|
47
29
|
const pageSize = args.maxResults;
|
|
48
30
|
const fetchMax = cursorOffset + pageSize;
|
|
49
31
|
const searchOptions = {
|
|
@@ -59,7 +41,7 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
59
41
|
const allResults = result.results;
|
|
60
42
|
const displayResults = cursorOffset > 0 ? allResults.slice(cursorOffset) : allResults;
|
|
61
43
|
const nextCursor = result.summary.truncated && displayResults.length > 0
|
|
62
|
-
?
|
|
44
|
+
? encodeOffsetCursor(cursorOffset + displayResults.length)
|
|
63
45
|
: undefined;
|
|
64
46
|
const relativeResults = [];
|
|
65
47
|
for (const entry of displayResults) {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -123,7 +123,18 @@ export interface ToolProgressSession {
|
|
|
123
123
|
fail: (message: string, minimumCurrent?: number) => void;
|
|
124
124
|
getCurrent: () => number;
|
|
125
125
|
}
|
|
126
|
+
export interface BatchProgressCallbacks {
|
|
127
|
+
progress: ToolProgressSession;
|
|
128
|
+
onItemComplete: () => void;
|
|
129
|
+
}
|
|
126
130
|
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
|
|
131
|
+
export declare function createBatchProgressCallbacks(extra: ToolExtra, params: {
|
|
132
|
+
toolLabel: string;
|
|
133
|
+
context: string;
|
|
134
|
+
totalItems: number;
|
|
135
|
+
itemVerb: string;
|
|
136
|
+
}): BatchProgressCallbacks;
|
|
137
|
+
export declare function resolveFinalProgressCurrent(progress: ToolProgressSession, ...candidates: number[]): number;
|
|
127
138
|
export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>, options: {
|
|
128
139
|
guard?: (() => boolean) | undefined;
|
|
129
140
|
progressMessage?: (args: Args) => string;
|
|
@@ -139,3 +150,11 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
|
|
|
139
150
|
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
140
151
|
*/
|
|
141
152
|
export declare function resolvePathOrRoot(pathValue: string | undefined): string;
|
|
153
|
+
export declare function encodeOffsetCursor(offset: number): string;
|
|
154
|
+
export declare function decodeOffsetCursor(cursor: string): number;
|
|
155
|
+
export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
|
|
156
|
+
export declare function buildBatchCompletionSuffix(summary: {
|
|
157
|
+
total?: number;
|
|
158
|
+
failed?: number;
|
|
159
|
+
succeeded?: number;
|
|
160
|
+
} | undefined, successWord: string, singularWord?: string): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import { channel } from 'node:diagnostics_channel';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { parseTrueEnvFlag } from '../lib/constants.js';
|
|
@@ -292,6 +293,15 @@ export function createToolProgressSession(extra, startMessage) {
|
|
|
292
293
|
cursor = value;
|
|
293
294
|
return cursor;
|
|
294
295
|
};
|
|
296
|
+
const finishProgress = (message, minimumCurrent) => {
|
|
297
|
+
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
298
|
+
notifyProgress(extra, {
|
|
299
|
+
current: finalCurrent,
|
|
300
|
+
total: finalCurrent,
|
|
301
|
+
...(message !== undefined ? { message } : {}),
|
|
302
|
+
});
|
|
303
|
+
cursor = finalCurrent;
|
|
304
|
+
};
|
|
295
305
|
return {
|
|
296
306
|
update: ({ current, total, message }) => {
|
|
297
307
|
const normalized = setCursor(current);
|
|
@@ -308,27 +318,27 @@ export function createToolProgressSession(extra, startMessage) {
|
|
|
308
318
|
message: messageForCurrent(next),
|
|
309
319
|
});
|
|
310
320
|
},
|
|
311
|
-
complete:
|
|
312
|
-
|
|
313
|
-
notifyProgress(extra, {
|
|
314
|
-
current: finalCurrent,
|
|
315
|
-
total: finalCurrent,
|
|
316
|
-
message,
|
|
317
|
-
});
|
|
318
|
-
cursor = finalCurrent;
|
|
319
|
-
},
|
|
320
|
-
fail: (message, minimumCurrent) => {
|
|
321
|
-
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
322
|
-
notifyProgress(extra, {
|
|
323
|
-
current: finalCurrent,
|
|
324
|
-
total: finalCurrent,
|
|
325
|
-
message,
|
|
326
|
-
});
|
|
327
|
-
cursor = finalCurrent;
|
|
328
|
-
},
|
|
321
|
+
complete: finishProgress,
|
|
322
|
+
fail: finishProgress,
|
|
329
323
|
getCurrent: () => cursor,
|
|
330
324
|
};
|
|
331
325
|
}
|
|
326
|
+
export function createBatchProgressCallbacks(extra, params) {
|
|
327
|
+
const progress = createToolProgressSession(extra, `${params.toolLabel}: ${params.context}`);
|
|
328
|
+
const onItemComplete = () => {
|
|
329
|
+
progress.increment((current) => `${params.toolLabel}: ${params.context} [${current}/${params.totalItems} ${params.itemVerb}]`);
|
|
330
|
+
};
|
|
331
|
+
return { progress, onItemComplete };
|
|
332
|
+
}
|
|
333
|
+
export function resolveFinalProgressCurrent(progress, ...candidates) {
|
|
334
|
+
let finalCurrent = progress.getCurrent() + 1;
|
|
335
|
+
for (const candidate of candidates) {
|
|
336
|
+
if (candidate > finalCurrent) {
|
|
337
|
+
finalCurrent = candidate;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return finalCurrent;
|
|
341
|
+
}
|
|
332
342
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
333
343
|
if (!canReportProgress(extra)) {
|
|
334
344
|
return run();
|
|
@@ -403,3 +413,39 @@ export function resolvePathOrRoot(pathValue) {
|
|
|
403
413
|
}
|
|
404
414
|
return root;
|
|
405
415
|
}
|
|
416
|
+
export function encodeOffsetCursor(offset) {
|
|
417
|
+
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
418
|
+
}
|
|
419
|
+
export function decodeOffsetCursor(cursor) {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
422
|
+
if (typeof parsed === 'object' &&
|
|
423
|
+
parsed !== null &&
|
|
424
|
+
typeof parsed.offset === 'number') {
|
|
425
|
+
const { offset } = parsed;
|
|
426
|
+
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// ignore malformed cursor
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
export function buildBatchPathContext(paths, unitLabel = 'paths') {
|
|
435
|
+
const normalizedLabel = paths.length === 1 ? unitLabel.replace(/s$/i, '') : unitLabel;
|
|
436
|
+
const first = path.basename(paths[0] ?? '');
|
|
437
|
+
const extraPaths = paths.length > 1
|
|
438
|
+
? `, ${path.basename(paths[1] ?? '')}${paths.length > 2 ? '…' : ''}`
|
|
439
|
+
: '';
|
|
440
|
+
return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
|
|
441
|
+
}
|
|
442
|
+
export function buildBatchCompletionSuffix(summary, successWord, singularWord) {
|
|
443
|
+
const total = summary?.total ?? 0;
|
|
444
|
+
const failed = summary?.failed ?? 0;
|
|
445
|
+
const succeeded = summary?.succeeded ?? 0;
|
|
446
|
+
if (failed) {
|
|
447
|
+
return `${succeeded}/${total} ${successWord}, ${failed} failed`;
|
|
448
|
+
}
|
|
449
|
+
const word = total === 1 && singularWord ? singularWord : successWord;
|
|
450
|
+
return `${total} ${word}`;
|
|
451
|
+
}
|