@j0hanz/filesystem-mcp 1.7.3 → 1.9.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 +635 -377
- package/dist/cli.js +2 -2
- package/dist/completions.js +2 -3
- package/dist/index.js +2 -2
- package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
- package/dist/lib/file-operations/{common.js → core.js} +45 -0
- package/dist/lib/file-operations/metadata.d.ts +73 -0
- package/dist/lib/file-operations/metadata.js +889 -0
- package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +29 -3
- package/dist/lib/file-operations/{search-content.js → search.js} +409 -82
- package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
- package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
- package/dist/lib/fs-helpers.d.ts +1 -0
- package/dist/lib/fs-helpers.js +10 -2
- package/dist/lib/observability.js +1 -1
- package/dist/lib/{path-validation.d.ts → paths.d.ts} +4 -0
- package/dist/lib/{path-validation.js → paths.js} +112 -1
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.js +33 -0
- package/dist/resources/generated-instructions.js +6 -6
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +3 -3
- package/dist/resources/workflows.js +10 -23
- package/dist/schemas.js +15 -15
- package/dist/server/bootstrap.d.ts +19 -1
- package/dist/server/bootstrap.js +103 -9
- package/dist/server/roots-manager.d.ts +2 -2
- package/dist/server/roots-manager.js +3 -3
- package/dist/tools/apply-patch.js +7 -3
- package/dist/tools/calculate-hash.js +15 -15
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +7 -4
- package/dist/tools/diff-files.js +2 -2
- package/dist/tools/edit-file.js +14 -13
- package/dist/tools/list-directory.js +5 -7
- package/dist/tools/move-file.js +2 -1
- package/dist/tools/read-multiple.js +13 -26
- package/dist/tools/read.js +3 -3
- package/dist/tools/replace-in-files.js +17 -20
- package/dist/tools/roots.js +4 -6
- package/dist/tools/search-content.js +9 -11
- package/dist/tools/search-files.js +4 -6
- package/dist/tools/shared.d.ts +18 -1
- package/dist/tools/shared.js +39 -19
- package/dist/tools/stat-many.js +14 -24
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +1 -1
- package/dist/tools/tree.js +3 -4
- package/dist/tools/write-file.js +1 -1
- package/package.json +9 -5
- package/dist/lib/file-operations/file-info.d.ts +0 -10
- package/dist/lib/file-operations/file-info.js +0 -143
- package/dist/lib/file-operations/gitignore.d.ts +0 -6
- package/dist/lib/file-operations/gitignore.js +0 -45
- package/dist/lib/file-operations/list-directory.d.ts +0 -14
- package/dist/lib/file-operations/list-directory.js +0 -256
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
- package/dist/lib/file-operations/read-multiple-files.js +0 -252
- package/dist/lib/file-operations/search-files.d.ts +0 -27
- package/dist/lib/file-operations/search-files.js +0 -218
- package/dist/lib/file-operations/search-worker.d.ts +0 -2
- package/dist/lib/file-operations/search-worker.js +0 -129
- package/dist/lib/file-operations/tree.d.ts +0 -28
- package/dist/lib/file-operations/tree.js +0 -269
- package/dist/lib/path-format.d.ts +0 -1
- package/dist/lib/path-format.js +0 -7
- package/dist/lib/path-policy.d.ts +0 -2
- package/dist/lib/path-policy.js +0 -100
- package/dist/lib/type-guards.d.ts +0 -1
- package/dist/lib/type-guards.js +0 -3
- package/dist/server/capabilities.d.ts +0 -10
- package/dist/server/capabilities.js +0 -48
- package/dist/server/logging.d.ts +0 -7
- package/dist/server/logging.js +0 -41
- package/dist/server/types.d.ts +0 -4
- package/dist/server/types.js +0 -1
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import RE2 from 're2';
|
|
3
|
-
import { formatOperationSummary, joinLines } from '../config.js';
|
|
4
3
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
5
4
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
|
-
import { searchContent } from '../lib/file-operations/search
|
|
5
|
+
import { searchContent } from '../lib/file-operations/search.js';
|
|
6
|
+
import { formatOperationSummary, joinLines } from '../config.js';
|
|
7
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
10
|
const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50;
|
|
11
11
|
export const SEARCH_CONTENT_TOOL = {
|
|
12
12
|
name: 'grep',
|
|
13
13
|
title: 'Search Content',
|
|
14
|
-
description: 'Search for text
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'Use `filePattern` to scope by file type (e.g. `**/*.ts`) and avoid noisy results. ' +
|
|
18
|
-
'Use includeHidden=true to include hidden files and directories.',
|
|
14
|
+
description: 'Search file contents for text (grep-like). Returns matching lines. ' +
|
|
15
|
+
'Scope with `filePattern` (e.g. `**/*.ts`) to reduce noise. ' +
|
|
16
|
+
'`includeHidden=true` for dotfiles.',
|
|
19
17
|
inputSchema: SearchContentInputSchema,
|
|
20
18
|
outputSchema: SearchContentOutputSchema,
|
|
21
19
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
22
20
|
nuances: [
|
|
23
|
-
'Inline
|
|
21
|
+
'Inline results capped at 50 matches; full results via `resourceUri`.',
|
|
24
22
|
'Skips binary and oversized files.',
|
|
25
23
|
],
|
|
26
24
|
gotchas: [
|
|
27
|
-
'Skips binary
|
|
25
|
+
'Skips binary/oversized files silently — verify with `stat` if no matches.',
|
|
28
26
|
],
|
|
29
27
|
taskSupport: 'optional',
|
|
30
28
|
};
|
|
@@ -242,7 +240,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
242
240
|
suffix += ' [truncated — max files]';
|
|
243
241
|
}
|
|
244
242
|
}
|
|
245
|
-
const finalCurrent =
|
|
243
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
246
244
|
progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
|
|
247
245
|
return result;
|
|
248
246
|
}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import { formatOperationSummary, joinLines } from '../config.js';
|
|
3
2
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
|
-
import { searchFiles } from '../lib/file-operations/search
|
|
4
|
+
import { searchFiles } from '../lib/file-operations/search.js';
|
|
5
|
+
import { formatOperationSummary, joinLines } from '../config.js';
|
|
6
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
7
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
9
|
export const SEARCH_FILES_TOOL = {
|
|
10
10
|
name: 'find',
|
|
11
11
|
title: 'Find Files',
|
|
12
|
-
description: 'Find files by glob pattern (e.g
|
|
13
|
-
'
|
|
14
|
-
'For text search inside files, use grep. ' +
|
|
15
|
-
'To bulk-edit the matched files, pass the same glob pattern to search_and_replace.',
|
|
12
|
+
description: 'Find files by glob pattern (e.g. `**/*.ts`). Returns matching files with metadata. ' +
|
|
13
|
+
'For content search, use `grep`. For bulk edits, pass the same glob to `search_and_replace`.',
|
|
16
14
|
inputSchema: SearchFilesInputSchema,
|
|
17
15
|
outputSchema: SearchFilesOutputSchema,
|
|
18
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import type { FileInfo } from '../config.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
4
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
5
|
+
import type { FileInfo } from '../config.js';
|
|
6
6
|
export { type ToolContract } from './contract.js';
|
|
7
7
|
export declare const READ_ONLY_TOOL_ANNOTATIONS: {
|
|
8
8
|
readonly readOnlyHint: true;
|
|
@@ -12,6 +12,7 @@ export declare const READ_ONLY_TOOL_ANNOTATIONS: {
|
|
|
12
12
|
};
|
|
13
13
|
export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
|
|
14
14
|
readonly readOnlyHint: false;
|
|
15
|
+
readonly idempotentHint: false;
|
|
15
16
|
readonly destructiveHint: true;
|
|
16
17
|
readonly openWorldHint: false;
|
|
17
18
|
};
|
|
@@ -123,7 +124,18 @@ export interface ToolProgressSession {
|
|
|
123
124
|
fail: (message: string, minimumCurrent?: number) => void;
|
|
124
125
|
getCurrent: () => number;
|
|
125
126
|
}
|
|
127
|
+
export interface BatchProgressCallbacks {
|
|
128
|
+
progress: ToolProgressSession;
|
|
129
|
+
onItemComplete: () => void;
|
|
130
|
+
}
|
|
126
131
|
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
|
|
132
|
+
export declare function createBatchProgressCallbacks(extra: ToolExtra, params: {
|
|
133
|
+
toolLabel: string;
|
|
134
|
+
context: string;
|
|
135
|
+
totalItems: number;
|
|
136
|
+
itemVerb: string;
|
|
137
|
+
}): BatchProgressCallbacks;
|
|
138
|
+
export declare function resolveFinalProgressCurrent(progress: ToolProgressSession, ...candidates: number[]): number;
|
|
127
139
|
export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>, options: {
|
|
128
140
|
guard?: (() => boolean) | undefined;
|
|
129
141
|
progressMessage?: (args: Args) => string;
|
|
@@ -142,3 +154,8 @@ export declare function resolvePathOrRoot(pathValue: string | undefined): string
|
|
|
142
154
|
export declare function encodeOffsetCursor(offset: number): string;
|
|
143
155
|
export declare function decodeOffsetCursor(cursor: string): number;
|
|
144
156
|
export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
|
|
157
|
+
export declare function buildBatchCompletionSuffix(summary: {
|
|
158
|
+
total?: number;
|
|
159
|
+
failed?: number;
|
|
160
|
+
succeeded?: number;
|
|
161
|
+
} | undefined, successWord: string, singularWord?: string): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseTrueEnvFlag } from '../lib/constants.js';
|
|
|
5
5
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
6
6
|
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
7
7
|
import { withToolDiagnostics } from '../lib/observability.js';
|
|
8
|
-
import { getAllowedDirectories } from '../lib/
|
|
8
|
+
import { getAllowedDirectories } from '../lib/paths.js';
|
|
9
9
|
export {} from './contract.js';
|
|
10
10
|
const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
|
|
11
11
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
@@ -24,6 +24,7 @@ export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
|
24
24
|
};
|
|
25
25
|
export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
|
|
26
26
|
readOnlyHint: false,
|
|
27
|
+
idempotentHint: false,
|
|
27
28
|
destructiveHint: true,
|
|
28
29
|
openWorldHint: false,
|
|
29
30
|
};
|
|
@@ -293,6 +294,15 @@ export function createToolProgressSession(extra, startMessage) {
|
|
|
293
294
|
cursor = value;
|
|
294
295
|
return cursor;
|
|
295
296
|
};
|
|
297
|
+
const finishProgress = (message, minimumCurrent) => {
|
|
298
|
+
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
299
|
+
notifyProgress(extra, {
|
|
300
|
+
current: finalCurrent,
|
|
301
|
+
total: finalCurrent,
|
|
302
|
+
...(message !== undefined ? { message } : {}),
|
|
303
|
+
});
|
|
304
|
+
cursor = finalCurrent;
|
|
305
|
+
};
|
|
296
306
|
return {
|
|
297
307
|
update: ({ current, total, message }) => {
|
|
298
308
|
const normalized = setCursor(current);
|
|
@@ -309,27 +319,27 @@ export function createToolProgressSession(extra, startMessage) {
|
|
|
309
319
|
message: messageForCurrent(next),
|
|
310
320
|
});
|
|
311
321
|
},
|
|
312
|
-
complete:
|
|
313
|
-
|
|
314
|
-
notifyProgress(extra, {
|
|
315
|
-
current: finalCurrent,
|
|
316
|
-
total: finalCurrent,
|
|
317
|
-
message,
|
|
318
|
-
});
|
|
319
|
-
cursor = finalCurrent;
|
|
320
|
-
},
|
|
321
|
-
fail: (message, minimumCurrent) => {
|
|
322
|
-
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
323
|
-
notifyProgress(extra, {
|
|
324
|
-
current: finalCurrent,
|
|
325
|
-
total: finalCurrent,
|
|
326
|
-
message,
|
|
327
|
-
});
|
|
328
|
-
cursor = finalCurrent;
|
|
329
|
-
},
|
|
322
|
+
complete: finishProgress,
|
|
323
|
+
fail: finishProgress,
|
|
330
324
|
getCurrent: () => cursor,
|
|
331
325
|
};
|
|
332
326
|
}
|
|
327
|
+
export function createBatchProgressCallbacks(extra, params) {
|
|
328
|
+
const progress = createToolProgressSession(extra, `${params.toolLabel}: ${params.context}`);
|
|
329
|
+
const onItemComplete = () => {
|
|
330
|
+
progress.increment((current) => `${params.toolLabel}: ${params.context} [${current}/${params.totalItems} ${params.itemVerb}]`);
|
|
331
|
+
};
|
|
332
|
+
return { progress, onItemComplete };
|
|
333
|
+
}
|
|
334
|
+
export function resolveFinalProgressCurrent(progress, ...candidates) {
|
|
335
|
+
let finalCurrent = progress.getCurrent() + 1;
|
|
336
|
+
for (const candidate of candidates) {
|
|
337
|
+
if (candidate > finalCurrent) {
|
|
338
|
+
finalCurrent = candidate;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return finalCurrent;
|
|
342
|
+
}
|
|
333
343
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
334
344
|
if (!canReportProgress(extra)) {
|
|
335
345
|
return run();
|
|
@@ -430,3 +440,13 @@ export function buildBatchPathContext(paths, unitLabel = 'paths') {
|
|
|
430
440
|
: '';
|
|
431
441
|
return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
|
|
432
442
|
}
|
|
443
|
+
export function buildBatchCompletionSuffix(summary, successWord, singularWord) {
|
|
444
|
+
const total = summary?.total ?? 0;
|
|
445
|
+
const failed = summary?.failed ?? 0;
|
|
446
|
+
const succeeded = summary?.succeeded ?? 0;
|
|
447
|
+
if (failed) {
|
|
448
|
+
return `${succeeded}/${total} ${successWord}, ${failed} failed`;
|
|
449
|
+
}
|
|
450
|
+
const word = total === 1 && singularWord ? singularWord : successWord;
|
|
451
|
+
return `${total} ${word}`;
|
|
452
|
+
}
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,36 +1,21 @@
|
|
|
1
|
-
import { formatBytes, joinLines } from '../config.js';
|
|
2
1
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
2
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
|
-
import { getMultipleFileInfo } from '../lib/file-operations/
|
|
3
|
+
import { getMultipleFileInfo } from '../lib/file-operations/metadata.js';
|
|
4
|
+
import { formatBytes, joinLines } from '../config.js';
|
|
5
5
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse,
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
9
9
|
name: 'stat_many',
|
|
10
10
|
title: 'Get Multiple File Info',
|
|
11
|
-
description: 'Get metadata
|
|
11
|
+
description: 'Get metadata for multiple files/directories in one request. ' +
|
|
12
|
+
'Use `tokenEstimate` (size\u00f74) to pre-screen token cost before reading.',
|
|
12
13
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
13
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
14
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
15
16
|
taskSupport: 'optional',
|
|
16
17
|
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
17
18
|
};
|
|
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
|
-
}
|
|
34
19
|
function formatFileInfoDetail(info) {
|
|
35
20
|
const lines = [
|
|
36
21
|
`${info.name} (${info.type})`,
|
|
@@ -90,13 +75,18 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
90
75
|
context: { path: primaryPath },
|
|
91
76
|
run: async (signal) => {
|
|
92
77
|
const context = buildBatchPathContext(args.paths);
|
|
93
|
-
const { progress,
|
|
78
|
+
const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
|
|
79
|
+
toolLabel: '🕮 stat_many',
|
|
80
|
+
context,
|
|
81
|
+
totalItems: args.paths.length,
|
|
82
|
+
itemVerb: 'scanned',
|
|
83
|
+
});
|
|
94
84
|
try {
|
|
95
|
-
const result = await handleGetMultipleFileInfo(args, signal,
|
|
85
|
+
const result = await handleGetMultipleFileInfo(args, signal, onItemComplete);
|
|
96
86
|
const sc = result.structuredContent;
|
|
97
|
-
const suffix =
|
|
87
|
+
const suffix = buildBatchCompletionSuffix(sc.summary, 'OK');
|
|
98
88
|
const total = sc.summary?.total ?? 0;
|
|
99
|
-
const finalCurrent =
|
|
89
|
+
const finalCurrent = resolveFinalProgressCurrent(progress, total);
|
|
100
90
|
progress.complete(`🕮 stat_many: ${context} • ${suffix}`, finalCurrent);
|
|
101
91
|
return result;
|
|
102
92
|
}
|
package/dist/tools/stat.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
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
|
-
import { getFileInfo } from '../lib/file-operations/
|
|
4
|
+
import { getFileInfo } from '../lib/file-operations/metadata.js';
|
|
5
|
+
import { formatBytes, joinLines } from '../config.js';
|
|
6
6
|
import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
export const GET_FILE_INFO_TOOL = {
|
|
9
9
|
name: 'stat',
|
|
10
10
|
title: 'Get File Info',
|
|
11
|
-
description: 'Get metadata
|
|
11
|
+
description: 'Get file/directory metadata: size, modified, permissions, mime, tokenEstimate. ' +
|
|
12
|
+
'Use `tokenEstimate` (size\u00f74) to pre-screen token cost before reading.',
|
|
12
13
|
inputSchema: GetFileInfoInputSchema,
|
|
13
14
|
outputSchema: GetFileInfoOutputSchema,
|
|
14
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { channel } from 'node:diagnostics_channel';
|
|
2
2
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
4
|
-
import { isRecord } from '../lib/
|
|
4
|
+
import { isRecord } from '../lib/utils.js';
|
|
5
5
|
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
6
6
|
function isExperimentalTaskRegistration(value) {
|
|
7
7
|
if (!value || typeof value !== 'object')
|
package/dist/tools/tree.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
|
-
import { formatTreeAscii, treeDirectory } from '../lib/file-operations/
|
|
4
|
+
import { formatTreeAscii, treeDirectory, } from '../lib/file-operations/metadata.js';
|
|
5
5
|
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
6
6
|
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const TREE_TOOL = {
|
|
9
9
|
name: 'tree',
|
|
10
10
|
title: 'Tree',
|
|
11
|
-
description: 'Render a directory tree (bounded recursion). ' +
|
|
12
|
-
'
|
|
13
|
-
'Note: maxDepth=0 returns only the root node with empty children array.',
|
|
11
|
+
description: 'Render a directory tree (bounded recursion). Returns ASCII tree + structured JSON. ' +
|
|
12
|
+
'`maxDepth=0` returns only the root node.',
|
|
14
13
|
inputSchema: TreeInputSchema,
|
|
15
14
|
outputSchema: TreeOutputSchema,
|
|
16
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/write-file.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
5
|
-
import { validatePathForWrite } from '../lib/
|
|
5
|
+
import { validatePathForWrite } from '../lib/paths.js';
|
|
6
6
|
import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -35,8 +35,11 @@
|
|
|
35
35
|
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
36
36
|
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
37
37
|
"lint": "eslint .",
|
|
38
|
+
"lint:tests": "eslint src/__tests__",
|
|
38
39
|
"lint:fix": "eslint . --fix",
|
|
39
40
|
"test": "node scripts/tasks.mjs test",
|
|
41
|
+
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
|
|
42
|
+
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
40
43
|
"knip": "knip",
|
|
41
44
|
"knip:fix": "knip --fix",
|
|
42
45
|
"inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js ${workspaceFolder}",
|
|
@@ -65,18 +68,19 @@
|
|
|
65
68
|
"commander": "^14.0.3",
|
|
66
69
|
"diff": "^8.0.3",
|
|
67
70
|
"ignore": "^7.0.5",
|
|
68
|
-
"re2": "^1.23.
|
|
71
|
+
"re2": "^1.23.3",
|
|
69
72
|
"safe-regex2": "^5.0.0",
|
|
70
73
|
"zod": "^4.3.6"
|
|
71
74
|
},
|
|
72
75
|
"devDependencies": {
|
|
73
76
|
"@eslint/js": "^10.0.1",
|
|
74
|
-
"eslint": "^10.0.2",
|
|
75
77
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
76
78
|
"@types/node": "^24",
|
|
79
|
+
"eslint": "^10.0.2",
|
|
77
80
|
"eslint-config-prettier": "^10.1.8",
|
|
78
|
-
"eslint-plugin-de-morgan": "^2.
|
|
79
|
-
"eslint-plugin-depend": "^1.
|
|
81
|
+
"eslint-plugin-de-morgan": "^2.1.1",
|
|
82
|
+
"eslint-plugin-depend": "^1.5.0",
|
|
83
|
+
"eslint-plugin-sonarjs": "^4.0.0",
|
|
80
84
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
81
85
|
"jscpd": "^4.0.8",
|
|
82
86
|
"knip": "^5.85.0",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { FileInfo, GetMultipleFileInfoResult } from '../../config.js';
|
|
2
|
-
interface FileInfoOptions {
|
|
3
|
-
includeMimeType?: boolean | undefined;
|
|
4
|
-
signal?: AbortSignal | undefined;
|
|
5
|
-
onProgress?: () => void;
|
|
6
|
-
}
|
|
7
|
-
export declare function getFileInfo(filePath: string, options?: FileInfoOptions): Promise<FileInfo>;
|
|
8
|
-
type GetMultipleFileInfoOptions = FileInfoOptions;
|
|
9
|
-
export declare function getMultipleFileInfo(paths: readonly string[], options?: GetMultipleFileInfoOptions): Promise<GetMultipleFileInfoResult>;
|
|
10
|
-
export {};
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { getMimeType, PARALLEL_CONCURRENCY } from '../constants.js';
|
|
4
|
-
import { isAbortError } from '../errors.js';
|
|
5
|
-
import { assertNotAborted, getFileType, isHidden, processInParallel, withAbort, } from '../fs-helpers.js';
|
|
6
|
-
import { assertAllowedFileAccess } from '../path-policy.js';
|
|
7
|
-
import { validateExistingPathDetailed } from '../path-validation.js';
|
|
8
|
-
import { applyIndexedErrors, applyIndexedValues } from './common.js';
|
|
9
|
-
const PERM_STRINGS = [
|
|
10
|
-
'---',
|
|
11
|
-
'--x',
|
|
12
|
-
'-w-',
|
|
13
|
-
'-wx',
|
|
14
|
-
'r--',
|
|
15
|
-
'r-x',
|
|
16
|
-
'rw-',
|
|
17
|
-
'rwx',
|
|
18
|
-
];
|
|
19
|
-
const UNKNOWN_PATH = '(unknown)';
|
|
20
|
-
function getPermissions(mode) {
|
|
21
|
-
const ownerIndex = (mode >> 6) & 0b111;
|
|
22
|
-
const groupIndex = (mode >> 3) & 0b111;
|
|
23
|
-
const otherIndex = mode & 0b111;
|
|
24
|
-
const owner = PERM_STRINGS[ownerIndex] ?? '---';
|
|
25
|
-
const group = PERM_STRINGS[groupIndex] ?? '---';
|
|
26
|
-
const other = PERM_STRINGS[otherIndex] ?? '---';
|
|
27
|
-
return `${owner}${group}${other}`;
|
|
28
|
-
}
|
|
29
|
-
function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget) {
|
|
30
|
-
const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;
|
|
31
|
-
return {
|
|
32
|
-
name,
|
|
33
|
-
path: requestedPath,
|
|
34
|
-
type: isSymlink ? 'symlink' : getFileType(stats),
|
|
35
|
-
size: stats.size,
|
|
36
|
-
...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
|
|
37
|
-
created: stats.birthtime,
|
|
38
|
-
modified: stats.mtime,
|
|
39
|
-
accessed: stats.atime,
|
|
40
|
-
permissions: getPermissions(stats.mode),
|
|
41
|
-
isHidden: isHidden(name),
|
|
42
|
-
...(mimeType !== undefined ? { mimeType } : {}),
|
|
43
|
-
...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
async function getSymlinkTarget(pathToRead, signal) {
|
|
47
|
-
assertNotAborted(signal);
|
|
48
|
-
try {
|
|
49
|
-
return await withAbort(fsp.readlink(pathToRead), signal);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
if (isAbortError(error))
|
|
53
|
-
throw error;
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
export async function getFileInfo(filePath, options = {}) {
|
|
58
|
-
const { signal } = options;
|
|
59
|
-
assertNotAborted(signal);
|
|
60
|
-
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
|
|
61
|
-
assertAllowedFileAccess(requestedPath, resolvedPath);
|
|
62
|
-
const name = path.basename(requestedPath);
|
|
63
|
-
const ext = path.extname(name).toLowerCase();
|
|
64
|
-
const includeMimeType = options.includeMimeType !== false;
|
|
65
|
-
const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
|
|
66
|
-
const symlinkTarget = isSymlink
|
|
67
|
-
? await getSymlinkTarget(requestedPath, signal)
|
|
68
|
-
: undefined;
|
|
69
|
-
const stats = await withAbort(fsp.stat(resolvedPath), signal);
|
|
70
|
-
return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
|
|
71
|
-
}
|
|
72
|
-
function buildEmptyResult() {
|
|
73
|
-
return {
|
|
74
|
-
results: [],
|
|
75
|
-
summary: { total: 0, succeeded: 0, failed: 0, totalSize: 0 },
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
async function processFileInfo(filePath, options) {
|
|
79
|
-
const info = await getFileInfo(filePath, options);
|
|
80
|
-
return { path: filePath, info };
|
|
81
|
-
}
|
|
82
|
-
function buildIndexedPathTasks(paths) {
|
|
83
|
-
const tasks = [];
|
|
84
|
-
for (let index = 0; index < paths.length; index += 1) {
|
|
85
|
-
const filePath = paths[index];
|
|
86
|
-
if (filePath === undefined)
|
|
87
|
-
continue;
|
|
88
|
-
tasks.push({ filePath, index });
|
|
89
|
-
}
|
|
90
|
-
return tasks;
|
|
91
|
-
}
|
|
92
|
-
async function readFileInfoInParallel(paths, options) {
|
|
93
|
-
return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) => {
|
|
94
|
-
const value = await processFileInfo(filePath, options);
|
|
95
|
-
options.onProgress?.();
|
|
96
|
-
return { index, value };
|
|
97
|
-
}, PARALLEL_CONCURRENCY, options.signal);
|
|
98
|
-
}
|
|
99
|
-
function calculateSummary(results) {
|
|
100
|
-
let succeeded = 0;
|
|
101
|
-
let failed = 0;
|
|
102
|
-
let totalSize = 0;
|
|
103
|
-
for (const result of results) {
|
|
104
|
-
if (result.info !== undefined) {
|
|
105
|
-
succeeded++;
|
|
106
|
-
totalSize += result.info.size;
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
failed++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
total: results.length,
|
|
114
|
-
succeeded,
|
|
115
|
-
failed,
|
|
116
|
-
totalSize,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
export async function getMultipleFileInfo(paths, options = {}) {
|
|
120
|
-
if (paths.length === 0)
|
|
121
|
-
return buildEmptyResult();
|
|
122
|
-
const output = new Array(paths.length);
|
|
123
|
-
for (let index = 0; index < paths.length; index += 1) {
|
|
124
|
-
output[index] = { path: paths[index] ?? UNKNOWN_PATH };
|
|
125
|
-
}
|
|
126
|
-
const { results, errors } = await readFileInfoInParallel(paths, options);
|
|
127
|
-
applyIndexedValues(output, results);
|
|
128
|
-
applyIndexedErrors({
|
|
129
|
-
output,
|
|
130
|
-
errors,
|
|
131
|
-
resolveIndex: (failureIndex) => failureIndex >= 0 && failureIndex < output.length
|
|
132
|
-
? failureIndex
|
|
133
|
-
: undefined,
|
|
134
|
-
buildValue: (resolvedIndex, error) => ({
|
|
135
|
-
path: paths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
136
|
-
error: error.message,
|
|
137
|
-
}),
|
|
138
|
-
});
|
|
139
|
-
return {
|
|
140
|
-
results: output,
|
|
141
|
-
summary: calculateSummary(output),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Ignore } from 'ignore';
|
|
2
|
-
export declare function loadRootGitignore(root: string, signal?: AbortSignal): Promise<Ignore | null>;
|
|
3
|
-
export declare function isIgnoredByGitignore(matcher: Ignore, root: string, absolutePath: string, options?: {
|
|
4
|
-
isDirectory?: boolean;
|
|
5
|
-
relativePath?: string;
|
|
6
|
-
}): boolean;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import ignore, {} from 'ignore';
|
|
4
|
-
import { isNodeError } from '../errors.js';
|
|
5
|
-
import { toPosixPath } from '../path-format.js';
|
|
6
|
-
function parseGitignoreLines(contents) {
|
|
7
|
-
const lines = [];
|
|
8
|
-
for (const line of contents.split(/\r?\n/u)) {
|
|
9
|
-
const trimmed = line.trim();
|
|
10
|
-
if (trimmed.length > 0) {
|
|
11
|
-
lines.push(trimmed);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return lines;
|
|
15
|
-
}
|
|
16
|
-
export async function loadRootGitignore(root, signal) {
|
|
17
|
-
const gitignorePath = path.join(root, '.gitignore');
|
|
18
|
-
let contents;
|
|
19
|
-
try {
|
|
20
|
-
contents = await fs.readFile(gitignorePath, {
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
signal,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
const matcher = ignore();
|
|
32
|
-
matcher.add(parseGitignoreLines(contents));
|
|
33
|
-
return matcher;
|
|
34
|
-
}
|
|
35
|
-
export function isIgnoredByGitignore(matcher, root, absolutePath, options = {}) {
|
|
36
|
-
let relative = options.relativePath;
|
|
37
|
-
relative ??= path.relative(root, absolutePath);
|
|
38
|
-
if (relative.length === 0)
|
|
39
|
-
return false;
|
|
40
|
-
const normalized = toPosixPath(relative);
|
|
41
|
-
if (options.isDirectory) {
|
|
42
|
-
return matcher.ignores(normalized.endsWith('/') ? normalized : `${normalized}/`);
|
|
43
|
-
}
|
|
44
|
-
return matcher.ignores(normalized);
|
|
45
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ListDirectoryResult } from '../../config.js';
|
|
2
|
-
interface ListDirectoryOptions {
|
|
3
|
-
includeHidden?: boolean;
|
|
4
|
-
excludePatterns?: readonly string[];
|
|
5
|
-
maxDepth?: number;
|
|
6
|
-
maxEntries?: number;
|
|
7
|
-
sortBy?: 'name' | 'size' | 'modified' | 'type';
|
|
8
|
-
includeSymlinkTargets?: boolean;
|
|
9
|
-
pattern?: string;
|
|
10
|
-
timeoutMs?: number;
|
|
11
|
-
signal?: AbortSignal;
|
|
12
|
-
}
|
|
13
|
-
export declare function listDirectory(dirPath: string, options?: ListDirectoryOptions): Promise<ListDirectoryResult>;
|
|
14
|
-
export {};
|