@j0hanz/filesystem-mcp 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +4 -2
- package/dist/config.js +2 -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 +15 -8
- 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 +232 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +96 -44
- package/dist/tools/apply-patch.js +23 -22
- package/dist/tools/calculate-hash.js +41 -43
- package/dist/tools/create-directory.js +17 -19
- package/dist/tools/delete-file.js +35 -37
- package/dist/tools/diff-files.js +15 -19
- package/dist/tools/edit-file.js +15 -18
- package/dist/tools/list-directory.js +24 -23
- package/dist/tools/move-file.js +17 -19
- package/dist/tools/read-multiple.js +55 -66
- package/dist/tools/read.js +26 -30
- package/dist/tools/replace-in-files.js +27 -33
- package/dist/tools/roots.js +8 -8
- package/dist/tools/search-content.js +73 -72
- package/dist/tools/search-files.js +44 -50
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +44 -66
- package/dist/tools/stat.js +10 -37
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +12 -28
- package/dist/tools/write-file.js +17 -19
- package/dist/tools.js +23 -18
- package/package.json +6 -7
|
@@ -2,11 +2,9 @@ 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 { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
6
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
7
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse,
|
|
9
|
-
import {
|
|
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. ' +
|
|
@@ -14,25 +12,15 @@ const READ_MULTIPLE_FILES_TOOL = {
|
|
|
14
12
|
'For single file, use read for simpler output.',
|
|
15
13
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
16
14
|
outputSchema: ReadMultipleFilesOutputSchema,
|
|
17
|
-
annotations:
|
|
18
|
-
readOnlyHint: true,
|
|
19
|
-
idempotentHint: true,
|
|
20
|
-
openWorldHint: false,
|
|
21
|
-
},
|
|
15
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
22
16
|
};
|
|
23
17
|
async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
24
18
|
const options = {
|
|
25
19
|
...(signal ? { signal } : {}),
|
|
20
|
+
...(args.head !== undefined ? { head: args.head } : {}),
|
|
21
|
+
...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
|
|
22
|
+
...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
|
|
26
23
|
};
|
|
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
24
|
const results = await readMultipleFiles(args.paths, options);
|
|
37
25
|
const maxTotalSize = DEFAULT_READ_MANY_MAX_TOTAL_SIZE;
|
|
38
26
|
const mappedResults = results.map((result) => {
|
|
@@ -65,78 +53,79 @@ async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
|
65
53
|
truncationReason: 'externalized',
|
|
66
54
|
};
|
|
67
55
|
});
|
|
56
|
+
let succeeded = 0;
|
|
57
|
+
let failed = 0;
|
|
58
|
+
for (const result of mappedResults) {
|
|
59
|
+
if (result.error === undefined)
|
|
60
|
+
succeeded += 1;
|
|
61
|
+
else
|
|
62
|
+
failed += 1;
|
|
63
|
+
}
|
|
68
64
|
const structured = {
|
|
69
65
|
ok: true,
|
|
70
66
|
results: mappedResults.map((result) => ({
|
|
71
67
|
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
|
-
|
|
68
|
+
...(result.content !== undefined ? { content: result.content } : {}),
|
|
69
|
+
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
70
|
+
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
71
|
+
...(result.head !== undefined ? { head: result.head } : {}),
|
|
72
|
+
...(result.startLine !== undefined
|
|
73
|
+
? { startLine: result.startLine }
|
|
74
|
+
: {}),
|
|
75
|
+
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
76
|
+
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
77
|
+
...(result.totalLines !== undefined
|
|
78
|
+
? { totalLines: result.totalLines }
|
|
79
|
+
: {}),
|
|
80
|
+
...(result.truncationReason
|
|
81
|
+
? { truncationReason: result.truncationReason }
|
|
82
|
+
: {}),
|
|
83
|
+
...(result.error ? { error: result.error } : {}),
|
|
85
84
|
})),
|
|
86
85
|
summary: {
|
|
87
86
|
total: mappedResults.length,
|
|
88
|
-
succeeded
|
|
89
|
-
failed
|
|
87
|
+
succeeded,
|
|
88
|
+
failed,
|
|
90
89
|
},
|
|
91
90
|
};
|
|
92
|
-
const resourceLinks =
|
|
91
|
+
const resourceLinks = [];
|
|
92
|
+
for (const result of mappedResults) {
|
|
93
93
|
if (!result.resourceUri)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
];
|
|
102
|
-
});
|
|
94
|
+
continue;
|
|
95
|
+
resourceLinks.push(buildResourceLink({
|
|
96
|
+
uri: result.resourceUri,
|
|
97
|
+
name: `read:${path.basename(result.path)}`,
|
|
98
|
+
description: 'Full file contents',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
103
101
|
const text = mappedResults
|
|
104
102
|
.map((result) => {
|
|
103
|
+
const header = `=== ${result.path} ===`;
|
|
105
104
|
if (result.error) {
|
|
106
|
-
return `${
|
|
105
|
+
return `${header}\nError: ${result.error}`;
|
|
107
106
|
}
|
|
108
|
-
return result.
|
|
107
|
+
return `${header}\n${result.content ?? ''}`;
|
|
109
108
|
})
|
|
110
|
-
.join('\n');
|
|
111
|
-
return buildToolResponse(text, structured, resourceLinks
|
|
109
|
+
.join('\n\n');
|
|
110
|
+
return buildToolResponse(text, structured, resourceLinks);
|
|
112
111
|
}
|
|
113
112
|
export function registerReadMultipleFilesTool(server, options = {}) {
|
|
114
113
|
const handler = (args, extra) => {
|
|
115
114
|
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 });
|
|
115
|
+
return executeToolWithDiagnostics({
|
|
116
|
+
toolName: 'read_many',
|
|
117
|
+
extra,
|
|
118
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
119
|
+
context: { path: primaryPath },
|
|
120
|
+
run: (signal) => handleReadMultipleFiles(args, signal, options.resourceStore),
|
|
121
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
|
|
122
|
+
});
|
|
125
123
|
};
|
|
126
124
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
127
125
|
guard: options.isInitialized,
|
|
128
126
|
progressMessage: (args) => `🕮 read_many: ${args.paths.length} files`,
|
|
129
127
|
});
|
|
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));
|
|
128
|
+
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
139
129
|
return;
|
|
140
|
-
}
|
|
141
130
|
server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), wrappedHandler);
|
|
142
131
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -2,10 +2,9 @@ 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 { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
6
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
7
5
|
import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, maybeExternalizeTextContent,
|
|
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. ' +
|
|
@@ -13,11 +12,7 @@ const READ_FILE_TOOL = {
|
|
|
13
12
|
'For multiple files, use read_many for efficiency.',
|
|
14
13
|
inputSchema: ReadFileInputSchema,
|
|
15
14
|
outputSchema: ReadFileOutputSchema,
|
|
16
|
-
annotations:
|
|
17
|
-
readOnlyHint: true,
|
|
18
|
-
idempotentHint: true,
|
|
19
|
-
openWorldHint: false,
|
|
20
|
-
},
|
|
15
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
21
16
|
};
|
|
22
17
|
async function handleReadFile(args, signal, resourceStore) {
|
|
23
18
|
const options = {
|
|
@@ -42,19 +37,18 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
42
37
|
ok: true,
|
|
43
38
|
path: args.path,
|
|
44
39
|
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,
|
|
40
|
+
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
41
|
+
...(result.totalLines !== undefined
|
|
42
|
+
? { totalLines: result.totalLines }
|
|
43
|
+
: {}),
|
|
44
|
+
...(result.head !== undefined ? { head: result.head } : {}),
|
|
45
|
+
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
46
|
+
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
47
|
+
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
54
48
|
};
|
|
55
49
|
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: `read:${path.basename(args.path)}`, mimeType: 'text/plain' });
|
|
56
50
|
if (!externalized) {
|
|
57
|
-
return buildToolResponse(result.content, structured
|
|
51
|
+
return buildToolResponse(result.content, structured);
|
|
58
52
|
}
|
|
59
53
|
const { entry, preview } = externalized;
|
|
60
54
|
const structuredWithResource = {
|
|
@@ -75,19 +69,18 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
75
69
|
mimeType: entry.mimeType,
|
|
76
70
|
description: 'Full file contents',
|
|
77
71
|
}),
|
|
78
|
-
]
|
|
72
|
+
]);
|
|
79
73
|
}
|
|
80
74
|
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, {
|
|
75
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
76
|
+
toolName: 'read',
|
|
77
|
+
extra,
|
|
78
|
+
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
79
|
+
context: { path: args.path },
|
|
80
|
+
run: (signal) => handleReadFile(args, signal, options.resourceStore),
|
|
81
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
|
|
82
|
+
});
|
|
83
|
+
const wrappedHandler = wrapToolHandler(handler, {
|
|
91
84
|
guard: options.isInitialized,
|
|
92
85
|
progressMessage: (args) => {
|
|
93
86
|
const name = path.basename(args.path);
|
|
@@ -97,5 +90,8 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
97
90
|
}
|
|
98
91
|
return `🕮 read: ${name}`;
|
|
99
92
|
},
|
|
100
|
-
})
|
|
93
|
+
});
|
|
94
|
+
if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
95
|
+
return;
|
|
96
|
+
server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), wrappedHandler);
|
|
101
97
|
}
|
|
@@ -2,24 +2,24 @@ 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
10
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter,
|
|
13
|
-
import {
|
|
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
21
|
outputSchema: SearchAndReplaceOutputSchema,
|
|
19
|
-
annotations:
|
|
20
|
-
readOnlyHint: false,
|
|
21
|
-
openWorldHint: false,
|
|
22
|
-
},
|
|
22
|
+
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
23
23
|
};
|
|
24
24
|
const MAX_FAILURES = 20;
|
|
25
25
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
@@ -110,7 +110,7 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
110
110
|
newContent = content.replace(regex, args.replacement);
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
|
-
newContent = content.replaceAll(args.searchPattern, args.replacement);
|
|
113
|
+
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
114
114
|
}
|
|
115
115
|
await atomicWriteFile(validPath, newContent, {
|
|
116
116
|
encoding: 'utf-8',
|
|
@@ -183,13 +183,13 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
183
183
|
onProgress({ current });
|
|
184
184
|
}
|
|
185
185
|
async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
186
|
-
const maxFileSize =
|
|
186
|
+
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
187
187
|
const root = await resolveSearchRoot(args.path, signal);
|
|
188
188
|
const regex = createReplacementRegex(args);
|
|
189
189
|
const entries = globEntries({
|
|
190
190
|
cwd: root,
|
|
191
191
|
pattern: args.filePattern,
|
|
192
|
-
excludePatterns:
|
|
192
|
+
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
193
193
|
includeHidden: false,
|
|
194
194
|
baseNameMatch: false,
|
|
195
195
|
caseSensitiveMatch: true, // Default to sensitive for file paths
|
|
@@ -225,13 +225,16 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
export function registerSearchAndReplaceTool(server, options = {}) {
|
|
228
|
-
const handler = (args, extra) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
229
|
+
toolName: 'search_and_replace',
|
|
230
|
+
extra,
|
|
231
|
+
timedSignal: {},
|
|
232
|
+
...(args.path ? { context: { path: args.path } } : {}),
|
|
233
|
+
run: async (signal) => {
|
|
234
|
+
notifyProgress(extra, {
|
|
235
|
+
current: 0,
|
|
236
|
+
message: `🛠 search_and_replace: ${args.filePattern}`,
|
|
237
|
+
});
|
|
235
238
|
const result = await handleSearchAndReplace(args, signal, createProgressReporter(extra));
|
|
236
239
|
const sc = result.structuredContent;
|
|
237
240
|
const finalCurrent = (sc.processedFiles ?? 0) + 1;
|
|
@@ -240,23 +243,14 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
240
243
|
message: `🛠 search_and_replace: ${args.filePattern} ➟ ${String(sc.filesChanged ?? 0)} files`,
|
|
241
244
|
});
|
|
242
245
|
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), args.path ? { path: args.path } : {});
|
|
246
|
+
},
|
|
247
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
248
|
+
});
|
|
248
249
|
const { isInitialized } = options;
|
|
249
250
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
250
251
|
guard: isInitialized,
|
|
251
252
|
});
|
|
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));
|
|
253
|
+
if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
259
254
|
return;
|
|
260
|
-
}
|
|
261
255
|
server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), wrappedHandler);
|
|
262
256
|
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
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
4
|
import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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. ' +
|
|
@@ -11,11 +10,7 @@ const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
|
11
10
|
'All other tools only work within these directories.',
|
|
12
11
|
inputSchema: ListAllowedDirectoriesInputSchema,
|
|
13
12
|
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
14
|
-
annotations:
|
|
15
|
-
readOnlyHint: true,
|
|
16
|
-
idempotentHint: true,
|
|
17
|
-
openWorldHint: false,
|
|
18
|
-
},
|
|
13
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
19
14
|
};
|
|
20
15
|
function buildTextRoots(dirs) {
|
|
21
16
|
if (dirs.length === 0) {
|
|
@@ -37,7 +32,12 @@ function handleListAllowedDirectories() {
|
|
|
37
32
|
return buildToolResponse(buildTextRoots(dirs), structured);
|
|
38
33
|
}
|
|
39
34
|
export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
40
|
-
const handler = (
|
|
35
|
+
const handler = (_args, extra) => executeToolWithDiagnostics({
|
|
36
|
+
toolName: 'roots',
|
|
37
|
+
extra,
|
|
38
|
+
run: () => handleListAllowedDirectories(),
|
|
39
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
|
|
40
|
+
});
|
|
41
41
|
server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
42
42
|
guard: options.isInitialized,
|
|
43
43
|
progressMessage: () => '≣ roots',
|
|
@@ -4,24 +4,20 @@ 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 { withToolDiagnostics } from '../lib/observability.js';
|
|
8
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
9
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter,
|
|
10
|
-
import {
|
|
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
19
|
outputSchema: SearchContentOutputSchema,
|
|
20
|
-
annotations:
|
|
21
|
-
readOnlyHint: true,
|
|
22
|
-
idempotentHint: true,
|
|
23
|
-
openWorldHint: false,
|
|
24
|
-
},
|
|
20
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
25
21
|
};
|
|
26
22
|
function assertValidRegexPattern(pattern) {
|
|
27
23
|
try {
|
|
@@ -73,35 +69,47 @@ function formatSearchMatchLine(match) {
|
|
|
73
69
|
}
|
|
74
70
|
function buildStructuredSearchResult(result, normalizedMatches, options) {
|
|
75
71
|
const { summary } = result;
|
|
72
|
+
const matches = [];
|
|
73
|
+
for (const match of normalizedMatches) {
|
|
74
|
+
matches.push(buildSearchMatchPayload(match));
|
|
75
|
+
}
|
|
76
76
|
return {
|
|
77
77
|
ok: true,
|
|
78
78
|
patternType: options.patternType,
|
|
79
79
|
caseSensitive: options.caseSensitive,
|
|
80
|
-
matches
|
|
80
|
+
matches,
|
|
81
81
|
totalMatches: summary.matches,
|
|
82
|
-
truncated: summary.truncated,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
83
|
+
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
84
|
+
...(summary.skippedTooLarge
|
|
85
|
+
? { skippedTooLarge: summary.skippedTooLarge }
|
|
86
|
+
: {}),
|
|
87
|
+
...(summary.skippedBinary ? { skippedBinary: summary.skippedBinary } : {}),
|
|
88
|
+
...(summary.skippedInaccessible
|
|
89
|
+
? { skippedInaccessible: summary.skippedInaccessible }
|
|
90
|
+
: {}),
|
|
91
|
+
...(summary.linesSkippedDueToRegexTimeout
|
|
92
|
+
? { linesSkippedDueToRegexTimeout: summary.linesSkippedDueToRegexTimeout }
|
|
93
|
+
: {}),
|
|
89
94
|
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
90
95
|
};
|
|
91
96
|
}
|
|
92
97
|
function normalizeSearchMatches(result) {
|
|
93
98
|
const relativeByFile = new Map();
|
|
94
|
-
const normalized =
|
|
99
|
+
const normalized = [];
|
|
100
|
+
let index = 0;
|
|
101
|
+
for (const match of result.matches) {
|
|
95
102
|
const cached = relativeByFile.get(match.file);
|
|
96
103
|
const relative = cached ?? path.relative(result.basePath, match.file);
|
|
97
104
|
if (!cached)
|
|
98
105
|
relativeByFile.set(match.file, relative);
|
|
99
|
-
|
|
106
|
+
normalized.push({
|
|
100
107
|
...match,
|
|
101
108
|
relativeFile: relative,
|
|
102
109
|
index,
|
|
103
|
-
};
|
|
104
|
-
|
|
110
|
+
});
|
|
111
|
+
index += 1;
|
|
112
|
+
}
|
|
105
113
|
normalized.sort((a, b) => {
|
|
106
114
|
const fileCompare = a.relativeFile.localeCompare(b.relativeFile);
|
|
107
115
|
if (fileCompare !== 0)
|
|
@@ -127,7 +135,6 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
127
135
|
wholeWord: args.wholeWord,
|
|
128
136
|
contextLines: args.contextLines,
|
|
129
137
|
maxResults: args.maxResults,
|
|
130
|
-
maxFilesScanned: args.maxFilesScanned,
|
|
131
138
|
isLiteral: !args.isRegex,
|
|
132
139
|
};
|
|
133
140
|
if (signal) {
|
|
@@ -156,22 +163,14 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
156
163
|
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
157
164
|
}
|
|
158
165
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
166
|
+
const previewPayload = [];
|
|
167
|
+
for (const match of previewMatches) {
|
|
168
|
+
previewPayload.push(buildSearchMatchPayload(match));
|
|
169
|
+
}
|
|
159
170
|
const previewStructured = {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
caseSensitive: args.caseSensitive,
|
|
163
|
-
matches: previewMatches.map(buildSearchMatchPayload),
|
|
164
|
-
totalMatches: structuredFull.totalMatches,
|
|
171
|
+
...structuredFull,
|
|
172
|
+
matches: previewPayload,
|
|
165
173
|
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
174
|
resourceUri: undefined,
|
|
176
175
|
};
|
|
177
176
|
const entry = resourceStore.putText({
|
|
@@ -180,10 +179,13 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
180
179
|
text: JSON.stringify(structuredFull),
|
|
181
180
|
});
|
|
182
181
|
previewStructured.resourceUri = entry.uri;
|
|
183
|
-
const
|
|
182
|
+
const textLines = [
|
|
184
183
|
`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`,
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
];
|
|
185
|
+
for (const match of previewMatches) {
|
|
186
|
+
textLines.push(formatSearchMatchLine(match));
|
|
187
|
+
}
|
|
188
|
+
const text = joinLines(textLines);
|
|
187
189
|
return buildToolResponse(text, previewStructured, [
|
|
188
190
|
buildResourceLink({
|
|
189
191
|
uri: entry.uri,
|
|
@@ -194,44 +196,43 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
194
196
|
]);
|
|
195
197
|
}
|
|
196
198
|
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
|
-
|
|
199
|
+
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
200
|
+
toolName: 'grep',
|
|
201
|
+
extra,
|
|
202
|
+
context: { path: args.path ?? '.' },
|
|
203
|
+
run: async (signal) => {
|
|
204
|
+
const normalizedArgs = SearchContentInputSchema.parse(args);
|
|
205
|
+
notifyProgress(extra, {
|
|
206
|
+
current: 0,
|
|
207
|
+
message: `🔎︎ grep: ${normalizedArgs.pattern}`,
|
|
208
|
+
});
|
|
209
|
+
const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, createProgressReporter(extra));
|
|
210
|
+
const sc = result.structuredContent;
|
|
211
|
+
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
212
|
+
let suffix;
|
|
213
|
+
if (count === 0) {
|
|
214
|
+
suffix = 'No matches';
|
|
215
|
+
}
|
|
216
|
+
else if (count === 1) {
|
|
217
|
+
suffix = '1 match';
|
|
218
|
+
}
|
|
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
|
+
},
|
|
229
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
230
|
+
});
|
|
223
231
|
const { isInitialized } = options;
|
|
224
232
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
225
233
|
guard: isInitialized,
|
|
226
234
|
});
|
|
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));
|
|
235
|
+
if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
234
236
|
return;
|
|
235
|
-
}
|
|
236
237
|
server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), wrappedHandler);
|
|
237
238
|
}
|