@j0hanz/filesystem-mcp 1.7.2 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/file-operations/common.d.ts +42 -0
- package/dist/lib/file-operations/common.js +87 -0
- package/dist/lib/file-operations/file-info.js +13 -19
- package/dist/lib/file-operations/glob-engine.d.ts +1 -6
- package/dist/lib/file-operations/glob-engine.js +0 -9
- package/dist/lib/file-operations/list-directory.js +19 -39
- package/dist/lib/file-operations/read-multiple-files.js +11 -19
- package/dist/lib/file-operations/search-content.js +106 -113
- package/dist/lib/file-operations/search-files.js +28 -72
- package/dist/lib/file-operations/tree.d.ts +2 -2
- package/dist/lib/file-operations/tree.js +20 -32
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +14 -14
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +5 -5
- package/dist/resources/workflows.js +17 -17
- package/dist/schemas.js +21 -90
- package/dist/server/bootstrap.js +2 -2
- package/dist/tools/list-directory.js +3 -21
- package/dist/tools/read-multiple.js +21 -19
- package/dist/tools/search-files.js +3 -21
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/stat-many.js +20 -20
- package/package.json +1 -1
|
@@ -3,9 +3,9 @@ import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constan
|
|
|
3
3
|
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
4
|
import { isSensitivePath } from '../path-policy.js';
|
|
5
5
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
|
-
import { needsStatsForSort, withOptionalStoppedReason } from './common.js';
|
|
6
|
+
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './common.js';
|
|
7
7
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
8
|
-
import { globEntries
|
|
8
|
+
import { globEntries } from './glob-engine.js';
|
|
9
9
|
// Internal default for find tool - not exposed to MCP users
|
|
10
10
|
const INTERNAL_MAX_RESULTS = 1000;
|
|
11
11
|
function normalizeOptions(options) {
|
|
@@ -41,17 +41,17 @@ function buildSearchResult(entry, entryType, needsStats) {
|
|
|
41
41
|
...(modified !== undefined ? { modified } : {}),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
function markStopped(state, reason) {
|
|
45
|
-
state.truncated = true;
|
|
46
|
-
state.stoppedReason = reason;
|
|
47
|
-
}
|
|
48
44
|
function shouldStopCollecting(state, normalized, signal) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const stopReason = resolveStopReason({
|
|
46
|
+
signal,
|
|
47
|
+
current: state.filesScanned,
|
|
48
|
+
max: normalized.maxFilesScanned,
|
|
49
|
+
abortedReason: 'timeout',
|
|
50
|
+
maxReason: 'maxFiles',
|
|
51
|
+
});
|
|
52
|
+
if (stopReason !== undefined) {
|
|
53
|
+
state.truncated = true;
|
|
54
|
+
state.stoppedReason = stopReason;
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
return false;
|
|
@@ -99,7 +99,8 @@ function buildCollectResult(state) {
|
|
|
99
99
|
function handleEntry(entry, entryType, needsStats, normalized, state) {
|
|
100
100
|
state.results.push(buildSearchResult(entry, entryType, needsStats));
|
|
101
101
|
if (state.results.length >= normalized.maxResults) {
|
|
102
|
-
|
|
102
|
+
state.truncated = true;
|
|
103
|
+
state.stoppedReason = 'maxResults';
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
function reportSearchFilesProgress(onProgress, current, total, force = false) {
|
|
@@ -109,7 +110,7 @@ function reportSearchFilesProgress(onProgress, current, total, force = false) {
|
|
|
109
110
|
return;
|
|
110
111
|
onProgress({ current, total });
|
|
111
112
|
}
|
|
112
|
-
async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress) {
|
|
113
|
+
async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
|
|
113
114
|
for await (const entry of stream) {
|
|
114
115
|
if (shouldStopCollecting(state, normalized, signal))
|
|
115
116
|
break;
|
|
@@ -122,7 +123,7 @@ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher
|
|
|
122
123
|
if (!shouldIncludeEntry(entryType, normalized)) {
|
|
123
124
|
continue;
|
|
124
125
|
}
|
|
125
|
-
const isAccessible = await
|
|
126
|
+
const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
|
|
126
127
|
if (!isAccessible) {
|
|
127
128
|
state.skippedInaccessible++;
|
|
128
129
|
continue;
|
|
@@ -138,31 +139,21 @@ function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
|
138
139
|
return false;
|
|
139
140
|
return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
|
|
140
141
|
}
|
|
141
|
-
async function isEntryAccessible(entry, entryType, rootDirectories, signal) {
|
|
142
|
-
if (entryType === 'symlink') {
|
|
143
|
-
try {
|
|
144
|
-
const validated = await validateExistingPathDetailed(entry.path, signal);
|
|
145
|
-
return !isSensitivePath(validated.requestedPath, validated.resolvedPath);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const resolvedPath = normalizePath(entry.path);
|
|
152
|
-
if (!isPathWithinDirectories(resolvedPath, rootDirectories)) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
return !isSensitivePath(entry.path, resolvedPath);
|
|
156
|
-
}
|
|
157
142
|
async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
158
143
|
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
159
144
|
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
160
145
|
const state = createCollectState();
|
|
161
146
|
const rootDirectories = [root];
|
|
147
|
+
const accessDeps = {
|
|
148
|
+
normalizePath,
|
|
149
|
+
isPathWithinDirectories,
|
|
150
|
+
isSensitivePath,
|
|
151
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
152
|
+
};
|
|
162
153
|
const gitignoreMatcher = normalized.respectGitignore
|
|
163
154
|
? await loadRootGitignore(root, signal)
|
|
164
155
|
: null;
|
|
165
|
-
await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress);
|
|
156
|
+
await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
|
|
166
157
|
return buildCollectResult(state);
|
|
167
158
|
}
|
|
168
159
|
function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
|
|
@@ -174,27 +165,17 @@ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, ski
|
|
|
174
165
|
};
|
|
175
166
|
return withOptionalStoppedReason(summary, stoppedReason);
|
|
176
167
|
}
|
|
177
|
-
const collator = new Intl.Collator(undefined, { numeric: true });
|
|
178
|
-
function compareString(a, b) {
|
|
179
|
-
return collator.compare(a ?? '', b ?? '');
|
|
180
|
-
}
|
|
181
168
|
function compareNameThenPath(a, b) {
|
|
182
|
-
const nameCompare =
|
|
169
|
+
const nameCompare = compareStringValues(a.name, b.name);
|
|
183
170
|
if (nameCompare !== 0)
|
|
184
171
|
return nameCompare;
|
|
185
|
-
return
|
|
172
|
+
return compareStringValues(a.path, b.path);
|
|
186
173
|
}
|
|
187
174
|
function comparePathThenName(a, b) {
|
|
188
|
-
const pathCompare =
|
|
175
|
+
const pathCompare = compareStringValues(a.path, b.path);
|
|
189
176
|
if (pathCompare !== 0)
|
|
190
177
|
return pathCompare;
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
function compareOptionalNumberDesc(left, right, tieBreak) {
|
|
194
|
-
const diff = (right ?? 0) - (left ?? 0);
|
|
195
|
-
if (diff !== 0)
|
|
196
|
-
return diff;
|
|
197
|
-
return tieBreak();
|
|
178
|
+
return compareStringValues(a.name, b.name);
|
|
198
179
|
}
|
|
199
180
|
const SORT_COMPARATORS = {
|
|
200
181
|
size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
|
|
@@ -204,32 +185,7 @@ const SORT_COMPARATORS = {
|
|
|
204
185
|
};
|
|
205
186
|
export function sortSearchResults(results, sortBy) {
|
|
206
187
|
if (sortBy === 'name') {
|
|
207
|
-
|
|
208
|
-
for (let index = 0; index < results.length; index += 1) {
|
|
209
|
-
const item = results[index];
|
|
210
|
-
if (!item)
|
|
211
|
-
continue;
|
|
212
|
-
decorated.push({
|
|
213
|
-
item,
|
|
214
|
-
baseName: path.basename(item.path ?? ''),
|
|
215
|
-
index,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
decorated.sort((a, b) => {
|
|
219
|
-
const baseCompare = compareString(a.baseName, b.baseName);
|
|
220
|
-
if (baseCompare !== 0)
|
|
221
|
-
return baseCompare;
|
|
222
|
-
const pathCompare = compareString(a.item.path, b.item.path);
|
|
223
|
-
if (pathCompare !== 0)
|
|
224
|
-
return pathCompare;
|
|
225
|
-
return a.index - b.index;
|
|
226
|
-
});
|
|
227
|
-
for (let index = 0; index < decorated.length; index += 1) {
|
|
228
|
-
const entry = decorated[index];
|
|
229
|
-
if (entry) {
|
|
230
|
-
results[index] = entry.item;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
188
|
+
stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
233
189
|
return;
|
|
234
190
|
}
|
|
235
191
|
const comparator = SORT_COMPARATORS[sortBy];
|
|
@@ -4,8 +4,9 @@ import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
|
4
4
|
import { toPosixPath } from '../path-format.js';
|
|
5
5
|
import { isSensitivePath } from '../path-policy.js';
|
|
6
6
|
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
7
|
+
import { isEntryAccessibleByType, resolveEntryType, resolveStopReason, } from './common.js';
|
|
7
8
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
8
|
-
import { globEntries
|
|
9
|
+
import { globEntries } from './glob-engine.js';
|
|
9
10
|
function toSafeNonNegativeInt(value, fallback) {
|
|
10
11
|
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
11
12
|
return fallback;
|
|
@@ -90,36 +91,11 @@ function getTreeTypeRank(type) {
|
|
|
90
91
|
return 1;
|
|
91
92
|
return 2;
|
|
92
93
|
}
|
|
93
|
-
function
|
|
94
|
-
if (signal.aborted) {
|
|
95
|
-
return 'aborted';
|
|
96
|
-
}
|
|
97
|
-
if (totalEntries >= maxEntries) {
|
|
98
|
-
return 'maxEntries';
|
|
99
|
-
}
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal) {
|
|
94
|
+
async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
|
|
103
95
|
const type = resolveEntryType(entry.dirent);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (isSensitivePath(entry.path, normalized)) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
try {
|
|
115
|
-
const validated = await validateExistingPathDetailed(entry.path, signal);
|
|
116
|
-
if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
96
|
+
const isAccessible = await isEntryAccessibleByType(entry.path, type, rootDirectories, signal, accessDeps);
|
|
97
|
+
if (!isAccessible) {
|
|
98
|
+
return null;
|
|
123
99
|
}
|
|
124
100
|
if (gitignoreMatcher &&
|
|
125
101
|
isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
|
|
@@ -222,6 +198,12 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
222
198
|
const root = await validateExistingDirectory(dirPath, signal);
|
|
223
199
|
const rootNormalized = normalizePath(root);
|
|
224
200
|
const rootDirectories = [rootNormalized];
|
|
201
|
+
const accessDeps = {
|
|
202
|
+
normalizePath,
|
|
203
|
+
isPathWithinDirectories,
|
|
204
|
+
isSensitivePath,
|
|
205
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
206
|
+
};
|
|
225
207
|
try {
|
|
226
208
|
const excludePatterns = normalized.includeIgnored
|
|
227
209
|
? []
|
|
@@ -253,12 +235,18 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
253
235
|
suppressErrors: true,
|
|
254
236
|
});
|
|
255
237
|
for await (const entry of stream) {
|
|
256
|
-
const stopReason =
|
|
238
|
+
const stopReason = resolveStopReason({
|
|
239
|
+
signal,
|
|
240
|
+
current: totalEntries,
|
|
241
|
+
max: normalized.maxEntries,
|
|
242
|
+
abortedReason: 'aborted',
|
|
243
|
+
maxReason: 'maxEntries',
|
|
244
|
+
});
|
|
257
245
|
if (stopReason) {
|
|
258
246
|
truncated = true;
|
|
259
247
|
break;
|
|
260
248
|
}
|
|
261
|
-
const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal);
|
|
249
|
+
const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
|
|
262
250
|
if (!resolved) {
|
|
263
251
|
continue;
|
|
264
252
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { withDefaultIcons } from './tools/shared.js';
|
|
3
3
|
const HELP_PROMPT_NAME = 'get-help';
|
|
4
4
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
5
|
-
const HELP_PROMPT_DESCRIPTION = '
|
|
5
|
+
const HELP_PROMPT_DESCRIPTION = 'Return filesystem-mcp usage instructions.';
|
|
6
6
|
function filterInstructionsByTopic(instructions, topic) {
|
|
7
7
|
const normalized = topic.trim().toLowerCase();
|
|
8
8
|
if (!normalized)
|
|
@@ -16,7 +16,7 @@ function filterInstructionsByTopic(instructions, topic) {
|
|
|
16
16
|
.map((sec) => sec.split('\n')[0]?.replace(/^##\s*/u, '') ?? '')
|
|
17
17
|
.filter(Boolean)
|
|
18
18
|
.join(', ');
|
|
19
|
-
return `Section '${topic}' not found. Available
|
|
19
|
+
return `Section '${topic}' not found. Available: ${available}\n\n${instructions}`;
|
|
20
20
|
}
|
|
21
21
|
export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
22
22
|
const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
|
|
@@ -26,7 +26,7 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
|
26
26
|
topic: z
|
|
27
27
|
.string()
|
|
28
28
|
.optional()
|
|
29
|
-
.describe('
|
|
29
|
+
.describe('Optional section heading prefix (example: "error handling"). Omit to return full instructions.'),
|
|
30
30
|
},
|
|
31
31
|
}, ({ topic }) => {
|
|
32
32
|
const text = topic
|
|
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
|
2
2
|
import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
4
|
const INSTRUCTIONS_HEADER = `<role>
|
|
5
|
-
|
|
5
|
+
Filesystem agent for local paths only. Operate inside allowed roots. Discover before action. Never guess paths.
|
|
6
6
|
</role>
|
|
7
7
|
|
|
8
8
|
<tools_overview>
|
|
@@ -15,16 +15,16 @@ Expert filesystem agent. Operate ONLY within allowed roots. Always discover befo
|
|
|
15
15
|
</tools_overview>
|
|
16
16
|
|
|
17
17
|
<resources>
|
|
18
|
-
- \`internal://instructions\`: Full
|
|
19
|
-
- \`internal://tool-catalog\`: Tool routing and
|
|
20
|
-
- \`internal://workflows\`: Standard
|
|
21
|
-
- \`internal://tool-info/{name}\`: Per-tool
|
|
22
|
-
- \`filesystem-mcp://result/{id}\`:
|
|
23
|
-
- \`filesystem-mcp://metrics\`:
|
|
18
|
+
- \`internal://instructions\`: Full usage reference.
|
|
19
|
+
- \`internal://tool-catalog\`: Tool routing and data-flow rules.
|
|
20
|
+
- \`internal://workflows\`: Standard execution sequences.
|
|
21
|
+
- \`internal://tool-info/{name}\`: Per-tool nuances and gotchas (example: \`internal://tool-info/read\`).
|
|
22
|
+
- \`filesystem-mcp://result/{id}\`: Cached large output. If \`resourceUri\` is returned, call \`resources/read\` immediately.
|
|
23
|
+
- \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
|
|
24
24
|
</resources>
|
|
25
25
|
|
|
26
26
|
<task_protocol>
|
|
27
|
-
Async execution:
|
|
27
|
+
Async execution: pass \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
|
|
28
28
|
Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
29
29
|
</task_protocol>
|
|
30
30
|
`;
|
|
@@ -35,19 +35,19 @@ ${getSharedConstraints()
|
|
|
35
35
|
</constraints>
|
|
36
36
|
|
|
37
37
|
<error_handling>
|
|
38
|
-
- \`E_ACCESS_DENIED\`
|
|
39
|
-
- \`E_NOT_FOUND\`
|
|
40
|
-
- \`E_TOO_LARGE\`
|
|
41
|
-
- \`E_TIMEOUT\`
|
|
38
|
+
- \`E_ACCESS_DENIED\` => call \`roots\`, then use an allowed path.
|
|
39
|
+
- \`E_NOT_FOUND\` => call \`ls\` or \`find\`, then verify spelling.
|
|
40
|
+
- \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
|
|
41
|
+
- \`E_TIMEOUT\` => reduce scope or result limits.
|
|
42
42
|
</error_handling>
|
|
43
43
|
`;
|
|
44
44
|
function formatToolSection(tool) {
|
|
45
45
|
const parts = [`### ${tool.name}\n${tool.description}`];
|
|
46
46
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
47
|
-
parts.push(...tool.nuances.map((n) =>
|
|
47
|
+
parts.push(...tool.nuances.map((n) => `- Nuance: ${n}`));
|
|
48
48
|
}
|
|
49
49
|
if (tool.gotchas && tool.gotchas.length > 0) {
|
|
50
|
-
parts.push(...tool.gotchas.map((g) =>
|
|
50
|
+
parts.push(...tool.gotchas.map((g) => `- Gotcha: ${g}`));
|
|
51
51
|
}
|
|
52
52
|
return parts.join('\n');
|
|
53
53
|
}
|
|
@@ -9,21 +9,21 @@ diff_files (patch text) -> apply_patch.patch
|
|
|
9
9
|
|
|
10
10
|
## Search Strategy
|
|
11
11
|
|
|
12
|
-
- Use \`find\` for glob
|
|
13
|
-
- Use \`grep\` for
|
|
14
|
-
- Use \`search_and_replace\`
|
|
12
|
+
- Use \`find\` for glob file discovery.
|
|
13
|
+
- Use \`grep\` for text search.
|
|
14
|
+
- Use \`search_and_replace\` only for replacement, never discovery.
|
|
15
15
|
|
|
16
16
|
## Write Strategy
|
|
17
17
|
|
|
18
|
-
- Use \`edit\` for precise,
|
|
19
|
-
- Use \`write\` to create
|
|
20
|
-
- Use \`search_and_replace\` for bulk
|
|
18
|
+
- Use \`edit\` for precise, first-occurrence replacements in existing files.
|
|
19
|
+
- Use \`write\` to create files or overwrite full contents.
|
|
20
|
+
- Use \`search_and_replace\` for bulk multi-file replacements.
|
|
21
21
|
|
|
22
22
|
## Patch Management
|
|
23
23
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- \`apply_patch\`
|
|
24
|
+
- Generate patches with \`diff_files\` first.
|
|
25
|
+
- Run \`apply_patch\` with \`dryRun: true\` before writing.
|
|
26
|
+
- \`apply_patch\` accepts unified diffs only.
|
|
27
27
|
</tool_selection_guide>
|
|
28
28
|
`;
|
|
29
29
|
export function buildToolCatalog() {
|
|
@@ -37,10 +37,10 @@ export function buildCoreContextPack() {
|
|
|
37
37
|
}
|
|
38
38
|
export function getSharedConstraints() {
|
|
39
39
|
return [
|
|
40
|
-
'
|
|
41
|
-
'Sensitive
|
|
42
|
-
`
|
|
43
|
-
'If a response includes `resourceUri`, call `resources/read` immediately
|
|
40
|
+
'Use allowed roots only (provided by CLI negotiation).',
|
|
41
|
+
'Sensitive paths are denylisted by default.',
|
|
42
|
+
`Limits are enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files and ${DEFAULT_SEARCH_CONTENT_RESULTS} lines.`,
|
|
43
|
+
'If a response includes `resourceUri`, call `resources/read` immediately; cached results expire on process restart.',
|
|
44
44
|
];
|
|
45
45
|
}
|
|
46
46
|
export function buildToolInfo(name) {
|
|
@@ -49,7 +49,7 @@ export function buildToolInfo(name) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
const lines = [`## ${entry.name}`, '', entry.description];
|
|
51
51
|
if (entry.annotations && entry.annotations.length > 0) {
|
|
52
|
-
lines.push('', `**
|
|
52
|
+
lines.push('', `**Hints:** ${entry.annotations.join(', ')}`);
|
|
53
53
|
}
|
|
54
54
|
if (entry.nuances && entry.nuances.length > 0) {
|
|
55
55
|
lines.push('', '**Nuances:**');
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
export function buildWorkflowGuide() {
|
|
2
2
|
return `<workflows>
|
|
3
3
|
### A: EXPLORE
|
|
4
|
-
Use when:
|
|
5
|
-
1. \`roots\` (
|
|
6
|
-
2. \`ls\` (
|
|
7
|
-
3. \`stat\`
|
|
8
|
-
4. \`read\`
|
|
9
|
-
> **Strict:**
|
|
4
|
+
Use when: you need directory layout or file content.
|
|
5
|
+
1. \`roots\` (list allowed paths).
|
|
6
|
+
2. \`ls\` (flat view) or \`tree\` (recursive view).
|
|
7
|
+
3. \`stat\` or \`stat_many\` (type and size checks).
|
|
8
|
+
4. \`read\` or \`read_many\` (read content).
|
|
9
|
+
> **Strict:** Resolve paths first. Never guess.
|
|
10
10
|
|
|
11
11
|
### B: SEARCH
|
|
12
|
-
Use when:
|
|
12
|
+
Use when: you need files by pattern or content.
|
|
13
13
|
1. \`find\` (glob candidates).
|
|
14
|
-
2. \`grep\` (content
|
|
15
|
-
3. \`read\` (verify context).
|
|
16
|
-
> **Strict:**
|
|
14
|
+
2. \`grep\` (content matches).
|
|
15
|
+
3. \`read\` (verify matched context).
|
|
16
|
+
> **Strict:** Do content search with \`grep\`, not \`find\`.
|
|
17
17
|
|
|
18
18
|
### C: EDIT
|
|
19
|
-
Use when:
|
|
20
|
-
1. \`edit\` (
|
|
21
|
-
2. \`search_and_replace\` (bulk
|
|
22
|
-
3. \`mv\`
|
|
23
|
-
4. \`mkdir\` (
|
|
19
|
+
Use when: you need to modify files or layout.
|
|
20
|
+
1. \`edit\` (targeted string replacement).
|
|
21
|
+
2. \`search_and_replace\` (bulk replacements).
|
|
22
|
+
3. \`mv\` or \`rm\` (layout changes).
|
|
23
|
+
4. \`mkdir\` (directory creation).
|
|
24
24
|
> **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
|
|
25
25
|
|
|
26
26
|
### D: PATCH
|
|
27
|
-
Use when: applying
|
|
27
|
+
Use when: applying unified diffs from \`diff_files\`.
|
|
28
28
|
1. \`diff_files\` (generate).
|
|
29
29
|
2. \`apply_patch\` (dryRun: true).
|
|
30
30
|
3. \`apply_patch\` (dryRun: false).
|
|
31
|
-
> **Tip:**
|
|
31
|
+
> **Tip:** Feed \`diff_files\` output directly to \`apply_patch\`.
|
|
32
32
|
</workflows>`;
|
|
33
33
|
}
|
package/dist/schemas.js
CHANGED
|
@@ -18,6 +18,9 @@ function isSafeGlobPattern(value) {
|
|
|
18
18
|
const MAX_PATH_LENGTH = 4096;
|
|
19
19
|
const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots exist. Examples: "src", "src/components"';
|
|
20
20
|
const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
|
|
21
|
+
function defaultFalseBoolean(description) {
|
|
22
|
+
return z.boolean().optional().default(false).describe(description);
|
|
23
|
+
}
|
|
21
24
|
const PathSchemaBase = z
|
|
22
25
|
.string()
|
|
23
26
|
.max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
|
|
@@ -109,16 +112,8 @@ const OperationSummarySchema = z.strictObject({
|
|
|
109
112
|
});
|
|
110
113
|
export const ListDirectoryInputSchema = z.strictObject({
|
|
111
114
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
112
|
-
includeHidden:
|
|
113
|
-
|
|
114
|
-
.optional()
|
|
115
|
-
.default(false)
|
|
116
|
-
.describe('Include hidden items (starting with .)'),
|
|
117
|
-
includeIgnored: z
|
|
118
|
-
.boolean()
|
|
119
|
-
.optional()
|
|
120
|
-
.default(false)
|
|
121
|
-
.describe('Include ignored items (node_modules, .git, etc).'),
|
|
115
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
116
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, .git, etc).'),
|
|
122
117
|
maxDepth: z
|
|
123
118
|
.number()
|
|
124
119
|
.int({ error: 'Must be integer' })
|
|
@@ -143,11 +138,7 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
143
138
|
.max(1000, 'Max 1000 chars')
|
|
144
139
|
.optional()
|
|
145
140
|
.describe('Optional glob pattern filter (e.g. "**/*.ts")'),
|
|
146
|
-
includeSymlinkTargets:
|
|
147
|
-
.boolean()
|
|
148
|
-
.optional()
|
|
149
|
-
.default(false)
|
|
150
|
-
.describe('Resolve and include symlink targets in results'),
|
|
141
|
+
includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
|
|
151
142
|
cursor: z
|
|
152
143
|
.string()
|
|
153
144
|
.optional()
|
|
@@ -174,16 +165,8 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
174
165
|
.optional()
|
|
175
166
|
.default(DEFAULT_SEARCH_RESULTS)
|
|
176
167
|
.describe(`Max results (1-${MAX_SEARCH_RESULTS}). Default: ${DEFAULT_SEARCH_RESULTS}`),
|
|
177
|
-
includeIgnored:
|
|
178
|
-
|
|
179
|
-
.optional()
|
|
180
|
-
.default(false)
|
|
181
|
-
.describe('Include ignored items (node_modules, etc).'),
|
|
182
|
-
includeHidden: z
|
|
183
|
-
.boolean()
|
|
184
|
-
.optional()
|
|
185
|
-
.default(false)
|
|
186
|
-
.describe('Include hidden items (starting with .)'),
|
|
168
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
|
|
169
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
187
170
|
sortBy: SearchFilesSortSchema.optional()
|
|
188
171
|
.default('path')
|
|
189
172
|
.describe('Sort by path, name, size, or modified'),
|
|
@@ -217,16 +200,8 @@ export const TreeInputSchema = z.strictObject({
|
|
|
217
200
|
.optional()
|
|
218
201
|
.default(DEFAULT_TREE_ENTRIES)
|
|
219
202
|
.describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
|
|
220
|
-
includeHidden:
|
|
221
|
-
|
|
222
|
-
.optional()
|
|
223
|
-
.default(false)
|
|
224
|
-
.describe('Include hidden items (starting with .)'),
|
|
225
|
-
includeIgnored: z
|
|
226
|
-
.boolean()
|
|
227
|
-
.optional()
|
|
228
|
-
.default(false)
|
|
229
|
-
.describe('Include ignored items. Disables .gitignore.'),
|
|
203
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
204
|
+
includeIgnored: defaultFalseBoolean('Include ignored items. Disables .gitignore.'),
|
|
230
205
|
});
|
|
231
206
|
export const SearchContentInputSchema = z.strictObject({
|
|
232
207
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
@@ -235,21 +210,9 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
235
210
|
.min(1, 'Pattern required')
|
|
236
211
|
.max(1000, 'Max 1000 chars')
|
|
237
212
|
.describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
|
|
238
|
-
isRegex:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.default(false)
|
|
242
|
-
.describe('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
|
|
243
|
-
caseSensitive: z
|
|
244
|
-
.boolean()
|
|
245
|
-
.optional()
|
|
246
|
-
.default(false)
|
|
247
|
-
.describe('Case-sensitive matching (default: false — searches are case-insensitive).'),
|
|
248
|
-
wholeWord: z
|
|
249
|
-
.boolean()
|
|
250
|
-
.optional()
|
|
251
|
-
.default(false)
|
|
252
|
-
.describe('Match whole words only'),
|
|
213
|
+
isRegex: defaultFalseBoolean('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
|
|
214
|
+
caseSensitive: defaultFalseBoolean('Case-sensitive matching (default: false — searches are case-insensitive).'),
|
|
215
|
+
wholeWord: defaultFalseBoolean('Match whole words only'),
|
|
253
216
|
contextLines: z
|
|
254
217
|
.number()
|
|
255
218
|
.int({ error: 'Must be integer' })
|
|
@@ -273,16 +236,8 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
273
236
|
.optional()
|
|
274
237
|
.default('**/*')
|
|
275
238
|
.describe('Glob for candidate files (e.g. "**/*.ts")'),
|
|
276
|
-
includeHidden:
|
|
277
|
-
|
|
278
|
-
.optional()
|
|
279
|
-
.default(false)
|
|
280
|
-
.describe('Include hidden items (starting with .)'),
|
|
281
|
-
includeIgnored: z
|
|
282
|
-
.boolean()
|
|
283
|
-
.optional()
|
|
284
|
-
.default(false)
|
|
285
|
-
.describe('Include ignored items (node_modules, etc).'),
|
|
239
|
+
includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
|
|
240
|
+
includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
|
|
286
241
|
});
|
|
287
242
|
export const ReadFileInputSchema = z
|
|
288
243
|
.strictObject({
|
|
@@ -511,16 +466,8 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
511
466
|
}))
|
|
512
467
|
.min(1, 'Min 1 edit required')
|
|
513
468
|
.describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
|
|
514
|
-
dryRun:
|
|
515
|
-
|
|
516
|
-
.optional()
|
|
517
|
-
.default(false)
|
|
518
|
-
.describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
519
|
-
ignoreWhitespace: z
|
|
520
|
-
.boolean()
|
|
521
|
-
.optional()
|
|
522
|
-
.default(false)
|
|
523
|
-
.describe('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
469
|
+
dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
470
|
+
ignoreWhitespace: defaultFalseBoolean('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
524
471
|
});
|
|
525
472
|
export const EditFileOutputSchema = z.strictObject({
|
|
526
473
|
ok: z.boolean(),
|
|
@@ -562,16 +509,8 @@ export const MoveFileOutputSchema = z.strictObject({
|
|
|
562
509
|
});
|
|
563
510
|
export const DeleteFileInputSchema = z.strictObject({
|
|
564
511
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
565
|
-
recursive:
|
|
566
|
-
|
|
567
|
-
.optional()
|
|
568
|
-
.default(false)
|
|
569
|
-
.describe('Delete non-empty directories'),
|
|
570
|
-
ignoreIfNotExists: z
|
|
571
|
-
.boolean()
|
|
572
|
-
.optional()
|
|
573
|
-
.default(false)
|
|
574
|
-
.describe('No error if missing'),
|
|
512
|
+
recursive: defaultFalseBoolean('Delete non-empty directories'),
|
|
513
|
+
ignoreIfNotExists: defaultFalseBoolean('No error if missing'),
|
|
575
514
|
});
|
|
576
515
|
export const DeleteFileOutputSchema = z.strictObject({
|
|
577
516
|
ok: z.boolean(),
|
|
@@ -665,16 +604,8 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
665
604
|
.min(1, 'Search pattern required')
|
|
666
605
|
.describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
|
|
667
606
|
replacement: z.string().describe('Replacement text'),
|
|
668
|
-
isRegex:
|
|
669
|
-
|
|
670
|
-
.optional()
|
|
671
|
-
.default(false)
|
|
672
|
-
.describe('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
|
|
673
|
-
dryRun: z
|
|
674
|
-
.boolean()
|
|
675
|
-
.optional()
|
|
676
|
-
.default(false)
|
|
677
|
-
.describe('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
607
|
+
isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
|
|
608
|
+
dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
678
609
|
includeHidden: z
|
|
679
610
|
.boolean()
|
|
680
611
|
.optional()
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -66,8 +66,8 @@ export async function createServer(options = {}) {
|
|
|
66
66
|
if (serverInstructions) {
|
|
67
67
|
serverConfig.instructions =
|
|
68
68
|
'filesystem-mcp: Secure local filesystem MCP server. ' +
|
|
69
|
-
'
|
|
70
|
-
'
|
|
69
|
+
'Start with: roots -> ls/find -> stat -> read. Never guess paths. ' +
|
|
70
|
+
'For full guidance, read internal://instructions or run the get-help prompt.';
|
|
71
71
|
}
|
|
72
72
|
const server = new McpServer(withDefaultIcons({
|
|
73
73
|
name: 'filesystem-mcp',
|