@j0hanz/filesystem-mcp 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +26 -0
- package/dist/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/server/roots-manager.d.ts +1 -1
- package/dist/server/roots-manager.js +8 -13
- package/dist/tools/apply-patch.js +34 -38
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +183 -115
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +266 -217
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -5,8 +5,11 @@ import { readMultipleFiles } from '../lib/file-operations/metadata.js';
|
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
6
|
import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const READ_MANY_TOOL_NAME = 'read_many';
|
|
9
|
+
const READ_MANY_TOOL_LABEL = '🕮 read_many';
|
|
10
|
+
const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
|
|
11
|
+
export const READ_MANY_TOOL = {
|
|
12
|
+
name: READ_MANY_TOOL_NAME,
|
|
10
13
|
title: 'Read Multiple Files',
|
|
11
14
|
description: 'Read multiple text files in one request with contents and metadata. ' +
|
|
12
15
|
'For a single file, use `read`.',
|
|
@@ -19,26 +22,42 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
19
22
|
'Per-file `truncationReason` can be `head`, `tail`, `range`, or `externalized`.',
|
|
20
23
|
],
|
|
21
24
|
};
|
|
25
|
+
function buildReadManyResourceName(filePath) {
|
|
26
|
+
return `read:${path.basename(filePath)}`;
|
|
27
|
+
}
|
|
22
28
|
function toStructuredReadManyResult(result) {
|
|
23
|
-
|
|
29
|
+
const structuredResult = {
|
|
24
30
|
path: result.path,
|
|
25
|
-
...(result.content !== undefined ? { content: result.content } : {}),
|
|
26
|
-
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
27
|
-
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
28
|
-
...(result.head !== undefined ? { head: result.head } : {}),
|
|
29
|
-
...(result.tail !== undefined ? { tail: result.tail } : {}),
|
|
30
|
-
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
31
|
-
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
32
|
-
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
33
|
-
...(result.totalLines !== undefined
|
|
34
|
-
? { totalLines: result.totalLines }
|
|
35
|
-
: {}),
|
|
36
|
-
...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
|
|
37
|
-
...(result.truncationReason
|
|
38
|
-
? { truncationReason: result.truncationReason }
|
|
39
|
-
: {}),
|
|
40
|
-
...(result.error ? { error: result.error } : {}),
|
|
41
31
|
};
|
|
32
|
+
if (result.content !== undefined)
|
|
33
|
+
structuredResult.content = result.content;
|
|
34
|
+
if (result.truncated)
|
|
35
|
+
structuredResult.truncated = true;
|
|
36
|
+
if (result.resourceUri)
|
|
37
|
+
structuredResult.resourceUri = result.resourceUri;
|
|
38
|
+
if (result.head !== undefined)
|
|
39
|
+
structuredResult.head = result.head;
|
|
40
|
+
if (result.tail !== undefined)
|
|
41
|
+
structuredResult.tail = result.tail;
|
|
42
|
+
if (result.startLine !== undefined) {
|
|
43
|
+
structuredResult.startLine = result.startLine;
|
|
44
|
+
}
|
|
45
|
+
if (result.endLine !== undefined)
|
|
46
|
+
structuredResult.endLine = result.endLine;
|
|
47
|
+
if (result.hasMoreLines)
|
|
48
|
+
structuredResult.hasMoreLines = true;
|
|
49
|
+
if (result.totalLines !== undefined) {
|
|
50
|
+
structuredResult.totalLines = result.totalLines;
|
|
51
|
+
}
|
|
52
|
+
if (result.linesRead !== undefined) {
|
|
53
|
+
structuredResult.linesRead = result.linesRead;
|
|
54
|
+
}
|
|
55
|
+
if (result.truncationReason) {
|
|
56
|
+
structuredResult.truncationReason = result.truncationReason;
|
|
57
|
+
}
|
|
58
|
+
if (result.error)
|
|
59
|
+
structuredResult.error = result.error;
|
|
60
|
+
return structuredResult;
|
|
42
61
|
}
|
|
43
62
|
function resolveReadManyTruncationReason(result) {
|
|
44
63
|
if (!result.truncated)
|
|
@@ -60,7 +79,7 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
|
|
|
60
79
|
if (!result.content) {
|
|
61
80
|
return baseResult;
|
|
62
81
|
}
|
|
63
|
-
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name:
|
|
82
|
+
const externalized = maybeExternalizeTextContent(resourceStore, result.content, { name: buildReadManyResourceName(result.path), mimeType: 'text/plain' });
|
|
64
83
|
if (!externalized) {
|
|
65
84
|
return baseResult;
|
|
66
85
|
}
|
|
@@ -72,66 +91,83 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
|
|
|
72
91
|
truncationReason: 'externalized',
|
|
73
92
|
};
|
|
74
93
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
uri: result.resourceUri,
|
|
82
|
-
name: `read:${path.basename(result.path)}`,
|
|
83
|
-
description: 'Full file contents',
|
|
84
|
-
}),
|
|
85
|
-
];
|
|
86
|
-
});
|
|
94
|
+
function buildReadManyTextSection(result) {
|
|
95
|
+
const header = `=== ${result.path} ===`;
|
|
96
|
+
if (result.error) {
|
|
97
|
+
return `${header}\nError: ${result.error}`;
|
|
98
|
+
}
|
|
99
|
+
return `${header}\n${result.content ?? ''}`;
|
|
87
100
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
function buildReadMultipleOptions(args, signal, onReadComplete) {
|
|
102
|
+
const options = {};
|
|
103
|
+
if (signal)
|
|
104
|
+
options.signal = signal;
|
|
105
|
+
if (args.head !== undefined)
|
|
106
|
+
options.head = args.head;
|
|
107
|
+
if (args.tail !== undefined)
|
|
108
|
+
options.tail = args.tail;
|
|
109
|
+
if (args.startLine !== undefined)
|
|
110
|
+
options.startLine = args.startLine;
|
|
111
|
+
if (args.endLine !== undefined)
|
|
112
|
+
options.endLine = args.endLine;
|
|
113
|
+
if (onReadComplete)
|
|
114
|
+
options.onReadComplete = onReadComplete;
|
|
115
|
+
return options;
|
|
116
|
+
}
|
|
117
|
+
function buildReadManyResponsePayload(results, resourceStore) {
|
|
118
|
+
const resourceLinks = [];
|
|
119
|
+
const structuredResults = [];
|
|
120
|
+
const textSections = [];
|
|
121
|
+
let succeeded = 0;
|
|
122
|
+
for (const result of results) {
|
|
123
|
+
const mappedResult = maybeExternalizeReadManyResult(result, resourceStore);
|
|
124
|
+
structuredResults.push(toStructuredReadManyResult(mappedResult));
|
|
125
|
+
textSections.push(buildReadManyTextSection(mappedResult));
|
|
126
|
+
if (mappedResult.resourceUri) {
|
|
127
|
+
resourceLinks.push(buildResourceLink({
|
|
128
|
+
uri: mappedResult.resourceUri,
|
|
129
|
+
name: buildReadManyResourceName(mappedResult.path),
|
|
130
|
+
description: FULL_FILE_CONTENTS_DESCRIPTION,
|
|
131
|
+
}));
|
|
94
132
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
133
|
+
if (mappedResult.error === undefined)
|
|
134
|
+
succeeded += 1;
|
|
135
|
+
}
|
|
136
|
+
const total = structuredResults.length;
|
|
137
|
+
return {
|
|
138
|
+
resourceLinks,
|
|
139
|
+
structuredResults,
|
|
140
|
+
summary: {
|
|
141
|
+
total,
|
|
142
|
+
succeeded,
|
|
143
|
+
failed: total - succeeded,
|
|
144
|
+
},
|
|
145
|
+
text: textSections.join('\n\n'),
|
|
146
|
+
};
|
|
98
147
|
}
|
|
99
148
|
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
100
|
-
const options =
|
|
101
|
-
...(signal ? { signal } : {}),
|
|
102
|
-
...(args.head !== undefined ? { head: args.head } : {}),
|
|
103
|
-
...(args.tail !== undefined ? { tail: args.tail } : {}),
|
|
104
|
-
...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
|
|
105
|
-
...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
|
|
106
|
-
...(onReadComplete ? { onReadComplete } : {}),
|
|
107
|
-
};
|
|
149
|
+
const options = buildReadMultipleOptions(args, signal, onReadComplete);
|
|
108
150
|
const results = await readMultipleFiles(args.paths, options);
|
|
109
|
-
const
|
|
110
|
-
const succeeded = mappedResults.filter((r) => r.error === undefined).length;
|
|
111
|
-
const failed = mappedResults.length - succeeded;
|
|
151
|
+
const payload = buildReadManyResponsePayload(results, resourceStore);
|
|
112
152
|
const structured = {
|
|
113
153
|
ok: true,
|
|
114
|
-
results:
|
|
115
|
-
summary:
|
|
116
|
-
total: mappedResults.length,
|
|
117
|
-
succeeded,
|
|
118
|
-
failed,
|
|
119
|
-
},
|
|
154
|
+
results: payload.structuredResults,
|
|
155
|
+
summary: payload.summary,
|
|
120
156
|
};
|
|
121
|
-
return buildToolResponse(
|
|
157
|
+
return buildToolResponse(payload.text, structured, payload.resourceLinks);
|
|
122
158
|
}
|
|
123
159
|
export function registerReadMultipleFilesTool(server, options = {}) {
|
|
124
160
|
const handler = (args, extra) => {
|
|
125
161
|
const primaryPath = args.paths[0] ?? '';
|
|
126
162
|
return executeToolWithDiagnostics({
|
|
127
|
-
toolName:
|
|
163
|
+
toolName: READ_MANY_TOOL_NAME,
|
|
128
164
|
extra,
|
|
129
165
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
130
166
|
context: { path: primaryPath },
|
|
131
167
|
run: async (signal) => {
|
|
132
168
|
const context = buildBatchPathContext(args.paths, 'files');
|
|
133
169
|
const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
|
|
134
|
-
toolLabel:
|
|
170
|
+
toolLabel: READ_MANY_TOOL_LABEL,
|
|
135
171
|
context,
|
|
136
172
|
totalItems: args.paths.length,
|
|
137
173
|
itemVerb: 'read',
|
|
@@ -142,11 +178,11 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
142
178
|
const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
|
|
143
179
|
const total = sc.summary?.total ?? 0;
|
|
144
180
|
const finalCurrent = resolveFinalProgressCurrent(progress, total);
|
|
145
|
-
progress.complete(
|
|
181
|
+
progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
|
|
146
182
|
return result;
|
|
147
183
|
}
|
|
148
184
|
catch (error) {
|
|
149
|
-
progress.fail(
|
|
185
|
+
progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • failed`);
|
|
150
186
|
throw error;
|
|
151
187
|
}
|
|
152
188
|
},
|
|
@@ -157,7 +193,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
157
193
|
guard: options.isInitialized,
|
|
158
194
|
});
|
|
159
195
|
const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
|
|
160
|
-
if (registerToolTaskIfAvailable(server,
|
|
196
|
+
if (registerToolTaskIfAvailable(server, READ_MANY_TOOL_NAME, READ_MANY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
161
197
|
return;
|
|
162
|
-
server.registerTool(
|
|
198
|
+
server.registerTool(READ_MANY_TOOL_NAME, withDefaultIcons({ ...READ_MANY_TOOL }, options.iconInfo), validatedHandler);
|
|
163
199
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -20,51 +20,61 @@ export const READ_FILE_TOOL = {
|
|
|
20
20
|
],
|
|
21
21
|
taskSupport: 'forbidden',
|
|
22
22
|
};
|
|
23
|
-
|
|
23
|
+
const READ_TOOL_NAME = 'read';
|
|
24
|
+
const READ_TOOL_LABEL = '🕮 read';
|
|
25
|
+
const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
|
|
26
|
+
function buildReadResourceName(filePath) {
|
|
27
|
+
return `read:${path.basename(filePath)}`;
|
|
28
|
+
}
|
|
29
|
+
function buildReadOptions(args, signal) {
|
|
24
30
|
const options = {
|
|
25
31
|
encoding: 'utf-8',
|
|
26
32
|
maxSize: MAX_TEXT_FILE_SIZE,
|
|
27
33
|
skipBinary: true,
|
|
28
34
|
};
|
|
29
|
-
if (
|
|
35
|
+
if (signal)
|
|
36
|
+
options.signal = signal;
|
|
37
|
+
if (args.head !== undefined)
|
|
30
38
|
options.head = args.head;
|
|
31
|
-
|
|
32
|
-
if (args.tail !== undefined) {
|
|
39
|
+
if (args.tail !== undefined)
|
|
33
40
|
options.tail = args.tail;
|
|
34
|
-
|
|
35
|
-
if (args.startLine !== undefined) {
|
|
41
|
+
if (args.startLine !== undefined)
|
|
36
42
|
options.startLine = args.startLine;
|
|
37
|
-
|
|
38
|
-
if (args.endLine !== undefined) {
|
|
43
|
+
if (args.endLine !== undefined)
|
|
39
44
|
options.endLine = args.endLine;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
const result = await readFile(args.path, options);
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
function toStructuredReadFileResult(args, result) {
|
|
45
48
|
const structured = {
|
|
46
49
|
ok: true,
|
|
47
50
|
path: args.path,
|
|
48
51
|
content: result.content,
|
|
49
|
-
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
50
|
-
...(result.totalLines !== undefined
|
|
51
|
-
? { totalLines: result.totalLines }
|
|
52
|
-
: {}),
|
|
53
|
-
...(result.head !== undefined ? { head: result.head } : {}),
|
|
54
|
-
...(result.tail !== undefined ? { tail: result.tail } : {}),
|
|
55
|
-
...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
|
|
56
|
-
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
57
|
-
...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
|
|
58
|
-
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
59
52
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
if (result.truncated)
|
|
54
|
+
structured.truncated = true;
|
|
55
|
+
if (result.totalLines !== undefined)
|
|
56
|
+
structured.totalLines = result.totalLines;
|
|
57
|
+
if (result.head !== undefined)
|
|
58
|
+
structured.head = result.head;
|
|
59
|
+
if (result.tail !== undefined)
|
|
60
|
+
structured.tail = result.tail;
|
|
61
|
+
if (result.startLine !== undefined)
|
|
62
|
+
structured.startLine = result.startLine;
|
|
63
|
+
if (result.endLine !== undefined)
|
|
64
|
+
structured.endLine = result.endLine;
|
|
65
|
+
if (result.linesRead !== undefined)
|
|
66
|
+
structured.linesRead = result.linesRead;
|
|
67
|
+
if (result.hasMoreLines)
|
|
68
|
+
structured.hasMoreLines = true;
|
|
69
|
+
return structured;
|
|
70
|
+
}
|
|
71
|
+
function maybeBuildExternalizedReadResponse(filePath, content, structured, resourceStore) {
|
|
72
|
+
const externalized = maybeExternalizeTextContent(resourceStore, content, {
|
|
73
|
+
name: buildReadResourceName(filePath),
|
|
74
|
+
mimeType: 'text/plain',
|
|
75
|
+
});
|
|
66
76
|
if (!externalized) {
|
|
67
|
-
return
|
|
77
|
+
return undefined;
|
|
68
78
|
}
|
|
69
79
|
const { entry, preview } = externalized;
|
|
70
80
|
const structuredWithResource = {
|
|
@@ -74,7 +84,7 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
74
84
|
resourceUri: entry.uri,
|
|
75
85
|
};
|
|
76
86
|
const text = [
|
|
77
|
-
`Output too large to inline (${
|
|
87
|
+
`Output too large to inline (${content.length} chars).`,
|
|
78
88
|
'Preview:',
|
|
79
89
|
preview,
|
|
80
90
|
].join('\n');
|
|
@@ -83,14 +93,70 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
83
93
|
uri: entry.uri,
|
|
84
94
|
name: entry.name,
|
|
85
95
|
mimeType: entry.mimeType,
|
|
86
|
-
description:
|
|
96
|
+
description: FULL_FILE_CONTENTS_DESCRIPTION,
|
|
87
97
|
expiresAt: entry.expiresAt,
|
|
88
98
|
}),
|
|
89
99
|
]);
|
|
90
100
|
}
|
|
101
|
+
function buildReadProgressMessage(args) {
|
|
102
|
+
const name = path.basename(args.path);
|
|
103
|
+
if (args.startLine !== undefined) {
|
|
104
|
+
const end = args.endLine ?? '…';
|
|
105
|
+
return `${READ_TOOL_LABEL}: ${name} [lines ${args.startLine}–${end}]`;
|
|
106
|
+
}
|
|
107
|
+
if (args.head !== undefined)
|
|
108
|
+
return `${READ_TOOL_LABEL}: ${name} [head ${args.head}]`;
|
|
109
|
+
if (args.tail !== undefined)
|
|
110
|
+
return `${READ_TOOL_LABEL}: ${name} [tail ${args.tail}]`;
|
|
111
|
+
return `${READ_TOOL_LABEL}: ${name}`;
|
|
112
|
+
}
|
|
113
|
+
function buildReadCompletionMessage(args, result) {
|
|
114
|
+
const name = path.basename(args.path);
|
|
115
|
+
if (result.isError)
|
|
116
|
+
return `${READ_TOOL_LABEL}: ${name} • failed`;
|
|
117
|
+
const structured = result.structuredContent;
|
|
118
|
+
if (!structured.ok)
|
|
119
|
+
return `${READ_TOOL_LABEL}: ${name} • failed`;
|
|
120
|
+
const lines = structured.linesRead ?? structured.totalLines;
|
|
121
|
+
if (structured.startLine !== undefined) {
|
|
122
|
+
const end = structured.linesRead
|
|
123
|
+
? structured.startLine + structured.linesRead - 1
|
|
124
|
+
: (structured.endLine ?? '…');
|
|
125
|
+
return `${READ_TOOL_LABEL}: ${name} • lines ${structured.startLine}–${end}`;
|
|
126
|
+
}
|
|
127
|
+
if (structured.head !== undefined) {
|
|
128
|
+
return structured.hasMoreLines
|
|
129
|
+
? `${READ_TOOL_LABEL}: ${name} • first ${String(lines ?? structured.head)} lines`
|
|
130
|
+
: `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.head)} lines`;
|
|
131
|
+
}
|
|
132
|
+
if (structured.tail !== undefined) {
|
|
133
|
+
return structured.hasMoreLines
|
|
134
|
+
? `${READ_TOOL_LABEL}: ${name} • last ${String(lines ?? structured.tail)} lines`
|
|
135
|
+
: `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.tail)} lines`;
|
|
136
|
+
}
|
|
137
|
+
if (structured.truncated) {
|
|
138
|
+
return `${READ_TOOL_LABEL}: ${name} • truncated [${String(lines)} lines]`;
|
|
139
|
+
}
|
|
140
|
+
return `${READ_TOOL_LABEL}: ${name} • ${String(lines)} lines`;
|
|
141
|
+
}
|
|
142
|
+
async function handleReadFile(args, signal, resourceStore) {
|
|
143
|
+
const options = buildReadOptions(args, signal);
|
|
144
|
+
const result = await readFile(args.path, options);
|
|
145
|
+
const structured = toStructuredReadFileResult(args, result);
|
|
146
|
+
if (args.includeHash) {
|
|
147
|
+
structured.contentHash = createHash('sha256')
|
|
148
|
+
.update(result.content, 'utf-8')
|
|
149
|
+
.digest('hex');
|
|
150
|
+
}
|
|
151
|
+
const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
|
|
152
|
+
if (externalizedResponse) {
|
|
153
|
+
return externalizedResponse;
|
|
154
|
+
}
|
|
155
|
+
return buildToolResponse(result.content, structured);
|
|
156
|
+
}
|
|
91
157
|
export function registerReadFileTool(server, options = {}) {
|
|
92
158
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
93
|
-
toolName:
|
|
159
|
+
toolName: READ_TOOL_NAME,
|
|
94
160
|
extra,
|
|
95
161
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
96
162
|
context: { path: args.path },
|
|
@@ -99,49 +165,11 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
99
165
|
});
|
|
100
166
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
101
167
|
guard: options.isInitialized,
|
|
102
|
-
progressMessage:
|
|
103
|
-
|
|
104
|
-
if (args.startLine !== undefined) {
|
|
105
|
-
const end = args.endLine ?? '…';
|
|
106
|
-
return `🕮 read: ${name} [lines ${args.startLine}–${end}]`;
|
|
107
|
-
}
|
|
108
|
-
if (args.head !== undefined)
|
|
109
|
-
return `🕮 read: ${name} [head ${args.head}]`;
|
|
110
|
-
if (args.tail !== undefined)
|
|
111
|
-
return `🕮 read: ${name} [tail ${args.tail}]`;
|
|
112
|
-
return `🕮 read: ${name}`;
|
|
113
|
-
},
|
|
114
|
-
completionMessage: (args, result) => {
|
|
115
|
-
const name = path.basename(args.path);
|
|
116
|
-
if (result.isError)
|
|
117
|
-
return `🕮 read: ${name} • failed`;
|
|
118
|
-
const sc = result.structuredContent;
|
|
119
|
-
if (!sc.ok)
|
|
120
|
-
return `🕮 read: ${name} • failed`;
|
|
121
|
-
const lines = sc.linesRead ?? sc.totalLines;
|
|
122
|
-
if (sc.startLine !== undefined) {
|
|
123
|
-
const end = sc.linesRead
|
|
124
|
-
? sc.startLine + sc.linesRead - 1
|
|
125
|
-
: (sc.endLine ?? '…');
|
|
126
|
-
return `🕮 read: ${name} • lines ${sc.startLine}–${end}`;
|
|
127
|
-
}
|
|
128
|
-
if (sc.head !== undefined) {
|
|
129
|
-
return sc.hasMoreLines
|
|
130
|
-
? `🕮 read: ${name} • first ${lines ?? sc.head} lines`
|
|
131
|
-
: `🕮 read: ${name} • ${lines ?? sc.head} lines`;
|
|
132
|
-
}
|
|
133
|
-
if (sc.tail !== undefined) {
|
|
134
|
-
return sc.hasMoreLines
|
|
135
|
-
? `🕮 read: ${name} • last ${lines ?? sc.tail} lines`
|
|
136
|
-
: `🕮 read: ${name} • ${lines ?? sc.tail} lines`;
|
|
137
|
-
}
|
|
138
|
-
if (sc.truncated)
|
|
139
|
-
return `🕮 read: ${name} • truncated [${String(lines)} lines]`;
|
|
140
|
-
return `🕮 read: ${name} • ${String(lines)} lines`;
|
|
141
|
-
},
|
|
168
|
+
progressMessage: buildReadProgressMessage,
|
|
169
|
+
completionMessage: buildReadCompletionMessage,
|
|
142
170
|
});
|
|
143
171
|
const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
|
|
144
|
-
if (registerToolTaskIfAvailable(server,
|
|
172
|
+
if (registerToolTaskIfAvailable(server, READ_TOOL_NAME, READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
145
173
|
return;
|
|
146
|
-
server.registerTool(
|
|
174
|
+
server.registerTool(READ_TOOL_NAME, withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
|
|
147
175
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
3
4
|
import { createTwoFilesPatch } from 'diff';
|
|
4
5
|
import RE2 from 're2';
|
|
5
6
|
import safeRegex from 'safe-regex2';
|
|
6
7
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
7
8
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
8
9
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
9
|
-
import { atomicWriteFile
|
|
10
|
+
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
10
11
|
import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
|
|
11
12
|
import { reportPeriodicProgress } from '../lib/utils.js';
|
|
12
13
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
@@ -57,42 +58,57 @@ function createRegexMatcher(pattern, caseSensitive) {
|
|
|
57
58
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
class RegexReplacementMatcher {
|
|
62
|
+
regex;
|
|
63
|
+
constructor(regex) {
|
|
64
|
+
this.regex = regex;
|
|
65
|
+
}
|
|
66
|
+
count(content) {
|
|
67
|
+
this.regex.lastIndex = 0;
|
|
63
68
|
let matchCount = 0;
|
|
64
|
-
while (regex.exec(content) !== null) {
|
|
69
|
+
while (this.regex.exec(content) !== null) {
|
|
65
70
|
matchCount++;
|
|
66
|
-
if (regex.lastIndex === 0) {
|
|
67
|
-
regex.lastIndex++;
|
|
71
|
+
if (this.regex.lastIndex === 0) {
|
|
72
|
+
this.regex.lastIndex++;
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
return matchCount;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
regex.lastIndex = 0;
|
|
74
|
-
return content.replace(regex, replacement);
|
|
75
|
-
}
|
|
76
|
-
return { count, replace };
|
|
76
|
+
}
|
|
77
|
+
replace(content, replacement) {
|
|
78
|
+
this.regex.lastIndex = 0;
|
|
79
|
+
return content.replace(this.regex, replacement);
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
class LiteralReplacementMatcher {
|
|
83
|
+
searchPattern;
|
|
84
|
+
caseSensitive;
|
|
85
|
+
patternLength;
|
|
86
|
+
searchBuffer;
|
|
87
|
+
constructor(searchPattern, caseSensitive) {
|
|
88
|
+
this.searchPattern = searchPattern;
|
|
89
|
+
this.caseSensitive = caseSensitive;
|
|
90
|
+
this.patternLength = searchPattern.length;
|
|
91
|
+
this.searchBuffer = caseSensitive
|
|
92
|
+
? Buffer.from(searchPattern, 'utf8')
|
|
93
|
+
: null;
|
|
94
|
+
}
|
|
95
|
+
testBuffer(buffer) {
|
|
96
|
+
if (!this.searchBuffer)
|
|
97
|
+
return true;
|
|
98
|
+
return buffer.indexOf(this.searchBuffer) !== -1;
|
|
83
99
|
}
|
|
84
|
-
|
|
100
|
+
count(content) {
|
|
85
101
|
let matchCount = 0;
|
|
86
|
-
let pos = content.indexOf(searchPattern);
|
|
87
|
-
const patternLength = searchPattern.length;
|
|
102
|
+
let pos = content.indexOf(this.searchPattern);
|
|
88
103
|
while (pos !== -1) {
|
|
89
104
|
matchCount++;
|
|
90
|
-
pos = content.indexOf(searchPattern, pos + patternLength);
|
|
105
|
+
pos = content.indexOf(this.searchPattern, pos + this.patternLength);
|
|
91
106
|
}
|
|
92
107
|
return matchCount;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
}
|
|
109
|
+
replace(content, replacement) {
|
|
110
|
+
return content.replaceAll(this.searchPattern, () => replacement);
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
function buildReplacementPlan(content, replacement, matcher) {
|
|
98
114
|
const matchCount = matcher.count(content);
|
|
@@ -135,7 +151,7 @@ async function processEntry(entryPath, context) {
|
|
|
135
151
|
summary.totalMatches += plan.matchCount;
|
|
136
152
|
summary.filesChanged++;
|
|
137
153
|
recordChangedFile(summary, validPath, plan.matchCount);
|
|
138
|
-
maybeAppendPatchDiff(summary, {
|
|
154
|
+
await maybeAppendPatchDiff(summary, {
|
|
139
155
|
filePath: validPath,
|
|
140
156
|
originalContent: plan.originalContent,
|
|
141
157
|
updatedContent: plan.updatedContent,
|
|
@@ -157,24 +173,50 @@ async function processEntry(entryPath, context) {
|
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
175
|
async function readReplacementPlan(validPath, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
176
|
+
let fileHandle;
|
|
177
|
+
try {
|
|
178
|
+
const fd = await fs.open(validPath, 'r');
|
|
179
|
+
fileHandle = fd;
|
|
180
|
+
const stats = await fileHandle.stat();
|
|
181
|
+
if (stats.size > context.maxFileSize) {
|
|
182
|
+
throw new Error(formatFileTooLargeError(validPath, stats.size, context.maxFileSize));
|
|
183
|
+
}
|
|
184
|
+
let content;
|
|
185
|
+
if (context.matcher.testBuffer) {
|
|
186
|
+
const buffer = await fileHandle.readFile({ signal: context.signal });
|
|
187
|
+
if (!context.matcher.testBuffer(buffer)) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
content = buffer.toString('utf-8');
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
content = await fileHandle.readFile({
|
|
194
|
+
encoding: 'utf-8',
|
|
195
|
+
signal: context.signal,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return buildReplacementPlan(content, context.replacement, context.matcher);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (fileHandle) {
|
|
202
|
+
await fileHandle.close();
|
|
203
|
+
}
|
|
163
204
|
}
|
|
164
|
-
const content = await fs.readFile(validPath, {
|
|
165
|
-
encoding: 'utf-8',
|
|
166
|
-
signal: context.signal,
|
|
167
|
-
});
|
|
168
|
-
return buildReplacementPlan(content, context.replacement, context.matcher);
|
|
169
205
|
}
|
|
170
|
-
function maybeAppendPatchDiff(summary, params) {
|
|
206
|
+
async function maybeAppendPatchDiff(summary, params) {
|
|
171
207
|
if (!params.includeDiff)
|
|
172
208
|
return;
|
|
173
209
|
if (summary.diff.length >= MAX_DIFF_SIZE) {
|
|
174
210
|
summary.diffTruncated = true;
|
|
175
211
|
return;
|
|
176
212
|
}
|
|
177
|
-
const patch =
|
|
213
|
+
const patch = await new Promise((resolve) => {
|
|
214
|
+
createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
|
|
215
|
+
callback: (res) => {
|
|
216
|
+
resolve(res ?? '');
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
});
|
|
178
220
|
if (summary.diff.length + patch.length <=
|
|
179
221
|
MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
|
|
180
222
|
summary.diff += patch;
|
|
@@ -247,9 +289,14 @@ function createReplacementRegex(args) {
|
|
|
247
289
|
function createReplacementMatcher(args) {
|
|
248
290
|
const regex = createReplacementRegex(args);
|
|
249
291
|
if (regex) {
|
|
250
|
-
return
|
|
292
|
+
return new RegexReplacementMatcher(regex);
|
|
293
|
+
}
|
|
294
|
+
if (!args.caseSensitive) {
|
|
295
|
+
const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
296
|
+
const caseInsensitiveRegex = new RE2(escaped, 'gi');
|
|
297
|
+
return new RegexReplacementMatcher(caseInsensitiveRegex);
|
|
251
298
|
}
|
|
252
|
-
return
|
|
299
|
+
return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
|
|
253
300
|
}
|
|
254
301
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
255
302
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|