mcp-fs-shell-windows 0.2.4

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.
Files changed (59) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +147 -0
  3. package/dist/append_files/handler.js +32 -0
  4. package/dist/append_files/schema.js +7 -0
  5. package/dist/checksum_files/handler.js +33 -0
  6. package/dist/checksum_files/schema.js +5 -0
  7. package/dist/checksum_files_verif/handler.js +66 -0
  8. package/dist/checksum_files_verif/helpers.js +6 -0
  9. package/dist/checksum_files_verif/schema.js +8 -0
  10. package/dist/content_diff/handler.js +11 -0
  11. package/dist/content_diff/schema.js +7 -0
  12. package/dist/copy_files/handler.js +43 -0
  13. package/dist/copy_files/helpers.js +24 -0
  14. package/dist/copy_files/schema.js +7 -0
  15. package/dist/count_lines/handler.js +144 -0
  16. package/dist/count_lines/schema.js +9 -0
  17. package/dist/create_directories/handler.js +29 -0
  18. package/dist/create_directories/schema.js +4 -0
  19. package/dist/delete_files/handler.js +42 -0
  20. package/dist/delete_files/schema.js +5 -0
  21. package/dist/delete_files_by_pattern/handler.js +46 -0
  22. package/dist/delete_files_by_pattern/schema.js +6 -0
  23. package/dist/directory_tree/handler.js +7 -0
  24. package/dist/directory_tree/helpers.js +47 -0
  25. package/dist/directory_tree/schema.js +5 -0
  26. package/dist/edit_files/handler.js +62 -0
  27. package/dist/edit_files/schema.js +11 -0
  28. package/dist/file_diff/handler.js +28 -0
  29. package/dist/file_diff/schema.js +5 -0
  30. package/dist/file_info/handler.js +30 -0
  31. package/dist/file_info/schema.js +4 -0
  32. package/dist/fuzzy_find_files/handler.js +74 -0
  33. package/dist/fuzzy_find_files/schema.js +8 -0
  34. package/dist/helpers/checksum.js +10 -0
  35. package/dist/helpers/diff.js +10 -0
  36. package/dist/helpers/path.js +87 -0
  37. package/dist/index.js +36 -0
  38. package/dist/list_directory/handler.js +42 -0
  39. package/dist/list_directory/schema.js +9 -0
  40. package/dist/move_files/handler.js +68 -0
  41. package/dist/move_files/schema.js +8 -0
  42. package/dist/patch_files/handler.js +34 -0
  43. package/dist/patch_files/helpers.js +103 -0
  44. package/dist/patch_files/schema.js +18 -0
  45. package/dist/read_files/handler.js +36 -0
  46. package/dist/read_files/schema.js +6 -0
  47. package/dist/search_files/handler.js +15 -0
  48. package/dist/search_files/helpers.js +71 -0
  49. package/dist/search_files/schema.js +8 -0
  50. package/dist/search_glob/handler.js +77 -0
  51. package/dist/search_glob/schema.js +8 -0
  52. package/dist/search_regex/handler.js +159 -0
  53. package/dist/search_regex/schema.js +10 -0
  54. package/dist/server.js +698 -0
  55. package/dist/shell/handler.js +687 -0
  56. package/dist/shell/schema.js +69 -0
  57. package/dist/write_new_files/handler.js +55 -0
  58. package/dist/write_new_files/schema.js +9 -0
  59. package/package.json +37 -0
