@j0hanz/filesystem-mcp 1.1.2 → 1.2.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 +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +3 -2
- package/dist/config.js +1 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +11 -6
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +213 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +85 -44
- package/dist/tools/apply-patch.js +24 -24
- package/dist/tools/calculate-hash.js +42 -45
- package/dist/tools/create-directory.js +18 -21
- package/dist/tools/delete-file.js +36 -39
- package/dist/tools/diff-files.js +16 -21
- package/dist/tools/edit-file.js +16 -20
- package/dist/tools/list-directory.js +25 -25
- package/dist/tools/move-file.js +18 -21
- package/dist/tools/read-multiple.js +56 -68
- package/dist/tools/read.js +27 -32
- package/dist/tools/replace-in-files.js +28 -35
- package/dist/tools/roots.js +9 -10
- package/dist/tools/search-content.js +74 -74
- package/dist/tools/search-files.js +45 -52
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +45 -68
- package/dist/tools/stat.js +11 -39
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +13 -30
- package/dist/tools/write-file.js +18 -21
- package/dist/tools.js +23 -18
- package/package.json +6 -7
|
@@ -2,37 +2,24 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, getExperimentalTaskRegistration, maybeExternalizeTextContent, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
|
|
9
|
-
import { createToolTaskHandler } from './task-support.js';
|
|
5
|
+
import { ReadMultipleFilesInputSchema, } from '../schemas.js';
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
8
|
const READ_MULTIPLE_FILES_TOOL = {
|
|
11
9
|
title: 'Read Multiple Files',
|
|
12
10
|
description: 'Read multiple text files in a single request. ' +
|
|
13
11
|
'Returns contents and metadata for each file. ' +
|
|
14
12
|
'For single file, use read for simpler output.',
|
|
15
13
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
16
|
-
|
|
17
|
-
annotations: {
|
|
18
|
-
readOnlyHint: true,
|
|
19
|
-
idempotentHint: true,
|
|
20
|
-
openWorldHint: false,
|
|
21
|
-
},
|
|
14
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
22
15
|
};
|
|
23
16
|
async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
24
17
|
const options = {
|
|
25
18
|
...(signal ? { signal } : {}),
|
|
19
|
+
...(args.head !== undefined ? { head: args.head } : {}),
|
|
20
|
+
...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
|
|
21
|
+
...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
|
|
26
22
|
};
|
|
27
|
-
if (args.head !== undefined) {
|
|
28
|
-
options.head = args.head;
|
|
29
|
-
}
|
|
30
|
-
if (args.startLine !== undefined) {
|
|
31
|
-
options.startLine = args.startLine;
|
|
32
|
-
}
|
|
33
|
-
if (args.endLine !== undefined) {
|
|
34
|
-
options.endLine = args.endLine;
|
|
35
|
-
}
|
|
36
23
|
const results = await readMultipleFiles(args.paths, options);
|
|
37
24
|
const maxTotalSize = DEFAULT_READ_MANY_MAX_TOTAL_SIZE;
|
|
38
25
|
const mappedResults = results.map((result) => {
|
|
@@ -65,78 +52,79 @@ async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
|
65
52
|
truncationReason: 'externalized',
|
|
66
53
|
};
|
|
67
54
|
});
|
|
55
|
+
let succeeded = 0;
|
|
56
|
+
let failed = 0;
|
|
57
|
+
for (const result of mappedResults) {
|
|
58
|
+
if (result.error === undefined)
|
|
59
|
+
succeeded += 1;
|
|
60
|
+
else
|
|
61
|
+
failed += 1;
|
|
62
|
+
}
|
|
68
63
|
const structured = {
|
|
69
64
|
ok: true,
|
|
70
65
|
results: mappedResults.map((result) => ({
|
|
71
66
|
path: result.path,
|
|
72
|
-
content: result.content,
|
|
73
|
-
truncated: result.truncated,
|
|
74
|
-
resourceUri: result.resourceUri,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
hasMoreLines: result.hasMoreLines,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
...(result.content !== undefined ? { content: result.content } : {}),
|
|
68
|
+
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
69
|
+
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
70
|
+
...(result.head !== undefined ? { head: result.head } : {}),
|
|
71
|
+
...(result.startLine !== undefined
|
|
72
|
+
? { startLine: result.startLine }
|
|
73
|
+
: {}),
|
|
74
|
+
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
75
|
+
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
76
|
+
...(result.totalLines !== undefined
|
|
77
|
+
? { totalLines: result.totalLines }
|
|
78
|
+
: {}),
|
|
79
|
+
...(result.truncationReason
|
|
80
|
+
? { truncationReason: result.truncationReason }
|
|
81
|
+
: {}),
|
|
82
|
+
...(result.error ? { error: result.error } : {}),
|
|
85
83
|
})),
|
|
86
84
|
summary: {
|
|
87
85
|
total: mappedResults.length,
|
|
88
|
-
succeeded
|
|
89
|
-
failed
|
|
86
|
+
succeeded,
|
|
87
|
+
failed,
|
|
90
88
|
},
|
|
91
89
|
};
|
|
92
|
-
const resourceLinks =
|
|
90
|
+
const resourceLinks = [];
|
|
91
|
+
for (const result of mappedResults) {
|
|
93
92
|
if (!result.resourceUri)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
];
|
|
102
|
-
});
|
|
93
|
+
continue;
|
|
94
|
+
resourceLinks.push(buildResourceLink({
|
|
95
|
+
uri: result.resourceUri,
|
|
96
|
+
name: `read:${path.basename(result.path)}`,
|
|
97
|
+
description: 'Full file contents',
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
103
100
|
const text = mappedResults
|
|
104
101
|
.map((result) => {
|
|
102
|
+
const header = `=== ${result.path} ===`;
|
|
105
103
|
if (result.error) {
|
|
106
|
-
return `${
|
|
104
|
+
return `${header}\nError: ${result.error}`;
|
|
107
105
|
}
|
|
108
|
-
return result.
|
|
106
|
+
return `${header}\n${result.content ?? ''}`;
|
|
109
107
|
})
|
|
110
|
-
.join('\n');
|
|
111
|
-
return buildToolResponse(text, structured, resourceLinks
|
|
108
|
+
.join('\n\n');
|
|
109
|
+
return buildToolResponse(text, structured, resourceLinks);
|
|
112
110
|
}
|
|
113
111
|
export function registerReadMultipleFilesTool(server, options = {}) {
|
|
114
112
|
const handler = (args, extra) => {
|
|
115
113
|
const primaryPath = args.paths[0] ?? '';
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath)), { path: primaryPath });
|
|
114
|
+
return executeToolWithDiagnostics({
|
|
115
|
+
toolName: 'read_many',
|
|
116
|
+
extra,
|
|
117
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
118
|
+
context: { path: primaryPath },
|
|
119
|
+
run: (signal) => handleReadMultipleFiles(args, signal, options.resourceStore),
|
|
120
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
|
|
121
|
+
});
|
|
125
122
|
};
|
|
126
123
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
127
124
|
guard: options.isInitialized,
|
|
128
125
|
progressMessage: (args) => `🕮 read_many: ${args.paths.length} files`,
|
|
129
126
|
});
|
|
130
|
-
|
|
131
|
-
? { guard: options.isInitialized }
|
|
132
|
-
: undefined;
|
|
133
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
134
|
-
if (tasks?.registerToolTask) {
|
|
135
|
-
tasks.registerToolTask('read_many', withDefaultIcons({
|
|
136
|
-
...READ_MULTIPLE_FILES_TOOL,
|
|
137
|
-
execution: { taskSupport: 'optional' },
|
|
138
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
127
|
+
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
139
128
|
return;
|
|
140
|
-
}
|
|
141
129
|
server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), wrappedHandler);
|
|
142
130
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -2,22 +2,16 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readFile } from '../lib/fs-helpers.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, maybeExternalizeTextContent, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
|
|
5
|
+
import { ReadFileInputSchema } from '../schemas.js';
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
8
|
const READ_FILE_TOOL = {
|
|
10
9
|
title: 'Read File',
|
|
11
10
|
description: 'Read the text contents of a file. ' +
|
|
12
11
|
'Use head parameter to preview the first N lines of large files. ' +
|
|
13
12
|
'For multiple files, use read_many for efficiency.',
|
|
14
13
|
inputSchema: ReadFileInputSchema,
|
|
15
|
-
|
|
16
|
-
annotations: {
|
|
17
|
-
readOnlyHint: true,
|
|
18
|
-
idempotentHint: true,
|
|
19
|
-
openWorldHint: false,
|
|
20
|
-
},
|
|
14
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
21
15
|
};
|
|
22
16
|
async function handleReadFile(args, signal, resourceStore) {
|
|
23
17
|
const options = {
|
|
@@ -42,19 +36,18 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
42
36
|
ok: true,
|
|
43
37
|
path: args.path,
|
|
44
38
|
content: result.content,
|
|
45
|
-
truncated: result.truncated,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
head: result.head,
|
|
50
|
-
startLine: result.startLine,
|
|
51
|
-
endLine: result.endLine,
|
|
52
|
-
|
|
53
|
-
hasMoreLines: result.hasMoreLines,
|
|
39
|
+
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
40
|
+
...(result.totalLines !== undefined
|
|
41
|
+
? { totalLines: result.totalLines }
|
|
42
|
+
: {}),
|
|
43
|
+
...(result.head !== undefined ? { head: result.head } : {}),
|
|
44
|
+
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
45
|
+
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
46
|
+
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
54
47
|
};
|
|
55
48
|
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(args.path)}`, mimeType: 'text/plain' });
|
|
56
49
|
if (!externalized) {
|
|
57
|
-
return buildToolResponse(result.content, structured
|
|
50
|
+
return buildToolResponse(result.content, structured);
|
|
58
51
|
}
|
|
59
52
|
const { entry, preview } = externalized;
|
|
60
53
|
const structuredWithResource = {
|
|
@@ -75,19 +68,18 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
75
68
|
mimeType: entry.mimeType,
|
|
76
69
|
description: 'Full file contents',
|
|
77
70
|
}),
|
|
78
|
-
]
|
|
71
|
+
]);
|
|
79
72
|
}
|
|
80
73
|
export function registerReadFileTool(server, options = {}) {
|
|
81
|
-
const handler = (args, extra) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
74
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
75
|
+
toolName: 'read',
|
|
76
|
+
extra,
|
|
77
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
78
|
+
context: { path: args.path },
|
|
79
|
+
run: (signal) => handleReadFile(args, signal, options.resourceStore),
|
|
80
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
|
|
81
|
+
});
|
|
82
|
+
const wrappedHandler = wrapToolHandler(handler, {
|
|
91
83
|
guard: options.isInitialized,
|
|
92
84
|
progressMessage: (args) => {
|
|
93
85
|
const name = path.basename(args.path);
|
|
@@ -97,5 +89,8 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
97
89
|
}
|
|
98
90
|
return `🕮 read: ${name}`;
|
|
99
91
|
},
|
|
100
|
-
})
|
|
92
|
+
});
|
|
93
|
+
if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
94
|
+
return;
|
|
95
|
+
server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), wrappedHandler);
|
|
101
96
|
}
|
|
@@ -2,24 +2,23 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import RE2 from 're2';
|
|
4
4
|
import safeRegex from 'safe-regex2';
|
|
5
|
-
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
5
|
+
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
6
6
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
7
7
|
import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
8
|
-
import { atomicWriteFile,
|
|
9
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
8
|
+
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
10
9
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
11
|
-
import { SearchAndReplaceInputSchema,
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter,
|
|
13
|
-
import {
|
|
10
|
+
import { SearchAndReplaceInputSchema, } from '../schemas.js';
|
|
11
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
12
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
13
|
const SEARCH_AND_REPLACE_TOOL = {
|
|
15
14
|
title: 'Search and Replace',
|
|
16
|
-
description: 'Search and replace text across multiple files.'
|
|
15
|
+
description: 'Search and replace text across multiple files matching a glob pattern. ' +
|
|
16
|
+
'Replaces ALL occurrences in each file (unlike `edit` which replaces only the first). ' +
|
|
17
|
+
'Use `filePattern` to scope which files are touched. ' +
|
|
18
|
+
'Always run with `dryRun: true` first to verify matches before writing. ' +
|
|
19
|
+
'Literal mode (default) matches exact text; `isRegex: true` enables RE2 regex with capture groups ($1, $2).',
|
|
17
20
|
inputSchema: SearchAndReplaceInputSchema,
|
|
18
|
-
|
|
19
|
-
annotations: {
|
|
20
|
-
readOnlyHint: false,
|
|
21
|
-
openWorldHint: false,
|
|
22
|
-
},
|
|
21
|
+
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
23
22
|
};
|
|
24
23
|
const MAX_FAILURES = 20;
|
|
25
24
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
@@ -110,7 +109,7 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
110
109
|
newContent = content.replace(regex, args.replacement);
|
|
111
110
|
}
|
|
112
111
|
else {
|
|
113
|
-
newContent = content.replaceAll(args.searchPattern, args.replacement);
|
|
112
|
+
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
114
113
|
}
|
|
115
114
|
await atomicWriteFile(validPath, newContent, {
|
|
116
115
|
encoding: 'utf-8',
|
|
@@ -183,13 +182,13 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
183
182
|
onProgress({ current });
|
|
184
183
|
}
|
|
185
184
|
async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
186
|
-
const maxFileSize =
|
|
185
|
+
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
187
186
|
const root = await resolveSearchRoot(args.path, signal);
|
|
188
187
|
const regex = createReplacementRegex(args);
|
|
189
188
|
const entries = globEntries({
|
|
190
189
|
cwd: root,
|
|
191
190
|
pattern: args.filePattern,
|
|
192
|
-
excludePatterns:
|
|
191
|
+
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
193
192
|
includeHidden: false,
|
|
194
193
|
baseNameMatch: false,
|
|
195
194
|
caseSensitiveMatch: true, // Default to sensitive for file paths
|
|
@@ -225,13 +224,16 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
225
224
|
});
|
|
226
225
|
}
|
|
227
226
|
export function registerSearchAndReplaceTool(server, options = {}) {
|
|
228
|
-
const handler = (args, extra) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
228
|
+
toolName: 'search_and_replace',
|
|
229
|
+
extra,
|
|
230
|
+
timedSignal: {},
|
|
231
|
+
...(args.path ? { context: { path: args.path } } : {}),
|
|
232
|
+
run: async (signal) => {
|
|
233
|
+
notifyProgress(extra, {
|
|
234
|
+
current: 0,
|
|
235
|
+
message: `🛠 search_and_replace: ${args.filePattern}`,
|
|
236
|
+
});
|
|
235
237
|
const result = await handleSearchAndReplace(args, signal, createProgressReporter(extra));
|
|
236
238
|
const sc = result.structuredContent;
|
|
237
239
|
const finalCurrent = (sc.processedFiles ?? 0) + 1;
|
|
@@ -240,23 +242,14 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
240
242
|
message: `🛠 search_and_replace: ${args.filePattern} ➟ ${String(sc.filesChanged ?? 0)} files`,
|
|
241
243
|
});
|
|
242
244
|
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), args.path ? { path: args.path } : {});
|
|
245
|
+
},
|
|
246
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
247
|
+
});
|
|
248
248
|
const { isInitialized } = options;
|
|
249
249
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
250
250
|
guard: isInitialized,
|
|
251
251
|
});
|
|
252
|
-
|
|
253
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
254
|
-
if (tasks?.registerToolTask) {
|
|
255
|
-
tasks.registerToolTask('search_and_replace', withDefaultIcons({
|
|
256
|
-
...SEARCH_AND_REPLACE_TOOL,
|
|
257
|
-
execution: { taskSupport: 'optional' },
|
|
258
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
252
|
+
if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
259
253
|
return;
|
|
260
|
-
}
|
|
261
254
|
server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), wrappedHandler);
|
|
262
255
|
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { joinLines } from '../config.js';
|
|
2
2
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
4
3
|
import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
5
|
-
import { ListAllowedDirectoriesInputSchema,
|
|
6
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
4
|
+
import { ListAllowedDirectoriesInputSchema, } from '../schemas.js';
|
|
5
|
+
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
6
|
const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
8
7
|
title: 'Workspace Roots',
|
|
9
8
|
description: 'List the workspace roots this server can access. ' +
|
|
10
9
|
'Call this first to see available directories. ' +
|
|
11
10
|
'All other tools only work within these directories.',
|
|
12
11
|
inputSchema: ListAllowedDirectoriesInputSchema,
|
|
13
|
-
|
|
14
|
-
annotations: {
|
|
15
|
-
readOnlyHint: true,
|
|
16
|
-
idempotentHint: true,
|
|
17
|
-
openWorldHint: false,
|
|
18
|
-
},
|
|
12
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
19
13
|
};
|
|
20
14
|
function buildTextRoots(dirs) {
|
|
21
15
|
if (dirs.length === 0) {
|
|
@@ -37,7 +31,12 @@ function handleListAllowedDirectories() {
|
|
|
37
31
|
return buildToolResponse(buildTextRoots(dirs), structured);
|
|
38
32
|
}
|
|
39
33
|
export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
40
|
-
const handler = (
|
|
34
|
+
const handler = (_args, extra) => executeToolWithDiagnostics({
|
|
35
|
+
toolName: 'roots',
|
|
36
|
+
extra,
|
|
37
|
+
run: () => handleListAllowedDirectories(),
|
|
38
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
|
|
39
|
+
});
|
|
41
40
|
server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
42
41
|
guard: options.isInitialized,
|
|
43
42
|
progressMessage: () => '≣ roots',
|
|
@@ -4,24 +4,19 @@ import { formatOperationSummary, joinLines } from '../config.js';
|
|
|
4
4
|
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
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { createToolTaskHandler } from './task-support.js';
|
|
7
|
+
import { SearchContentInputSchema, } from '../schemas.js';
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
9
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
11
10
|
const MAX_INLINE_MATCHES = 50;
|
|
12
11
|
const SEARCH_CONTENT_TOOL = {
|
|
13
12
|
title: 'Search Content',
|
|
14
13
|
description: 'Search for text within file contents (grep-like). ' +
|
|
15
14
|
'Returns matching lines. ' +
|
|
16
15
|
'Path may be a directory or a single file. ' +
|
|
16
|
+
'Use `filePattern` to scope by file type (e.g. `**/*.ts`) and avoid noisy results. ' +
|
|
17
17
|
'Use includeHidden=true to include hidden files and directories.',
|
|
18
18
|
inputSchema: SearchContentInputSchema,
|
|
19
|
-
|
|
20
|
-
annotations: {
|
|
21
|
-
readOnlyHint: true,
|
|
22
|
-
idempotentHint: true,
|
|
23
|
-
openWorldHint: false,
|
|
24
|
-
},
|
|
19
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
25
20
|
};
|
|
26
21
|
function assertValidRegexPattern(pattern) {
|
|
27
22
|
try {
|
|
@@ -73,35 +68,47 @@ function formatSearchMatchLine(match) {
|
|
|
73
68
|
}
|
|
74
69
|
function buildStructuredSearchResult(result, normalizedMatches, options) {
|
|
75
70
|
const { summary } = result;
|
|
71
|
+
const matches = [];
|
|
72
|
+
for (const match of normalizedMatches) {
|
|
73
|
+
matches.push(buildSearchMatchPayload(match));
|
|
74
|
+
}
|
|
76
75
|
return {
|
|
77
76
|
ok: true,
|
|
78
77
|
patternType: options.patternType,
|
|
79
78
|
caseSensitive: options.caseSensitive,
|
|
80
|
-
matches
|
|
79
|
+
matches,
|
|
81
80
|
totalMatches: summary.matches,
|
|
82
|
-
truncated: summary.truncated,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
82
|
+
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
83
|
+
...(summary.skippedTooLarge
|
|
84
|
+
? { skippedTooLarge: summary.skippedTooLarge }
|
|
85
|
+
: {}),
|
|
86
|
+
...(summary.skippedBinary ? { skippedBinary: summary.skippedBinary } : {}),
|
|
87
|
+
...(summary.skippedInaccessible
|
|
88
|
+
? { skippedInaccessible: summary.skippedInaccessible }
|
|
89
|
+
: {}),
|
|
90
|
+
...(summary.linesSkippedDueToRegexTimeout
|
|
91
|
+
? { linesSkippedDueToRegexTimeout: summary.linesSkippedDueToRegexTimeout }
|
|
92
|
+
: {}),
|
|
89
93
|
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
function normalizeSearchMatches(result) {
|
|
93
97
|
const relativeByFile = new Map();
|
|
94
|
-
const normalized =
|
|
98
|
+
const normalized = [];
|
|
99
|
+
let index = 0;
|
|
100
|
+
for (const match of result.matches) {
|
|
95
101
|
const cached = relativeByFile.get(match.file);
|
|
96
102
|
const relative = cached ?? path.relative(result.basePath, match.file);
|
|
97
103
|
if (!cached)
|
|
98
104
|
relativeByFile.set(match.file, relative);
|
|
99
|
-
|
|
105
|
+
normalized.push({
|
|
100
106
|
...match,
|
|
101
107
|
relativeFile: relative,
|
|
102
108
|
index,
|
|
103
|
-
};
|
|
104
|
-
|
|
109
|
+
});
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
105
112
|
normalized.sort((a, b) => {
|
|
106
113
|
const fileCompare = a.relativeFile.localeCompare(b.relativeFile);
|
|
107
114
|
if (fileCompare !== 0)
|
|
@@ -127,7 +134,6 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
127
134
|
wholeWord: args.wholeWord,
|
|
128
135
|
contextLines: args.contextLines,
|
|
129
136
|
maxResults: args.maxResults,
|
|
130
|
-
maxFilesScanned: args.maxFilesScanned,
|
|
131
137
|
isLiteral: !args.isRegex,
|
|
132
138
|
};
|
|
133
139
|
if (signal) {
|
|
@@ -156,22 +162,14 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
156
162
|
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
157
163
|
}
|
|
158
164
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
165
|
+
const previewPayload = [];
|
|
166
|
+
for (const match of previewMatches) {
|
|
167
|
+
previewPayload.push(buildSearchMatchPayload(match));
|
|
168
|
+
}
|
|
159
169
|
const previewStructured = {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
caseSensitive: args.caseSensitive,
|
|
163
|
-
matches: previewMatches.map(buildSearchMatchPayload),
|
|
164
|
-
totalMatches: structuredFull.totalMatches,
|
|
170
|
+
...structuredFull,
|
|
171
|
+
matches: previewPayload,
|
|
165
172
|
truncated: true,
|
|
166
|
-
filesScanned: structuredFull.filesScanned,
|
|
167
|
-
filesMatched: structuredFull.filesMatched,
|
|
168
|
-
skippedTooLarge: structuredFull.skippedTooLarge,
|
|
169
|
-
skippedBinary: structuredFull.skippedBinary,
|
|
170
|
-
skippedInaccessible: structuredFull.skippedInaccessible,
|
|
171
|
-
linesSkippedDueToRegexTimeout: structuredFull.linesSkippedDueToRegexTimeout,
|
|
172
|
-
...(structuredFull.stoppedReason
|
|
173
|
-
? { stoppedReason: structuredFull.stoppedReason }
|
|
174
|
-
: {}),
|
|
175
173
|
resourceUri: undefined,
|
|
176
174
|
};
|
|
177
175
|
const entry = resourceStore.putText({
|
|
@@ -180,10 +178,13 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
180
178
|
text: JSON.stringify(structuredFull),
|
|
181
179
|
});
|
|
182
180
|
previewStructured.resourceUri = entry.uri;
|
|
183
|
-
const
|
|
181
|
+
const textLines = [
|
|
184
182
|
`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`,
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
];
|
|
184
|
+
for (const match of previewMatches) {
|
|
185
|
+
textLines.push(formatSearchMatchLine(match));
|
|
186
|
+
}
|
|
187
|
+
const text = joinLines(textLines);
|
|
187
188
|
return buildToolResponse(text, previewStructured, [
|
|
188
189
|
buildResourceLink({
|
|
189
190
|
uri: entry.uri,
|
|
@@ -194,44 +195,43 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
194
195
|
]);
|
|
195
196
|
}
|
|
196
197
|
export function registerSearchContentTool(server, options = {}) {
|
|
197
|
-
const handler = (args, extra) =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
198
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
199
|
+
toolName: 'grep',
|
|
200
|
+
extra,
|
|
201
|
+
context: { path: args.path ?? '.' },
|
|
202
|
+
run: async (signal) => {
|
|
203
|
+
const normalizedArgs = SearchContentInputSchema.parse(args);
|
|
204
|
+
notifyProgress(extra, {
|
|
205
|
+
current: 0,
|
|
206
|
+
message: `🔎︎ grep: ${normalizedArgs.pattern}`,
|
|
207
|
+
});
|
|
208
|
+
const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, createProgressReporter(extra));
|
|
209
|
+
const sc = result.structuredContent;
|
|
210
|
+
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
211
|
+
let suffix;
|
|
212
|
+
if (count === 0) {
|
|
213
|
+
suffix = 'No matches';
|
|
214
|
+
}
|
|
215
|
+
else if (count === 1) {
|
|
216
|
+
suffix = '1 match';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
suffix = `${count} matches`;
|
|
220
|
+
}
|
|
221
|
+
const finalCurrent = (sc.filesScanned ?? 0) + 1;
|
|
222
|
+
notifyProgress(extra, {
|
|
223
|
+
current: finalCurrent,
|
|
224
|
+
message: `🔎︎ grep: ${normalizedArgs.pattern} ➟ ${suffix}`,
|
|
225
|
+
});
|
|
226
|
+
return result;
|
|
227
|
+
},
|
|
228
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
229
|
+
});
|
|
223
230
|
const { isInitialized } = options;
|
|
224
231
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
225
232
|
guard: isInitialized,
|
|
226
233
|
});
|
|
227
|
-
|
|
228
|
-
const tasks = getExperimentalTaskRegistration(server);
|
|
229
|
-
if (tasks?.registerToolTask) {
|
|
230
|
-
tasks.registerToolTask('grep', withDefaultIcons({
|
|
231
|
-
...SEARCH_CONTENT_TOOL,
|
|
232
|
-
execution: { taskSupport: 'optional' },
|
|
233
|
-
}, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
|
|
234
|
+
if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
234
235
|
return;
|
|
235
|
-
}
|
|
236
236
|
server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), wrappedHandler);
|
|
237
237
|
}
|