@j0hanz/filesystem-mcp 1.2.1 → 1.2.3
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 +11 -0
- package/dist/cli.js +14 -8
- package/dist/completions.js +15 -6
- package/dist/lib/file-operations/glob-engine.js +3 -9
- package/dist/lib/file-operations/read-multiple-files.js +4 -23
- package/dist/lib/file-operations/search-content.js +11 -18
- package/dist/lib/observability.js +4 -4
- package/dist/lib/path-validation.js +22 -9
- package/dist/lib/resource-store.js +1 -1
- package/dist/pkg-info.d.ts +6 -0
- package/dist/pkg-info.js +9 -0
- package/dist/schemas.d.ts +28 -28
- package/dist/schemas.js +26 -26
- package/dist/server/bootstrap.d.ts +4 -0
- package/dist/server/bootstrap.js +117 -0
- package/dist/server/capabilities.d.ts +10 -0
- package/dist/server/capabilities.js +40 -0
- package/dist/server/logging.d.ts +7 -0
- package/dist/server/logging.js +41 -0
- package/dist/server/roots-manager.d.ts +19 -0
- package/dist/server/roots-manager.js +173 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +1 -0
- package/dist/server.d.ts +2 -8
- package/dist/server.js +1 -346
- package/dist/tools/apply-patch.js +17 -3
- package/dist/tools/calculate-hash.js +48 -13
- package/dist/tools/create-directory.js +13 -3
- package/dist/tools/delete-file.js +9 -2
- package/dist/tools/diff-files.js +16 -2
- package/dist/tools/edit-file.js +8 -7
- package/dist/tools/list-directory.js +13 -2
- package/dist/tools/move-file.js +11 -3
- package/dist/tools/read-multiple.js +21 -3
- package/dist/tools/read.js +16 -2
- package/dist/tools/replace-in-files.js +44 -11
- package/dist/tools/roots.js +12 -2
- package/dist/tools/search-content.js +62 -29
- package/dist/tools/search-files.js +60 -13
- package/dist/tools/shared.d.ts +5 -1
- package/dist/tools/shared.js +55 -8
- package/dist/tools/stat-many.js +22 -3
- package/dist/tools/stat.js +12 -2
- package/dist/tools/task-support.js +60 -13
- package/dist/tools/tree.js +15 -2
- package/dist/tools/write-file.js +13 -3
- package/package.json +4 -2
|
@@ -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, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const READ_MULTIPLE_FILES_TOOL = {
|
|
9
9
|
title: 'Read Multiple Files',
|
|
@@ -121,9 +121,27 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
121
121
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
|
|
122
122
|
});
|
|
123
123
|
};
|
|
124
|
-
const
|
|
124
|
+
const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, handler);
|
|
125
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
125
126
|
guard: options.isInitialized,
|
|
126
|
-
progressMessage: (args) =>
|
|
127
|
+
progressMessage: (args) => {
|
|
128
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
129
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
130
|
+
return `🕮 read_many: ${args.paths.length} files [${first}${extra}]`;
|
|
131
|
+
},
|
|
132
|
+
completionMessage: (args, result) => {
|
|
133
|
+
if (result.isError)
|
|
134
|
+
return `🕮 read_many: ${args.paths.length} files • failed`;
|
|
135
|
+
const sc = result.structuredContent;
|
|
136
|
+
if (!sc.ok)
|
|
137
|
+
return `🕮 read_many: ${args.paths.length} files • failed`;
|
|
138
|
+
const total = sc.summary?.total ?? 0;
|
|
139
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
140
|
+
const failed = sc.summary?.failed ?? 0;
|
|
141
|
+
if (failed)
|
|
142
|
+
return `🕮 read_many: ${succeeded}/${total} read, ${failed} failed`;
|
|
143
|
+
return `🕮 read_many: ${total} files read`;
|
|
144
|
+
},
|
|
127
145
|
});
|
|
128
146
|
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
129
147
|
return;
|
package/dist/tools/read.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readFile } from '../lib/fs-helpers.js';
|
|
5
5
|
import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
|
|
6
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const READ_FILE_TOOL = {
|
|
9
9
|
title: 'Read File',
|
|
@@ -80,7 +80,8 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
80
80
|
run: (signal) => handleReadFile(args, signal, options.resourceStore),
|
|
81
81
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
|
|
82
82
|
});
|
|
83
|
-
const
|
|
83
|
+
const validatedHandler = withValidatedArgs(ReadFileInputSchema, handler);
|
|
84
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
84
85
|
guard: options.isInitialized,
|
|
85
86
|
progressMessage: (args) => {
|
|
86
87
|
const name = path.basename(args.path);
|
|
@@ -90,6 +91,19 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
90
91
|
}
|
|
91
92
|
return `🕮 read: ${name}`;
|
|
92
93
|
},
|
|
94
|
+
completionMessage: (args, result) => {
|
|
95
|
+
const name = path.basename(args.path);
|
|
96
|
+
if (result.isError)
|
|
97
|
+
return `🕮 read: ${name} • failed`;
|
|
98
|
+
const sc = result.structuredContent;
|
|
99
|
+
if (!sc.ok)
|
|
100
|
+
return `🕮 read: ${name} • failed`;
|
|
101
|
+
if (sc.hasMoreLines)
|
|
102
|
+
return `🕮 read: ${name} • truncated [${sc.totalLines ?? '?'} lines]`;
|
|
103
|
+
if (sc.startLine !== undefined)
|
|
104
|
+
return `🕮 read: ${name} • lines ${sc.startLine}–${sc.endLine ?? '?'}`;
|
|
105
|
+
return `🕮 read: ${name} • ${sc.totalLines ?? '?'} lines`;
|
|
106
|
+
},
|
|
93
107
|
});
|
|
94
108
|
if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
95
109
|
return;
|
|
@@ -8,7 +8,7 @@ import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
|
8
8
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
9
9
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
10
10
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
11
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
11
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
12
12
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
13
13
|
const SEARCH_AND_REPLACE_TOOL = {
|
|
14
14
|
title: 'Search and Replace',
|
|
@@ -231,23 +231,56 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
231
231
|
timedSignal: {},
|
|
232
232
|
...(args.path ? { context: { path: args.path } } : {}),
|
|
233
233
|
run: async (signal) => {
|
|
234
|
+
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
235
|
+
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
236
|
+
let progressCursor = 0;
|
|
234
237
|
notifyProgress(extra, {
|
|
235
238
|
current: 0,
|
|
236
|
-
message: `🛠 search_and_replace: ${
|
|
239
|
+
message: `🛠 search_and_replace: ${context}`,
|
|
237
240
|
});
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
const baseReporter = createProgressReporter(extra);
|
|
242
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
243
|
+
if (current > progressCursor)
|
|
244
|
+
progressCursor = current;
|
|
245
|
+
baseReporter({
|
|
246
|
+
current,
|
|
247
|
+
...(total !== undefined ? { total } : {}),
|
|
248
|
+
message: `🛠 search_and_replace: "${args.searchPattern}" — ${current} files processed`,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
253
|
+
const sc = result.structuredContent;
|
|
254
|
+
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1, progressCursor + 1);
|
|
255
|
+
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
256
|
+
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
257
|
+
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
258
|
+
if (sc.failedFiles)
|
|
259
|
+
endSuffix += `, ${sc.failedFiles} failed`;
|
|
260
|
+
if (sc.dryRun)
|
|
261
|
+
endSuffix += ' [dry run]';
|
|
262
|
+
notifyProgress(extra, {
|
|
263
|
+
current: finalCurrent,
|
|
264
|
+
total: finalCurrent,
|
|
265
|
+
message: `🛠 search_and_replace: ${context} • ${endSuffix}`,
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
271
|
+
notifyProgress(extra, {
|
|
272
|
+
current: finalCurrent,
|
|
273
|
+
total: finalCurrent,
|
|
274
|
+
message: `🛠 search_and_replace: ${context} • failed`,
|
|
275
|
+
});
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
246
278
|
},
|
|
247
279
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
248
280
|
});
|
|
249
281
|
const { isInitialized } = options;
|
|
250
|
-
const
|
|
282
|
+
const validatedHandler = withValidatedArgs(SearchAndReplaceInputSchema, handler);
|
|
283
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
251
284
|
guard: isInitialized,
|
|
252
285
|
});
|
|
253
286
|
if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
package/dist/tools/roots.js
CHANGED
|
@@ -2,7 +2,7 @@ import { joinLines } from '../config.js';
|
|
|
2
2
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
3
|
import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
4
4
|
import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
|
|
5
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
5
|
+
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
6
|
const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
7
7
|
title: 'Workspace Roots',
|
|
8
8
|
description: 'List the workspace roots this server can access. ' +
|
|
@@ -38,8 +38,18 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
|
38
38
|
run: () => handleListAllowedDirectories(),
|
|
39
39
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
|
|
40
40
|
});
|
|
41
|
-
|
|
41
|
+
const validatedHandler = withValidatedArgs(ListAllowedDirectoriesInputSchema, handler);
|
|
42
|
+
server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
|
|
42
43
|
guard: options.isInitialized,
|
|
43
44
|
progressMessage: () => '≣ roots',
|
|
45
|
+
completionMessage: (_args, result) => {
|
|
46
|
+
if (result.isError)
|
|
47
|
+
return `≣ roots • failed`;
|
|
48
|
+
const sc = result.structuredContent;
|
|
49
|
+
if (!sc.ok)
|
|
50
|
+
return `≣ roots • failed`;
|
|
51
|
+
const count = sc.rootsCount ?? 0;
|
|
52
|
+
return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
|
|
53
|
+
},
|
|
44
54
|
}));
|
|
45
55
|
}
|
|
@@ -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, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, 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 = 50;
|
|
11
11
|
const SEARCH_CONTENT_TOOL = {
|
|
@@ -69,16 +69,14 @@ function formatSearchMatchLine(match) {
|
|
|
69
69
|
}
|
|
70
70
|
function buildStructuredSearchResult(result, normalizedMatches, options) {
|
|
71
71
|
const { summary } = result;
|
|
72
|
-
const matches =
|
|
73
|
-
for (const match of normalizedMatches) {
|
|
74
|
-
matches.push(buildSearchMatchPayload(match));
|
|
75
|
-
}
|
|
72
|
+
const matches = normalizedMatches.map((match) => buildSearchMatchPayload(match));
|
|
76
73
|
return {
|
|
77
74
|
ok: true,
|
|
78
75
|
patternType: options.patternType,
|
|
79
76
|
caseSensitive: options.caseSensitive,
|
|
80
77
|
matches,
|
|
81
78
|
totalMatches: summary.matches,
|
|
79
|
+
filesScanned: summary.filesScanned,
|
|
82
80
|
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
83
81
|
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
84
82
|
...(summary.skippedTooLarge
|
|
@@ -163,10 +161,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
163
161
|
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
164
162
|
}
|
|
165
163
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
166
|
-
const previewPayload =
|
|
167
|
-
for (const match of previewMatches) {
|
|
168
|
-
previewPayload.push(buildSearchMatchPayload(match));
|
|
169
|
-
}
|
|
164
|
+
const previewPayload = previewMatches.map((match) => buildSearchMatchPayload(match));
|
|
170
165
|
const previewStructured = {
|
|
171
166
|
...structuredFull,
|
|
172
167
|
matches: previewPayload,
|
|
@@ -201,35 +196,73 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
201
196
|
extra,
|
|
202
197
|
context: { path: args.path ?? '.' },
|
|
203
198
|
run: async (signal) => {
|
|
204
|
-
const
|
|
199
|
+
const scope = args.filePattern;
|
|
200
|
+
const { pattern } = args;
|
|
201
|
+
let progressCursor = 0;
|
|
205
202
|
notifyProgress(extra, {
|
|
206
203
|
current: 0,
|
|
207
|
-
message: `🔎︎ grep: ${
|
|
204
|
+
message: `🔎︎ grep: ${pattern} in ${scope}`,
|
|
208
205
|
});
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
const baseReporter = createProgressReporter(extra);
|
|
207
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
208
|
+
if (current > progressCursor)
|
|
209
|
+
progressCursor = current;
|
|
210
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
211
|
+
baseReporter({
|
|
212
|
+
current,
|
|
213
|
+
...(total !== undefined ? { total } : {}),
|
|
214
|
+
message: `🔎︎ grep: ${pattern} — ${current} ${fileWord} scanned`,
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
219
|
+
const sc = result.structuredContent;
|
|
220
|
+
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
221
|
+
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
222
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
223
|
+
let suffix;
|
|
224
|
+
if (count === 0) {
|
|
225
|
+
suffix = `No matches in ${scope}`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const matchWord = count === 1 ? 'match' : 'matches';
|
|
229
|
+
const fileInfo = filesMatched > 0
|
|
230
|
+
? ` in ${filesMatched} ${filesMatched === 1 ? 'file' : 'files'}`
|
|
231
|
+
: '';
|
|
232
|
+
suffix = `${count} ${matchWord}${fileInfo}`;
|
|
233
|
+
if (stoppedReason === 'timeout') {
|
|
234
|
+
suffix += ' [stopped — timeout]';
|
|
235
|
+
}
|
|
236
|
+
else if (stoppedReason === 'maxResults') {
|
|
237
|
+
suffix += ' [truncated — max results]';
|
|
238
|
+
}
|
|
239
|
+
else if (stoppedReason === 'maxFiles') {
|
|
240
|
+
suffix += ' [truncated — max files]';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
244
|
+
notifyProgress(extra, {
|
|
245
|
+
current: finalCurrent,
|
|
246
|
+
total: finalCurrent,
|
|
247
|
+
message: `🔎︎ grep: ${pattern} • ${suffix}`,
|
|
248
|
+
});
|
|
249
|
+
return result;
|
|
215
250
|
}
|
|
216
|
-
|
|
217
|
-
|
|
251
|
+
catch (error) {
|
|
252
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
253
|
+
notifyProgress(extra, {
|
|
254
|
+
current: finalCurrent,
|
|
255
|
+
total: finalCurrent,
|
|
256
|
+
message: `🔎︎ grep: ${pattern} in ${scope} • failed`,
|
|
257
|
+
});
|
|
258
|
+
throw error;
|
|
218
259
|
}
|
|
219
|
-
else {
|
|
220
|
-
suffix = `${count} matches`;
|
|
221
|
-
}
|
|
222
|
-
const finalCurrent = (sc.filesScanned ?? 0) + 1;
|
|
223
|
-
notifyProgress(extra, {
|
|
224
|
-
current: finalCurrent,
|
|
225
|
-
message: `🔎︎ grep: ${normalizedArgs.pattern} ➟ ${suffix}`,
|
|
226
|
-
});
|
|
227
|
-
return result;
|
|
228
260
|
},
|
|
229
261
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
230
262
|
});
|
|
231
263
|
const { isInitialized } = options;
|
|
232
|
-
const
|
|
264
|
+
const validatedHandler = withValidatedArgs(SearchContentInputSchema, handler);
|
|
265
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
233
266
|
guard: isInitialized,
|
|
234
267
|
});
|
|
235
268
|
if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
@@ -4,7 +4,7 @@ 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, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const SEARCH_FILES_TOOL = {
|
|
10
10
|
title: 'Find Files',
|
|
@@ -43,6 +43,7 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
43
43
|
pattern: args.pattern,
|
|
44
44
|
results: relativeResults,
|
|
45
45
|
totalMatches: result.summary.matched,
|
|
46
|
+
filesScanned: result.summary.filesScanned,
|
|
46
47
|
...(result.summary.truncated
|
|
47
48
|
? { truncated: result.summary.truncated }
|
|
48
49
|
: {}),
|
|
@@ -89,24 +90,70 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
89
90
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
90
91
|
context: { path: args.path ?? '.' },
|
|
91
92
|
run: async (signal) => {
|
|
93
|
+
const rawScopeLabel = args.path ? path.basename(args.path) : '.';
|
|
94
|
+
const scopeLabel = rawScopeLabel || '.';
|
|
95
|
+
const { pattern } = args;
|
|
96
|
+
const context = `${pattern} in ${scopeLabel}`;
|
|
97
|
+
let progressCursor = 0;
|
|
92
98
|
notifyProgress(extra, {
|
|
93
99
|
current: 0,
|
|
94
|
-
message: `🔎︎ find: ${
|
|
100
|
+
message: `🔎︎ find: ${context}`,
|
|
95
101
|
});
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const baseReporter = createProgressReporter(extra);
|
|
103
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
104
|
+
if (current > progressCursor)
|
|
105
|
+
progressCursor = current;
|
|
106
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
107
|
+
baseReporter({
|
|
108
|
+
current,
|
|
109
|
+
...(total !== undefined ? { total } : {}),
|
|
110
|
+
message: `🔎︎ find: ${pattern} [${current} ${fileWord} scanned]`,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
try {
|
|
114
|
+
const result = await handleSearchFiles(args, signal, progressWithMessage);
|
|
115
|
+
const sc = result.structuredContent;
|
|
116
|
+
const count = sc.ok ? (sc.totalMatches ?? 0) : 0;
|
|
117
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
118
|
+
let suffix;
|
|
119
|
+
if (count === 0) {
|
|
120
|
+
suffix = `No matches in ${scopeLabel}`;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
|
|
124
|
+
if (stoppedReason === 'timeout') {
|
|
125
|
+
suffix += ' [stopped — timeout]';
|
|
126
|
+
}
|
|
127
|
+
else if (stoppedReason === 'maxResults') {
|
|
128
|
+
suffix += ' [truncated — max results]';
|
|
129
|
+
}
|
|
130
|
+
else if (stoppedReason === 'maxFiles') {
|
|
131
|
+
suffix += ' [truncated — max files]';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
135
|
+
notifyProgress(extra, {
|
|
136
|
+
current: finalCurrent,
|
|
137
|
+
total: finalCurrent,
|
|
138
|
+
message: `🔎︎ find: ${context} • ${suffix}`,
|
|
139
|
+
});
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
144
|
+
notifyProgress(extra, {
|
|
145
|
+
current: finalCurrent,
|
|
146
|
+
total: finalCurrent,
|
|
147
|
+
message: `🔎︎ find: ${context} • failed`,
|
|
148
|
+
});
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
105
151
|
},
|
|
106
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
152
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
107
153
|
});
|
|
108
154
|
const { isInitialized } = options;
|
|
109
|
-
const
|
|
155
|
+
const validatedHandler = withValidatedArgs(SearchFilesInputSchema, handler);
|
|
156
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
110
157
|
guard: isInitialized,
|
|
111
158
|
});
|
|
112
159
|
if (registerToolTaskIfAvailable(server, 'find', SEARCH_FILES_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import type { FileInfo } from '../config.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
@@ -19,6 +19,8 @@ export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
|
|
|
19
19
|
readonly idempotentHint: true;
|
|
20
20
|
readonly openWorldHint: false;
|
|
21
21
|
};
|
|
22
|
+
export declare function shouldStripStructuredOutput(): boolean;
|
|
23
|
+
export declare function maybeStripStructuredContentFromResult<T extends object>(result: T): T;
|
|
22
24
|
type ResourceEntry = ReturnType<ResourceStore['putText']>;
|
|
23
25
|
export declare function maybeExternalizeTextContent(resourceStore: ResourceStore | undefined, content: string, params: {
|
|
24
26
|
name: string;
|
|
@@ -45,6 +47,8 @@ interface ToolErrorResponse extends Record<string, unknown> {
|
|
|
45
47
|
isError: true;
|
|
46
48
|
}
|
|
47
49
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
50
|
+
export declare function parseToolArgs<Schema extends z.ZodType>(schema: Schema, args: unknown): z.infer<Schema>;
|
|
51
|
+
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>;
|
|
48
52
|
type ProgressToken = string | number;
|
|
49
53
|
export interface ToolExtra {
|
|
50
54
|
signal?: AbortSignal;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
2
3
|
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
3
4
|
import { withToolDiagnostics } from '../lib/observability.js';
|
|
@@ -5,6 +6,7 @@ import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
|
5
6
|
const MAX_INLINE_CONTENT_CHARS = 20_000;
|
|
6
7
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
7
8
|
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
9
|
+
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
|
|
8
10
|
export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
9
11
|
readOnlyHint: true,
|
|
10
12
|
idempotentHint: true,
|
|
@@ -20,6 +22,30 @@ export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
|
20
22
|
idempotentHint: true,
|
|
21
23
|
openWorldHint: false,
|
|
22
24
|
};
|
|
25
|
+
export function shouldStripStructuredOutput() {
|
|
26
|
+
const value = process.env['FS_CONTEXT_STRIP_STRUCTURED'];
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
export function maybeStripStructuredContentFromResult(result) {
|
|
32
|
+
if (!shouldStripStructuredOutput())
|
|
33
|
+
return result;
|
|
34
|
+
if (!Object.hasOwn(result, 'structuredContent'))
|
|
35
|
+
return result;
|
|
36
|
+
const rest = { ...result };
|
|
37
|
+
delete rest['structuredContent'];
|
|
38
|
+
return rest;
|
|
39
|
+
}
|
|
40
|
+
function maybeStripOutputSchema(tool) {
|
|
41
|
+
if (!shouldStripStructuredOutput())
|
|
42
|
+
return tool;
|
|
43
|
+
if (!Object.hasOwn(tool, 'outputSchema'))
|
|
44
|
+
return tool;
|
|
45
|
+
const mutable = { ...tool };
|
|
46
|
+
delete mutable['outputSchema'];
|
|
47
|
+
return mutable;
|
|
48
|
+
}
|
|
23
49
|
function buildTextPreview(text) {
|
|
24
50
|
if (text.length <= MAX_INLINE_PREVIEW_CHARS)
|
|
25
51
|
return text;
|
|
@@ -66,18 +92,33 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
66
92
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
67
93
|
return buildContentBlock(text, structuredContent, extraContent);
|
|
68
94
|
}
|
|
95
|
+
export function parseToolArgs(schema, args) {
|
|
96
|
+
const candidate = args === undefined ? {} : args;
|
|
97
|
+
const parsed = schema.safeParse(candidate);
|
|
98
|
+
if (parsed.success) {
|
|
99
|
+
return parsed.data;
|
|
100
|
+
}
|
|
101
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments: ${parsed.error.message}`, undefined, { errors: z.treeifyError(parsed.error) });
|
|
102
|
+
}
|
|
103
|
+
export function withValidatedArgs(schema, handler) {
|
|
104
|
+
return async (args, extra) => {
|
|
105
|
+
const normalizedArgs = parseToolArgs(schema, args);
|
|
106
|
+
return handler(normalizedArgs, extra);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
69
109
|
function canSendProgress(extra) {
|
|
70
110
|
return (extra._meta?.progressToken !== undefined &&
|
|
71
111
|
extra.sendNotification !== undefined);
|
|
72
112
|
}
|
|
73
113
|
export function withDefaultIcons(tool, iconInfo) {
|
|
74
|
-
if (!iconInfo)
|
|
75
|
-
return tool;
|
|
114
|
+
if (!iconInfo) {
|
|
115
|
+
return maybeStripOutputSchema(tool);
|
|
116
|
+
}
|
|
76
117
|
const existingIcons = tool.icons;
|
|
77
118
|
if (existingIcons && existingIcons.length > 0) {
|
|
78
|
-
return tool;
|
|
119
|
+
return maybeStripOutputSchema(tool);
|
|
79
120
|
}
|
|
80
|
-
|
|
121
|
+
const withIcons = {
|
|
81
122
|
...tool,
|
|
82
123
|
icons: [
|
|
83
124
|
{
|
|
@@ -86,6 +127,7 @@ export function withDefaultIcons(tool, iconInfo) {
|
|
|
86
127
|
},
|
|
87
128
|
],
|
|
88
129
|
};
|
|
130
|
+
return maybeStripOutputSchema(withIcons);
|
|
89
131
|
}
|
|
90
132
|
export function buildFileInfoPayload(info) {
|
|
91
133
|
return {
|
|
@@ -184,9 +226,11 @@ export function createProgressReporter(extra) {
|
|
|
184
226
|
return (progress) => {
|
|
185
227
|
const { current, total, message } = progress;
|
|
186
228
|
// Enforce monotonic progress to prevent client confusion. Client behavior on
|
|
229
|
+
// out-of-order progress is undefined in the MCP spec.
|
|
187
230
|
if (current <= lastProgress)
|
|
188
231
|
return;
|
|
189
|
-
// Enforce rate-limiting to prevent client flooding. Progress updates
|
|
232
|
+
// Enforce rate-limiting to prevent client flooding. Progress updates faster
|
|
233
|
+
// than PROGRESS_RATE_LIMIT_MS are silently dropped.
|
|
190
234
|
const now = Date.now();
|
|
191
235
|
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
192
236
|
return;
|
|
@@ -239,6 +283,7 @@ async function withProgress(message, extra, run, getCompletionMessage) {
|
|
|
239
283
|
progressToken: token,
|
|
240
284
|
progress: total,
|
|
241
285
|
total,
|
|
286
|
+
message: `${message} • failed`,
|
|
242
287
|
});
|
|
243
288
|
throw error;
|
|
244
289
|
}
|
|
@@ -247,7 +292,7 @@ export function wrapToolHandler(handler, options) {
|
|
|
247
292
|
return async (args, extra) => {
|
|
248
293
|
const resolvedExtra = extra ?? {};
|
|
249
294
|
if (options.guard && !options.guard()) {
|
|
250
|
-
return buildNotInitializedResult();
|
|
295
|
+
return maybeStripStructuredContentFromResult(buildNotInitializedResult());
|
|
251
296
|
}
|
|
252
297
|
if (options.progressMessage) {
|
|
253
298
|
const message = options.progressMessage(args);
|
|
@@ -255,9 +300,11 @@ export function wrapToolHandler(handler, options) {
|
|
|
255
300
|
const completionFn = completionMessage
|
|
256
301
|
? (result) => completionMessage(args, result)
|
|
257
302
|
: undefined;
|
|
258
|
-
|
|
303
|
+
const result = await withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
|
|
304
|
+
return maybeStripStructuredContentFromResult(result);
|
|
259
305
|
}
|
|
260
|
-
|
|
306
|
+
const result = await handler(args, resolvedExtra);
|
|
307
|
+
return maybeStripStructuredContentFromResult(result);
|
|
261
308
|
};
|
|
262
309
|
}
|
|
263
310
|
export function resolvePathOrRoot(pathValue) {
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import { formatBytes, joinLines } from '../config.js';
|
|
2
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
5
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
5
6
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
9
|
const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
9
10
|
title: 'Get Multiple File Info',
|
|
@@ -72,9 +73,27 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
72
73
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
|
|
73
74
|
});
|
|
74
75
|
};
|
|
75
|
-
const
|
|
76
|
+
const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, handler);
|
|
77
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
76
78
|
guard: options.isInitialized,
|
|
77
|
-
progressMessage: (args) =>
|
|
79
|
+
progressMessage: (args) => {
|
|
80
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
81
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
82
|
+
return `🕮 stat_many: ${args.paths.length} paths [${first}${extra}]`;
|
|
83
|
+
},
|
|
84
|
+
completionMessage: (args, result) => {
|
|
85
|
+
if (result.isError)
|
|
86
|
+
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
87
|
+
const sc = result.structuredContent;
|
|
88
|
+
if (!sc.ok)
|
|
89
|
+
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
90
|
+
const total = sc.summary?.total ?? 0;
|
|
91
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
92
|
+
const failed = sc.summary?.failed ?? 0;
|
|
93
|
+
if (failed)
|
|
94
|
+
return `🕮 stat_many: ${succeeded}/${total} OK, ${failed} failed`;
|
|
95
|
+
return `🕮 stat_many: ${total} OK`;
|
|
96
|
+
},
|
|
78
97
|
});
|
|
79
98
|
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
80
99
|
return;
|
package/dist/tools/stat.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 { getFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
6
|
import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
const GET_FILE_INFO_TOOL = {
|
|
9
9
|
title: 'Get File Info',
|
|
10
10
|
description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
|
|
@@ -45,8 +45,18 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
45
45
|
run: (signal) => handleGetFileInfo(args, signal),
|
|
46
46
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path),
|
|
47
47
|
});
|
|
48
|
-
|
|
48
|
+
const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, handler);
|
|
49
|
+
server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
|
|
49
50
|
guard: options.isInitialized,
|
|
50
51
|
progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
|
|
52
|
+
completionMessage: (args, result) => {
|
|
53
|
+
const name = path.basename(args.path);
|
|
54
|
+
if (result.isError)
|
|
55
|
+
return `🕮 stat: ${name} • failed`;
|
|
56
|
+
const sc = result.structuredContent;
|
|
57
|
+
if (!sc.ok || !sc.info)
|
|
58
|
+
return `🕮 stat: ${name} • failed`;
|
|
59
|
+
return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
|
|
60
|
+
},
|
|
51
61
|
}));
|
|
52
62
|
}
|