@@ -0,0 +1,42 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ export async function handleDeleteFiles(paths, recursive, allowedDirectories) {
4
+ const results = [];
5
+ const errors = [];
6
+ await Promise.all(paths.map(async (filePath) => {
7
+ try {
8
+ // Validate path is within allowed directories
9
+ const validPath = await validatePath(filePath, allowedDirectories);
10
+ // Get file stats to determine if it's a file or directory
11
+ const stats = await fs.stat(validPath);
12
+ if (stats.isDirectory()) {
13
+ if (recursive) {
14
+ await fs.rm(validPath, { recursive: true, force: true });
15
+ results.push(`Successfully deleted directory: ${filePath}`);
16
+ }
17
+ else {
18
+ throw new Error("Cannot delete directory without recursive flag");
19
+ }
20
+ }
21
+ else {
22
+ // Delete the file
23
+ await fs.unlink(validPath);
24
+ results.push(`Successfully deleted file: ${filePath}`);
25
+ }
26
+ }
27
+ catch (error) {
28
+ const errorMessage = error instanceof Error ? error.message : String(error);
29
+ errors.push(`Failed to delete ${filePath}: ${errorMessage}`);
30
+ }
31
+ }));
32
+ // Format the results
33
+ const successCount = results.length;
34
+ const errorCount = errors.length;
35
+ let output = `Processed ${successCount + errorCount} paths:\n`;
36
+ output += `- ${successCount} items deleted successfully\n`;
37
+ if (errorCount > 0) {
38
+ output += `- ${errorCount} items failed\n\n`;
39
+ output += "Errors:\n" + errors.join("\n");
40
+ }
41
+ return output;
42
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const DeleteFilesArgsSchema = z.object({
3
+ paths: z.array(z.string()).describe("Paths to files or directories to delete"),
4
+ recursive: z.boolean().default(false).describe("Whether to recursively delete directories"),
5
+ });
@@ -0,0 +1,46 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath } from "../helpers/path.js";
4
+ export async function handleDeleteFilesByPattern(directory, pattern, includeDirectories, allowedDirectories) {
5
+ const validPath = await validatePath(directory, allowedDirectories);
6
+ let regex;
7
+ try {
8
+ regex = new RegExp(pattern);
9
+ }
10
+ catch (error) {
11
+ throw new Error(`Invalid regular expression: ${pattern}`);
12
+ }
13
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
14
+ const deleted = [];
15
+ const skipped = [];
16
+ for (const entry of entries) {
17
+ if (!regex.test(entry.name))
18
+ continue;
19
+ const fullPath = path.join(validPath, entry.name);
20
+ try {
21
+ if (entry.isDirectory()) {
22
+ if (includeDirectories) {
23
+ await fs.rm(fullPath, { recursive: true, force: true });
24
+ deleted.push(`${fullPath} (directory)`);
25
+ }
26
+ else {
27
+ skipped.push(`${fullPath} (directory; set includeDirectories: true to delete)`);
28
+ }
29
+ }
30
+ else {
31
+ await fs.unlink(fullPath);
32
+ deleted.push(fullPath);
33
+ }
34
+ }
35
+ catch (error) {
36
+ const errorMessage = error instanceof Error ? error.message : String(error);
37
+ skipped.push(`${fullPath} (${errorMessage})`);
38
+ }
39
+ }
40
+ let output = `Pattern "${pattern}" in ${validPath}:\n`;
41
+ output += `- ${deleted.length} deleted\n`;
42
+ if (skipped.length > 0) {
43
+ output += `- ${skipped.length} skipped:\n` + skipped.map((s) => ` ${s}`).join("\n");
44
+ }
45
+ return output;
46
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ export const DeleteFilesByPatternArgsSchema = z.object({
3
+ directory: z.string().describe("Directory to scan (single level; subdirectories are not scanned)."),
4
+ pattern: z.string().describe("Regular expression matched against each entry name (e.g. '^temp_.*\\.txt$')."),
5
+ includeDirectories: z.boolean().optional().default(false).describe("Also delete matching directories (recursively). Default: false (files only)."),
6
+ });
@@ -0,0 +1,7 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { buildDirectoryTree } from "./helpers.js";
3
+ export async function handleDirectoryTree(directoryPath, excludePatterns, allowedDirectories) {
4
+ const validRootPath = await validatePath(directoryPath, allowedDirectories);
5
+ const treeData = await buildDirectoryTree(validRootPath, validRootPath, excludePatterns, allowedDirectories);
6
+ return JSON.stringify(treeData, null, 2);
7
+ }
@@ -0,0 +1,47 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { minimatch } from 'minimatch';
4
+ import { validatePath } from "../helpers/path.js";
5
+ export async function buildDirectoryTree(currentPath, rootPath, excludePatterns = [], allowedDirectories) {
6
+ const validPath = await validatePath(currentPath, allowedDirectories);
7
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
8
+ const result = [];
9
+ for (const entry of entries) {
10
+ const entryPath = path.join(currentPath, entry.name);
11
+ // Check if path should be excluded based on patterns
12
+ const relativePath = path.relative(rootPath, entryPath);
13
+ const shouldExclude = excludePatterns.some(pattern => {
14
+ // Apply same pattern logic as in searchFiles
15
+ let globPattern = pattern;
16
+ if (!pattern.includes('*') && !pattern.includes('?')) {
17
+ if (pattern.includes('/')) {
18
+ globPattern = `**/${pattern}/**`;
19
+ }
20
+ else {
21
+ globPattern = `**/*${pattern}*/**`;
22
+ }
23
+ }
24
+ return minimatch(relativePath, globPattern, {
25
+ dot: true,
26
+ nocase: true,
27
+ matchBase: true
28
+ });
29
+ });
30
+ if (shouldExclude) {
31
+ continue;
32
+ }
33
+ const entryData = {
34
+ name: entry.name,
35
+ type: entry.isDirectory() ? 'directory' : 'file'
36
+ };
37
+ if (entry.isDirectory()) {
38
+ entryData.children = await buildDirectoryTree(entryPath, rootPath, excludePatterns, allowedDirectories);
39
+ // Skip empty directories after exclusion
40
+ if (entryData.children.length === 0) {
41
+ continue;
42
+ }
43
+ }
44
+ result.push(entryData);
45
+ }
46
+ return result;
47
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const DirectoryTreeArgsSchema = z.object({
3
+ path: z.string(),
4
+ excludePatterns: z.array(z.string()).optional().default([]),
5
+ });
@@ -0,0 +1,62 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ // Detect the dominant line ending of a file so it can be preserved on write.
4
+ function detectEol(text) {
5
+ if (text.includes("\r\n"))
6
+ return "\r\n";
7
+ if (text.includes("\r"))
8
+ return "\r";
9
+ return "\n";
10
+ }
11
+ const toLf = (t) => t.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
12
+ const fromLf = (t, eol) => (eol === "\n" ? t : t.replace(/\n/g, eol));
13
+ export async function handleEditFiles(files, allowedDirectories) {
14
+ const results = [];
15
+ const errors = [];
16
+ for (const file of files) {
17
+ try {
18
+ const validPath = await validatePath(file.path, allowedDirectories);
19
+ const raw = await fs.readFile(validPath, "utf-8");
20
+ const eol = detectEol(raw);
21
+ let current = toLf(raw);
22
+ const editLog = [];
23
+ for (let i = 0; i < file.edits.length; i++) {
24
+ const { oldString, newString, replaceAll } = file.edits[i];
25
+ const oldN = toLf(oldString);
26
+ const newN = toLf(newString);
27
+ if (oldN.length === 0) {
28
+ throw new Error(`Edit ${i + 1}: oldString must not be empty`);
29
+ }
30
+ const count = current.split(oldN).length - 1;
31
+ if (count === 0) {
32
+ throw new Error(`Edit ${i + 1}: oldString not found`);
33
+ }
34
+ if (!replaceAll && count > 1) {
35
+ throw new Error(`Edit ${i + 1}: oldString matched ${count} times (expected exactly 1; set replaceAll: true to replace all)`);
36
+ }
37
+ current = current.split(oldN).join(newN);
38
+ editLog.push(`Edit ${i + 1}: replaced ${replaceAll ? count : 1} occurrence(s)`);
39
+ }
40
+ // Write back with the file's original dominant line ending.
41
+ await fs.writeFile(validPath, fromLf(current, eol), "utf-8");
42
+ results.push(`File: ${file.path}\n${editLog.join("\n")}`);
43
+ }
44
+ catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
+ errors.push(`Failed to edit ${file.path}: ${errorMessage}`);
47
+ }
48
+ }
49
+ const successCount = results.length;
50
+ const errorCount = errors.length;
51
+ let output = `Processed ${successCount + errorCount} files:\n`;
52
+ output += `- ${successCount} files edited successfully\n`;
53
+ if (errorCount > 0) {
54
+ output += `- ${errorCount} files failed\n\n`;
55
+ output += "Errors:\n" + errors.join("\n");
56
+ }
57
+ if (successCount > 0) {
58
+ output += "\n\nEdit details:\n" + "=".repeat(40) + "\n";
59
+ output += results.join("\n" + "=".repeat(40) + "\n");
60
+ }
61
+ return output;
62
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ export const EditFilesArgsSchema = z.object({
3
+ files: z.array(z.object({
4
+ path: z.string().describe("Path to the file to edit"),
5
+ edits: z.array(z.object({
6
+ oldString: z.string().describe("Exact literal text to find. Must not be empty."),
7
+ newString: z.string().describe("Replacement text. May be empty to delete the matched text."),
8
+ replaceAll: z.boolean().optional().default(false).describe("When true, replace every occurrence. Default: require exactly one match."),
9
+ })).describe("Array of edits applied in order"),
10
+ })).describe("Array of files to edit"),
11
+ });
@@ -0,0 +1,28 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ import { createUnifiedDiff } from "../helpers/diff.js";
4
+ export async function handleFileDiff(file1Path, file2Path, allowedDirectories) {
5
+ const validFile1Path = await validatePath(file1Path, allowedDirectories);
6
+ const validFile2Path = await validatePath(file2Path, allowedDirectories);
7
+ try {
8
+ // Read both files
9
+ const file1Content = await fs.readFile(validFile1Path, 'utf-8');
10
+ const file2Content = await fs.readFile(validFile2Path, 'utf-8');
11
+ // Create a unified diff
12
+ const diff = createUnifiedDiff(file1Content, file2Content, file1Path, file2Path);
13
+ // Format diff with appropriate number of backticks
14
+ let numBackticks = 3;
15
+ while (diff.includes('`'.repeat(numBackticks))) {
16
+ numBackticks++;
17
+ }
18
+ const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
19
+ if (diff.trim() === '') {
20
+ return `Files are identical.`;
21
+ }
22
+ return formattedDiff;
23
+ }
24
+ catch (error) {
25
+ const errorMessage = error instanceof Error ? error.message : String(error);
26
+ throw new Error(`Error comparing files: ${errorMessage}`);
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const FileDiffArgsSchema = z.object({
3
+ file1: z.string(),
4
+ file2: z.string(),
5
+ });
@@ -0,0 +1,30 @@
1
+ import fs from "fs/promises";
2
+ import { validatePath } from "../helpers/path.js";
3
+ export async function getFileStats(filePath) {
4
+ const stats = await fs.stat(filePath);
5
+ return {
6
+ size: stats.size,
7
+ created: stats.birthtime,
8
+ modified: stats.mtime,
9
+ accessed: stats.atime,
10
+ isDirectory: stats.isDirectory(),
11
+ isFile: stats.isFile(),
12
+ permissions: stats.mode.toString(8).slice(-3),
13
+ };
14
+ }
15
+ export async function handleGetFileInfo(filePath, allowedDirectories) {
16
+ const validPath = await validatePath(filePath, allowedDirectories);
17
+ const stats = await fs.stat(validPath);
18
+ const formattedInfo = {
19
+ path: filePath,
20
+ size: `${stats.size} bytes`,
21
+ type: stats.isDirectory() ? 'directory' : (stats.isFile() ? 'file' : 'other'),
22
+ created: stats.birthtime.toISOString(),
23
+ modified: stats.mtime.toISOString(),
24
+ accessed: stats.atime.toISOString(),
25
+ permissions: stats.mode.toString(8).slice(-3)
26
+ };
27
+ return Object.entries(formattedInfo)
28
+ .map(([key, value]) => `${key}: ${value}`)
29
+ .join("\n");
30
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export const GetFileInfoArgsSchema = z.object({
3
+ path: z.string(),
4
+ });
@@ -0,0 +1,74 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath } from "../helpers/path.js";
4
+ function levenshtein(a, b) {
5
+ if (a === b)
6
+ return 0;
7
+ if (a.length === 0)
8
+ return b.length;
9
+ if (b.length === 0)
10
+ return a.length;
11
+ let prev = new Array(b.length + 1);
12
+ let curr = new Array(b.length + 1);
13
+ for (let j = 0; j <= b.length; j++)
14
+ prev[j] = j;
15
+ for (let i = 1; i <= a.length; i++) {
16
+ curr[0] = i;
17
+ for (let j = 1; j <= b.length; j++) {
18
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
19
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
20
+ }
21
+ [prev, curr] = [curr, prev];
22
+ }
23
+ return prev[b.length];
24
+ }
25
+ export async function handleFuzzyFindFiles(query, searchPath, recursive, maxResults, scanLimit, allowedDirectories) {
26
+ const validRoot = await validatePath(searchPath, allowedDirectories);
27
+ const q = query.toLowerCase();
28
+ const candidates = [];
29
+ let scanned = 0;
30
+ let truncated = false;
31
+ async function walk(dir) {
32
+ if (truncated)
33
+ return;
34
+ let entries;
35
+ try {
36
+ entries = await fs.readdir(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ if (truncated)
43
+ break;
44
+ scanned++;
45
+ if (scanned > scanLimit) {
46
+ truncated = true;
47
+ break;
48
+ }
49
+ const fullPath = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ if (recursive) {
52
+ await walk(fullPath);
53
+ }
54
+ }
55
+ else if (entry.isFile()) {
56
+ const rel = path.relative(validRoot, fullPath).toLowerCase();
57
+ const name = entry.name.toLowerCase();
58
+ const distance = Math.min(levenshtein(q, name), levenshtein(q, rel));
59
+ candidates.push({ path: fullPath, distance });
60
+ }
61
+ }
62
+ }
63
+ await walk(validRoot);
64
+ candidates.sort((a, b) => a.distance - b.distance || a.path.localeCompare(b.path));
65
+ const top = candidates.slice(0, maxResults);
66
+ if (top.length === 0) {
67
+ return `No files found for query: ${query} (scanned ${scanned} entries)`;
68
+ }
69
+ let output = `Top ${top.length} fuzzy matches for "${query}" (scanned ${scanned} entries${truncated ? ", stopped at scan limit" : ""}):\n`;
70
+ top.forEach((c, i) => {
71
+ output += `${i + 1}. (distance ${c.distance}) ${c.path}\n`;
72
+ });
73
+ return output;
74
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ export const FuzzyFindFilesArgsSchema = z.object({
3
+ query: z.string().describe("Search query to match against file names/paths (typo-tolerant)."),
4
+ path: z.string().describe("Root directory to search in."),
5
+ recursive: z.boolean().optional().default(true).describe("Recursively search subdirectories. Default: true."),
6
+ maxResults: z.number().int().min(1).max(50).optional().default(5).describe("Maximum number of results to return. Default: 5."),
7
+ scanLimit: z.number().int().min(1).optional().default(20000).describe("Maximum entries to scan before stopping (safety for huge trees/network shares). Default: 20000."),
8
+ });
@@ -0,0 +1,10 @@
1
+ import fs from "fs/promises";
2
+ import crypto from "crypto";
3
+ export async function calculateFileHash(filePath, algorithm = "sha256") {
4
+ // Read the file
5
+ const data = await fs.readFile(filePath);
6
+ // Calculate hash
7
+ const hash = crypto.createHash(algorithm);
8
+ hash.update(data);
9
+ return hash.digest("hex");
10
+ }
@@ -0,0 +1,10 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ export function normalizeLineEndings(text) {
3
+ return text.replace(/\r\n/g, '\n');
4
+ }
5
+ export function createUnifiedDiff(originalContent, newContent, originalFilePath = 'file1', newFilePath = 'file2') {
6
+ // Ensure consistent line endings for diff
7
+ const normalizedOriginal = normalizeLineEndings(originalContent);
8
+ const normalizedNew = normalizeLineEndings(newContent);
9
+ return createTwoFilesPatch(originalFilePath, newFilePath, normalizedOriginal, normalizedNew, 'original', 'modified');
10
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import os from 'os';
4
+ // Normalize all paths consistently
5
+ export function normalizePath(p) {
6
+ return path.normalize(p);
7
+ }
8
+ export function expandHome(filepath) {
9
+ if (filepath.startsWith('~/') || filepath === '~') {
10
+ return path.join(os.homedir(), filepath.slice(1));
11
+ }
12
+ return filepath;
13
+ }
14
+ // Security utilities
15
+ export async function validatePath(requestedPath, allowedDirectories) {
16
+ const expandedPath = expandHome(requestedPath);
17
+ const absolute = path.isAbsolute(expandedPath)
18
+ ? path.resolve(expandedPath)
19
+ : path.resolve(process.cwd(), expandedPath);
20
+ const normalizedRequested = normalizePath(absolute);
21
+ // Check if path is within allowed directories
22
+ const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir));
23
+ if (!isAllowed) {
24
+ throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
25
+ }
26
+ // Handle symlinks by checking their real path
27
+ try {
28
+ const realPath = await fs.realpath(absolute);
29
+ const normalizedReal = normalizePath(realPath);
30
+ const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir));
31
+ if (!isRealPathAllowed) {
32
+ throw new Error("Access denied - symlink target outside allowed directories");
33
+ }
34
+ return realPath;
35
+ }
36
+ catch (error) {
37
+ // Target doesn't exist yet: walk up to the nearest existing ancestor
38
+ // and verify it is within an allowed directory
39
+ let ancestor = path.dirname(absolute);
40
+ let realAncestor = null;
41
+ while (true) {
42
+ try {
43
+ realAncestor = await fs.realpath(ancestor);
44
+ break;
45
+ }
46
+ catch {
47
+ const up = path.dirname(ancestor);
48
+ if (up === ancestor) {
49
+ break;
50
+ }
51
+ ancestor = up;
52
+ }
53
+ }
54
+ if (realAncestor === null) {
55
+ throw new Error(`Parent directory does not exist: ${path.dirname(absolute)}`);
56
+ }
57
+ const normalizedAncestor = normalizePath(realAncestor);
58
+ const isAncestorAllowed = allowedDirectories.some(dir => normalizedAncestor.startsWith(dir));
59
+ if (!isAncestorAllowed) {
60
+ throw new Error("Access denied - parent directory outside allowed directories");
61
+ }
62
+ return absolute;
63
+ }
64
+ }
65
+ // Create a directory (and parents) if needed. Tolerates volume roots,
66
+ // which Node's recursive mkdir can reject with EPERM on Windows.
67
+ export async function ensureDirectoryExists(dir) {
68
+ try {
69
+ const stats = await fs.stat(dir);
70
+ if (stats.isDirectory()) {
71
+ return;
72
+ }
73
+ }
74
+ catch {
75
+ // Doesn't exist or is inaccessible - fall through and try to create
76
+ }
77
+ try {
78
+ await fs.mkdir(dir, { recursive: true });
79
+ }
80
+ catch (error) {
81
+ const code = error.code;
82
+ if (code === 'EPERM' || code === 'EEXIST') {
83
+ return;
84
+ }
85
+ throw error;
86
+ }
87
+ }
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { normalizePath, expandHome } from "./helpers/path.js";
5
+ import { FilesystemServer } from "./server.js";
6
+ // Command line argument parsing
7
+ const args = process.argv.slice(2);
8
+ if (args.length === 0) {
9
+ console.error("Usage: mcp-fs-shell-windows <allowed-directory> [additional-directories...]");
10
+ process.exit(1);
11
+ }
12
+ // Store allowed directories in normalized form
13
+ const allowedDirectories = args.map(dir => normalizePath(path.resolve(expandHome(dir))));
14
+ // Validate that all directories exist and are accessible
15
+ (async () => {
16
+ try {
17
+ await Promise.all(args.map(async (dir) => {
18
+ try {
19
+ const stats = await fs.stat(dir);
20
+ if (!stats.isDirectory()) {
21
+ console.error(`Warning: ${dir} is not a directory; continuing without it`);
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error(`Warning: cannot access ${dir} (offline or invalid); continuing without it`);
26
+ }
27
+ }));
28
+ // Start server
29
+ const server = new FilesystemServer(allowedDirectories);
30
+ await server.connect();
31
+ }
32
+ catch (error) {
33
+ console.error("Fatal error running server:", error);
34
+ process.exit(1);
35
+ }
36
+ })();
@@ -0,0 +1,42 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { validatePath } from "../helpers/path.js";
4
+ import { minimatch } from "minimatch";
5
+ export async function handleListDirectory(directoryPath, allowedDirectories, type = "all", ignore = [], offset = 1, limit = 100, includeSizes = true) {
6
+ const validPath = await validatePath(directoryPath, allowedDirectories);
7
+ const entries = await fs.readdir(validPath, { withFileTypes: true });
8
+ let filtered = entries.filter((entry) => {
9
+ if (type === "files" && !entry.isFile())
10
+ return false;
11
+ if (type === "directories" && !entry.isDirectory())
12
+ return false;
13
+ return !ignore.some((pattern) => minimatch(entry.name, pattern, { dot: true }));
14
+ });
15
+ // Stable ordering for pagination
16
+ filtered = filtered.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
17
+ const total = filtered.length;
18
+ if (total === 0) {
19
+ return `No entries in ${validPath}`;
20
+ }
21
+ if (offset > total) {
22
+ return `No entries in ${validPath} (offset ${offset} is beyond ${total} entries)`;
23
+ }
24
+ const start = offset - 1;
25
+ const shown = filtered.slice(start, start + limit);
26
+ const lines = [];
27
+ for (const entry of shown) {
28
+ let sizeText = "";
29
+ if (includeSizes && entry.isFile()) {
30
+ try {
31
+ const stats = await fs.stat(path.join(validPath, entry.name));
32
+ sizeText = ` (${stats.size} bytes)`;
33
+ }
34
+ catch {
35
+ sizeText = "";
36
+ }
37
+ }
38
+ lines.push(`${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}${sizeText}`);
39
+ }
40
+ const end = start + shown.length;
41
+ return `Showing ${offset}-${end} of ${total} entries in ${validPath}\n` + lines.join("\n");
42
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export const ListDirectoryArgsSchema = z.object({
3
+ path: z.string(),
4
+ type: z.enum(["files", "directories", "all"]).optional().default("all").describe("What to return: \"files\", \"directories\", or \"all\". Default: \"all\"."),
5
+ ignore: z.array(z.string()).optional().default([]).describe("Glob patterns to skip (e.g. [\"node_modules\", \"dist\", \"*.log\"])."),
6
+ offset: z.number().int().min(1).optional().default(1).describe("1-indexed result number to start from. Default: 1."),
7
+ limit: z.number().int().min(1).optional().default(100).describe("Maximum number of entries to return. Default: 100."),
8
+ includeSizes: z.boolean().optional().default(true).describe("Include file sizes in the listing. Default: true (one stat per shown file entry)."),
9
+ });