@j0hanz/filesystem-mcp 1.6.2 → 1.7.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/README.md +19 -17
- package/dist/lib/errors.js +20 -16
- package/dist/lib/file-operations/glob-engine.js +37 -20
- package/dist/lib/fs-helpers.js +7 -8
- package/dist/lib/path-policy.js +30 -14
- package/dist/resources/generated-instructions.js +4 -0
- package/dist/tools/calculate-hash.js +6 -22
- package/dist/tools/read-multiple.js +34 -43
- package/dist/tools/replace-in-files.js +52 -56
- package/dist/tools/search-content.js +6 -27
- package/dist/tools/shared.d.ts +12 -0
- package/dist/tools/shared.js +49 -0
- package/dist/tools/stat-many.js +6 -24
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -474,10 +474,14 @@ Replace text in all files matching a glob. Replaces **all** occurrences per file
|
|
|
474
474
|
|
|
475
475
|
### Resources
|
|
476
476
|
|
|
477
|
-
| URI | Description
|
|
478
|
-
| :----------------------------- |
|
|
479
|
-
| `internal://instructions` | Usage guidance for models
|
|
480
|
-
| `
|
|
477
|
+
| URI | Description | MIME Type |
|
|
478
|
+
| :----------------------------- | :---------------------------------- | :----------------- |
|
|
479
|
+
| `internal://instructions` | Usage guidance for models | `text/markdown` |
|
|
480
|
+
| `internal://tool-catalog` | Tool routing and data-flow guide | `text/markdown` |
|
|
481
|
+
| `internal://workflows` | Explore/search/edit/patch workflows | `text/markdown` |
|
|
482
|
+
| `internal://tool-info/{name}` | Per-tool nuances and gotchas | `text/markdown` |
|
|
483
|
+
| `filesystem-mcp://metrics` | Live per-tool metrics snapshot | `application/json` |
|
|
484
|
+
| `filesystem-mcp://result/{id}` | Ephemeral cached large tool output | varies |
|
|
481
485
|
|
|
482
486
|
When a tool response includes a `resource_link`/`resourceUri`, treat it as authoritative for full payload retrieval and call `resources/read` with that URI.
|
|
483
487
|
|
|
@@ -565,7 +569,7 @@ Set `FS_CONTEXT_STRIP_STRUCTURED=1` to strip `structuredContent` from tool resul
|
|
|
565
569
|
- **Input limits**: Paths are bounded to 4,096 characters; patterns to 1,000 characters.
|
|
566
570
|
- **Atomic writes**: File writes use an atomic write-then-rename strategy to prevent partial writes.
|
|
567
571
|
- **Docker**: The container runs as a non-root user (`mcp`).
|
|
568
|
-
- **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `
|
|
572
|
+
- **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_API_KEY` configured.
|
|
569
573
|
|
|
570
574
|
> [!IMPORTANT]
|
|
571
575
|
> All diagnostic output goes to `stderr`. Tool handlers must never write to `stdout`, as doing so would corrupt the stdio transport.
|
|
@@ -580,18 +584,16 @@ npm ci
|
|
|
580
584
|
|
|
581
585
|
### Scripts
|
|
582
586
|
|
|
583
|
-
| Script
|
|
584
|
-
|
|
|
585
|
-
| `dev`
|
|
586
|
-
| `dev:run`
|
|
587
|
-
| `build`
|
|
588
|
-
| `test`
|
|
589
|
-
| `
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `
|
|
593
|
-
| `format` | `prettier --write .` | Format all files |
|
|
594
|
-
| `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
|
|
587
|
+
| Script | Command | Purpose |
|
|
588
|
+
| :----------- | :---------------------------------- | :---------------------------------- |
|
|
589
|
+
| `dev` | `tsc --watch` | Watch-mode TypeScript compilation |
|
|
590
|
+
| `dev:run` | `node --watch dist/index.js` | Run built server with file watching |
|
|
591
|
+
| `build` | `node scripts/tasks.mjs build` | Production build |
|
|
592
|
+
| `test` | `node scripts/tasks.mjs test` | Run full test suite |
|
|
593
|
+
| `lint` | `eslint .` | Lint source files |
|
|
594
|
+
| `lint:fix` | `eslint . --fix` | Auto-fix lint issues |
|
|
595
|
+
| `format` | `prettier --write .` | Format all files |
|
|
596
|
+
| `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
|
|
595
597
|
|
|
596
598
|
### MCP Inspector
|
|
597
599
|
|
package/dist/lib/errors.js
CHANGED
|
@@ -125,15 +125,16 @@ function walkErrorChain(error, visitor) {
|
|
|
125
125
|
let current = error;
|
|
126
126
|
const visited = new Set();
|
|
127
127
|
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const visitedResult = visitor(current);
|
|
129
|
+
if (visitedResult !== undefined)
|
|
130
|
+
return visitedResult;
|
|
130
131
|
if (!isNativeError(current))
|
|
131
132
|
break;
|
|
132
133
|
visited.add(current);
|
|
133
134
|
const next = current.cause;
|
|
134
135
|
current = next;
|
|
135
136
|
}
|
|
136
|
-
return
|
|
137
|
+
return undefined;
|
|
137
138
|
}
|
|
138
139
|
function isAbortErrorSingle(error) {
|
|
139
140
|
if (!isNativeError(error))
|
|
@@ -144,7 +145,7 @@ function isAbortErrorSingle(error) {
|
|
|
144
145
|
return code === 'ABORT_ERR';
|
|
145
146
|
}
|
|
146
147
|
export function isAbortError(error) {
|
|
147
|
-
return walkErrorChain(error, isAbortErrorSingle);
|
|
148
|
+
return (walkErrorChain(error, (candidate) => isAbortErrorSingle(candidate) ? true : undefined) === true);
|
|
148
149
|
}
|
|
149
150
|
function isTimeoutErrorSingle(error) {
|
|
150
151
|
if (!isNativeError(error))
|
|
@@ -158,7 +159,7 @@ function isTimeoutErrorSingle(error) {
|
|
|
158
159
|
return message.includes('timed out') || message.includes('timeout');
|
|
159
160
|
}
|
|
160
161
|
export function isTimeoutLikeError(error) {
|
|
161
|
-
return walkErrorChain(error, isTimeoutErrorSingle);
|
|
162
|
+
return (walkErrorChain(error, (candidate) => isTimeoutErrorSingle(candidate) ? true : undefined) === true);
|
|
162
163
|
}
|
|
163
164
|
export class McpError extends Error {
|
|
164
165
|
code;
|
|
@@ -224,17 +225,20 @@ function classifyMessageError(error) {
|
|
|
224
225
|
return undefined;
|
|
225
226
|
}
|
|
226
227
|
function classifyError(error) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
228
|
+
let timeoutCode;
|
|
229
|
+
let fallbackCode;
|
|
230
|
+
const terminalCode = walkErrorChain(error, (candidate) => {
|
|
231
|
+
if (isAbortErrorSingle(candidate)) {
|
|
232
|
+
return ErrorCode.E_CANCELLED;
|
|
233
|
+
}
|
|
234
|
+
if (timeoutCode === undefined && isTimeoutErrorSingle(candidate)) {
|
|
235
|
+
timeoutCode = ErrorCode.E_TIMEOUT;
|
|
236
|
+
}
|
|
237
|
+
fallbackCode ??=
|
|
238
|
+
getDirectErrorCode(candidate) ?? classifyMessageError(candidate);
|
|
239
|
+
return undefined;
|
|
240
|
+
});
|
|
241
|
+
return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.E_UNKNOWN;
|
|
238
242
|
}
|
|
239
243
|
export function createDetailedError(error, path, additionalDetails) {
|
|
240
244
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -106,40 +106,57 @@ function buildHiddenPatterns(normalizedPattern, maxDepth) {
|
|
|
106
106
|
function shouldUseGlobDirents(options) {
|
|
107
107
|
return !options.stats && !options.followSymbolicLinks;
|
|
108
108
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new TypeError('globEntries: options must be an object');
|
|
113
|
-
}
|
|
114
|
-
const o = unknownOptions;
|
|
115
|
-
if (typeof o.cwd !== 'string') {
|
|
116
|
-
throw new TypeError('globEntries: options.cwd must be a string');
|
|
109
|
+
function assertOptionString(options, key) {
|
|
110
|
+
if (typeof options[key] !== 'string') {
|
|
111
|
+
throw new TypeError(`globEntries: options.${key} must be a string`);
|
|
117
112
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!Array.isArray(o.excludePatterns)) {
|
|
113
|
+
}
|
|
114
|
+
function assertExcludePatternsOption(options) {
|
|
115
|
+
if (!Array.isArray(options.excludePatterns)) {
|
|
122
116
|
throw new TypeError('globEntries: options.excludePatterns must be an array');
|
|
123
117
|
}
|
|
124
|
-
for (const
|
|
125
|
-
if (typeof
|
|
118
|
+
for (const pattern of options.excludePatterns) {
|
|
119
|
+
if (typeof pattern !== 'string') {
|
|
126
120
|
throw new TypeError('globEntries: options.excludePatterns must contain only strings');
|
|
127
121
|
}
|
|
128
122
|
}
|
|
123
|
+
}
|
|
124
|
+
function assertBooleanOptions(options) {
|
|
129
125
|
for (const key of GLOB_BOOLEAN_OPTION_KEYS) {
|
|
130
|
-
if (typeof
|
|
126
|
+
if (typeof options[key] !== 'boolean') {
|
|
131
127
|
throw new TypeError(`globEntries: options.${key} must be a boolean`);
|
|
132
128
|
}
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
}
|
|
131
|
+
function assertOptionalMaxDepth(options) {
|
|
132
|
+
const { maxDepth } = options;
|
|
133
|
+
if (maxDepth === undefined)
|
|
134
|
+
return;
|
|
135
|
+
if (typeof maxDepth !== 'number' || !Number.isFinite(maxDepth)) {
|
|
136
|
+
throw new TypeError('globEntries: options.maxDepth must be a finite number');
|
|
138
137
|
}
|
|
139
|
-
|
|
138
|
+
}
|
|
139
|
+
function assertOptionalSuppressErrors(options) {
|
|
140
|
+
const { suppressErrors } = options;
|
|
141
|
+
if (suppressErrors === undefined)
|
|
142
|
+
return;
|
|
143
|
+
if (typeof suppressErrors !== 'boolean') {
|
|
140
144
|
throw new TypeError('globEntries: options.suppressErrors must be a boolean');
|
|
141
145
|
}
|
|
142
146
|
}
|
|
147
|
+
function assertOptionsShape(options) {
|
|
148
|
+
const unknownOptions = options;
|
|
149
|
+
if (unknownOptions === null || typeof unknownOptions !== 'object') {
|
|
150
|
+
throw new TypeError('globEntries: options must be an object');
|
|
151
|
+
}
|
|
152
|
+
const o = unknownOptions;
|
|
153
|
+
assertOptionString(o, 'cwd');
|
|
154
|
+
assertOptionString(o, 'pattern');
|
|
155
|
+
assertExcludePatternsOption(o);
|
|
156
|
+
assertBooleanOptions(o);
|
|
157
|
+
assertOptionalMaxDepth(o);
|
|
158
|
+
assertOptionalSuppressErrors(o);
|
|
159
|
+
}
|
|
143
160
|
function normalizeOptions(options) {
|
|
144
161
|
const cwd = path.resolve(options.cwd);
|
|
145
162
|
const normalizedPattern = normalizePattern(options.pattern, options.baseNameMatch);
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -120,23 +120,22 @@ function createParallelAbortError() {
|
|
|
120
120
|
return createAbortError();
|
|
121
121
|
}
|
|
122
122
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
123
|
-
|
|
123
|
+
const itemCount = items.length;
|
|
124
|
+
if (itemCount === 0)
|
|
124
125
|
return { results: [], errors: [] };
|
|
125
126
|
const effectiveConcurrency = normalizeConcurrency(concurrency);
|
|
126
127
|
// Pre-allocate slots by index to guarantee input-order output.
|
|
127
|
-
const resultSlots = new Array(
|
|
128
|
+
const resultSlots = new Array(itemCount);
|
|
128
129
|
const errors = [];
|
|
129
130
|
if (signal?.aborted)
|
|
130
131
|
throw createParallelAbortError();
|
|
131
132
|
let nextIndex = 0;
|
|
132
133
|
const next = async () => {
|
|
133
|
-
while (nextIndex <
|
|
134
|
+
while (nextIndex < itemCount) {
|
|
134
135
|
if (signal?.aborted)
|
|
135
136
|
throw createParallelAbortError();
|
|
136
|
-
const index = nextIndex
|
|
137
|
-
|
|
138
|
-
if (index >= items.length)
|
|
139
|
-
break;
|
|
137
|
+
const index = nextIndex;
|
|
138
|
+
nextIndex += 1;
|
|
140
139
|
const item = items[index];
|
|
141
140
|
try {
|
|
142
141
|
const result = await processor(item);
|
|
@@ -154,7 +153,7 @@ export async function processInParallel(items, processor, concurrency = PARALLEL
|
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
};
|
|
157
|
-
const workerCount = Math.min(
|
|
156
|
+
const workerCount = Math.min(itemCount, effectiveConcurrency);
|
|
158
157
|
const workers = new Array(workerCount);
|
|
159
158
|
for (let index = 0; index < workerCount; index += 1) {
|
|
160
159
|
workers[index] = next();
|
package/dist/lib/path-policy.js
CHANGED
|
@@ -36,45 +36,61 @@ function compilePatterns(patterns) {
|
|
|
36
36
|
const normalized = normalizeForMatch(pattern);
|
|
37
37
|
const matchesPath = normalized.includes('/');
|
|
38
38
|
compiled.push({
|
|
39
|
-
raw: normalized,
|
|
40
39
|
globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
|
|
41
40
|
matchesPath,
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
43
|
return compiled;
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
const
|
|
45
|
+
function toPatternSet(patterns) {
|
|
46
|
+
const pathGlobs = new Set();
|
|
47
|
+
const nameGlobs = new Set();
|
|
48
|
+
for (const pattern of patterns) {
|
|
49
|
+
const target = pattern.matchesPath ? pathGlobs : nameGlobs;
|
|
50
|
+
for (const glob of pattern.globs) {
|
|
51
|
+
target.add(glob);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
pathGlobs: [...pathGlobs],
|
|
56
|
+
nameGlobs: [...nameGlobs],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
|
|
60
|
+
const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
|
|
48
61
|
function uniquePair(primary, secondary) {
|
|
49
62
|
if (!secondary || secondary === primary)
|
|
50
63
|
return [primary];
|
|
51
64
|
return [primary, secondary];
|
|
52
65
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
66
|
+
function matchesAnyGlobs(globs, candidates) {
|
|
67
|
+
if (globs.length === 0 || candidates.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
for (const glob of globs) {
|
|
71
|
+
if (path.posix.matchesGlob(candidate, glob))
|
|
72
|
+
return true;
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
return false;
|
|
64
76
|
}
|
|
65
77
|
export function isSensitivePath(requestedPath, resolvedPath) {
|
|
66
|
-
if (DENY_PATTERNS.length === 0
|
|
78
|
+
if (DENY_PATTERNS.pathGlobs.length === 0 &&
|
|
79
|
+
DENY_PATTERNS.nameGlobs.length === 0) {
|
|
67
80
|
return false;
|
|
81
|
+
}
|
|
68
82
|
const normalizedRequested = normalizeForMatch(requestedPath);
|
|
69
83
|
const normalizedResolved = resolvedPath
|
|
70
84
|
? normalizeForMatch(resolvedPath)
|
|
71
85
|
: undefined;
|
|
72
86
|
const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
|
|
73
87
|
const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
|
|
74
|
-
if (
|
|
88
|
+
if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
|
|
89
|
+
matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
|
|
75
90
|
return false;
|
|
76
91
|
}
|
|
77
|
-
return
|
|
92
|
+
return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
|
|
93
|
+
matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
|
|
78
94
|
}
|
|
79
95
|
export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
80
96
|
if (!isSensitivePath(requestedPath, resolvedPath))
|
|
@@ -15,6 +15,10 @@ Expert filesystem agent. Operate ONLY within allowed roots. Always discover befo
|
|
|
15
15
|
</tools_overview>
|
|
16
16
|
|
|
17
17
|
<resources>
|
|
18
|
+
- \`internal://instructions\`: Full server usage guide.
|
|
19
|
+
- \`internal://tool-catalog\`: Tool routing and cross-tool data-flow guide.
|
|
20
|
+
- \`internal://workflows\`: Standard operating sequences (explore/search/edit/patch).
|
|
21
|
+
- \`internal://tool-info/{name}\`: Per-tool details (nuances/gotchas), e.g. \`internal://tool-info/read\`.
|
|
18
22
|
- \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
|
|
19
23
|
- \`filesystem-mcp://metrics\`: Live per-tool stats.
|
|
20
24
|
</resources>
|
|
@@ -9,7 +9,7 @@ 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
11
|
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
12
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
13
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
14
|
const WINDOWS_PATH_SEPARATOR = /\\/gu;
|
|
15
15
|
export const CALCULATE_HASH_TOOL = {
|
|
@@ -158,17 +158,10 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
158
158
|
context: { path: args.path },
|
|
159
159
|
run: async (signal) => {
|
|
160
160
|
const baseName = path.basename(args.path);
|
|
161
|
-
|
|
162
|
-
notifyProgress(extra, {
|
|
163
|
-
current: 0,
|
|
164
|
-
message: `🕮 calculate_hash: ${baseName}`,
|
|
165
|
-
});
|
|
166
|
-
const baseReporter = createProgressReporter(extra);
|
|
161
|
+
const progress = createToolProgressSession(extra, `🕮 calculate_hash: ${baseName}`);
|
|
167
162
|
const progressWithMessage = ({ current, total, }) => {
|
|
168
|
-
if (current > progressCursor)
|
|
169
|
-
progressCursor = current;
|
|
170
163
|
const fileWord = current === 1 ? 'file' : 'files';
|
|
171
|
-
|
|
164
|
+
progress.update({
|
|
172
165
|
current,
|
|
173
166
|
...(total !== undefined ? { total } : {}),
|
|
174
167
|
message: `🕮 calculate_hash: ${baseName} [${current} ${fileWord} hashed]`,
|
|
@@ -178,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
178
171
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
179
172
|
const sc = result.structuredContent;
|
|
180
173
|
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
181
|
-
const finalCurrent = Math.max(totalFiles + 1,
|
|
174
|
+
const finalCurrent = Math.max(totalFiles + 1, progress.getCurrent() + 1);
|
|
182
175
|
let suffix;
|
|
183
176
|
if (!sc.ok) {
|
|
184
177
|
suffix = 'failed';
|
|
@@ -189,20 +182,11 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
189
182
|
else {
|
|
190
183
|
suffix = `${(sc.hash ?? '').slice(0, 8)}...`;
|
|
191
184
|
}
|
|
192
|
-
|
|
193
|
-
current: finalCurrent,
|
|
194
|
-
total: finalCurrent,
|
|
195
|
-
message: `🕮 calculate_hash: ${baseName} • ${suffix}`,
|
|
196
|
-
});
|
|
185
|
+
progress.complete(`🕮 calculate_hash: ${baseName} • ${suffix}`, finalCurrent);
|
|
197
186
|
return result;
|
|
198
187
|
}
|
|
199
188
|
catch (error) {
|
|
200
|
-
|
|
201
|
-
notifyProgress(extra, {
|
|
202
|
-
current: finalCurrent,
|
|
203
|
-
total: finalCurrent,
|
|
204
|
-
message: `🕮 calculate_hash: ${baseName} • failed`,
|
|
205
|
-
});
|
|
189
|
+
progress.fail(`🕮 calculate_hash: ${baseName} • failed`);
|
|
206
190
|
throw error;
|
|
207
191
|
}
|
|
208
192
|
},
|
|
@@ -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 { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, 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',
|
|
@@ -20,6 +20,33 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
20
20
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
21
21
|
],
|
|
22
22
|
};
|
|
23
|
+
function toStructuredReadManyResult(result) {
|
|
24
|
+
const structured = {
|
|
25
|
+
path: result.path,
|
|
26
|
+
};
|
|
27
|
+
if (result.content !== undefined)
|
|
28
|
+
structured.content = result.content;
|
|
29
|
+
if (result.truncated)
|
|
30
|
+
structured.truncated = result.truncated;
|
|
31
|
+
if (result.resourceUri)
|
|
32
|
+
structured.resourceUri = result.resourceUri;
|
|
33
|
+
if (result.head !== undefined)
|
|
34
|
+
structured.head = result.head;
|
|
35
|
+
if (result.startLine !== undefined)
|
|
36
|
+
structured.startLine = result.startLine;
|
|
37
|
+
if (result.endLine !== undefined)
|
|
38
|
+
structured.endLine = result.endLine;
|
|
39
|
+
if (result.hasMoreLines)
|
|
40
|
+
structured.hasMoreLines = result.hasMoreLines;
|
|
41
|
+
if (result.totalLines !== undefined)
|
|
42
|
+
structured.totalLines = result.totalLines;
|
|
43
|
+
if (result.truncationReason) {
|
|
44
|
+
structured.truncationReason = result.truncationReason;
|
|
45
|
+
}
|
|
46
|
+
if (result.error)
|
|
47
|
+
structured.error = result.error;
|
|
48
|
+
return structured;
|
|
49
|
+
}
|
|
23
50
|
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
24
51
|
const options = {
|
|
25
52
|
...(signal ? { signal } : {}),
|
|
@@ -70,25 +97,7 @@ async function handleReadMultipleFiles(args, signal, resourceStore, onReadComple
|
|
|
70
97
|
}
|
|
71
98
|
const structured = {
|
|
72
99
|
ok: true,
|
|
73
|
-
results: mappedResults.map((result) => (
|
|
74
|
-
path: result.path,
|
|
75
|
-
...(result.content !== undefined ? { content: result.content } : {}),
|
|
76
|
-
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
77
|
-
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
78
|
-
...(result.head !== undefined ? { head: result.head } : {}),
|
|
79
|
-
...(result.startLine !== undefined
|
|
80
|
-
? { startLine: result.startLine }
|
|
81
|
-
: {}),
|
|
82
|
-
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
83
|
-
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
84
|
-
...(result.totalLines !== undefined
|
|
85
|
-
? { totalLines: result.totalLines }
|
|
86
|
-
: {}),
|
|
87
|
-
...(result.truncationReason
|
|
88
|
-
? { truncationReason: result.truncationReason }
|
|
89
|
-
: {}),
|
|
90
|
-
...(result.error ? { error: result.error } : {}),
|
|
91
|
-
})),
|
|
100
|
+
results: mappedResults.map((result) => toStructuredReadManyResult(result)),
|
|
92
101
|
summary: {
|
|
93
102
|
total: mappedResults.length,
|
|
94
103
|
succeeded,
|
|
@@ -130,18 +139,9 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
130
139
|
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
131
140
|
: '';
|
|
132
141
|
const context = `${args.paths.length} files [${first}${extraPaths}]`;
|
|
133
|
-
|
|
134
|
-
notifyProgress(extra, {
|
|
135
|
-
current: 0,
|
|
136
|
-
message: `🕮 read_many: ${context}`,
|
|
137
|
-
});
|
|
138
|
-
const baseReporter = createProgressReporter(extra);
|
|
142
|
+
const progress = createToolProgressSession(extra, `🕮 read_many: ${context}`);
|
|
139
143
|
const onReadComplete = () => {
|
|
140
|
-
|
|
141
|
-
baseReporter({
|
|
142
|
-
current: progressCursor,
|
|
143
|
-
message: `🕮 read_many: ${context} [${progressCursor}/${args.paths.length} read]`,
|
|
144
|
-
});
|
|
144
|
+
progress.increment((current) => `🕮 read_many: ${context} [${current}/${args.paths.length} read]`);
|
|
145
145
|
};
|
|
146
146
|
try {
|
|
147
147
|
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onReadComplete);
|
|
@@ -156,21 +156,12 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
156
156
|
else {
|
|
157
157
|
suffix = `${total} files read`;
|
|
158
158
|
}
|
|
159
|
-
const finalCurrent = Math.max(total,
|
|
160
|
-
|
|
161
|
-
current: finalCurrent,
|
|
162
|
-
total: finalCurrent,
|
|
163
|
-
message: `🕮 read_many: ${context} • ${suffix}`,
|
|
164
|
-
});
|
|
159
|
+
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
160
|
+
progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
|
|
165
161
|
return result;
|
|
166
162
|
}
|
|
167
163
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
notifyProgress(extra, {
|
|
170
|
-
current: finalCurrent,
|
|
171
|
-
total: finalCurrent,
|
|
172
|
-
message: `🕮 read_many: ${context} • failed`,
|
|
173
|
-
});
|
|
164
|
+
progress.fail(`🕮 read_many: ${context} • failed`);
|
|
174
165
|
throw error;
|
|
175
166
|
}
|
|
176
167
|
},
|
|
@@ -9,7 +9,7 @@ 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
11
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
12
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
13
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
14
|
export const SEARCH_AND_REPLACE_TOOL = {
|
|
15
15
|
name: 'search_and_replace',
|
|
@@ -56,31 +56,42 @@ function createRegexMatcher(pattern) {
|
|
|
56
56
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
regex.lastIndex
|
|
59
|
+
function createRegexReplacementMatcher(regex) {
|
|
60
|
+
const count = (content) => {
|
|
61
|
+
regex.lastIndex = 0;
|
|
62
|
+
let matchCount = 0;
|
|
63
|
+
while (regex.exec(content) !== null) {
|
|
64
|
+
matchCount++;
|
|
65
|
+
if (regex.lastIndex === 0) {
|
|
66
|
+
regex.lastIndex++;
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
return matchCount;
|
|
70
|
+
};
|
|
71
|
+
const replace = (content, replacement) => {
|
|
72
|
+
regex.lastIndex = 0;
|
|
73
|
+
return content.replace(regex, replacement);
|
|
74
|
+
};
|
|
75
|
+
return { count, replace };
|
|
69
76
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
function createLiteralReplacementMatcher(searchPattern) {
|
|
78
|
+
const count = (content) => {
|
|
79
|
+
let matchCount = 0;
|
|
80
|
+
let pos = content.indexOf(searchPattern);
|
|
81
|
+
const patternLength = searchPattern.length;
|
|
82
|
+
while (pos !== -1) {
|
|
83
|
+
matchCount++;
|
|
84
|
+
pos = content.indexOf(searchPattern, pos + patternLength);
|
|
85
|
+
}
|
|
86
|
+
return matchCount;
|
|
87
|
+
};
|
|
88
|
+
const replace = (content, replacement) => content.replaceAll(searchPattern, () => replacement);
|
|
89
|
+
return { count, replace };
|
|
79
90
|
}
|
|
80
91
|
function formatFileTooLargeError(filePath, size, maxFileSize) {
|
|
81
92
|
return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
|
|
82
93
|
}
|
|
83
|
-
async function processEntry(entryPath,
|
|
94
|
+
async function processEntry(entryPath, options, replacement, matcher, maxFileSize, signal, summary) {
|
|
84
95
|
let validPath;
|
|
85
96
|
try {
|
|
86
97
|
validPath = await validatePathForWrite(entryPath, signal);
|
|
@@ -107,22 +118,13 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
107
118
|
encoding: 'utf-8',
|
|
108
119
|
signal,
|
|
109
120
|
});
|
|
110
|
-
const matchCount =
|
|
111
|
-
? countRegexMatches(content, regex)
|
|
112
|
-
: countLiteralMatches(content, args.searchPattern);
|
|
121
|
+
const matchCount = matcher.count(content);
|
|
113
122
|
if (matchCount > 0) {
|
|
114
123
|
summary.totalMatches += matchCount;
|
|
115
124
|
summary.filesChanged++;
|
|
116
125
|
recordChangedFile(summary, validPath, matchCount);
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
regex.lastIndex = 0;
|
|
120
|
-
newContent = content.replace(regex, args.replacement);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
124
|
-
}
|
|
125
|
-
if ((args.dryRun || args.returnDiff) &&
|
|
126
|
+
const newContent = matcher.replace(content, replacement);
|
|
127
|
+
if ((options.dryRun || options.returnDiff) &&
|
|
126
128
|
summary.diff.length < MAX_DIFF_SIZE) {
|
|
127
129
|
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
128
130
|
// Only append if it won't exceed the limit too much
|
|
@@ -130,7 +132,7 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
130
132
|
summary.diff += patch;
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
|
-
if (!
|
|
135
|
+
if (!options.dryRun) {
|
|
134
136
|
await atomicWriteFile(validPath, newContent, {
|
|
135
137
|
encoding: 'utf-8',
|
|
136
138
|
signal,
|
|
@@ -195,6 +197,13 @@ function createReplacementRegex(args) {
|
|
|
195
197
|
}
|
|
196
198
|
return createRegexMatcher(args.searchPattern);
|
|
197
199
|
}
|
|
200
|
+
function createReplacementMatcher(args) {
|
|
201
|
+
const regex = createReplacementRegex(args);
|
|
202
|
+
if (regex) {
|
|
203
|
+
return createRegexReplacementMatcher(regex);
|
|
204
|
+
}
|
|
205
|
+
return createLiteralReplacementMatcher(args.searchPattern);
|
|
206
|
+
}
|
|
198
207
|
function reportReplaceProgress(onProgress, current, force = false) {
|
|
199
208
|
if (current === 0)
|
|
200
209
|
return;
|
|
@@ -205,7 +214,7 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
205
214
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
206
215
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
207
216
|
const root = await resolveSearchRoot(args.path, signal);
|
|
208
|
-
const
|
|
217
|
+
const matcher = createReplacementMatcher(args);
|
|
209
218
|
const entries = globEntries({
|
|
210
219
|
cwd: root,
|
|
211
220
|
pattern: args.filePattern,
|
|
@@ -226,7 +235,10 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
226
235
|
summary.processedFiles++;
|
|
227
236
|
reportReplaceProgress(onProgress, summary.processedFiles);
|
|
228
237
|
},
|
|
229
|
-
runEntry: async (entryPath) => processEntry(entryPath,
|
|
238
|
+
runEntry: async (entryPath) => processEntry(entryPath, {
|
|
239
|
+
dryRun: args.dryRun,
|
|
240
|
+
returnDiff: args.returnDiff ?? false,
|
|
241
|
+
}, args.replacement, matcher, maxFileSize, signal, summary),
|
|
230
242
|
});
|
|
231
243
|
reportReplaceProgress(onProgress, summary.processedFiles, true);
|
|
232
244
|
const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
|
|
@@ -256,16 +268,9 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
256
268
|
run: async (signal) => {
|
|
257
269
|
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
258
270
|
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
259
|
-
|
|
260
|
-
notifyProgress(extra, {
|
|
261
|
-
current: 0,
|
|
262
|
-
message: `🛠 search_and_replace: ${context}`,
|
|
263
|
-
});
|
|
264
|
-
const baseReporter = createProgressReporter(extra);
|
|
271
|
+
const progress = createToolProgressSession(extra, `🛠 search_and_replace: ${context}`);
|
|
265
272
|
const progressWithMessage = ({ current, total, }) => {
|
|
266
|
-
|
|
267
|
-
progressCursor = current;
|
|
268
|
-
baseReporter({
|
|
273
|
+
progress.update({
|
|
269
274
|
current,
|
|
270
275
|
...(total !== undefined ? { total } : {}),
|
|
271
276
|
message: `🛠 search_and_replace: ${args.searchPattern} [${current} files processed]`,
|
|
@@ -274,7 +279,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
274
279
|
try {
|
|
275
280
|
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
276
281
|
const sc = result.structuredContent;
|
|
277
|
-
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1,
|
|
282
|
+
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1, progress.getCurrent() + 1);
|
|
278
283
|
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
279
284
|
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
280
285
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
@@ -282,20 +287,11 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
282
287
|
endSuffix += `, ${sc.failedFiles} failed`;
|
|
283
288
|
if (sc.dryRun)
|
|
284
289
|
endSuffix += ' [dry run]';
|
|
285
|
-
|
|
286
|
-
current: finalCurrent,
|
|
287
|
-
total: finalCurrent,
|
|
288
|
-
message: `🛠 search_and_replace: ${context} • ${endSuffix}`,
|
|
289
|
-
});
|
|
290
|
+
progress.complete(`🛠 search_and_replace: ${context} • ${endSuffix}`, finalCurrent);
|
|
290
291
|
return result;
|
|
291
292
|
}
|
|
292
293
|
catch (error) {
|
|
293
|
-
|
|
294
|
-
notifyProgress(extra, {
|
|
295
|
-
current: finalCurrent,
|
|
296
|
-
total: finalCurrent,
|
|
297
|
-
message: `🛠 search_and_replace: ${context} • failed`,
|
|
298
|
-
});
|
|
294
|
+
progress.fail(`🛠 search_and_replace: ${context} • failed`);
|
|
299
295
|
throw error;
|
|
300
296
|
}
|
|
301
297
|
},
|
|
@@ -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,
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, 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 = {
|
|
@@ -207,28 +207,16 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
207
207
|
run: async (signal) => {
|
|
208
208
|
const scope = args.filePattern;
|
|
209
209
|
const { pattern } = args;
|
|
210
|
-
|
|
211
|
-
notifyProgress(extra, {
|
|
212
|
-
current: 0,
|
|
213
|
-
message: `🔎︎ grep: ${pattern} in ${scope}`,
|
|
214
|
-
});
|
|
215
|
-
const baseReporter = createProgressReporter(extra);
|
|
210
|
+
const progress = createToolProgressSession(extra, `🔎︎ grep: ${pattern} in ${scope}`);
|
|
216
211
|
const progressWithMessage = ({ current, total, }) => {
|
|
217
|
-
if (current > progressCursor)
|
|
218
|
-
progressCursor = current;
|
|
219
212
|
const fileWord = current === 1 ? 'file' : 'files';
|
|
220
|
-
|
|
213
|
+
progress.update({
|
|
221
214
|
current,
|
|
222
215
|
...(total !== undefined ? { total } : {}),
|
|
223
216
|
message: `🔎︎ grep: ${pattern} [${current} ${fileWord} scanned]`,
|
|
224
217
|
});
|
|
225
218
|
};
|
|
226
219
|
try {
|
|
227
|
-
if (signal) {
|
|
228
|
-
signal.addEventListener('abort', () => {
|
|
229
|
-
console.error('searchContent signal aborted!');
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
220
|
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
233
221
|
const sc = result.structuredContent;
|
|
234
222
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
@@ -254,21 +242,12 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
254
242
|
suffix += ' [truncated — max files]';
|
|
255
243
|
}
|
|
256
244
|
}
|
|
257
|
-
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1,
|
|
258
|
-
|
|
259
|
-
current: finalCurrent,
|
|
260
|
-
total: finalCurrent,
|
|
261
|
-
message: `🔎︎ grep: ${pattern} • ${suffix}`,
|
|
262
|
-
});
|
|
245
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progress.getCurrent() + 1);
|
|
246
|
+
progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
|
|
263
247
|
return result;
|
|
264
248
|
}
|
|
265
249
|
catch (error) {
|
|
266
|
-
|
|
267
|
-
notifyProgress(extra, {
|
|
268
|
-
current: finalCurrent,
|
|
269
|
-
total: finalCurrent,
|
|
270
|
-
message: `🔎︎ grep: ${pattern} in ${scope} • failed`,
|
|
271
|
-
});
|
|
250
|
+
progress.fail(`🔎︎ grep: ${pattern} in ${scope} • failed`);
|
|
272
251
|
throw error;
|
|
273
252
|
}
|
|
274
253
|
},
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -112,6 +112,18 @@ export declare function notifyProgress(extra: ToolExtra, progress: {
|
|
|
112
112
|
total?: number;
|
|
113
113
|
message?: string;
|
|
114
114
|
}): void;
|
|
115
|
+
export interface ToolProgressSession {
|
|
116
|
+
update: (progress: {
|
|
117
|
+
current: number;
|
|
118
|
+
total?: number;
|
|
119
|
+
message: string;
|
|
120
|
+
}) => void;
|
|
121
|
+
increment: (messageForCurrent: (current: number) => string) => void;
|
|
122
|
+
complete: (message: string, minimumCurrent?: number) => void;
|
|
123
|
+
fail: (message: string, minimumCurrent?: number) => void;
|
|
124
|
+
getCurrent: () => number;
|
|
125
|
+
}
|
|
126
|
+
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
|
|
115
127
|
export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>, options: {
|
|
116
128
|
guard?: (() => boolean) | undefined;
|
|
117
129
|
progressMessage?: (args: Args) => string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -280,6 +280,55 @@ export function notifyProgress(extra, progress) {
|
|
|
280
280
|
return;
|
|
281
281
|
void reportProgress(extra, progress);
|
|
282
282
|
}
|
|
283
|
+
export function createToolProgressSession(extra, startMessage) {
|
|
284
|
+
notifyProgress(extra, {
|
|
285
|
+
current: 0,
|
|
286
|
+
message: startMessage,
|
|
287
|
+
});
|
|
288
|
+
let cursor = 0;
|
|
289
|
+
const baseReporter = createProgressReporter(extra);
|
|
290
|
+
const setCursor = (value) => {
|
|
291
|
+
if (value > cursor)
|
|
292
|
+
cursor = value;
|
|
293
|
+
return cursor;
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
update: ({ current, total, message }) => {
|
|
297
|
+
const normalized = setCursor(current);
|
|
298
|
+
baseReporter({
|
|
299
|
+
current: normalized,
|
|
300
|
+
...(total !== undefined ? { total } : {}),
|
|
301
|
+
message,
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
increment: (messageForCurrent) => {
|
|
305
|
+
const next = setCursor(cursor + 1);
|
|
306
|
+
baseReporter({
|
|
307
|
+
current: next,
|
|
308
|
+
message: messageForCurrent(next),
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
complete: (message, minimumCurrent) => {
|
|
312
|
+
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
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
|
+
},
|
|
329
|
+
getCurrent: () => cursor,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
283
332
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
284
333
|
if (!canReportProgress(extra)) {
|
|
285
334
|
return run();
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
6
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse,
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
10
10
|
name: 'stat_many',
|
|
@@ -79,18 +79,9 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
79
79
|
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
80
80
|
: '';
|
|
81
81
|
const context = `${args.paths.length} paths [${first}${extraPaths}]`;
|
|
82
|
-
|
|
83
|
-
notifyProgress(extra, {
|
|
84
|
-
current: 0,
|
|
85
|
-
message: `🕮 stat_many: ${context}`,
|
|
86
|
-
});
|
|
87
|
-
const baseReporter = createProgressReporter(extra);
|
|
82
|
+
const progress = createToolProgressSession(extra, `🕮 stat_many: ${context}`);
|
|
88
83
|
const onProgress = () => {
|
|
89
|
-
|
|
90
|
-
baseReporter({
|
|
91
|
-
current: progressCursor,
|
|
92
|
-
message: `🕮 stat_many: ${context} [${progressCursor}/${args.paths.length} scanned]`,
|
|
93
|
-
});
|
|
84
|
+
progress.increment((current) => `🕮 stat_many: ${context} [${current}/${args.paths.length} scanned]`);
|
|
94
85
|
};
|
|
95
86
|
try {
|
|
96
87
|
const result = await handleGetMultipleFileInfo(args, signal, onProgress);
|
|
@@ -105,21 +96,12 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
105
96
|
else {
|
|
106
97
|
suffix = `${total} OK`;
|
|
107
98
|
}
|
|
108
|
-
const finalCurrent = Math.max(total,
|
|
109
|
-
|
|
110
|
-
current: finalCurrent,
|
|
111
|
-
total: finalCurrent,
|
|
112
|
-
message: `🕮 stat_many: ${context} • ${suffix}`,
|
|
113
|
-
});
|
|
99
|
+
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
100
|
+
progress.complete(`🕮 stat_many: ${context} • ${suffix}`, finalCurrent);
|
|
114
101
|
return result;
|
|
115
102
|
}
|
|
116
103
|
catch (error) {
|
|
117
|
-
|
|
118
|
-
notifyProgress(extra, {
|
|
119
|
-
current: finalCurrent,
|
|
120
|
-
total: finalCurrent,
|
|
121
|
-
message: `🕮 stat_many: ${context} • failed`,
|
|
122
|
-
});
|
|
104
|
+
progress.fail(`🕮 stat_many: ${context} • failed`);
|
|
123
105
|
throw error;
|
|
124
106
|
}
|
|
125
107
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,6 @@
|
|
|
37
37
|
"lint": "eslint .",
|
|
38
38
|
"lint:fix": "eslint . --fix",
|
|
39
39
|
"test": "node scripts/tasks.mjs test",
|
|
40
|
-
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
|
|
41
|
-
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
42
40
|
"knip": "knip",
|
|
43
41
|
"knip:fix": "knip --fix",
|
|
44
42
|
"inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js ${workspaceFolder}",
|