@j0hanz/filesystem-mcp 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +3 -2
- package/dist/config.js +1 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +11 -6
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +213 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +85 -44
- package/dist/tools/apply-patch.js +24 -24
- package/dist/tools/calculate-hash.js +42 -45
- package/dist/tools/create-directory.js +18 -21
- package/dist/tools/delete-file.js +36 -39
- package/dist/tools/diff-files.js +16 -21
- package/dist/tools/edit-file.js +16 -20
- package/dist/tools/list-directory.js +25 -25
- package/dist/tools/move-file.js +18 -21
- package/dist/tools/read-multiple.js +56 -68
- package/dist/tools/read.js +27 -32
- package/dist/tools/replace-in-files.js +28 -35
- package/dist/tools/roots.js +9 -10
- package/dist/tools/search-content.js +74 -74
- package/dist/tools/search-files.js +45 -52
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +45 -68
- package/dist/tools/stat.js +11 -39
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +13 -30
- package/dist/tools/write-file.js +18 -21
- package/dist/tools.js +23 -18
- package/package.json +6 -7
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
5
5
|
import packageJsonRaw from '../package.json' with { type: 'json' };
|
|
6
6
|
import { getReservedDeviceNameForPath, isWindowsDriveRelativePath, normalizePath, } from './lib/path-validation.js';
|
|
7
|
+
import { isRecord } from './lib/type-guards.js';
|
|
7
8
|
const PackageJsonSchema = z.object({ version: z.string() });
|
|
8
9
|
const { version: SERVER_VERSION } = PackageJsonSchema.parse(packageJsonRaw);
|
|
9
10
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -27,16 +28,30 @@ function validateCliPath(inputPath) {
|
|
|
27
28
|
throw new InvalidArgumentError(`Windows reserved device name not allowed: ${reserved}.`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
31
|
+
function getNodeErrorProperty(error, key) {
|
|
32
|
+
if (!isRecord(error))
|
|
32
33
|
return undefined;
|
|
33
|
-
const
|
|
34
|
+
const value = error[key];
|
|
35
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function collectStringValues(values) {
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
result.push(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function getNodeErrorCode(error) {
|
|
50
|
+
const code = getNodeErrorProperty(error, 'code');
|
|
34
51
|
return typeof code === 'string' ? code : undefined;
|
|
35
52
|
}
|
|
36
53
|
function getNodeErrorErrno(error) {
|
|
37
|
-
|
|
38
|
-
return undefined;
|
|
39
|
-
const { errno } = error;
|
|
54
|
+
const errno = getNodeErrorProperty(error, 'errno');
|
|
40
55
|
return typeof errno === 'number' ? errno : undefined;
|
|
41
56
|
}
|
|
42
57
|
function normalizeDirectoryError(error, inputPath) {
|
|
@@ -77,20 +92,22 @@ async function validateDirectoryPath(inputPath) {
|
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
async function normalizeCliDirectories(args) {
|
|
80
|
-
|
|
95
|
+
const validations = [];
|
|
96
|
+
for (const arg of args) {
|
|
97
|
+
validations.push(validateDirectoryPath(arg));
|
|
98
|
+
}
|
|
99
|
+
return Promise.all(validations);
|
|
81
100
|
}
|
|
82
101
|
function parseAllowedDirArgument(value, previous) {
|
|
83
102
|
validateCliPath(value);
|
|
84
|
-
const values = Array.isArray(previous)
|
|
85
|
-
? previous.filter((item) => typeof item === 'string')
|
|
86
|
-
: [];
|
|
103
|
+
const values = Array.isArray(previous) ? collectStringValues(previous) : [];
|
|
87
104
|
return [...values, value];
|
|
88
105
|
}
|
|
89
106
|
function getParsedAllowedDirs(cli) {
|
|
90
107
|
const [allowedDirs] = cli.processedArgs;
|
|
91
108
|
if (!Array.isArray(allowedDirs))
|
|
92
109
|
return [];
|
|
93
|
-
return allowedDirs
|
|
110
|
+
return collectStringValues(allowedDirs);
|
|
94
111
|
}
|
|
95
112
|
function createCliProgram(output) {
|
|
96
113
|
const cli = new Command();
|
|
@@ -163,7 +180,7 @@ export async function parseArgs() {
|
|
|
163
180
|
const options = cli.opts();
|
|
164
181
|
const allowCwd = options.allowCwd === true;
|
|
165
182
|
const positionals = getParsedAllowedDirs(cli);
|
|
166
|
-
let allowedDirs
|
|
183
|
+
let allowedDirs;
|
|
167
184
|
try {
|
|
168
185
|
allowedDirs =
|
|
169
186
|
positionals.length > 0 ? await normalizeCliDirectories(positionals) : [];
|
package/dist/completions.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { CompleteRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { toPosixPath } from './lib/path-format.js';
|
|
4
5
|
import { getAllowedDirectories, isPathWithinDirectories, normalizePath, } from './lib/path-validation.js';
|
|
6
|
+
import { isRecord } from './lib/type-guards.js';
|
|
5
7
|
const MAX_COMPLETION_ITEMS = 100;
|
|
6
8
|
const PATH_ARGUMENTS = new Set([
|
|
7
9
|
'path',
|
|
@@ -14,9 +16,6 @@ const PATH_ARGUMENTS = new Set([
|
|
|
14
16
|
'root',
|
|
15
17
|
'cwd',
|
|
16
18
|
]);
|
|
17
|
-
function isRecord(value) {
|
|
18
|
-
return typeof value === 'object' && value !== null;
|
|
19
|
-
}
|
|
20
19
|
function isPathLikeArgumentName(argName) {
|
|
21
20
|
return (PATH_ARGUMENTS.has(argName) ||
|
|
22
21
|
argName.endsWith('paths') ||
|
|
@@ -95,10 +94,17 @@ function extractContextArguments(value) {
|
|
|
95
94
|
const context = value['arguments'];
|
|
96
95
|
if (!isRecord(context))
|
|
97
96
|
return undefined;
|
|
98
|
-
const
|
|
99
|
-
|
|
97
|
+
const normalized = {};
|
|
98
|
+
let count = 0;
|
|
99
|
+
for (const [key, entryValue] of Object.entries(context)) {
|
|
100
|
+
if (typeof entryValue !== 'string')
|
|
101
|
+
continue;
|
|
102
|
+
normalized[key.toLowerCase()] = entryValue;
|
|
103
|
+
count += 1;
|
|
104
|
+
}
|
|
105
|
+
if (count === 0)
|
|
100
106
|
return undefined;
|
|
101
|
-
return
|
|
107
|
+
return normalized;
|
|
102
108
|
}
|
|
103
109
|
function hasTrailingSeparator(value) {
|
|
104
110
|
return (value.endsWith(path.sep) || value.endsWith('/') || value.endsWith('\\'));
|
|
@@ -119,27 +125,34 @@ function resolveFromBase(base, rawValue, trailingSeparator) {
|
|
|
119
125
|
};
|
|
120
126
|
}
|
|
121
127
|
function resolveNamedRootContext(currentValue, allowed) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
if (!rootName)
|
|
128
|
+
const parsed = parseNamedRootInput(currentValue);
|
|
129
|
+
if (!parsed)
|
|
125
130
|
return undefined;
|
|
126
|
-
const root =
|
|
131
|
+
const root = findAllowedRootByName(parsed.rootName, allowed);
|
|
127
132
|
if (!root)
|
|
128
133
|
return undefined;
|
|
129
134
|
const trailingSeparator = hasTrailingSeparator(currentValue);
|
|
130
|
-
|
|
131
|
-
return resolveFromBase(root, remainder, trailingSeparator);
|
|
135
|
+
return resolveFromBase(root, parsed.remainder, trailingSeparator);
|
|
132
136
|
}
|
|
133
137
|
function resolveNamedRootPath(value, allowed) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
if (!rootName)
|
|
138
|
+
const parsed = parseNamedRootInput(value);
|
|
139
|
+
if (!parsed)
|
|
137
140
|
return undefined;
|
|
138
|
-
const root =
|
|
141
|
+
const root = findAllowedRootByName(parsed.rootName, allowed);
|
|
139
142
|
if (!root)
|
|
140
143
|
return undefined;
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
return normalizePath(path.resolve(root, parsed.remainder));
|
|
145
|
+
}
|
|
146
|
+
function parseNamedRootInput(value) {
|
|
147
|
+
const normalizedInput = toPosixPath(value);
|
|
148
|
+
const [rootName, ...rest] = normalizedInput.split('/');
|
|
149
|
+
if (!rootName)
|
|
150
|
+
return undefined;
|
|
151
|
+
return { rootName, remainder: rest.join(path.sep) };
|
|
152
|
+
}
|
|
153
|
+
function findAllowedRootByName(rootName, allowed) {
|
|
154
|
+
const normalizedRootName = rootName.toLowerCase();
|
|
155
|
+
return allowed.find((candidate) => path.basename(candidate).toLowerCase() === normalizedRootName);
|
|
143
156
|
}
|
|
144
157
|
function chooseContextKeys(argumentName) {
|
|
145
158
|
const normalized = argumentName.toLowerCase();
|
|
@@ -244,14 +257,22 @@ async function findMatchesInDirectory(searchDir, prefix, allowed) {
|
|
|
244
257
|
return matches;
|
|
245
258
|
}
|
|
246
259
|
function findRootPrefixMatches(currentValue, allowed) {
|
|
247
|
-
const normalizedInput = currentValue
|
|
260
|
+
const normalizedInput = toPosixPath(currentValue);
|
|
248
261
|
const rootPrefix = (normalizedInput.split('/')[0] ?? '').toLowerCase();
|
|
249
262
|
if (!rootPrefix) {
|
|
250
|
-
|
|
263
|
+
const matches = [];
|
|
264
|
+
for (const root of allowed) {
|
|
265
|
+
matches.push(`${root}${path.sep}`);
|
|
266
|
+
}
|
|
267
|
+
return matches;
|
|
251
268
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.
|
|
269
|
+
const matches = [];
|
|
270
|
+
for (const root of allowed) {
|
|
271
|
+
if (!path.basename(root).toLowerCase().startsWith(rootPrefix))
|
|
272
|
+
continue;
|
|
273
|
+
matches.push(`${root}${path.sep}`);
|
|
274
|
+
}
|
|
275
|
+
return matches;
|
|
255
276
|
}
|
|
256
277
|
function findMatchingRoots(searchDir, prefix, allowed) {
|
|
257
278
|
const matches = [];
|
|
@@ -296,7 +317,12 @@ export async function getPathCompletions(currentValue, options = {}) {
|
|
|
296
317
|
Promise.resolve(findMatchingRoots(searchDir, prefix, allowed)),
|
|
297
318
|
]);
|
|
298
319
|
// Deduplicate and sort
|
|
299
|
-
const
|
|
320
|
+
const unique = new Set();
|
|
321
|
+
for (const match of dirMatches)
|
|
322
|
+
unique.add(match);
|
|
323
|
+
for (const match of rootMatches)
|
|
324
|
+
unique.add(match);
|
|
325
|
+
const uniqueMatches = Array.from(unique);
|
|
300
326
|
uniqueMatches.sort((a, b) => {
|
|
301
327
|
const aIsDir = a.endsWith(path.sep);
|
|
302
328
|
const bIsDir = b.endsWith(path.sep);
|
package/dist/config.d.ts
CHANGED
|
@@ -111,7 +111,8 @@ export declare const ErrorCode: {
|
|
|
111
111
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
112
112
|
export declare function formatBytes(bytes: number): string;
|
|
113
113
|
export declare function joinLines(lines: readonly string[]): string;
|
|
114
|
-
export
|
|
114
|
+
export interface OperationSummary {
|
|
115
115
|
truncated?: boolean;
|
|
116
116
|
truncatedReason?: string;
|
|
117
|
-
}
|
|
117
|
+
}
|
|
118
|
+
export declare function formatOperationSummary(summary: OperationSummary): string;
|
package/dist/config.js
CHANGED
|
@@ -17,7 +17,7 @@ export function formatBytes(bytes) {
|
|
|
17
17
|
return '0 B';
|
|
18
18
|
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
19
19
|
const unit = BYTE_UNIT_LABELS[unitIndex] ?? 'B';
|
|
20
|
-
const value = bytes /
|
|
20
|
+
const value = bytes / 1024 ** unitIndex;
|
|
21
21
|
return `${parseFloat(value.toFixed(2))} ${unit}`;
|
|
22
22
|
}
|
|
23
23
|
export function joinLines(lines) {
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,16 @@ import { createServer, startServer } from './server.js';
|
|
|
9
9
|
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
10
10
|
let activeServer;
|
|
11
11
|
let shutdownStarted = false;
|
|
12
|
+
function isStdinEvent(event) {
|
|
13
|
+
return event === 'end' || event === 'close';
|
|
14
|
+
}
|
|
15
|
+
function registerShutdownTrigger(event) {
|
|
16
|
+
const target = isStdinEvent(event) ? process.stdin : process;
|
|
17
|
+
target.once(event, () => {
|
|
18
|
+
const reason = isStdinEvent(event) ? `stdin ${event}` : event;
|
|
19
|
+
void shutdown(reason, 0);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
12
22
|
async function shutdown(reason, exitCode = 0) {
|
|
13
23
|
if (shutdownStarted)
|
|
14
24
|
return;
|
|
@@ -75,18 +85,10 @@ async function main() {
|
|
|
75
85
|
activeServer = server;
|
|
76
86
|
await startServer(server);
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
void shutdown('SIGINT', 0);
|
|
83
|
-
});
|
|
84
|
-
process.stdin.once('end', () => {
|
|
85
|
-
void shutdown('stdin end', 0);
|
|
86
|
-
});
|
|
87
|
-
process.stdin.once('close', () => {
|
|
88
|
-
void shutdown('stdin close', 0);
|
|
89
|
-
});
|
|
88
|
+
registerShutdownTrigger('SIGTERM');
|
|
89
|
+
registerShutdownTrigger('SIGINT');
|
|
90
|
+
registerShutdownTrigger('end');
|
|
91
|
+
registerShutdownTrigger('close');
|
|
90
92
|
process.once('unhandledRejection', (reason) => {
|
|
91
93
|
console.error('Unhandled rejection:', formatUnknownErrorMessage(reason));
|
|
92
94
|
void shutdown('unhandledRejection', 1);
|
package/dist/instructions.md
CHANGED
|
@@ -6,9 +6,9 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
6
6
|
|
|
7
7
|
## CORE CAPABILITY
|
|
8
8
|
|
|
9
|
-
- Domain: Filesystem operations via an MCP server
|
|
10
|
-
- Primary Resources: Files,
|
|
11
|
-
- Tools: `roots`, `ls`, `find`, `tree`, `read`, `read_many`, `stat`, `stat_many`, `grep`, `calculate_hash`, `diff_files
|
|
9
|
+
- Domain: Filesystem operations via an MCP server for LLM agents that need safe read/search/edit/diff/patch workflows within allowed roots.
|
|
10
|
+
- Primary Resources: Files, directories, metadata, search matches, and ephemeral cached result resources.
|
|
11
|
+
- Tools: READ: `roots`, `ls`, `find`, `tree`, `read`, `read_many`, `stat`, `stat_many`, `grep`, `calculate_hash`, `diff_files`. WRITE: `mkdir`, `write`, `edit`, `mv`, `rm`, `apply_patch`, `search_and_replace`.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -21,54 +21,55 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
21
21
|
## RESOURCES & RESOURCE LINKS
|
|
22
22
|
|
|
23
23
|
- `internal://instructions`: This document.
|
|
24
|
-
- `filesystem-mcp://result/{id}`:
|
|
25
|
-
- If a tool response includes a `resourceUri` or `resource_link`, call `resources/read` with
|
|
24
|
+
- `filesystem-mcp://result/{id}`: Ephemeral cached tool output (in-memory); used when payloads are externalized.
|
|
25
|
+
- If a tool response includes a `resourceUri` or `resource_link`, call `resources/read` with that URI to fetch full content.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
29
|
## PROGRESS & TASKS
|
|
30
30
|
|
|
31
31
|
- Include `_meta.progressToken` in requests to receive `notifications/progress` updates for long-running tools.
|
|
32
|
-
- Task-augmented tool calls are supported for `grep`, `
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- Poll `tasks/get` and fetch results via `tasks/result`.
|
|
32
|
+
- Task-augmented tool calls are supported for `find`, `tree`, `read`, `read_many`, `stat_many`, `grep`, `mkdir`, `write`, `mv`, `rm`, `calculate_hash`, `apply_patch`, and `search_and_replace`:
|
|
33
|
+
- Send `tools/call` with `task` to create a task.
|
|
34
|
+
- Poll `tasks/get` and fetch final output with `tasks/result`.
|
|
36
35
|
- Use `tasks/cancel` to abort.
|
|
37
|
-
- Task
|
|
38
|
-
- Tools without task support (e.g., `read`, `stat`, `ls`) execute synchronously and do not support `task` invocation.
|
|
36
|
+
- Task status notifications are emitted via `notifications/tasks/status` when supported.
|
|
39
37
|
|
|
40
38
|
---
|
|
41
39
|
|
|
42
40
|
## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
|
|
43
41
|
|
|
44
|
-
### WORKFLOW A:
|
|
42
|
+
### WORKFLOW A: DISCOVER AND INSPECT
|
|
45
43
|
|
|
46
|
-
- Call `roots` to
|
|
47
|
-
- Call `ls`
|
|
48
|
-
- Call `stat` or `stat_many` to
|
|
49
|
-
|
|
44
|
+
- Call `roots` first to get allowed workspace roots.
|
|
45
|
+
- Call `ls` for non-recursive listing, or `tree` for bounded recursive overview.
|
|
46
|
+
- Call `stat` or `stat_many` to confirm path types/sizes before reading.
|
|
47
|
+
- Call `read` for one file or `read_many` for batches.
|
|
48
|
+
NOTE: Never guess paths. Resolve from `roots`/`ls`/`find` first.
|
|
50
49
|
|
|
51
|
-
### WORKFLOW B: SEARCH
|
|
50
|
+
### WORKFLOW B: SEARCH CONTENT SAFELY
|
|
52
51
|
|
|
53
|
-
- Call `find` to locate files by glob
|
|
54
|
-
- Call `grep` to search
|
|
55
|
-
-
|
|
56
|
-
-
|
|
52
|
+
- Call `find` to locate candidate files by glob.
|
|
53
|
+
- Call `grep` with `filePattern` to search content only in relevant file types.
|
|
54
|
+
- If output is truncated or externalized, call `resources/read` on returned `resourceUri`.
|
|
55
|
+
- Call `read` on exact hits to inspect surrounding context.
|
|
56
|
+
NOTE: `grep` regex uses RE2; do not rely on lookbehind/lookahead/backreferences.
|
|
57
57
|
|
|
58
|
-
### WORKFLOW C:
|
|
58
|
+
### WORKFLOW C: MODIFY FILES WITH LOW RISK
|
|
59
59
|
|
|
60
|
-
- Call `mkdir` to
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
NOTE:
|
|
60
|
+
- Call `mkdir` to prepare directories if needed.
|
|
61
|
+
- Use `edit` for precise first-occurrence replacements in one file.
|
|
62
|
+
- Use `search_and_replace` for bulk replacements across globs.
|
|
63
|
+
- Use `mv` to rename/move paths and `rm` to delete paths.
|
|
64
|
+
NOTE: Confirm destructive operations (`write`, `mv`, `rm`, bulk replace) with the user before execution.
|
|
65
65
|
|
|
66
|
-
### WORKFLOW D: DIFF
|
|
66
|
+
### WORKFLOW D: DIFF/PATCH LOOP
|
|
67
67
|
|
|
68
|
-
- Call `diff_files` to
|
|
69
|
-
- Call `apply_patch`
|
|
70
|
-
-
|
|
71
|
-
|
|
68
|
+
- Call `diff_files` to generate a unified diff.
|
|
69
|
+
- Call `apply_patch` with `dryRun: true` first.
|
|
70
|
+
- If dry run succeeds, call `apply_patch` again with `dryRun: false`.
|
|
71
|
+
- Call `diff_files` again to verify `isIdentical: true` when expected.
|
|
72
|
+
NOTE: If patch apply fails, regenerate patch against current file content and retry.
|
|
72
73
|
|
|
73
74
|
---
|
|
74
75
|
|
|
@@ -76,113 +77,124 @@ These instructions are available as a resource (internal://instructions) or prom
|
|
|
76
77
|
|
|
77
78
|
`roots`
|
|
78
79
|
|
|
79
|
-
- Purpose:
|
|
80
|
-
-
|
|
80
|
+
- Purpose: Enumerate allowed workspace roots.
|
|
81
|
+
- Gotcha: Other tools are constrained to these roots.
|
|
81
82
|
|
|
82
83
|
`ls`
|
|
83
84
|
|
|
84
|
-
- Purpose: List directory contents (non-recursive).
|
|
85
|
-
-
|
|
86
|
-
- Limits: Use `tree` for recursion (depth limited).
|
|
85
|
+
- Purpose: List directory contents (non-recursive by default).
|
|
86
|
+
- Nuance: `pattern` enables filtered recursive traversal up to `maxDepth`.
|
|
87
87
|
|
|
88
88
|
`find`
|
|
89
89
|
|
|
90
|
-
- Purpose:
|
|
91
|
-
-
|
|
92
|
-
- Output: Includes `root` and `pattern` for traceability.
|
|
90
|
+
- Purpose: Find files by glob.
|
|
91
|
+
- Output: Returns relative paths plus metadata; may truncate based on limits.
|
|
93
92
|
- Nuance: Respects `.gitignore` unless `includeIgnored=true`.
|
|
94
93
|
|
|
95
94
|
`tree`
|
|
96
95
|
|
|
97
|
-
- Purpose:
|
|
98
|
-
-
|
|
99
|
-
- Gotcha: `maxDepth=0` returns only the root node with empty children array.
|
|
96
|
+
- Purpose: Return both ASCII and JSON tree views.
|
|
97
|
+
- Gotcha: `maxDepth=0` returns only the root node.
|
|
100
98
|
|
|
101
99
|
`read`
|
|
102
100
|
|
|
103
|
-
- Purpose: Read file
|
|
104
|
-
-
|
|
105
|
-
- Gotcha: `head` is mutually exclusive with `startLine`/`endLine`. Large files return `resourceUri`; read it or use pagination.
|
|
101
|
+
- Purpose: Read a single text file with optional head/range.
|
|
102
|
+
- Gotcha: Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.
|
|
106
103
|
|
|
107
104
|
`read_many`
|
|
108
105
|
|
|
109
|
-
- Purpose:
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- Limits: Total budget capped by `MAX_READ_MANY_TOTAL_SIZE` (default 512 KB).
|
|
106
|
+
- Purpose: Batch read multiple files.
|
|
107
|
+
- Gotcha: Per-file `truncationReason` can be `head`, `range`, or `externalized`.
|
|
108
|
+
- Limits: Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.
|
|
113
109
|
|
|
114
110
|
`stat` / `stat_many`
|
|
115
111
|
|
|
116
|
-
- Purpose:
|
|
117
|
-
-
|
|
112
|
+
- Purpose: Return metadata including token estimate, MIME type, and timestamps.
|
|
113
|
+
- Nuance: Use before read/search when file size/type uncertainty exists.
|
|
118
114
|
|
|
119
115
|
`grep`
|
|
120
116
|
|
|
121
|
-
- Purpose: Search file
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
- Limits: Skips binaries and files larger than `MAX_SEARCH_SIZE` (default 1 MB). Returns max results per `maxResults` (default 500).
|
|
125
|
-
- Gotcha: Regex uses RE2 engine — no backreferences or lookahead/lookbehind.
|
|
117
|
+
- Purpose: Search file contents by literal or RE2 regex.
|
|
118
|
+
- Gotcha: Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.
|
|
119
|
+
- Limits: Skips binary and oversized files; reports skips in structured output.
|
|
126
120
|
|
|
127
|
-
`
|
|
121
|
+
`write`
|
|
128
122
|
|
|
129
|
-
- Purpose:
|
|
130
|
-
-
|
|
131
|
-
- Behavior: Auto-detects file vs directory using `fs.stat`.
|
|
132
|
-
- **Files**: Returns `{ hash, isDirectory: false }`.
|
|
133
|
-
- **Directories**: Returns `{ hash, isDirectory: true, fileCount }`. Uses deterministic hash-of-hashes pattern (lexicographically sorted paths, respects `.gitignore`).
|
|
123
|
+
- Purpose: Create or overwrite a file atomically.
|
|
124
|
+
- Side effects: Creates parent directories automatically; overwrites existing content.
|
|
134
125
|
|
|
135
|
-
`
|
|
126
|
+
`edit`
|
|
136
127
|
|
|
137
|
-
- Purpose:
|
|
138
|
-
-
|
|
139
|
-
- Output: Includes `isIdentical` (diff may be empty when true).
|
|
140
|
-
- Gotcha: Large diffs may be returned via `resourceUri`.
|
|
128
|
+
- Purpose: Apply sequential literal replacements (first occurrence per edit).
|
|
129
|
+
- Gotcha: `oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.
|
|
141
130
|
|
|
142
|
-
`
|
|
131
|
+
`mv`
|
|
132
|
+
|
|
133
|
+
- Purpose: Move or rename file/directory paths.
|
|
134
|
+
- Nuance: Cross-device moves fall back to copy+delete.
|
|
135
|
+
|
|
136
|
+
`rm`
|
|
137
|
+
|
|
138
|
+
- Purpose: Delete file/directory paths.
|
|
139
|
+
- Gotcha: Non-empty directory delete requires `recursive=true`; else returns actionable input error.
|
|
140
|
+
|
|
141
|
+
`calculate_hash`
|
|
142
|
+
|
|
143
|
+
- Purpose: SHA-256 for files or deterministic composite hash for directories.
|
|
144
|
+
- Nuance: Directory hashing respects root `.gitignore` and sorts paths for stable output.
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
-
|
|
147
|
-
- Gotcha: `
|
|
146
|
+
`diff_files`
|
|
147
|
+
|
|
148
|
+
- Purpose: Generate unified diff between two files.
|
|
149
|
+
- Gotcha: `isIdentical=true` means no hunks (`@@`) and empty diff.
|
|
148
150
|
|
|
149
151
|
`apply_patch`
|
|
150
152
|
|
|
151
|
-
- Purpose: Apply
|
|
152
|
-
-
|
|
153
|
+
- Purpose: Apply unified diff text to a file.
|
|
154
|
+
- Gotcha: Patch must include valid hunk headers; use `dryRun=true` first.
|
|
153
155
|
|
|
154
156
|
`search_and_replace`
|
|
155
157
|
|
|
156
|
-
- Purpose: Replace
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
- Gotcha: Review `processedFiles`, `failedFiles`, and `failures` for partial errors.
|
|
158
|
+
- Purpose: Replace all matches across files selected by `filePattern`.
|
|
159
|
+
- Gotcha: Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).
|
|
160
|
+
- Limits: Changed-file sample and failure sample are capped/truncated in output.
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
---
|
|
162
163
|
|
|
163
|
-
-
|
|
164
|
-
- Side effects: Destructive — overwrites existing content without confirmation.
|
|
164
|
+
## CROSS-FEATURE RELATIONSHIPS
|
|
165
165
|
|
|
166
|
-
`
|
|
166
|
+
- Use `roots` output to scope all other tool calls.
|
|
167
|
+
- Use `find` → `grep` → `read` as the default search triad.
|
|
168
|
+
- Use `diff_files` output as input to `apply_patch`.
|
|
169
|
+
- Use `resourceUri` from `read`, `read_many`, `grep`, and `diff_files` with `resources/read` for full payload retrieval.
|
|
170
|
+
- Use `stat`/`stat_many` before `read`/`read_many` when size/type may violate limits.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## CONSTRAINTS & LIMITATIONS
|
|
167
175
|
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
176
|
+
- Access is restricted to allowed roots negotiated from CLI and MCP Roots.
|
|
177
|
+
- If multiple roots are configured and no path is provided, tools requiring base path fail with disambiguation error.
|
|
178
|
+
- Default timeouts and size caps are enforced (`DEFAULT_SEARCH_TIMEOUT`, `MAX_FILE_SIZE`, `MAX_SEARCH_SIZE`, `MAX_READ_MANY_TOTAL_SIZE`).
|
|
179
|
+
- Sensitive files are denylisted by default unless explicitly allowed via environment settings.
|
|
180
|
+
- Binary files are skipped for content search/read workflows where text is required.
|
|
181
|
+
- Externalized resource cache is in-memory, bounded (entry size/count/total bytes), and ephemeral.
|
|
182
|
+
- Regex engine is RE2-based; advanced PCRE features are unsupported.
|
|
171
183
|
|
|
172
184
|
---
|
|
173
185
|
|
|
174
186
|
## ERROR HANDLING STRATEGY
|
|
175
187
|
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
178
|
-
- `E_NOT_FILE`: Path
|
|
179
|
-
- `E_NOT_DIRECTORY`: Path
|
|
180
|
-
- `E_TOO_LARGE`: File exceeds
|
|
181
|
-
- `E_TIMEOUT`:
|
|
182
|
-
- `E_INVALID_PATTERN`: Fix
|
|
183
|
-
- `E_INVALID_INPUT`:
|
|
184
|
-
- `E_PERMISSION_DENIED`: OS-level permission denied.
|
|
185
|
-
- `E_SYMLINK_NOT_ALLOWED`:
|
|
186
|
-
- `E_UNKNOWN`:
|
|
188
|
+
- `E_ACCESS_DENIED`: Path is outside allowed roots or roots are not configured. → Call `roots`, then retry with an allowed path.
|
|
189
|
+
- `E_NOT_FOUND`: Path or resource does not exist. → Call `ls`/`find` to verify existence and exact spelling.
|
|
190
|
+
- `E_NOT_FILE`: Path points to a directory/non-file for file-only operation. → Call `ls` or switch to directory tool.
|
|
191
|
+
- `E_NOT_DIRECTORY`: Path points to a file for directory operation. → Call `read` for file content or choose a directory path.
|
|
192
|
+
- `E_TOO_LARGE`: File/content exceeds limits. → Narrow scope, use range/head reads, or reduce candidate files.
|
|
193
|
+
- `E_TIMEOUT`: Operation exceeded timeout. → Reduce path scope, lower result limits, or simplify pattern.
|
|
194
|
+
- `E_INVALID_PATTERN`: Glob/regex invalid. → Fix syntax (RE2 for regex) and retry.
|
|
195
|
+
- `E_INVALID_INPUT`: Arguments are invalid for current context (e.g., ambiguous roots, bad patch, missing flags). → Correct parameters and retry.
|
|
196
|
+
- `E_PERMISSION_DENIED`: OS-level permission denied. → Adjust file permissions or choose accessible paths.
|
|
197
|
+
- `E_SYMLINK_NOT_ALLOWED`: Symlink traversal escapes allowed roots. → Use paths within allowed directories.
|
|
198
|
+
- `E_UNKNOWN`: Unclassified failure. → Inspect message details and retry with narrower, validated inputs.
|
|
187
199
|
|
|
188
200
|
---
|