@j0hanz/filesystem-mcp 1.7.1 → 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/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -1
- 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 +6 -8
- 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/dist/tools/task-support.js +7 -5
- package/package.json +1 -1
|
@@ -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
|
@@ -7,7 +7,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { registerCompletions } from '../completions.js';
|
|
10
|
-
import { DEFAULT_LOG_LEVEL, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
|
|
10
|
+
import { DEFAULT_LOG_LEVEL, parseEnvInt, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
|
|
11
11
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
12
12
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
13
13
|
import { pkgInfo } from '../pkg-info.js';
|
|
@@ -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',
|
|
@@ -104,16 +104,14 @@ export async function startServer(server) {
|
|
|
104
104
|
rootsManager.registerHandlers(server);
|
|
105
105
|
await rootsManager.recomputeAllowedDirectories();
|
|
106
106
|
await server.connect(transport);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
transportAny.onclose = () => {
|
|
107
|
+
const sdkOnClose = transport.onclose;
|
|
108
|
+
transport.onclose = () => {
|
|
110
109
|
rootsManager.destroy();
|
|
111
110
|
sdkOnClose?.();
|
|
112
111
|
};
|
|
113
112
|
rootsManager.logMissingDirectoriesIfNeeded(server);
|
|
114
113
|
}
|
|
115
|
-
const MAX_REQUEST_BODY_BYTES =
|
|
116
|
-
4 * 1024 * 1024; // 4 MB default
|
|
114
|
+
const MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_CONTEXT_MAX_REQUEST_BYTES', 4 * 1024 * 1024, 1024, 256 * 1024 * 1024);
|
|
117
115
|
class RequestBodyError extends Error {
|
|
118
116
|
statusCode;
|
|
119
117
|
constructor(message, statusCode) {
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { listDirectory } from '../lib/file-operations/list-directory.js';
|
|
6
6
|
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const LIST_DIRECTORY_TOOL = {
|
|
10
10
|
name: 'ls',
|
|
@@ -82,27 +82,9 @@ function buildStructuredListResult(result, nextCursor) {
|
|
|
82
82
|
...(nextCursor !== undefined ? { nextCursor } : {}),
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
function encodeCursor(offset) {
|
|
86
|
-
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
87
|
-
}
|
|
88
|
-
function decodeCursor(cursor) {
|
|
89
|
-
try {
|
|
90
|
-
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
91
|
-
if (typeof parsed === 'object' &&
|
|
92
|
-
parsed !== null &&
|
|
93
|
-
typeof parsed.offset === 'number') {
|
|
94
|
-
const { offset } = parsed;
|
|
95
|
-
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// ignore malformed cursor
|
|
100
|
-
}
|
|
101
|
-
return 0;
|
|
102
|
-
}
|
|
103
85
|
async function handleListDirectory(args, signal) {
|
|
104
86
|
const dirPath = resolvePathOrRoot(args.path);
|
|
105
|
-
const cursorOffset = args.cursor !== undefined ?
|
|
87
|
+
const cursorOffset = args.cursor !== undefined ? decodeOffsetCursor(args.cursor) : 0;
|
|
106
88
|
const pageSize = args.maxEntries;
|
|
107
89
|
const options = {
|
|
108
90
|
includeHidden: args.includeHidden,
|
|
@@ -117,7 +99,7 @@ async function handleListDirectory(args, signal) {
|
|
|
117
99
|
const result = await listDirectory(dirPath, options);
|
|
118
100
|
const displayEntries = cursorOffset > 0 ? result.entries.slice(cursorOffset) : result.entries;
|
|
119
101
|
const nextCursor = result.summary.truncated && displayEntries.length > 0
|
|
120
|
-
?
|
|
102
|
+
? encodeOffsetCursor(cursorOffset + displayEntries.length)
|
|
121
103
|
: undefined;
|
|
122
104
|
const displayResult = { ...result, entries: displayEntries };
|
|
123
105
|
return buildToolResponse(buildListTextResult(displayResult, nextCursor), buildStructuredListResult(displayResult, nextCursor));
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '..
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const READ_MULTIPLE_FILES_TOOL = {
|
|
9
9
|
name: 'read_many',
|
|
@@ -20,6 +20,23 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
20
20
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
21
21
|
],
|
|
22
22
|
};
|
|
23
|
+
function buildReadManyCompletionSuffix(summary) {
|
|
24
|
+
const total = summary?.total ?? 0;
|
|
25
|
+
const failed = summary?.failed ?? 0;
|
|
26
|
+
const succeeded = summary?.succeeded ?? 0;
|
|
27
|
+
if (failed) {
|
|
28
|
+
return `${succeeded}/${total} read, ${failed} failed`;
|
|
29
|
+
}
|
|
30
|
+
const label = total === 1 ? 'file' : 'files';
|
|
31
|
+
return `${total} ${label} read`;
|
|
32
|
+
}
|
|
33
|
+
function createReadManyProgressCallbacks(extra, context, totalPaths) {
|
|
34
|
+
const progress = createToolProgressSession(extra, `🕮 read_many: ${context}`);
|
|
35
|
+
const onReadComplete = () => {
|
|
36
|
+
progress.increment((current) => `🕮 read_many: ${context} [${current}/${totalPaths} read]`);
|
|
37
|
+
};
|
|
38
|
+
return { progress, onReadComplete };
|
|
39
|
+
}
|
|
23
40
|
function toStructuredReadManyResult(result) {
|
|
24
41
|
const structured = {
|
|
25
42
|
path: result.path,
|
|
@@ -134,28 +151,13 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
134
151
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
135
152
|
context: { path: primaryPath },
|
|
136
153
|
run: async (signal) => {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
140
|
-
: '';
|
|
141
|
-
const context = `${args.paths.length} files [${first}${extraPaths}]`;
|
|
142
|
-
const progress = createToolProgressSession(extra, `🕮 read_many: ${context}`);
|
|
143
|
-
const onReadComplete = () => {
|
|
144
|
-
progress.increment((current) => `🕮 read_many: ${context} [${current}/${args.paths.length} read]`);
|
|
145
|
-
};
|
|
154
|
+
const context = buildBatchPathContext(args.paths, 'files');
|
|
155
|
+
const { progress, onReadComplete } = createReadManyProgressCallbacks(extra, context, args.paths.length);
|
|
146
156
|
try {
|
|
147
157
|
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onReadComplete);
|
|
148
158
|
const sc = result.structuredContent;
|
|
159
|
+
const suffix = buildReadManyCompletionSuffix(sc.summary);
|
|
149
160
|
const total = sc.summary?.total ?? 0;
|
|
150
|
-
const failed = sc.summary?.failed ?? 0;
|
|
151
|
-
const succeeded = sc.summary?.succeeded ?? 0;
|
|
152
|
-
let suffix;
|
|
153
|
-
if (failed) {
|
|
154
|
-
suffix = `${succeeded}/${total} read, ${failed} failed`;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
suffix = `${total} files read`;
|
|
158
|
-
}
|
|
159
161
|
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
160
162
|
progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
|
|
161
163
|
return result;
|
|
@@ -4,26 +4,8 @@ import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/con
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { searchFiles } from '../lib/file-operations/search-files.js';
|
|
6
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
|
-
function encodeCursor(offset) {
|
|
10
|
-
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
11
|
-
}
|
|
12
|
-
function decodeCursor(cursor) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
15
|
-
if (typeof parsed === 'object' &&
|
|
16
|
-
parsed !== null &&
|
|
17
|
-
typeof parsed.offset === 'number') {
|
|
18
|
-
const { offset } = parsed;
|
|
19
|
-
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// ignore malformed cursor
|
|
24
|
-
}
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
9
|
export const SEARCH_FILES_TOOL = {
|
|
28
10
|
name: 'find',
|
|
29
11
|
title: 'Find Files',
|
|
@@ -43,7 +25,7 @@ export const SEARCH_FILES_TOOL = {
|
|
|
43
25
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
44
26
|
const basePath = resolvePathOrRoot(args.path);
|
|
45
27
|
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
46
|
-
const cursorOffset = args.cursor !== undefined ?
|
|
28
|
+
const cursorOffset = args.cursor !== undefined ? decodeOffsetCursor(args.cursor) : 0;
|
|
47
29
|
const pageSize = args.maxResults;
|
|
48
30
|
const fetchMax = cursorOffset + pageSize;
|
|
49
31
|
const searchOptions = {
|
|
@@ -59,7 +41,7 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
59
41
|
const allResults = result.results;
|
|
60
42
|
const displayResults = cursorOffset > 0 ? allResults.slice(cursorOffset) : allResults;
|
|
61
43
|
const nextCursor = result.summary.truncated && displayResults.length > 0
|
|
62
|
-
?
|
|
44
|
+
? encodeOffsetCursor(cursorOffset + displayResults.length)
|
|
63
45
|
: undefined;
|
|
64
46
|
const relativeResults = [];
|
|
65
47
|
for (const entry of displayResults) {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -139,3 +139,6 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
|
|
|
139
139
|
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
140
140
|
*/
|
|
141
141
|
export declare function resolvePathOrRoot(pathValue: string | undefined): string;
|
|
142
|
+
export declare function encodeOffsetCursor(offset: number): string;
|
|
143
|
+
export declare function decodeOffsetCursor(cursor: string): number;
|
|
144
|
+
export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import { channel } from 'node:diagnostics_channel';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { parseTrueEnvFlag } from '../lib/constants.js';
|
|
@@ -403,3 +404,29 @@ export function resolvePathOrRoot(pathValue) {
|
|
|
403
404
|
}
|
|
404
405
|
return root;
|
|
405
406
|
}
|
|
407
|
+
export function encodeOffsetCursor(offset) {
|
|
408
|
+
return Buffer.from(JSON.stringify({ offset })).toString('base64url');
|
|
409
|
+
}
|
|
410
|
+
export function decodeOffsetCursor(cursor) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
|
|
413
|
+
if (typeof parsed === 'object' &&
|
|
414
|
+
parsed !== null &&
|
|
415
|
+
typeof parsed.offset === 'number') {
|
|
416
|
+
const { offset } = parsed;
|
|
417
|
+
return Number.isInteger(offset) && offset >= 0 ? offset : 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// ignore malformed cursor
|
|
422
|
+
}
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
export function buildBatchPathContext(paths, unitLabel = 'paths') {
|
|
426
|
+
const normalizedLabel = paths.length === 1 ? unitLabel.replace(/s$/i, '') : unitLabel;
|
|
427
|
+
const first = path.basename(paths[0] ?? '');
|
|
428
|
+
const extraPaths = paths.length > 1
|
|
429
|
+
? `, ${path.basename(paths[1] ?? '')}${paths.length > 2 ? '…' : ''}`
|
|
430
|
+
: '';
|
|
431
|
+
return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
|
|
432
|
+
}
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
1
|
import { formatBytes, joinLines } from '../config.js';
|
|
3
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
4
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
5
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
8
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
10
9
|
name: 'stat_many',
|
|
@@ -16,6 +15,22 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
|
16
15
|
taskSupport: 'optional',
|
|
17
16
|
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
18
17
|
};
|
|
18
|
+
function buildStatManyCompletionSuffix(summary) {
|
|
19
|
+
const total = summary?.total ?? 0;
|
|
20
|
+
const failed = summary?.failed ?? 0;
|
|
21
|
+
const succeeded = summary?.succeeded ?? 0;
|
|
22
|
+
if (failed) {
|
|
23
|
+
return `${succeeded}/${total} OK, ${failed} failed`;
|
|
24
|
+
}
|
|
25
|
+
return `${total} OK`;
|
|
26
|
+
}
|
|
27
|
+
function createStatManyProgressCallbacks(extra, context, totalPaths) {
|
|
28
|
+
const progress = createToolProgressSession(extra, `🕮 stat_many: ${context}`);
|
|
29
|
+
const onProgress = () => {
|
|
30
|
+
progress.increment((current) => `🕮 stat_many: ${context} [${current}/${totalPaths} scanned]`);
|
|
31
|
+
};
|
|
32
|
+
return { progress, onProgress };
|
|
33
|
+
}
|
|
19
34
|
function formatFileInfoDetail(info) {
|
|
20
35
|
const lines = [
|
|
21
36
|
`${info.name} (${info.type})`,
|
|
@@ -74,28 +89,13 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
74
89
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
75
90
|
context: { path: primaryPath },
|
|
76
91
|
run: async (signal) => {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
80
|
-
: '';
|
|
81
|
-
const context = `${args.paths.length} paths [${first}${extraPaths}]`;
|
|
82
|
-
const progress = createToolProgressSession(extra, `🕮 stat_many: ${context}`);
|
|
83
|
-
const onProgress = () => {
|
|
84
|
-
progress.increment((current) => `🕮 stat_many: ${context} [${current}/${args.paths.length} scanned]`);
|
|
85
|
-
};
|
|
92
|
+
const context = buildBatchPathContext(args.paths);
|
|
93
|
+
const { progress, onProgress } = createStatManyProgressCallbacks(extra, context, args.paths.length);
|
|
86
94
|
try {
|
|
87
95
|
const result = await handleGetMultipleFileInfo(args, signal, onProgress);
|
|
88
96
|
const sc = result.structuredContent;
|
|
97
|
+
const suffix = buildStatManyCompletionSuffix(sc.summary);
|
|
89
98
|
const total = sc.summary?.total ?? 0;
|
|
90
|
-
const failed = sc.summary?.failed ?? 0;
|
|
91
|
-
const succeeded = sc.summary?.succeeded ?? 0;
|
|
92
|
-
let suffix;
|
|
93
|
-
if (failed) {
|
|
94
|
-
suffix = `${succeeded}/${total} OK, ${failed} failed`;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
suffix = `${total} OK`;
|
|
98
|
-
}
|
|
99
99
|
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
100
100
|
progress.complete(`🕮 stat_many: ${context} • ${suffix}`, finalCurrent);
|
|
101
101
|
return result;
|
|
@@ -20,14 +20,16 @@ function getExperimentalTaskRegistration(server) {
|
|
|
20
20
|
return tasks;
|
|
21
21
|
}
|
|
22
22
|
function hasTaskToolCapability(server) {
|
|
23
|
-
const
|
|
24
|
-
const serverRuntime =
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const serverRecord = server;
|
|
24
|
+
const { server: serverRuntime } = serverRecord;
|
|
25
|
+
if (!isRecord(serverRuntime))
|
|
26
|
+
return true;
|
|
27
|
+
const { getCapabilities } = serverRuntime;
|
|
28
|
+
if (typeof getCapabilities !== 'function') {
|
|
27
29
|
// Fallback for tests or custom wrappers that provide only registerTool/experimental.
|
|
28
30
|
return true;
|
|
29
31
|
}
|
|
30
|
-
const capabilities =
|
|
32
|
+
const capabilities = getCapabilities.call(serverRuntime);
|
|
31
33
|
if (!isRecord(capabilities))
|
|
32
34
|
return false;
|
|
33
35
|
const { tasks } = capabilities;
|