@j0hanz/filesystem-mcp 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 ? decodeCursor(args.cursor) : 0;
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
- ? encodeCursor(cursorOffset + displayEntries.length)
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 first = path.basename(args.paths[0] ?? '');
138
- const extraPaths = args.paths.length > 1
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 ? decodeCursor(args.cursor) : 0;
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
- ? encodeCursor(cursorOffset + displayResults.length)
44
+ ? encodeOffsetCursor(cursorOffset + displayResults.length)
63
45
  : undefined;
64
46
  const relativeResults = [];
65
47
  for (const entry of displayResults) {
@@ -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;
@@ -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
+ }
@@ -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 first = path.basename(args.paths[0] ?? '');
78
- const extraPaths = args.paths.length > 1
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
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",