mcp-fs-shell-windows 0.2.7 → 0.2.18
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 +1 -1
- package/dist/append_files/handler.js +2 -2
- package/dist/checksum_files/handler.js +2 -2
- package/dist/checksum_files_verif/handler.js +2 -2
- package/dist/convert_encoding/handler.js +40 -0
- package/dist/convert_encoding/schema.js +19 -0
- package/dist/copy_files/handler.js +2 -2
- package/dist/count_lines/handler.js +3 -3
- package/dist/create_directories/handler.js +2 -2
- package/dist/create_link/handler.js +90 -0
- package/dist/create_link/schema.js +25 -0
- package/dist/delete_files/handler.js +205 -14
- package/dist/delete_files/schema.js +4 -0
- package/dist/delete_files_by_pattern/handler.js +7 -7
- package/dist/dir_diff/handler.js +155 -0
- package/dist/dir_diff/schema.js +7 -0
- package/dist/directory_tree/helpers.js +5 -5
- package/dist/edit_files/handler.js +2 -2
- package/dist/file_diff/handler.js +2 -2
- package/dist/file_info/handler.js +39 -3
- package/dist/fuzzy_find_files/handler.js +2 -2
- package/dist/helpers/encoding.js +154 -0
- package/dist/helpers/path.js +133 -6
- package/dist/launch_file/handler.js +6 -4
- package/dist/list_directory/handler.js +16 -9
- package/dist/move_files/handler.js +2 -2
- package/dist/patch_files/handler.js +2 -2
- package/dist/patch_files/helpers.js +24 -4
- package/dist/read_bytes/handler.js +43 -0
- package/dist/read_bytes/schema.js +6 -0
- package/dist/read_files/handler.js +18 -9
- package/dist/read_files/schema.js +37 -3
- package/dist/replace_regex/handler.js +105 -0
- package/dist/replace_regex/schema.js +23 -0
- package/dist/search_files/handler.js +2 -2
- package/dist/search_glob/handler.js +2 -2
- package/dist/search_regex/handler.js +63 -40
- package/dist/search_regex/schema.js +1 -0
- package/dist/server.js +183 -9
- package/dist/wait_for_file/handler.js +91 -0
- package/dist/wait_for_file/schema.js +7 -0
- package/dist/write_bytes/handler.js +49 -0
- package/dist/write_bytes/schema.js +7 -0
- package/dist/write_new_files/handler.js +6 -3
- package/dist/write_new_files/schema.js +1 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
|
+
import { stripLongPathAll } from "./helpers/path.js";
|
|
5
6
|
// Import all schemas
|
|
6
7
|
import { ReadFilesArgsSchema } from "./read_files/schema.js";
|
|
7
8
|
import { WriteNewFilesArgsSchema } from "./write_new_files/schema.js";
|
|
@@ -17,6 +18,7 @@ import { ListDirectoryArgsSchema } from "./list_directory/schema.js";
|
|
|
17
18
|
import { DirectoryTreeArgsSchema } from "./directory_tree/schema.js";
|
|
18
19
|
import { SearchFilesArgsSchema } from "./search_files/schema.js";
|
|
19
20
|
import { SearchRegexArgsSchema } from "./search_regex/schema.js";
|
|
21
|
+
import { ReplaceRegexArgsSchema } from "./replace_regex/schema.js";
|
|
20
22
|
import { SearchGlobArgsSchema } from "./search_glob/schema.js";
|
|
21
23
|
import { CountLinesArgsSchema } from "./count_lines/schema.js";
|
|
22
24
|
import { ChecksumFilesArgsSchema } from "./checksum_files/schema.js";
|
|
@@ -26,6 +28,12 @@ import { EditFilesArgsSchema } from "./edit_files/schema.js";
|
|
|
26
28
|
import { FuzzyFindFilesArgsSchema } from "./fuzzy_find_files/schema.js";
|
|
27
29
|
import { DeleteFilesByPatternArgsSchema } from "./delete_files_by_pattern/schema.js";
|
|
28
30
|
import { LaunchFileArgsSchema } from "./launch_file/schema.js";
|
|
31
|
+
import { ReadBytesArgsSchema } from "./read_bytes/schema.js";
|
|
32
|
+
import { WriteBytesArgsSchema } from "./write_bytes/schema.js";
|
|
33
|
+
import { DirDiffArgsSchema } from "./dir_diff/schema.js";
|
|
34
|
+
import { ConvertEncodingArgsSchema } from "./convert_encoding/schema.js";
|
|
35
|
+
import { WaitForFileArgsSchema } from "./wait_for_file/schema.js";
|
|
36
|
+
import { CreateLinkArgsSchema } from "./create_link/schema.js";
|
|
29
37
|
import { ShellRunArgsSchema } from "./shell/schema.js";
|
|
30
38
|
import { ShellTestArgsSchema } from "./shell/schema.js";
|
|
31
39
|
import { ShellStartArgsSchema } from "./shell/schema.js";
|
|
@@ -49,6 +57,7 @@ import { handleListDirectory } from "./list_directory/handler.js";
|
|
|
49
57
|
import { handleDirectoryTree } from "./directory_tree/handler.js";
|
|
50
58
|
import { handleSearchFiles } from "./search_files/handler.js";
|
|
51
59
|
import { handleSearchRegex } from "./search_regex/handler.js";
|
|
60
|
+
import { handleReplaceRegex } from "./replace_regex/handler.js";
|
|
52
61
|
import { handleSearchGlob } from "./search_glob/handler.js";
|
|
53
62
|
import { handleCountLines } from "./count_lines/handler.js";
|
|
54
63
|
import { handleChecksumFiles } from "./checksum_files/handler.js";
|
|
@@ -58,6 +67,12 @@ import { handleEditFiles } from "./edit_files/handler.js";
|
|
|
58
67
|
import { handleFuzzyFindFiles } from "./fuzzy_find_files/handler.js";
|
|
59
68
|
import { handleDeleteFilesByPattern } from "./delete_files_by_pattern/handler.js";
|
|
60
69
|
import { handleLaunchFile } from "./launch_file/handler.js";
|
|
70
|
+
import { handleReadBytes } from "./read_bytes/handler.js";
|
|
71
|
+
import { handleWriteBytes } from "./write_bytes/handler.js";
|
|
72
|
+
import { handleDirDiff } from "./dir_diff/handler.js";
|
|
73
|
+
import { handleConvertEncoding } from "./convert_encoding/handler.js";
|
|
74
|
+
import { handleWaitForFile } from "./wait_for_file/handler.js";
|
|
75
|
+
import { handleCreateLink } from "./create_link/handler.js";
|
|
61
76
|
import { handleShellRun } from "./shell/handler.js";
|
|
62
77
|
import { handleShellTest } from "./shell/handler.js";
|
|
63
78
|
import { handleShellStart } from "./shell/handler.js";
|
|
@@ -74,7 +89,7 @@ export class FilesystemServer {
|
|
|
74
89
|
this.allowedDirectories = allowedDirectories;
|
|
75
90
|
this.server = new Server({
|
|
76
91
|
name: "secure-filesystem-server",
|
|
77
|
-
version: "0.2.
|
|
92
|
+
version: "0.2.18",
|
|
78
93
|
}, {
|
|
79
94
|
capabilities: {
|
|
80
95
|
tools: {},
|
|
@@ -93,6 +108,8 @@ export class FilesystemServer {
|
|
|
93
108
|
"Each line is prefixed with its line number (format: \"1: line content\"). " +
|
|
94
109
|
"Each file's content is returned with its path as a reference. " +
|
|
95
110
|
"Use offset (1-indexed start line) and/or limit (max lines) to read a range of a large file. " +
|
|
111
|
+
"Each paths item may be a plain string or an object {path, encoding?, offset?, limit?}. " +
|
|
112
|
+
"encoding decodes the file as 'utf8' | 'utf16le' | 'utf16be' | 'cp1252' (a matching BOM is stripped); without encoding the file's encoding is auto-detected (BOM FF FE -> utf16le, FE FF -> utf16be, EF BB BF -> utf8, otherwise strict UTF-8 -> utf8, else cp1252). " +
|
|
96
113
|
"Only works within allowed directories.",
|
|
97
114
|
inputSchema: zodToJsonSchema(ReadFilesArgsSchema),
|
|
98
115
|
},
|
|
@@ -100,7 +117,7 @@ export class FilesystemServer {
|
|
|
100
117
|
name: "write_new_files",
|
|
101
118
|
description: "Create multiple new files in a single operation. Works on a single file too. " +
|
|
102
119
|
"Fails if a file already exists unless its overwrite flag is true. " +
|
|
103
|
-
"Handles text content with UTF-8 encoding, or base64 content for binary files (encoding: \"base64\"). " +
|
|
120
|
+
"Handles text content with UTF-8 encoding (or a per-file textEncoding of 'utf8' | 'utf16le' | 'utf16be' | 'cp1252'; utf16le/utf16be write a BOM), or base64 content for binary files (encoding: \"base64\", which writes raw bytes and wins over textEncoding). " +
|
|
104
121
|
"Parent directories are created as needed. " +
|
|
105
122
|
"Partial failures won't stop the entire operation. " +
|
|
106
123
|
"Only works within allowed directories.",
|
|
@@ -119,8 +136,10 @@ export class FilesystemServer {
|
|
|
119
136
|
name: "delete_files",
|
|
120
137
|
description: "Delete multiple files or directories in a single operation. Works on a single file too. " +
|
|
121
138
|
"Set recursive flag to true to delete directories with contents. " +
|
|
139
|
+
"Reparse points (junctions/symlinks) are always removed as links without following them - the link target is never touched, including nested links inside recursively deleted directories. " +
|
|
140
|
+
"Set toRecycleBin to true (Windows only) to send files and directories to the Recycle Bin instead of deleting permanently; reparse points are still removed as links (the bin cannot hold links), and any path that cannot be recycled is reported as an error and never permanently deleted as a fallback. " +
|
|
122
141
|
"Partial failures won't stop the entire operation. " +
|
|
123
|
-
"Use with caution as operation is permanent. " +
|
|
142
|
+
"Use with caution as the default operation is permanent. " +
|
|
124
143
|
"Only works within allowed directories.",
|
|
125
144
|
inputSchema: zodToJsonSchema(DeleteFilesArgsSchema),
|
|
126
145
|
},
|
|
@@ -153,7 +172,7 @@ export class FilesystemServer {
|
|
|
153
172
|
"Each patch has an action: replace (default, replace lines startLine-endLine with newText), insertBefore (insert newText before startLine; startLine = total lines + 1 appends to the end), or delete (remove lines startLine-endLine). " +
|
|
154
173
|
"All patches reference the original line numbers. " +
|
|
155
174
|
"Shows git-style diffs for all changes (dryRun: true previews without writing). " +
|
|
156
|
-
"
|
|
175
|
+
"Preserves the file's dominant line ending (CRLF or LF). " +
|
|
157
176
|
"Partial failures won't stop the entire operation. " +
|
|
158
177
|
"Only works within allowed directories.",
|
|
159
178
|
inputSchema: zodToJsonSchema(PatchFilesArgsSchema),
|
|
@@ -169,10 +188,11 @@ export class FilesystemServer {
|
|
|
169
188
|
},
|
|
170
189
|
{
|
|
171
190
|
name: "list_directory",
|
|
172
|
-
description: "List directory contents with [FILE] or [
|
|
191
|
+
description: "List directory contents with [FILE], [DIR], or [LINK] prefixes (single level). " +
|
|
173
192
|
"Entries are sorted by name and paginated with offset/limit (default limit 100). " +
|
|
174
193
|
"File sizes are shown by default (includeSizes: false to omit). " +
|
|
175
194
|
"Filter with type (files/directories/all) and ignore glob patterns. " +
|
|
195
|
+
"Reparse points (junctions/symlinks) show as [LINK] and are excluded from the files and directories filters (only listed with type 'all'); links are not stat'ed for size. " +
|
|
176
196
|
"Use directory_tree for recursive views. " +
|
|
177
197
|
"Only works within allowed directories.",
|
|
178
198
|
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
|
|
@@ -182,6 +202,8 @@ export class FilesystemServer {
|
|
|
182
202
|
description: "Get a recursive tree view of files and directories as a JSON structure. " +
|
|
183
203
|
"Returns JSON with name, type, and children properties. " +
|
|
184
204
|
"Supports excluding files/directories using glob patterns. " +
|
|
205
|
+
"Reparse points (junctions/symlinks) appear as type 'link' with no children and are never recursed into. " +
|
|
206
|
+
"Empty directories are included. " +
|
|
185
207
|
"Only works within allowed directories.",
|
|
186
208
|
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema),
|
|
187
209
|
},
|
|
@@ -200,9 +222,23 @@ export class FilesystemServer {
|
|
|
200
222
|
"path can be a directory (recursively searched) or a single file. " +
|
|
201
223
|
"Supports filtering files by patterns. " +
|
|
202
224
|
"Returns matching lines with line numbers, paginated with offset/maxResults (default 100). " +
|
|
225
|
+
"Each occurrence of the pattern is counted (a line matching 4 times counts as 4), and the response " +
|
|
226
|
+
"always ends with the total match count plus per-file counts. " +
|
|
227
|
+
"Set countOnly=true to return only per-file match counts and the total, with no matching line text. " +
|
|
203
228
|
"Only searches within allowed directories.",
|
|
204
229
|
inputSchema: zodToJsonSchema(SearchRegexArgsSchema),
|
|
205
230
|
},
|
|
231
|
+
{
|
|
232
|
+
name: "replace_regex",
|
|
233
|
+
description: "In-place regular expression replacement across the given FILES. " +
|
|
234
|
+
"paths may list one or many files (directories are NOT accepted - a directory path is an error and nothing is written). " +
|
|
235
|
+
"The 'g' (global) flag is always applied so every match is replaced; additional flags such as 'i' are allowed. " +
|
|
236
|
+
"dryRun defaults to TRUE: it returns per-file match counts and a unified diff preview of the WOULD-BE change without writing anything. " +
|
|
237
|
+
"With dryRun=false the replacements are applied in place, preserving each file's dominant line ending (a pure-CRLF file stays fully CRLF), and per-file match counts plus a total are returned. " +
|
|
238
|
+
"Any missing path or an invalid regex produces a clear error and writes nothing. " +
|
|
239
|
+
"Only works within allowed directories.",
|
|
240
|
+
inputSchema: zodToJsonSchema(ReplaceRegexArgsSchema),
|
|
241
|
+
},
|
|
206
242
|
{
|
|
207
243
|
name: "search_glob",
|
|
208
244
|
description: "Find files using glob patterns. " +
|
|
@@ -240,7 +276,7 @@ export class FilesystemServer {
|
|
|
240
276
|
{
|
|
241
277
|
name: "get_file_info",
|
|
242
278
|
description: "Get detailed file/directory metadata. " +
|
|
243
|
-
"Returns size, creation time, modified time, access time, type, and
|
|
279
|
+
"Returns size, creation time, modified time, access time, type, permissions, isReparsePoint, and (on Windows) NTFS attributes. " +
|
|
244
280
|
"Only works within allowed directories.",
|
|
245
281
|
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
|
|
246
282
|
},
|
|
@@ -281,6 +317,19 @@ export class FilesystemServer {
|
|
|
281
317
|
"Only works within allowed directories.",
|
|
282
318
|
inputSchema: zodToJsonSchema(DeleteFilesByPatternArgsSchema),
|
|
283
319
|
},
|
|
320
|
+
{
|
|
321
|
+
name: "create_link",
|
|
322
|
+
description: "Create a link object at linkPath pointing at target (Windows hardlink, directory junction, or file/directory symlink). " +
|
|
323
|
+
"type: 'auto' (default) | 'symlink' | 'junction' | 'hardlink'. " +
|
|
324
|
+
"target must exist (lstat-checked; clear error otherwise); linkPath must NOT already exist (clear error); linkPath's parent directory must already exist (clear error - it is NOT auto-created). " +
|
|
325
|
+
"'hardlink' creates a second name for an existing file (target must be a file; same volume - cross-volume errors surface naturally). " +
|
|
326
|
+
"'junction' creates a directory junction (Windows only; target must be a directory; clear error on non-Windows). " +
|
|
327
|
+
"'symlink'/'auto' create a real symlink based on the target's type: directory target -> directory symlink, file target -> file symlink; 'auto' never creates a junction (junctions must be explicit). " +
|
|
328
|
+
"Both paths go through the standard path validation and must be within allowed directories. " +
|
|
329
|
+
"Response is short text containing the user-supplied linkPath and target (never any \\\\?\\-prefixed form) plus the actual link type created. " +
|
|
330
|
+
"Only works within allowed directories.",
|
|
331
|
+
inputSchema: zodToJsonSchema(CreateLinkArgsSchema),
|
|
332
|
+
},
|
|
284
333
|
{
|
|
285
334
|
name: "transfer_files",
|
|
286
335
|
description: "Move or rename multiple files and directories in a single operation. Works on a single file too. " +
|
|
@@ -364,6 +413,60 @@ export class FilesystemServer {
|
|
|
364
413
|
"Errors if the path does not exist, is outside the allowed directories, the URL scheme is not http/https, or the opener fails to spawn.",
|
|
365
414
|
inputSchema: zodToJsonSchema(LaunchFileArgsSchema),
|
|
366
415
|
},
|
|
416
|
+
{
|
|
417
|
+
name: "read_bytes",
|
|
418
|
+
description: "Read raw bytes from a file at a given (offset, length). " +
|
|
419
|
+
"All arguments are integers: offset >= 0, length >= 1. " +
|
|
420
|
+
"An offset at or beyond the file's end (offset >= size) is an error ('offset beyond EOF'); if offset+length exceeds the file size, the read is clamped to EOF. " +
|
|
421
|
+
"Returns JSON text: {path, offset, length, actualLength, size, dataBase64} where dataBase64 is base64 of the bytes actually read. " +
|
|
422
|
+
"Works on binary files of any size (e.g. GGUFs, blobs). " +
|
|
423
|
+
"Only works within allowed directories.",
|
|
424
|
+
inputSchema: zodToJsonSchema(ReadBytesArgsSchema),
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "write_bytes",
|
|
428
|
+
description: "Write raw bytes to an existing file at a given offset (base64 input). " +
|
|
429
|
+
"The file must already exist (no implicit create); invalid base64 is an error. " +
|
|
430
|
+
"With the default extend=false, offset+bytes.length must not exceed the file size (else an error) and the bytes overwrite in place. " +
|
|
431
|
+
"With extend=true the file is grown when the write ends past EOF, zero-filling any gap between the old size and the offset. " +
|
|
432
|
+
"Only works within allowed directories.",
|
|
433
|
+
inputSchema: zodToJsonSchema(WriteBytesArgsSchema),
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "dir_diff",
|
|
437
|
+
description: "Recursively compare the contents of two directory trees (files only). " +
|
|
438
|
+
"Each relative path is classified as added (exists only in dirB), removed (exists only in dirA), " +
|
|
439
|
+
"changed (present in both, content differs - decided by size first, then sha256), or identical. " +
|
|
440
|
+
"For changed files where BOTH sides look like text (size <= 1 MiB and no NUL byte in the first 8 KiB), " +
|
|
441
|
+
"a unified diff is included (diff? field, same helper as file_diff/content_diff). " +
|
|
442
|
+
"ignore: array of glob patterns matched against each file's relative path (minimatch, same semantics as list_directory's ignore param); ignored paths are excluded from all lists and counts. " +
|
|
443
|
+
"maxFiles: cap on total files compared (union of both trees); if the union exceeds it, the comparison stops and truncated=true (counts reflect what was compared). " +
|
|
444
|
+
"Returns JSON text: {added:[{path,size}], removed:[{path,size}], changed:[{path,sizeA,sizeB,shaA,shaB,diff?}], counts:{added,removed,changed,identical,compared}, truncated}. " +
|
|
445
|
+
"Only works within allowed directories.",
|
|
446
|
+
inputSchema: zodToJsonSchema(DirDiffArgsSchema),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "convert_encoding",
|
|
450
|
+
description: "Convert a file's text encoding to a new file. " +
|
|
451
|
+
"Reads the source file (encoding auto-detected from its bytes unless `from` is given: BOM FF FE -> utf16le, FE FF -> utf16be, EF BB BF -> utf8, otherwise strict UTF-8 -> utf8, else cp1252), decodes it, and writes outPath encoded as `to` ('utf8' | 'utf16le' | 'utf16be' | 'cp1252'; utf16le/utf16be output includes a BOM, utf8/cp1252 do not). " +
|
|
452
|
+
"outPath must differ from path and must not already exist unless overwrite=true. " +
|
|
453
|
+
"Returns JSON text: {path, outPath, from (detected or given), to, bytesIn, bytesOut}. " +
|
|
454
|
+
"Only works within allowed directories.",
|
|
455
|
+
inputSchema: zodToJsonSchema(ConvertEncodingArgsSchema),
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: "wait_for_file",
|
|
459
|
+
description: "Wait until at least one file matching a glob appears under a base directory (polling watcher). " +
|
|
460
|
+
"path = base directory (must be inside an allowed root); the glob is matched against each file's path relative to path " +
|
|
461
|
+
"(same glob machinery as search_glob: minimatch with dot:true; on Windows backslashes in the pattern are treated as path separators). " +
|
|
462
|
+
"timeoutMs defaults to 30000 (max 300000); intervalMs defaults to 500 (min 100). " +
|
|
463
|
+
"Polls with an async sleep between scans so the server thread is never blocked for longer than one interval; the tool call itself may legitimately take up to timeoutMs. " +
|
|
464
|
+
"As soon as at least one file matches it returns JSON text: {matched: [absolute paths], count, waitedMs, timedOut: false}; " +
|
|
465
|
+
"on timeout it returns the same shape with timedOut: true (matched may be empty). " +
|
|
466
|
+
"Only files are matched (directories are searched through, not matched). " +
|
|
467
|
+
"Only works within allowed directories.",
|
|
468
|
+
inputSchema: zodToJsonSchema(WaitForFileArgsSchema),
|
|
469
|
+
},
|
|
367
470
|
],
|
|
368
471
|
};
|
|
369
472
|
});
|
|
@@ -406,7 +509,7 @@ export class FilesystemServer {
|
|
|
406
509
|
if (!parsed.success) {
|
|
407
510
|
throw new Error(`Invalid arguments for delete_files: ${parsed.error}`);
|
|
408
511
|
}
|
|
409
|
-
const result = await handleDeleteFiles(parsed.data.paths, parsed.data.recursive, this.allowedDirectories);
|
|
512
|
+
const result = await handleDeleteFiles(parsed.data.paths, parsed.data.recursive, this.allowedDirectories, parsed.data.toRecycleBin);
|
|
410
513
|
return {
|
|
411
514
|
content: [{ type: "text", text: result }],
|
|
412
515
|
};
|
|
@@ -488,6 +591,16 @@ export class FilesystemServer {
|
|
|
488
591
|
}],
|
|
489
592
|
};
|
|
490
593
|
}
|
|
594
|
+
case "create_link": {
|
|
595
|
+
const parsed = CreateLinkArgsSchema.safeParse(args);
|
|
596
|
+
if (!parsed.success) {
|
|
597
|
+
throw new Error(`Invalid arguments for create_link: ${parsed.error}`);
|
|
598
|
+
}
|
|
599
|
+
const result = await handleCreateLink(parsed.data.target, parsed.data.linkPath, parsed.data.type, this.allowedDirectories);
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: result }],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
491
604
|
case "transfer_files": {
|
|
492
605
|
const parsed = MoveFilesArgsSchema.safeParse(args);
|
|
493
606
|
if (!parsed.success) {
|
|
@@ -513,7 +626,17 @@ export class FilesystemServer {
|
|
|
513
626
|
if (!parsed.success) {
|
|
514
627
|
throw new Error(`Invalid arguments for search_regex: ${parsed.error}`);
|
|
515
628
|
}
|
|
516
|
-
const result = await handleSearchRegex(parsed.data.path, parsed.data.pattern, parsed.data.filePatterns, parsed.data.excludePatterns, parsed.data.maxResults, parsed.data.caseSensitive, parsed.data.offset, this.allowedDirectories);
|
|
629
|
+
const result = await handleSearchRegex(parsed.data.path, parsed.data.pattern, parsed.data.filePatterns, parsed.data.excludePatterns, parsed.data.maxResults, parsed.data.caseSensitive, parsed.data.offset, this.allowedDirectories, parsed.data.countOnly);
|
|
630
|
+
return {
|
|
631
|
+
content: [{ type: "text", text: result }],
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
case "replace_regex": {
|
|
635
|
+
const parsed = ReplaceRegexArgsSchema.safeParse(args);
|
|
636
|
+
if (!parsed.success) {
|
|
637
|
+
throw new Error(`Invalid arguments for replace_regex: ${parsed.error}`);
|
|
638
|
+
}
|
|
639
|
+
const result = await handleReplaceRegex(parsed.data.paths, parsed.data.pattern, parsed.data.replacement, parsed.data.flags, parsed.data.dryRun, this.allowedDirectories);
|
|
517
640
|
return {
|
|
518
641
|
content: [{ type: "text", text: result }],
|
|
519
642
|
};
|
|
@@ -696,12 +819,63 @@ export class FilesystemServer {
|
|
|
696
819
|
content: [{ type: "text", text: result }],
|
|
697
820
|
};
|
|
698
821
|
}
|
|
822
|
+
case "read_bytes": {
|
|
823
|
+
const parsed = ReadBytesArgsSchema.safeParse(args);
|
|
824
|
+
if (!parsed.success) {
|
|
825
|
+
throw new Error(`Invalid arguments for read_bytes: ${parsed.error}`);
|
|
826
|
+
}
|
|
827
|
+
const result = await handleReadBytes(parsed.data.path, parsed.data.offset, parsed.data.length, this.allowedDirectories);
|
|
828
|
+
return {
|
|
829
|
+
content: [{ type: "text", text: result }],
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
case "write_bytes": {
|
|
833
|
+
const parsed = WriteBytesArgsSchema.safeParse(args);
|
|
834
|
+
if (!parsed.success) {
|
|
835
|
+
throw new Error(`Invalid arguments for write_bytes: ${parsed.error}`);
|
|
836
|
+
}
|
|
837
|
+
const result = await handleWriteBytes(parsed.data.path, parsed.data.offset, parsed.data.dataBase64, parsed.data.extend, this.allowedDirectories);
|
|
838
|
+
return {
|
|
839
|
+
content: [{ type: "text", text: result }],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
case "dir_diff": {
|
|
843
|
+
const parsed = DirDiffArgsSchema.safeParse(args);
|
|
844
|
+
if (!parsed.success) {
|
|
845
|
+
throw new Error(`Invalid arguments for dir_diff: ${parsed.error}`);
|
|
846
|
+
}
|
|
847
|
+
const result = await handleDirDiff(parsed.data.dirA, parsed.data.dirB, parsed.data.ignore, parsed.data.maxFiles, this.allowedDirectories);
|
|
848
|
+
return {
|
|
849
|
+
content: [{ type: "text", text: result }],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
case "convert_encoding": {
|
|
853
|
+
const parsed = ConvertEncodingArgsSchema.safeParse(args);
|
|
854
|
+
if (!parsed.success) {
|
|
855
|
+
throw new Error(`Invalid arguments for convert_encoding: ${parsed.error}`);
|
|
856
|
+
}
|
|
857
|
+
const result = await handleConvertEncoding(parsed.data.path, parsed.data.to, parsed.data.from, parsed.data.outPath, parsed.data.overwrite, this.allowedDirectories);
|
|
858
|
+
return {
|
|
859
|
+
content: [{ type: "text", text: result }],
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
case "wait_for_file": {
|
|
863
|
+
const parsed = WaitForFileArgsSchema.safeParse(args);
|
|
864
|
+
if (!parsed.success) {
|
|
865
|
+
throw new Error(`Invalid arguments for wait_for_file: ${parsed.error}`);
|
|
866
|
+
}
|
|
867
|
+
const result = await handleWaitForFile(parsed.data.path, parsed.data.glob, parsed.data.timeoutMs, parsed.data.intervalMs, this.allowedDirectories);
|
|
868
|
+
return {
|
|
869
|
+
content: [{ type: "text", text: result }],
|
|
870
|
+
};
|
|
871
|
+
}
|
|
699
872
|
default:
|
|
700
873
|
throw new Error(`Unknown tool: ${name}`);
|
|
701
874
|
}
|
|
702
875
|
}
|
|
703
876
|
catch (error) {
|
|
704
|
-
|
|
877
|
+
// Never surface the internal \\?\ long-path prefix in error text.
|
|
878
|
+
const errorMessage = stripLongPathAll(error instanceof Error ? error.message : String(error));
|
|
705
879
|
return {
|
|
706
880
|
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
707
881
|
isError: true,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { minimatch } from "minimatch";
|
|
4
|
+
import { validatePath, stripLongPath } from "../helpers/path.js";
|
|
5
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
// Recursively scan a directory tree and collect the files whose path relative
|
|
7
|
+
// to `root` matches `pattern` (minimatch, dot:true - same machinery as search_glob).
|
|
8
|
+
// Unreadable directories and entries that vanish mid-scan are skipped.
|
|
9
|
+
// Only files are matched; directories are searched through.
|
|
10
|
+
async function scanForMatches(root, pattern) {
|
|
11
|
+
const results = [];
|
|
12
|
+
async function walk(dir) {
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return; // skip directories we can't read
|
|
19
|
+
}
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
try {
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
await walk(fullPath);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!entry.isFile()) {
|
|
28
|
+
continue; // skip symlinks/devices/etc. - only real files are matched
|
|
29
|
+
}
|
|
30
|
+
// Normalize separators to '/' so minimatch matching is platform-consistent.
|
|
31
|
+
const relativePath = path.relative(root, fullPath).split(path.sep).join("/");
|
|
32
|
+
if (minimatch(relativePath, pattern, { dot: true })) {
|
|
33
|
+
results.push(stripLongPath(fullPath));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Skip anything that errors (e.g. raced with a deletion)
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await walk(root);
|
|
43
|
+
results.sort();
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
export async function handleWaitForFile(baseDir, glob, timeoutMs, intervalMs, allowedDirectories) {
|
|
47
|
+
const validRoot = await validatePath(baseDir, allowedDirectories);
|
|
48
|
+
let stats;
|
|
49
|
+
try {
|
|
50
|
+
stats = await fs.stat(validRoot);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const code = error.code;
|
|
54
|
+
if (code === "ENOENT") {
|
|
55
|
+
throw new Error(`Base directory does not exist: ${baseDir}`);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (!stats.isDirectory()) {
|
|
60
|
+
throw new Error(`Not a directory: ${baseDir}`);
|
|
61
|
+
}
|
|
62
|
+
// On Windows, treat backslashes in the pattern as path separators so both
|
|
63
|
+
// 'a/b/*.txt' and 'a\b\*.txt' work (minimatch otherwise parses '\\' as an escape).
|
|
64
|
+
const pattern = process.platform === "win32" ? glob.split("\\").join("/") : glob;
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
const deadline = startedAt + timeoutMs;
|
|
67
|
+
let matched = [];
|
|
68
|
+
for (;;) {
|
|
69
|
+
matched = await scanForMatches(validRoot, pattern);
|
|
70
|
+
if (matched.length > 0) {
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
matched,
|
|
73
|
+
count: matched.length,
|
|
74
|
+
waitedMs: Date.now() - startedAt,
|
|
75
|
+
timedOut: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const remaining = deadline - Date.now();
|
|
79
|
+
if (remaining <= 0) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
// Async sleep keeps the server event loop responsive between polls.
|
|
83
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
matched,
|
|
87
|
+
count: matched.length,
|
|
88
|
+
waitedMs: Date.now() - startedAt,
|
|
89
|
+
timedOut: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const WaitForFileArgsSchema = z.object({
|
|
3
|
+
path: z.string().describe("Base directory to watch (must be inside an allowed root). The glob is matched against each file's path relative to this directory."),
|
|
4
|
+
glob: z.string().describe("Glob pattern matched against each file's path relative to path (same glob machinery as search_glob: minimatch with dot:true; on Windows backslashes in the pattern are treated as path separators, so 'a\\b\\*.txt' and 'a/b/*.txt' both work)."),
|
|
5
|
+
timeoutMs: z.number().int().min(0).max(300000).optional().default(30000).describe("Maximum time to wait, in milliseconds (default 30000, max 300000)."),
|
|
6
|
+
intervalMs: z.number().int().min(100).max(300000).optional().default(500).describe("Poll interval, in milliseconds (default 500, min 100)."),
|
|
7
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { validatePath } from "../helpers/path.js";
|
|
3
|
+
// Strict base64: groups of 4, with valid padding at the end.
|
|
4
|
+
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
|
|
5
|
+
export async function handleWriteBytes(filePath, offset, dataBase64, extend, allowedDirectories) {
|
|
6
|
+
const validPath = await validatePath(filePath, allowedDirectories);
|
|
7
|
+
let stats;
|
|
8
|
+
try {
|
|
9
|
+
stats = await fs.stat(validPath);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const code = error.code;
|
|
13
|
+
if (code === "ENOENT") {
|
|
14
|
+
throw new Error(`File does not exist (write_bytes requires an existing file, no implicit create): ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
if (!stats.isFile()) {
|
|
19
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
const size = stats.size;
|
|
22
|
+
if (!BASE64_RE.test(dataBase64)) {
|
|
23
|
+
throw new Error(`Invalid base64 in dataBase64: ${JSON.stringify(dataBase64.slice(0, 80))}`);
|
|
24
|
+
}
|
|
25
|
+
const data = Buffer.from(dataBase64, "base64");
|
|
26
|
+
const end = offset + data.length;
|
|
27
|
+
const handle = await fs.open(validPath, "r+");
|
|
28
|
+
try {
|
|
29
|
+
if (end > size) {
|
|
30
|
+
if (!extend) {
|
|
31
|
+
throw new Error(`Write would extend file past EOF: offset ${offset} + ${data.length} bytes = ${end} > file size ${size}; use extend=true to grow the file`);
|
|
32
|
+
}
|
|
33
|
+
await handle.truncate(end);
|
|
34
|
+
}
|
|
35
|
+
const { bytesWritten } = await handle.write(data, 0, data.length, offset);
|
|
36
|
+
if (bytesWritten !== data.length) {
|
|
37
|
+
throw new Error(`Short write: expected ${data.length} bytes at offset ${offset}, wrote ${bytesWritten}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await handle.close();
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
path: filePath,
|
|
45
|
+
offset,
|
|
46
|
+
bytesWritten: data.length,
|
|
47
|
+
size: Math.max(size, end),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const WriteBytesArgsSchema = z.object({
|
|
3
|
+
path: z.string().describe("Path to the file to write (must already exist - no implicit create)"),
|
|
4
|
+
offset: z.number().int().min(0).describe("Byte offset at which to write (0-indexed)"),
|
|
5
|
+
dataBase64: z.string().describe("Base64-encoded bytes to write. Must be valid base64 (strict)."),
|
|
6
|
+
extend: z.boolean().optional().default(false).describe("If true, grow the file when the write ends past EOF, zero-filling any gap between the old size and the offset. Default: false (write past EOF is an error)."),
|
|
7
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
|
|
3
|
+
import { validatePath, ensureDirectoryExists, stripLongPathAll } from "../helpers/path.js";
|
|
4
|
+
import { encodeText } from "../helpers/encoding.js";
|
|
4
5
|
export async function handleWriteNewFiles(files, allowedDirectories) {
|
|
5
6
|
const results = [];
|
|
6
7
|
const errors = [];
|
|
@@ -30,12 +31,14 @@ export async function handleWriteNewFiles(files, allowedDirectories) {
|
|
|
30
31
|
await fs.writeFile(validPath, Buffer.from(file.content, "base64"));
|
|
31
32
|
}
|
|
32
33
|
else {
|
|
33
|
-
|
|
34
|
+
// textEncoding defaults to utf8 (byte-identical to the old behavior);
|
|
35
|
+
// base64 above already handled the raw-bytes case.
|
|
36
|
+
await fs.writeFile(validPath, encodeText(file.content, file.textEncoding ?? "utf8"));
|
|
34
37
|
}
|
|
35
38
|
results.push(`Successfully wrote to ${file.path}${file.overwrite ? " (overwritten)" : ""}`);
|
|
36
39
|
}
|
|
37
40
|
catch (error) {
|
|
38
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
41
|
+
const errorMessage = stripLongPathAll(error instanceof Error ? error.message : String(error));
|
|
39
42
|
errors.push(`Failed to write file ${file.path}: ${errorMessage}`);
|
|
40
43
|
}
|
|
41
44
|
}));
|
|
@@ -5,5 +5,6 @@ export const WriteNewFilesArgsSchema = z.object({
|
|
|
5
5
|
content: z.string().describe("Content to write to the file (UTF-8 text, or base64 when encoding is \"base64\")"),
|
|
6
6
|
overwrite: z.boolean().optional().default(false).describe("Replace the file if it already exists. Default: false (fail if it exists)."),
|
|
7
7
|
encoding: z.enum(["utf8", "base64"]).optional().default("utf8").describe("Content encoding. Use \"base64\" for binary files."),
|
|
8
|
+
textEncoding: z.enum(["utf8", "utf16le", "utf16be", "cp1252"]).optional().describe("Text encoding for text content: 'utf8' (default), 'utf16le' or 'utf16be' (a BOM is written), or 'cp1252' (no BOM). Ignored when encoding is \"base64\" (raw bytes win)."),
|
|
8
9
|
})).describe("Array of files to write"),
|
|
9
10
|
});
|
package/package.json
CHANGED