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.
- package/LICENSE +23 -0
- package/README.md +147 -0
- package/dist/append_files/handler.js +32 -0
- package/dist/append_files/schema.js +7 -0
- package/dist/checksum_files/handler.js +33 -0
- package/dist/checksum_files/schema.js +5 -0
- package/dist/checksum_files_verif/handler.js +66 -0
- package/dist/checksum_files_verif/helpers.js +6 -0
- package/dist/checksum_files_verif/schema.js +8 -0
- package/dist/content_diff/handler.js +11 -0
- package/dist/content_diff/schema.js +7 -0
- package/dist/copy_files/handler.js +43 -0
- package/dist/copy_files/helpers.js +24 -0
- package/dist/copy_files/schema.js +7 -0
- package/dist/count_lines/handler.js +144 -0
- package/dist/count_lines/schema.js +9 -0
- package/dist/create_directories/handler.js +29 -0
- package/dist/create_directories/schema.js +4 -0
- package/dist/delete_files/handler.js +42 -0
- package/dist/delete_files/schema.js +5 -0
- package/dist/delete_files_by_pattern/handler.js +46 -0
- package/dist/delete_files_by_pattern/schema.js +6 -0
- package/dist/directory_tree/handler.js +7 -0
- package/dist/directory_tree/helpers.js +47 -0
- package/dist/directory_tree/schema.js +5 -0
- package/dist/edit_files/handler.js +62 -0
- package/dist/edit_files/schema.js +11 -0
- package/dist/file_diff/handler.js +28 -0
- package/dist/file_diff/schema.js +5 -0
- package/dist/file_info/handler.js +30 -0
- package/dist/file_info/schema.js +4 -0
- package/dist/fuzzy_find_files/handler.js +74 -0
- package/dist/fuzzy_find_files/schema.js +8 -0
- package/dist/helpers/checksum.js +10 -0
- package/dist/helpers/diff.js +10 -0
- package/dist/helpers/path.js +87 -0
- package/dist/index.js +36 -0
- package/dist/list_directory/handler.js +42 -0
- package/dist/list_directory/schema.js +9 -0
- package/dist/move_files/handler.js +68 -0
- package/dist/move_files/schema.js +8 -0
- package/dist/patch_files/handler.js +34 -0
- package/dist/patch_files/helpers.js +103 -0
- package/dist/patch_files/schema.js +18 -0
- package/dist/read_files/handler.js +36 -0
- package/dist/read_files/schema.js +6 -0
- package/dist/search_files/handler.js +15 -0
- package/dist/search_files/helpers.js +71 -0
- package/dist/search_files/schema.js +8 -0
- package/dist/search_glob/handler.js +77 -0
- package/dist/search_glob/schema.js +8 -0
- package/dist/search_regex/handler.js +159 -0
- package/dist/search_regex/schema.js +10 -0
- package/dist/server.js +698 -0
- package/dist/shell/handler.js +687 -0
- package/dist/shell/schema.js +69 -0
- package/dist/write_new_files/handler.js +55 -0
- package/dist/write_new_files/schema.js +9 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 Anthropic, PBC (original @modelcontextprotocol/server-filesystem)
|
|
4
|
+
Copyright (c) 2025 fabienvauchelles (mcp-filesystem-extended base fork)
|
|
5
|
+
Copyright (c) 2026 Gerard DeLuca (robustness patches, shell tools, Windows hardening)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# mcp-fs-shell-windows — filesystem + standalone Windows shell MCP server
|
|
2
|
+
|
|
3
|
+
A Node.js MCP server exposing **filesystem operations** and a **standalone shell** as tools. Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended), which itself extends the official [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem).
|
|
4
|
+
|
|
5
|
+
Developed and tested primarily on **Windows**: drive-letter roots (`C:\`, `D:\`) and UNC network-share roots (`\\server\\share`) are first-class, and the shell tools run commands through `cmd.exe` byte-for-byte — including background job management and detached interactive console windows.
|
|
6
|
+
|
|
7
|
+
## Provenance
|
|
8
|
+
|
|
9
|
+
| Layer | Source |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
|
|
12
|
+
| Base fork | [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended) @ `b6c317a` (MIT) |
|
|
13
|
+
| This fork | v0.2.1 tool parity + `transfer_files` rename, robustness patches, 8 shell tools (v0.2.2), verbatim argv (v0.2.4) — see [Differences](#differences-from-the-base-fork) |
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **23 filesystem tools** — batched read/write/append/delete, move/copy, line-number patching, search (name / glob / regex / fuzzy), tree, counts, checksums, diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
|
|
18
|
+
- **8 shell tools** — synchronous, background, and interactive command execution implemented *inside the MCP server process*: no client-side permission toggles, survives client restarts, works with any MCP client.
|
|
19
|
+
- **Windows-first** — drive roots, UNC roots, `cmd.exe /d /c` with verbatim argv, whole-process-tree kills (`taskkill /T /F`), detached interactive console windows.
|
|
20
|
+
- **NAS-safe startup** — an allowed directory that is missing or offline (e.g., a powered-down network share) logs a warning and the server continues instead of exiting.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js >= 20 (developed and tested on Node 22).
|
|
25
|
+
- Windows for the shell tools (they spawn `cmd.exe`, `taskkill`, and `powershell`). The filesystem tools work on any platform Node runs on.
|
|
26
|
+
- `shell_python` requires Python on the system PATH.
|
|
27
|
+
|
|
28
|
+
## Build
|
|
29
|
+
|
|
30
|
+
```bat
|
|
31
|
+
npm ci
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run the server:
|
|
36
|
+
|
|
37
|
+
```bat
|
|
38
|
+
node dist\index.js <allowed-dir> [more-dirs...]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Example MCP client config (Claude Desktop / LM Studio `mcp.json` style):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"filesystem": {
|
|
49
|
+
"command": "node",
|
|
50
|
+
"args": [
|
|
51
|
+
"C:/path/to/repo/dist/index.js",
|
|
52
|
+
"C:\\",
|
|
53
|
+
"D:\\",
|
|
54
|
+
"\\\\server\\share"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The filesystem tools can only touch the passed directories (and anything under them). A root that cannot be accessed at startup logs a warning and is skipped; the server keeps running. Note that the shell tools are **not** root-restricted — see [Security](#security).
|
|
62
|
+
|
|
63
|
+
### Filesystem tools (23)
|
|
64
|
+
|
|
65
|
+
| Tool | Purpose |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `read_files` | Read one or more files, line-numbered; `offset`/`limit` paging |
|
|
68
|
+
| `write_new_files` | Create files (per-file `overwrite`, optional `base64` encoding) |
|
|
69
|
+
| `append_files` | Append to files (creates them if missing) |
|
|
70
|
+
| `delete_files` | Delete files/directories (`recursive`) |
|
|
71
|
+
| `copy_file` | Copy files/directories (`recursive`, `overwrite`) |
|
|
72
|
+
| `transfer_files` | Move/rename items (`overwrite`, creates parent dirs) |
|
|
73
|
+
| `patch_files` | Patch by line ranges (`replace`/`insertBefore`/`delete`, `dryRun`, git-diff output) |
|
|
74
|
+
| `create_directories` | Create directory paths (nested chains) |
|
|
75
|
+
| `list_directory` | Single-level listing (sizes, pagination, type filter, ignore globs) |
|
|
76
|
+
| `directory_tree` | Recursive tree view (JSON) with exclusion patterns |
|
|
77
|
+
| `search_files` | Recursive case-insensitive name-substring search |
|
|
78
|
+
| `search_regex` | Content grep over a single file or directory tree (paged) |
|
|
79
|
+
| `search_glob` | Glob-based file search (e.g. `**/*.ts`, paged) |
|
|
80
|
+
| `fuzzy_find_files` | Typo-tolerant name/path search (Levenshtein) |
|
|
81
|
+
| `delete_files_by_pattern` | Regex delete within a single directory |
|
|
82
|
+
| `count_lines` | Line counts with filters |
|
|
83
|
+
| `file_diff` | Unified diff of two files |
|
|
84
|
+
| `content_diff` | Unified diff of two strings |
|
|
85
|
+
| `checksum_files` | md5/sha1/sha256/sha512 for files |
|
|
86
|
+
| `checksum_files_verif` | Verify files against expected hashes |
|
|
87
|
+
| `get_file_info` | Size, timestamps, type, permissions |
|
|
88
|
+
| `edit_files` | Exact-match text replacement, EOL-tolerant |
|
|
89
|
+
| `list_allowed_directories` | List the allowed root directories |
|
|
90
|
+
|
|
91
|
+
### Shell tools (8, Windows-only)
|
|
92
|
+
|
|
93
|
+
User commands are spawned as `cmd.exe /d /c <command>` with **verbatim argv** — quotes, redirects, and metacharacters arrive byte-for-byte (Node `windowsVerbatimArguments`), and `/d` skips AutoRun registry entries.
|
|
94
|
+
|
|
95
|
+
| Tool | Purpose |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `shell_run` | Synchronous command (default 5 s, max 28 s); non-zero exit or timeout returns the captured stdout/stderr instead of hiding it |
|
|
98
|
+
| `shell_test` | Test wrapper (runs with `CI=true`); never errors on a failing test |
|
|
99
|
+
| `shell_start` | Background job — returns immediately with a job ID; auto-kills at `timeout_hours` (max 10); 256 KB in-memory tail + log file |
|
|
100
|
+
| `shell_check` | Job status, stdout/stderr tails, log file path |
|
|
101
|
+
| `shell_cancel` | Kill the whole process tree (`taskkill /T /F`, with PID-liveness fallback) |
|
|
102
|
+
| `shell_terminal` | Open a visible, separate, interactive `cmd /k` console window running the command; the window persists after the command finishes; close it with `shell_cancel` |
|
|
103
|
+
| `shell_python` | Run a Python snippet (temp `.py` + system Python) |
|
|
104
|
+
| `shell_cwd` | Get/set the default working directory for the other shell tools (every tool also accepts a per-call `cwd`) |
|
|
105
|
+
|
|
106
|
+
Practical notes:
|
|
107
|
+
|
|
108
|
+
- Background jobs live in an **in-memory registry** inside the server process: the table is lost on server restart, but job log files persist (under `%TEMP%\mcp-filesystem-extended\shell-jobs`).
|
|
109
|
+
- `timeout /t` can fail when stdin is not an interactive console — use `ping -n N 127.0.0.1 >nul` for sleeps.
|
|
110
|
+
- Synchronous calls are bounded (max 28 s) so they cannot wedge the MCP request channel; anything longer should go through `shell_start`.
|
|
111
|
+
|
|
112
|
+
## Docker
|
|
113
|
+
|
|
114
|
+
```bat
|
|
115
|
+
docker build -t mcp/filesystem-extended .
|
|
116
|
+
docker run -i --rm -v /path/to/dir:/data mcp/filesystem-extended /data
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The filesystem tools work in the image; the shell tools require a Windows host with `cmd.exe`.
|
|
120
|
+
|
|
121
|
+
## Differences from the base fork
|
|
122
|
+
|
|
123
|
+
**v0.2.1 — official-server parity:** head/tail reads (`offset`/`limit` in `read_files`), sizes in listings, `overwrite` in `write_new_files`, exact-match text editing (`edit_files`).
|
|
124
|
+
|
|
125
|
+
**`transfer_files` (renamed from `move_files`):** the canonical move/rename tool. Renamed because at least one MCP client (observed in LM Studio) silently drops tools named `move_files`/`mv_files` from the exposed tool list; `transfer_files` is registered last in the tools array.
|
|
126
|
+
|
|
127
|
+
**Robustness patches:**
|
|
128
|
+
|
|
129
|
+
- Missing/offline allowed roots warn at startup instead of exiting (NAS-down safe).
|
|
130
|
+
- `validatePath` walks to the nearest existing ancestor, so writes under brand-new nested directories work.
|
|
131
|
+
- `ensureDirectoryExists` at all `mkdir` sites (Node EPERM quirk on drive-root writes).
|
|
132
|
+
- Move-handler destination checks use errno `ENOENT`, so moves to fresh destinations succeed.
|
|
133
|
+
- `zod` pinned to `^3.23.8` (zod v4 breaks the SDK's schema conversion).
|
|
134
|
+
|
|
135
|
+
**v0.2.2 — shell tools** as listed above, with whole-process-tree kills and the detached interactive console window.
|
|
136
|
+
|
|
137
|
+
**v0.2.4 — verbatim argv:** `windowsVerbatimArguments` + `/d` on all user-command spawns (run/test/start and the terminal launcher).
|
|
138
|
+
|
|
139
|
+
## Security
|
|
140
|
+
|
|
141
|
+
- The filesystem tools are confined to the allowed directories passed at startup (and their subtrees).
|
|
142
|
+
- The shell tools are **not** confined: anything the server process's user can execute will run, including against reachable network shares. Grant this server the same trust you would grant an unsandboxed shell.
|
|
143
|
+
- The server communicates over stdio and opens no network endpoints of its own.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT — see [LICENSE](./LICENSE). This project derives from MIT-licensed code (Anthropic, PBC; fabienvauchelles); all copyright notices are in the LICENSE file.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
|
|
4
|
+
export async function handleAppendFiles(files, allowedDirectories) {
|
|
5
|
+
const results = [];
|
|
6
|
+
const errors = [];
|
|
7
|
+
await Promise.all(files.map(async (file) => {
|
|
8
|
+
try {
|
|
9
|
+
const validPath = await validatePath(file.path, allowedDirectories);
|
|
10
|
+
// Ensure parent directory exists
|
|
11
|
+
const directory = path.dirname(validPath);
|
|
12
|
+
await ensureDirectoryExists(directory);
|
|
13
|
+
// Append content to file (create if doesn't exist)
|
|
14
|
+
await fs.appendFile(validPath, file.content, "utf-8");
|
|
15
|
+
results.push(`Successfully appended to ${file.path}`);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
19
|
+
errors.push(`Failed to append to file ${file.path}: ${errorMessage}`);
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
// Format the results
|
|
23
|
+
const successCount = results.length;
|
|
24
|
+
const errorCount = errors.length;
|
|
25
|
+
let output = `Processed ${successCount + errorCount} files:\n`;
|
|
26
|
+
output += `- ${successCount} files appended successfully\n`;
|
|
27
|
+
if (errorCount > 0) {
|
|
28
|
+
output += `- ${errorCount} files failed\n\n`;
|
|
29
|
+
output += "Errors:\n" + errors.join("\n");
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const AppendFilesArgsSchema = z.object({
|
|
3
|
+
files: z.array(z.object({
|
|
4
|
+
path: z.string().describe("Path to the file to append to"),
|
|
5
|
+
content: z.string().describe("Content to append to the file"),
|
|
6
|
+
})).describe("Array of files to append to"),
|
|
7
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { validatePath } from "../helpers/path.js";
|
|
2
|
+
import { calculateFileHash } from "../helpers/checksum.js";
|
|
3
|
+
export async function handleChecksumFiles(filePaths, algorithm, allowedDirectories) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const errors = [];
|
|
6
|
+
await Promise.all(filePaths.map(async (filePath) => {
|
|
7
|
+
try {
|
|
8
|
+
// Validate the path
|
|
9
|
+
const validPath = await validatePath(filePath, allowedDirectories);
|
|
10
|
+
// Calculate the hash
|
|
11
|
+
const hash = await calculateFileHash(validPath, algorithm);
|
|
12
|
+
results.push({ path: filePath, hash });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
16
|
+
errors.push({ path: filePath, error: errorMessage });
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
// Format the results
|
|
20
|
+
let output = `Checksums (${algorithm}):\n\n`;
|
|
21
|
+
if (results.length > 0) {
|
|
22
|
+
for (const result of results) {
|
|
23
|
+
output += `${result.hash} ${result.path}\n`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (errors.length > 0) {
|
|
27
|
+
output += "\nErrors:\n";
|
|
28
|
+
for (const error of errors) {
|
|
29
|
+
output += `${error.path}: ${error.error}\n`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ChecksumFilesArgsSchema = z.object({
|
|
3
|
+
paths: z.array(z.string()).describe("Paths to the files to generate checksums for"),
|
|
4
|
+
algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm to use"),
|
|
5
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { validatePath } from "../helpers/path.js";
|
|
2
|
+
import { calculateFileHash } from "../helpers/checksum.js";
|
|
3
|
+
import { validateHash } from "./helpers.js";
|
|
4
|
+
export async function handleChecksumFilesVerif(files, algorithm, allowedDirectories) {
|
|
5
|
+
const results = [];
|
|
6
|
+
const errors = [];
|
|
7
|
+
await Promise.all(files.map(async (file) => {
|
|
8
|
+
try {
|
|
9
|
+
// Validate the path
|
|
10
|
+
const validPath = await validatePath(file.path, allowedDirectories);
|
|
11
|
+
// Calculate the hash
|
|
12
|
+
const actualHash = await calculateFileHash(validPath, algorithm);
|
|
13
|
+
// Compare with expected hash
|
|
14
|
+
const isValid = validateHash(actualHash, file.expectedHash);
|
|
15
|
+
results.push({
|
|
16
|
+
path: file.path,
|
|
17
|
+
expectedHash: file.expectedHash,
|
|
18
|
+
actualHash,
|
|
19
|
+
valid: isValid
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
24
|
+
errors.push({
|
|
25
|
+
path: file.path,
|
|
26
|
+
expectedHash: file.expectedHash,
|
|
27
|
+
error: errorMessage
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
// Count successes and failures
|
|
32
|
+
const validCount = results.filter(r => r.valid).length;
|
|
33
|
+
const invalidCount = results.filter(r => !r.valid).length;
|
|
34
|
+
const errorCount = errors.length;
|
|
35
|
+
// Format the results
|
|
36
|
+
let output = `Checksum Verification Results (${algorithm}):\n`;
|
|
37
|
+
output += `✅ Valid: ${validCount}\n`;
|
|
38
|
+
output += `❌ Invalid: ${invalidCount}\n`;
|
|
39
|
+
output += `⚠️ Errors: ${errorCount}\n\n`;
|
|
40
|
+
// Valid files
|
|
41
|
+
if (validCount > 0) {
|
|
42
|
+
output += "Valid Files:\n";
|
|
43
|
+
for (const result of results.filter(r => r.valid)) {
|
|
44
|
+
output += `✓ ${result.path}\n`;
|
|
45
|
+
}
|
|
46
|
+
output += "\n";
|
|
47
|
+
}
|
|
48
|
+
// Invalid files
|
|
49
|
+
if (invalidCount > 0) {
|
|
50
|
+
output += "Invalid Files:\n";
|
|
51
|
+
for (const result of results.filter(r => !r.valid)) {
|
|
52
|
+
output += `✗ ${result.path}\n`;
|
|
53
|
+
output += ` Expected: ${result.expectedHash}\n`;
|
|
54
|
+
output += ` Actual: ${result.actualHash}\n`;
|
|
55
|
+
}
|
|
56
|
+
output += "\n";
|
|
57
|
+
}
|
|
58
|
+
// Errors
|
|
59
|
+
if (errorCount > 0) {
|
|
60
|
+
output += "Errors:\n";
|
|
61
|
+
for (const error of errors) {
|
|
62
|
+
output += `! ${error.path}: ${error.error}\n`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function validateHash(hash, expectedHash) {
|
|
2
|
+
// Normalize hashes for comparison (convert to lowercase, trim)
|
|
3
|
+
const normalizedHash = hash.toLowerCase().trim();
|
|
4
|
+
const normalizedExpected = expectedHash.toLowerCase().trim();
|
|
5
|
+
return normalizedHash === normalizedExpected;
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ChecksumFilesVerifArgsSchema = z.object({
|
|
3
|
+
files: z.array(z.object({
|
|
4
|
+
path: z.string().describe("Path to the file to verify"),
|
|
5
|
+
expectedHash: z.string().describe("Expected hash value to compare against"),
|
|
6
|
+
})).describe("Array of files to verify with their expected hashes"),
|
|
7
|
+
algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm to use"),
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createUnifiedDiff } from "../helpers/diff.js";
|
|
2
|
+
export async function handleContentDiff(content1, content2, label1, label2) {
|
|
3
|
+
// Generate a diff between the two content strings
|
|
4
|
+
const diff = createUnifiedDiff(content1, content2, label1, label2);
|
|
5
|
+
// Format the output with code fence
|
|
6
|
+
let numBackticks = 3;
|
|
7
|
+
while (diff.includes('`'.repeat(numBackticks))) {
|
|
8
|
+
numBackticks++;
|
|
9
|
+
}
|
|
10
|
+
return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ContentDiffArgsSchema = z.object({
|
|
3
|
+
content1: z.string().describe("First content string to compare"),
|
|
4
|
+
content2: z.string().describe("Second content string to compare"),
|
|
5
|
+
label1: z.string().optional().default("original").describe("Label for the first content"),
|
|
6
|
+
label2: z.string().optional().default("modified").describe("Label for the second content"),
|
|
7
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { copyDir } from "./helpers.js";
|
|
3
|
+
import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
|
|
4
|
+
export async function handleCopyFile(sourcePath, destinationPath, recursive, overwrite, allowedDirectories) {
|
|
5
|
+
const validSourcePath = await validatePath(sourcePath, allowedDirectories);
|
|
6
|
+
const validDestPath = await validatePath(destinationPath, allowedDirectories);
|
|
7
|
+
try {
|
|
8
|
+
// Check if source exists
|
|
9
|
+
const sourceStats = await fs.stat(validSourcePath);
|
|
10
|
+
// Check if destination already exists
|
|
11
|
+
try {
|
|
12
|
+
await fs.access(validDestPath);
|
|
13
|
+
if (!overwrite) {
|
|
14
|
+
throw new Error(`Destination already exists: ${destinationPath}. Use overwrite=true to replace it.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// This is expected for new destinations
|
|
19
|
+
if (!(error instanceof Error) || !error.message.includes('ENOENT')) {
|
|
20
|
+
throw error; // If error is something else, rethrow it
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (sourceStats.isDirectory()) {
|
|
24
|
+
if (!recursive) {
|
|
25
|
+
throw new Error(`Source is a directory. Use recursive=true to copy directories.`);
|
|
26
|
+
}
|
|
27
|
+
// Create the destination directory if it doesn't exist
|
|
28
|
+
await ensureDirectoryExists(validDestPath);
|
|
29
|
+
// Copy directory recursively
|
|
30
|
+
await copyDir(validSourcePath, validDestPath, allowedDirectories);
|
|
31
|
+
return `Successfully copied directory ${sourcePath} to ${destinationPath}`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// It's a file, copy it directly
|
|
35
|
+
await fs.copyFile(validSourcePath, validDestPath);
|
|
36
|
+
return `Successfully copied file ${sourcePath} to ${destinationPath}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
41
|
+
throw new Error(`Error copying ${sourcePath} to ${destinationPath}: ${errorMessage}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { validatePath, ensureDirectoryExists } from "../helpers/path.js";
|
|
4
|
+
export async function copyDir(src, dest, allowedDirectories) {
|
|
5
|
+
// Create destination if it doesn't exist
|
|
6
|
+
await ensureDirectoryExists(dest);
|
|
7
|
+
// Get all files in the source directory
|
|
8
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const srcPath = path.join(src, entry.name);
|
|
11
|
+
const destPath = path.join(dest, entry.name);
|
|
12
|
+
// Make sure paths are validated
|
|
13
|
+
await validatePath(srcPath, allowedDirectories);
|
|
14
|
+
await validatePath(destPath, allowedDirectories);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
// Recursively copy subdirectories
|
|
17
|
+
await copyDir(srcPath, destPath, allowedDirectories);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Copy files
|
|
21
|
+
await fs.copyFile(srcPath, destPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const CopyFileArgsSchema = z.object({
|
|
3
|
+
source: z.string(),
|
|
4
|
+
destination: z.string(),
|
|
5
|
+
recursive: z.boolean().default(false).describe('Copy directories recursively'),
|
|
6
|
+
overwrite: z.boolean().default(false).describe('Overwrite destination if it exists'),
|
|
7
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
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 handleCountLines(filePath, recursive, pattern, filePattern, excludePatterns, ignoreEmptyLines, allowedDirectories) {
|
|
6
|
+
// Validate the path
|
|
7
|
+
const validPath = await validatePath(filePath, allowedDirectories);
|
|
8
|
+
// Create regex for line matching if pattern is provided
|
|
9
|
+
let regex;
|
|
10
|
+
if (pattern) {
|
|
11
|
+
try {
|
|
12
|
+
regex = new RegExp(pattern);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new Error(`Invalid regular expression: ${pattern}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Get file stats to determine if it's a file or directory
|
|
19
|
+
const stats = await fs.stat(validPath);
|
|
20
|
+
let files = [];
|
|
21
|
+
if (stats.isFile()) {
|
|
22
|
+
// Count lines in a single file
|
|
23
|
+
const count = await countLinesInFile(validPath, regex, ignoreEmptyLines);
|
|
24
|
+
files.push(count);
|
|
25
|
+
}
|
|
26
|
+
else if (stats.isDirectory() && recursive) {
|
|
27
|
+
// Recursively count lines in a directory
|
|
28
|
+
files = await countLinesInDirectory(validPath, filePattern, excludePatterns, regex, ignoreEmptyLines, allowedDirectories);
|
|
29
|
+
}
|
|
30
|
+
else if (stats.isDirectory() && !recursive) {
|
|
31
|
+
throw new Error(`Path is a directory. Use recursive=true to count lines in all files.`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`Path is neither a file nor a directory.`);
|
|
35
|
+
}
|
|
36
|
+
// Format results
|
|
37
|
+
let output = '';
|
|
38
|
+
if (files.length === 0) {
|
|
39
|
+
output = `No files found matching the criteria.`;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
let totalLines = 0;
|
|
43
|
+
let totalMatchingLines = 0;
|
|
44
|
+
output = "Line counts:\n\n";
|
|
45
|
+
// Sort by line count (descending)
|
|
46
|
+
files.sort((a, b) => b.count - a.count);
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
totalLines += file.count;
|
|
49
|
+
if (file.matchingCount !== undefined) {
|
|
50
|
+
totalMatchingLines += file.matchingCount;
|
|
51
|
+
}
|
|
52
|
+
if (pattern) {
|
|
53
|
+
output += `${file.file}: ${file.count} lines total, ${file.matchingCount} matching lines\n`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
output += `${file.file}: ${file.count} lines\n`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
output += "\n";
|
|
60
|
+
output += `Total: ${files.length} files, ${totalLines} lines`;
|
|
61
|
+
if (pattern) {
|
|
62
|
+
output += `, ${totalMatchingLines} matching lines`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
async function countLinesInFile(filePath, regex, ignoreEmptyLines) {
|
|
68
|
+
// Read file content
|
|
69
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
70
|
+
// Split into lines
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
let count = lines.length;
|
|
73
|
+
let matchingCount;
|
|
74
|
+
// Handle empty lines if needed
|
|
75
|
+
if (ignoreEmptyLines) {
|
|
76
|
+
count = lines.filter(line => line.trim() !== '').length;
|
|
77
|
+
}
|
|
78
|
+
// Count matching lines if regex is provided
|
|
79
|
+
if (regex) {
|
|
80
|
+
matchingCount = lines.filter(line => {
|
|
81
|
+
if (ignoreEmptyLines && line.trim() === '') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return regex.test(line);
|
|
85
|
+
}).length;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
file: filePath,
|
|
89
|
+
count,
|
|
90
|
+
matchingCount
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function countLinesInDirectory(dirPath, filePattern, excludePatterns, regex, ignoreEmptyLines, allowedDirectories) {
|
|
94
|
+
const results = [];
|
|
95
|
+
async function processDirectory(currentPath) {
|
|
96
|
+
try {
|
|
97
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
100
|
+
try {
|
|
101
|
+
// Validate each path before processing
|
|
102
|
+
await validatePath(fullPath, allowedDirectories);
|
|
103
|
+
// Get relative path for glob matching
|
|
104
|
+
const relativePath = path.relative(dirPath, fullPath);
|
|
105
|
+
// Check if path should be excluded
|
|
106
|
+
const shouldExclude = excludePatterns.some(excludePattern => {
|
|
107
|
+
return minimatch(relativePath, excludePattern, { dot: true });
|
|
108
|
+
});
|
|
109
|
+
if (shouldExclude) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
// Recursively process subdirectories
|
|
114
|
+
await processDirectory(fullPath);
|
|
115
|
+
}
|
|
116
|
+
else if (entry.isFile()) {
|
|
117
|
+
// Check if file matches the file pattern
|
|
118
|
+
if (minimatch(entry.name, filePattern, { dot: true }) ||
|
|
119
|
+
minimatch(relativePath, filePattern, { dot: true })) {
|
|
120
|
+
try {
|
|
121
|
+
const count = await countLinesInFile(fullPath, regex, ignoreEmptyLines);
|
|
122
|
+
results.push(count);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Skip files that can't be read as text
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Skip invalid paths
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Skip directories we can't read
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await processDirectory(dirPath);
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const CountLinesArgsSchema = z.object({
|
|
3
|
+
path: z.string().describe("Path to the file or directory to count lines"),
|
|
4
|
+
recursive: z.boolean().optional().default(false).describe("Whether to recursively count lines in directories"),
|
|
5
|
+
pattern: z.string().optional().describe("Regex pattern to match lines (only count matching lines)"),
|
|
6
|
+
filePattern: z.string().optional().default("**").describe("Glob pattern to match files when counting recursively"),
|
|
7
|
+
excludePatterns: z.array(z.string()).optional().default([]).describe("Glob patterns to exclude"),
|
|
8
|
+
ignoreEmptyLines: z.boolean().optional().default(false).describe("Whether to ignore empty lines"),
|
|
9
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { validatePath } from "../helpers/path.js";
|
|
3
|
+
export async function handleCreateDirectories(paths, allowedDirectories) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const errors = [];
|
|
6
|
+
await Promise.all(paths.map(async (dirPath) => {
|
|
7
|
+
try {
|
|
8
|
+
// Validate path is within allowed directories
|
|
9
|
+
const validPath = await validatePath(dirPath, allowedDirectories);
|
|
10
|
+
// Create directory (and parent directories if needed)
|
|
11
|
+
await fs.mkdir(validPath, { recursive: true });
|
|
12
|
+
results.push(`Successfully created directory: ${dirPath}`);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
16
|
+
errors.push(`Failed to create directory ${dirPath}: ${errorMessage}`);
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
// Format the results
|
|
20
|
+
const successCount = results.length;
|
|
21
|
+
const errorCount = errors.length;
|
|
22
|
+
let output = `Processed ${successCount + errorCount} directories:\n`;
|
|
23
|
+
output += `- ${successCount} directories created successfully\n`;
|
|
24
|
+
if (errorCount > 0) {
|
|
25
|
+
output += `- ${errorCount} directories failed\n\n`;
|
|
26
|
+
output += "Errors:\n" + errors.join("\n");
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
}
|