@n0zer0d4y/vulcan-file-ops 1.0.1 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.1] - 2025-11-11
9
+
10
+ ### Fixed
11
+
12
+ - Corrected Docker entrypoint to use dist/cli.js instead of dist/index.js for proper MCP server initialization
13
+ - Updated Node.js base image from node:22.12-alpine to node:22-alpine for better version compatibility
14
+
15
+ ### Changed
16
+
17
+ - Added keywords to package.json for improved NPM discoverability
18
+
19
+ ## [1.1.0] - 2025-11-08
20
+
21
+ ### Added
22
+
23
+ - Multi-file editing capability for edit_file tool
24
+ - Support for editing up to 50 files in a single operation
25
+ - Mode discriminator (single/multiple) for backward compatibility
26
+ - Atomic operations with automatic rollback on failure
27
+ - Per-file configuration options (matching strategy, dryRun, failOnAmbiguous)
28
+ - Concurrent file processing for improved performance
29
+ - Detailed multi-file diff output with summary statistics
30
+ - Comprehensive test suite for multi-file editing functionality
31
+ - Implementation plan documentation in local_docs folder
32
+
33
+ ### Changed
34
+
35
+ - Enhanced edit_file tool schema to support both single and multi-file modes
36
+ - Updated README documentation with complete edit_file feature specification
37
+ - Improved EditFileArgsSchema with explicit mode parameter for better MCP client compatibility
38
+
39
+ ### Fixed
40
+
41
+ - Test timeout issues in shell-tool.test.ts and shell-command-path-validation.test.ts
42
+
8
43
  ## [1.0.1] - 2025-11-03
9
44
 
10
45
  ### Security
package/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # Vulcan File Ops MCP Server
2
2
 
3
3
  ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)
4
+ ![MCP Dev](https://badge.mcpx.dev?type=dev "MCP Dev")
4
5
  [![MCP Server](https://badge.mcpx.dev?type=server "MCP Server")](https://modelcontextprotocol.io)
5
6
  [![MCP Server with Tools](https://badge.mcpx.dev?type=server&features=tools "MCP server with tools")](https://modelcontextprotocol.io)
6
7
  [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
7
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
9
 
9
- A configurable Model Context Protocol server for secure filesystem operations that absolutely **rocks**. Enables AI assistants to dynamically access and manage file system resources with runtime directory registration and selective tool activation.
10
-
11
10
  **Transform your desktop AI assistants into powerful development partners.** Vulcan File Ops bridges the gap between conversational AI (Claude Desktop, ChatGPT Desktop, etc.) and your local filesystem, unlocking the same file manipulation capabilities found in AI-powered IDEs like Cursor and Cline. Write code, refactor projects, manage documentation, and perform complex file operations—matching the power of dedicated AI coding assistants. With enterprise-grade security controls, dynamic directory registration, and intelligent tool filtering, you maintain complete control while your AI assistant handles the heavy lifting.
12
11
 
13
12
  ## Table of Contents
@@ -54,7 +53,7 @@ This enhanced implementation provides:
54
53
 
55
54
  - **Dynamic Directory Access**: Runtime directory registration through conversational commands
56
55
  - **Document Support**: Read/write PDF, DOCX, PPTX, XLSX, ODT with HTML-to-document conversion
57
- - **Batch Operations**: Read, write, copy, move, or rename multiple files concurrently
56
+ - **Batch Operations**: Read, write, edit, copy, move, or rename multiple files concurrently
58
57
  - **Advanced File Editing**: Pattern-based modifications with flexible matching and diff preview
59
58
  - **Flexible Reading Modes**: Full file, head/tail, or arbitrary line ranges
60
59
  - **Image Vision Support**: Attach images for AI analysis and description
@@ -76,28 +75,70 @@ This server supports multiple flexible approaches to directory access:
76
75
 
77
76
  ## Install
78
77
 
79
- This server requires Node.js and can be installed locally for full control.
78
+ This server requires Node.js and can be installed globally, locally, or run directly with npx. **Most users should use npx** for instant execution without installation.
79
+
80
+ ### Quick Start (Recommended for Most Users)
81
+
82
+ Run directly without installation:
80
83
 
81
84
  ```bash
82
- npm install -g vulcan-file-ops
85
+ npx @n0zer0d4y/vulcan-file-ops --help
83
86
  ```
84
87
 
85
- Or install in a specific project:
88
+ **For developers** who want to contribute or modify the code, see [Local Repository Execution](#option-4-local-repository-execution-for-developers) below.
89
+
90
+ ### Global Installation
91
+
92
+ Install globally for system-wide access:
86
93
 
87
94
  ```bash
88
- npm install vulcan-file-ops
95
+ npm install -g @n0zer0d4y/vulcan-file-ops
89
96
  ```
90
97
 
98
+ ### Local Installation
99
+
100
+ Install in a specific project:
101
+
102
+ ```bash
103
+ npm install @n0zer0d4y/vulcan-file-ops
104
+ ```
105
+
106
+ ### Prerequisites
107
+
108
+ **Node.js** (version 14 or higher) must be installed on your system. This provides npm and npx, which are required to run this package.
109
+
110
+ - **Download Node.js**: https://nodejs.org/
111
+ - **Check installation**: Run `node --version` and `npm --version`
112
+
91
113
  ### Dependencies
92
114
 
93
- Requires Node.js with support for ES2022 modules. The server has no external service dependencies and operates entirely locally.
115
+ The server has no external service dependencies and operates entirely locally. All required packages are automatically downloaded when using npx.
94
116
 
95
117
  ## Usage
96
118
 
119
+ This server can be used directly with npx (recommended) or installed globally/locally. The npx approach requires no installation and always uses the latest version.
120
+
97
121
  ### Basic Configuration
98
122
 
99
123
  Add to your MCP client configuration (e.g., `claude_desktop_config.json`):
100
124
 
125
+ #### Option 1: Using npx (Recommended - No Installation Required)
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "vulcan-file-ops": {
131
+ "command": "npx",
132
+ "args": ["@n0zer0d4y/vulcan-file-ops"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ #### Option 2: Using Global Installation
139
+
140
+ After running `npm install -g @n0zer0d4y/vulcan-file-ops`:
141
+
101
142
  ```json
102
143
  {
103
144
  "mcpServers": {
@@ -108,20 +149,61 @@ Add to your MCP client configuration (e.g., `claude_desktop_config.json`):
108
149
  }
109
150
  ```
110
151
 
152
+ #### Option 3: Using Local Installation
153
+
154
+ After running `npm install @n0zer0d4y/vulcan-file-ops` in your project:
155
+
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "vulcan-file-ops": {
160
+ "command": "./node_modules/.bin/vulcan-file-ops"
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ #### Option 4: Local Repository Execution (For Developers)
167
+
168
+ If you've cloned this repository and want to run from source:
169
+
170
+ ```bash
171
+ git clone https://github.com/n0zer0d4y/vulcan-file-ops.git
172
+ cd vulcan-file-ops
173
+ npm install
174
+ npm run build
175
+ ```
176
+
177
+ Then configure your MCP client:
178
+
179
+ ```json
180
+ {
181
+ "mcpServers": {
182
+ "vulcan-file-ops": {
183
+ "command": "vulcan-file-ops",
184
+ "args": ["--approved-folders", "/path/to/your/allowed/directories"]
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ **Note:** The `vulcan-file-ops` command will be available in your PATH after building, or you can use the full path: `./dist/cli.js`
191
+
111
192
  ### Advanced Configuration
112
193
 
113
194
  #### Approved Folders
114
195
 
115
196
  Pre-configure specific directories for immediate access on server start:
116
197
 
117
- **macOS/Linux:**
198
+ **macOS/Linux (npx):**
118
199
 
119
200
  ```json
120
201
  {
121
202
  "mcpServers": {
122
203
  "vulcan-file-ops": {
123
- "command": "vulcan-file-ops",
204
+ "command": "npx",
124
205
  "args": [
206
+ "@n0zer0d4y/vulcan-file-ops",
125
207
  "--approved-folders",
126
208
  "/Users/username/projects,/Users/username/documents"
127
209
  ]
@@ -130,14 +212,15 @@ Pre-configure specific directories for immediate access on server start:
130
212
  }
131
213
  ```
132
214
 
133
- **Windows:**
215
+ **Windows (npx):**
134
216
 
135
217
  ```json
136
218
  {
137
219
  "mcpServers": {
138
220
  "vulcan-file-ops": {
139
- "command": "vulcan-file-ops",
221
+ "command": "npx",
140
222
  "args": [
223
+ "@n0zer0d4y/vulcan-file-ops",
141
224
  "--approved-folders",
142
225
  "C:/Users/username/projects,C:/Users/username/documents"
143
226
  ]
@@ -146,6 +229,24 @@ Pre-configure specific directories for immediate access on server start:
146
229
  }
147
230
  ```
148
231
 
232
+ **Alternative: Local Repository Execution**
233
+
234
+ For users running from a cloned repository (after `npm run build`):
235
+
236
+ ```json
237
+ {
238
+ "mcpServers": {
239
+ "vulcan-file-ops": {
240
+ "command": "vulcan-file-ops",
241
+ "args": [
242
+ "--approved-folders",
243
+ "/Users/username/projects,/Users/username/documents"
244
+ ]
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
149
250
  **Path Format Note:**
150
251
 
151
252
  - **Windows**: Include drive letter (e.g., `C:/`, `D:/`). Use forward slashes in JSON to avoid escaping backslashes.
@@ -194,8 +295,12 @@ Exclude specific folders from directory listings:
194
295
  {
195
296
  "mcpServers": {
196
297
  "vulcan-file-ops": {
197
- "command": "vulcan-file-ops",
198
- "args": ["--ignored-folders", "node_modules,dist,.git,.next"]
298
+ "command": "npx",
299
+ "args": [
300
+ "@n0zer0d4y/vulcan-file-ops",
301
+ "--ignored-folders",
302
+ "node_modules,dist,.git,.next"
303
+ ]
199
304
  }
200
305
  }
201
306
  }
@@ -209,8 +314,12 @@ Enable only specific tool categories:
209
314
  {
210
315
  "mcpServers": {
211
316
  "vulcan-file-ops": {
212
- "command": "vulcan-file-ops",
213
- "args": ["--enabled-tool-categories", "read,filesystem"]
317
+ "command": "npx",
318
+ "args": [
319
+ "@n0zer0d4y/vulcan-file-ops",
320
+ "--enabled-tool-categories",
321
+ "read,filesystem"
322
+ ]
214
323
  }
215
324
  }
216
325
  }
@@ -222,8 +331,12 @@ Or enable individual tools:
222
331
  {
223
332
  "mcpServers": {
224
333
  "vulcan-file-ops": {
225
- "command": "vulcan-file-ops",
226
- "args": ["--enabled-tools", "read_file,list_directory,search_files"]
334
+ "command": "npx",
335
+ "args": [
336
+ "@n0zer0d4y/vulcan-file-ops",
337
+ "--enabled-tools",
338
+ "read_file,list_directory,grep_files"
339
+ ]
227
340
  }
228
341
  }
229
342
  }
@@ -233,14 +346,15 @@ Or enable individual tools:
233
346
 
234
347
  All configuration options can be combined:
235
348
 
236
- **Windows Example:**
349
+ **Windows Example (npx):**
237
350
 
238
351
  ```json
239
352
  {
240
353
  "mcpServers": {
241
354
  "vulcan-file-ops": {
242
- "command": "vulcan-file-ops",
355
+ "command": "npx",
243
356
  "args": [
357
+ "@n0zer0d4y/vulcan-file-ops",
244
358
  "--approved-folders",
245
359
  "C:/Users/username/projects,C:/Users/username/documents",
246
360
  "--ignored-folders",
@@ -250,14 +364,41 @@ All configuration options can be combined:
250
364
  "--enabled-tool-categories",
251
365
  "read,filesystem,shell",
252
366
  "--enabled-tools",
253
- "list_directory,search_files,register_directory,execute_shell"
367
+ "read_file,attach_image,read_multiple_files,write_file,write_multiple_files,edit_file,make_directory,list_directory,move_file,file_operations,delete_files,get_file_info,register_directory,list_allowed_directories,glob_files,grep_files,execute_shell"
254
368
  ]
255
369
  }
256
370
  }
257
371
  }
258
372
  ```
259
373
 
260
- **macOS/Linux Example:**
374
+ **macOS/Linux Example (npx):**
375
+
376
+ ```json
377
+ {
378
+ "mcpServers": {
379
+ "vulcan-file-ops": {
380
+ "command": "npx",
381
+ "args": [
382
+ "@n0zer0d4y/vulcan-file-ops",
383
+ "--approved-folders",
384
+ "/Users/username/projects,/Users/username/documents",
385
+ "--ignored-folders",
386
+ "node_modules,dist,.git",
387
+ "--approved-commands",
388
+ "npm,node,git,ls,pwd,cat,echo",
389
+ "--enabled-tool-categories",
390
+ "read,filesystem,shell",
391
+ "--enabled-tools",
392
+ "read_file,attach_image,read_multiple_files,write_file,write_multiple_files,edit_file,make_directory,list_directory,move_file,file_operations,delete_files,get_file_info,register_directory,list_allowed_directories,glob_files,grep_files,execute_shell"
393
+ ]
394
+ }
395
+ }
396
+ }
397
+ ```
398
+
399
+ **Alternative: Local Repository Execution**
400
+
401
+ For users running from a cloned repository (after `npm run build`):
261
402
 
262
403
  ```json
263
404
  {
@@ -274,7 +415,7 @@ All configuration options can be combined:
274
415
  "--enabled-tool-categories",
275
416
  "read,filesystem,shell",
276
417
  "--enabled-tools",
277
- "list_directory,search_files,register_directory,execute_shell"
418
+ "read_file,attach_image,read_multiple_files,write_file,write_multiple_files,edit_file,make_directory,list_directory,move_file,file_operations,delete_files,get_file_info,register_directory,list_allowed_directories,glob_files,grep_files,execute_shell"
278
419
  ]
279
420
  }
280
421
  }
@@ -360,20 +501,46 @@ Create or replace multiple files concurrently
360
501
 
361
502
  ##### edit_file
362
503
 
363
- Intelligent file modification with pattern matching
504
+ Apply precise modifications to text and code files with intelligent matching. Supports both single-file and multi-file operations.
364
505
 
365
- **Input:**
506
+ **Single File Input (mode: 'single'):**
366
507
 
508
+ - `mode` (string, optional): Set to `"single"` (default if omitted for backward compatibility)
367
509
  - `path` (string): File path
368
- - `edits` (array): List of edit operations (oldText, newText)
369
- - `dryRun` (boolean, optional): Preview changes without writing
510
+ - `edits` (array): List of edit operations, each containing:
511
+ - `oldText` (string): Text to search for (include 3-5 lines of context)
512
+ - `newText` (string): Text to replace with
513
+ - `instruction` (string, optional): Description of what this edit does
514
+ - `expectedOccurrences` (number, optional): Expected match count (default: 1)
370
515
  - `matchingStrategy` (string, optional): Matching strategy
371
- - `exact` - Character-for-character match
372
- - `flexible` - Whitespace-insensitive matching
373
- - `fuzzy` - Token-based regex matching
516
+ - `exact` - Character-for-character match (fastest, safest)
517
+ - `flexible` - Whitespace-insensitive matching, preserves indentation
518
+ - `fuzzy` - Token-based regex matching (most permissive)
374
519
  - `auto` - Try exact → flexible → fuzzy (default)
520
+ - `dryRun` (boolean, optional): Preview changes without writing (default: false)
521
+ - `failOnAmbiguous` (boolean, optional): Fail when matches are ambiguous (default: true)
522
+
523
+ **Multi-File Input (mode: 'multiple'):**
524
+
525
+ - `mode` (string): Set to `"multiple"`
526
+ - `files` (array): Array of file edit requests (max 50), each containing:
527
+ - `path` (string): File path
528
+ - `edits` (array): List of edit operations for this file (same structure as above)
529
+ - `matchingStrategy` (string, optional): Per-file matching strategy
530
+ - `dryRun` (boolean, optional): Per-file dry-run mode
531
+ - `failOnAmbiguous` (boolean, optional): Per-file ambiguity handling
532
+ - `failFast` (boolean, optional): Stop on first failure with rollback (true, default) or continue (false)
375
533
 
376
- **Output:** Detailed diff with statistics showing changes made
534
+ **Features:**
535
+
536
+ - Concurrent processing for multi-file operations
537
+ - Atomic operations with automatic rollback on failure (when failFast: true)
538
+ - Cross-platform line ending preservation
539
+ - Detailed diff output with statistics
540
+
541
+ **Output:** Detailed diff with statistics. For multi-file operations, includes per-file results and summary statistics with rollback information for atomic operations.
542
+
543
+ **Important:** Use actual newline characters in oldText/newText, NOT escape sequences like `\n`.
377
544
 
378
545
  #### Filesystem Operations
379
546
 
@@ -525,6 +692,76 @@ Execute shell commands with security controls
525
692
 
526
693
  **Security:** All file/directory paths in command arguments are automatically extracted and validated against allowed directories. Commands referencing paths outside approved directories are blocked, preventing directory restriction bypasses.
527
694
 
695
+ ### Multi-File Edit Examples
696
+
697
+ **Batch refactor across multiple files:**
698
+
699
+ ```typescript
700
+ {
701
+ files: [
702
+ {
703
+ path: "src/utils.ts",
704
+ edits: [{
705
+ instruction: "Update deprecated function call",
706
+ oldText: "oldApi.getData()",
707
+ newText: "newApi.fetchData()"
708
+ }]
709
+ },
710
+ {
711
+ path: "src/components/Button.tsx",
712
+ edits: [{
713
+ instruction: "Update component prop",
714
+ oldText: "onClick={oldHandler}",
715
+ newText: "onClick={newHandler}"
716
+ }]
717
+ },
718
+ {
719
+ path: "src/hooks/useData.ts",
720
+ edits: [{
721
+ instruction: "Update hook implementation",
722
+ oldText: "const data = oldApi.getData()",
723
+ newText: "const data = newApi.fetchData()"
724
+ }]
725
+ }
726
+ ],
727
+ failFast: true // Atomic operation - rollback all if any fails
728
+ }
729
+ ```
730
+
731
+ **Per-file configuration:**
732
+
733
+ ```typescript
734
+ {
735
+ files: [
736
+ {
737
+ path: "config.json",
738
+ edits: [{
739
+ oldText: '"version": "1.0.0"',
740
+ newText: '"version": "1.1.0"'
741
+ }],
742
+ matchingStrategy: "exact" // JSON needs exact matches
743
+ },
744
+ {
745
+ path: "src/app.py",
746
+ edits: [{
747
+ oldText: "def old_function():",
748
+ newText: "def new_function():"
749
+ }],
750
+ matchingStrategy: "flexible" // Python indentation may vary
751
+ },
752
+ {
753
+ path: "README.md",
754
+ edits: [{
755
+ oldText: "## Old Section",
756
+ newText: "## New Section"
757
+ }],
758
+ matchingStrategy: "auto" // Let AI decide best strategy
759
+ }
760
+ ],
761
+ failFast: false // Continue even if some files fail
762
+ }
763
+ ```
764
+
528
765
  ---
529
766
 
530
767
  For detailed usage examples, see [Tool Usage Guide](docs/TOOL_USAGE_GUIDE.md)
@@ -3,7 +3,7 @@ import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import path from "path";
4
4
  import { promises as fs } from "fs";
5
5
  import { WriteFileArgsSchema, WriteMultipleFilesArgsSchema, EditFileArgsSchema, } from "../types/index.js";
6
- import { validatePath, writeFileContent, applyFileEdits, } from "../utils/lib.js";
6
+ import { validatePath, writeFileContent, readFileContent, applyFileEdits, } from "../utils/lib.js";
7
7
  import { isHTMLContent, convertHTMLToPDF, convertHTMLToDOCX, } from "../utils/html-to-document.js";
8
8
  const ToolInputSchema = ToolSchema.shape.inputSchema;
9
9
  /**
@@ -53,6 +53,169 @@ async function writeFileBasedOnExtension(validPath, content) {
53
53
  await writeFileContent(validPath, content);
54
54
  }
55
55
  }
56
+ async function processFileEditRequest(request, failOnAmbiguous = true) {
57
+ try {
58
+ const validPath = await validatePath(request.path);
59
+ const result = await applyFileEdits(validPath, request.edits, request.dryRun || false, request.matchingStrategy || "auto", request.failOnAmbiguous !== undefined
60
+ ? request.failOnAmbiguous
61
+ : failOnAmbiguous, true // Return metadata
62
+ );
63
+ if (typeof result === "string") {
64
+ throw new Error("Expected metadata but got string result");
65
+ }
66
+ // Aggregate metadata from all edits
67
+ const totalOccurrences = result.metadata.reduce((sum, r) => sum + r.occurrences, 0);
68
+ const usedStrategies = [...new Set(result.metadata.map((r) => r.strategy))];
69
+ const finalStrategy = result.metadata[result.metadata.length - 1]?.strategy || "exact";
70
+ const warning = result.metadata.find((r) => r.warning)?.warning;
71
+ const ambiguity = result.metadata.find((r) => r.ambiguity)?.ambiguity;
72
+ return {
73
+ path: request.path,
74
+ success: true,
75
+ strategy: finalStrategy,
76
+ occurrences: totalOccurrences,
77
+ diff: result.diff,
78
+ dryRun: request.dryRun,
79
+ };
80
+ }
81
+ catch (error) {
82
+ return {
83
+ path: request.path,
84
+ success: false,
85
+ error: error instanceof Error ? error.message : String(error),
86
+ };
87
+ }
88
+ }
89
+ async function processMultiFileEdits(files, failFast = true) {
90
+ const results = [];
91
+ const rollbackData = [];
92
+ let hasFailures = false;
93
+ try {
94
+ // Process files sequentially if failFast is true, concurrently if false
95
+ if (failFast) {
96
+ // Process sequentially to stop on first failure
97
+ for (const request of files) {
98
+ // For rollback capability, read original content before editing
99
+ let originalContent;
100
+ if (failFast && !request.dryRun) {
101
+ try {
102
+ originalContent = await readFileContent(await validatePath(request.path));
103
+ }
104
+ catch (error) {
105
+ // If we can't read the original content, we can't provide rollback
106
+ // This is acceptable - the file might not exist or be readable
107
+ }
108
+ }
109
+ const result = await processFileEditRequest(request);
110
+ results.push(result);
111
+ // Track successful edits for potential rollback
112
+ if (result.success &&
113
+ !request.dryRun &&
114
+ originalContent !== undefined) {
115
+ rollbackData.push({
116
+ path: request.path,
117
+ originalContent,
118
+ });
119
+ }
120
+ if (!result.success) {
121
+ hasFailures = true;
122
+ // Rollback all previously successful edits
123
+ await performRollback(rollbackData);
124
+ break; // Stop processing remaining files
125
+ }
126
+ }
127
+ }
128
+ else {
129
+ // Process all concurrently, collect all results (no rollback for concurrent mode)
130
+ const promises = files.map((request) => processFileEditRequest(request));
131
+ const allResults = await Promise.allSettled(promises);
132
+ for (let i = 0; i < allResults.length; i++) {
133
+ const settled = allResults[i];
134
+ const request = files[i];
135
+ if (settled.status === "fulfilled") {
136
+ results.push(settled.value);
137
+ if (!settled.value.success)
138
+ hasFailures = true;
139
+ }
140
+ else {
141
+ // Handle unexpected promise rejections
142
+ results.push({
143
+ path: request.path,
144
+ success: false,
145
+ error: `Unexpected error: ${settled.reason}`,
146
+ });
147
+ hasFailures = true;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ catch (error) {
153
+ // If there's an unexpected error during processing, attempt rollback
154
+ if (failFast && rollbackData.length > 0) {
155
+ await performRollback(rollbackData);
156
+ }
157
+ throw error;
158
+ }
159
+ return {
160
+ results,
161
+ summary: {
162
+ total: files.length,
163
+ successful: results.filter((r) => r.success).length,
164
+ failed: results.filter((r) => !r.success).length,
165
+ hasFailures,
166
+ failFast,
167
+ },
168
+ };
169
+ }
170
+ async function performRollback(rollbackData) {
171
+ for (const item of rollbackData.reverse()) {
172
+ // Rollback in reverse order
173
+ try {
174
+ await writeFileContent(item.path, item.originalContent);
175
+ }
176
+ catch (rollbackError) {
177
+ // Log rollback failure but don't throw - we want to attempt all rollbacks
178
+ console.error(`Failed to rollback ${item.path}: ${rollbackError}`);
179
+ }
180
+ }
181
+ }
182
+ function formatMultiFileEditResults(editResults) {
183
+ let output = "";
184
+ // Summary header
185
+ output += `Multi-File Edit Summary:\n`;
186
+ output += `Total files: ${editResults.summary.total}\n`;
187
+ output += `Successful: ${editResults.summary.successful}\n`;
188
+ output += `Failed: ${editResults.summary.failed}\n`;
189
+ output += `Mode: ${editResults.summary.failFast ? "failFast (atomic)" : "continueOnError"}\n`;
190
+ if (editResults.summary.failFast && editResults.summary.hasFailures) {
191
+ output += `⚠️ Atomic operation failed - all successful edits were rolled back\n`;
192
+ }
193
+ output += `\n`;
194
+ // Individual file results
195
+ editResults.results.forEach((result, index) => {
196
+ output += `File ${index + 1}: ${result.path}\n`;
197
+ output += `Status: ${result.success ? "✓ SUCCESS" : "✗ FAILED"}\n`;
198
+ if (result.success) {
199
+ if (result.strategy) {
200
+ output += `Strategy: ${result.strategy}\n`;
201
+ }
202
+ if (result.occurrences !== undefined) {
203
+ output += `Occurrences: ${result.occurrences}\n`;
204
+ }
205
+ if (result.dryRun) {
206
+ output += `Mode: DRY RUN (no changes made)\n`;
207
+ }
208
+ if (result.diff) {
209
+ output += `\n${result.diff}\n`;
210
+ }
211
+ }
212
+ else {
213
+ output += `Error: ${result.error}\n`;
214
+ }
215
+ output += "\n" + "=".repeat(50) + "\n\n";
216
+ });
217
+ return output.trim();
218
+ }
56
219
  export function getWriteTools() {
57
220
  return [
58
221
  {
@@ -80,31 +243,34 @@ export function getWriteTools() {
80
243
  },
81
244
  {
82
245
  name: "edit_file",
83
- description: "Apply precise modifications to text and code files with intelligent matching. " +
84
- "Performs exact text substitution with automatic fallback to flexible whitespace-insensitive matching " +
85
- "and fuzzy token-based matching for maximum reliability. " +
86
- "Supports multiple sequential edits in a single operation. " +
87
- "Provides detailed diff output with change statistics and strategy information. " +
88
- "Preserves original file formatting including indentation and line endings. " +
89
- "\n\n" +
90
- "Matching Strategies (in order when using 'auto'):\n" +
246
+ description: "Apply precise modifications to text and code files with intelligent matching.\n\n" +
247
+ "**Single File Editing (mode: 'single'):**\n" +
248
+ "Edit one file with multiple sequential edits using exact, flexible, or fuzzy matching strategies.\n\n" +
249
+ "**Multi-File Editing (mode: 'multiple'):**\n" +
250
+ "Edit multiple files concurrently in a single operation. Each file can have its own edit configuration.\n\n" +
251
+ "**Matching Strategies:**\n" +
91
252
  "1. Exact: Character-for-character match (fastest, safest)\n" +
92
253
  "2. Flexible: Whitespace-insensitive, preserves original indentation\n" +
93
- "3. Fuzzy: Token-based regex matching for maximum compatibility\n" +
94
- "\n" +
95
- "Best Practices:\n" +
254
+ "3. Fuzzy: Token-based regex matching for maximum compatibility\n\n" +
255
+ "**Features:**\n" +
256
+ "- Concurrent processing for multi-file operations\n" +
257
+ "- Per-file matching strategy control\n" +
258
+ "- Dry-run preview mode\n" +
259
+ "- Detailed diff output with statistics\n" +
260
+ "- Atomic operations with rollback capability\n" +
261
+ "- Cross-platform line ending preservation\n\n" +
262
+ "**Maximum:** 50 files per multi-file operation\n\n" +
263
+ "**Best Practices:**\n" +
96
264
  "- Include 3-5 lines of context before and after the change for reliability\n" +
97
265
  "- Add 'instruction' field to describe the purpose of each edit\n" +
98
266
  "- Use 'dryRun: true' to preview changes before applying\n" +
99
267
  "- For multiple related changes, use array of edits (applied sequentially)\n" +
100
268
  "- Set 'expectedOccurrences' to validate replacement count\n" +
101
- "- Use 'matchingStrategy' to control matching behavior (defaults to 'auto')\n" +
102
- "\n" +
103
- "CRITICAL - Multi-line Content:\n" +
269
+ "- Use 'matchingStrategy' to control matching behavior (defaults to 'auto')\n\n" +
270
+ "**CRITICAL - Multi-line Content:**\n" +
104
271
  "- Use actual newline characters in oldText/newText strings, NOT \\n escape sequences\n" +
105
272
  "- The MCP/JSON layer handles encoding automatically\n" +
106
- "- Using \\n literally will search for/write backslash+n characters (wrong!)\n" +
107
- "\n" +
273
+ "- Using \\n literally will search for/write backslash+n characters (wrong!)\n\n" +
108
274
  "Only works within allowed directories.",
109
275
  inputSchema: zodToJsonSchema(EditFileArgsSchema),
110
276
  },
@@ -146,11 +312,41 @@ export async function handleWriteTool(name, args) {
146
312
  if (!parsed.success) {
147
313
  throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
148
314
  }
149
- const validPath = await validatePath(parsed.data.path);
150
- const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun, parsed.data.matchingStrategy, parsed.data.failOnAmbiguous);
151
- return {
152
- content: [{ type: "text", text: result }],
153
- };
315
+ // Determine mode and route to appropriate handler
316
+ const mode = parsed.data.mode || "single";
317
+ if (mode === "single") {
318
+ // Single file mode (backward compatible)
319
+ if (!parsed.data.path || !parsed.data.edits) {
320
+ throw new Error("Single mode requires 'path' and 'edits' fields");
321
+ }
322
+ const result = await processFileEditRequest({
323
+ path: parsed.data.path,
324
+ edits: parsed.data.edits,
325
+ matchingStrategy: parsed.data.matchingStrategy,
326
+ dryRun: parsed.data.dryRun,
327
+ failOnAmbiguous: parsed.data.failOnAmbiguous,
328
+ });
329
+ if (!result.success) {
330
+ throw new Error(result.error);
331
+ }
332
+ return {
333
+ content: [{ type: "text", text: result.diff }],
334
+ };
335
+ }
336
+ else if (mode === "multiple") {
337
+ // Multi-file mode
338
+ if (!parsed.data.files) {
339
+ throw new Error("Multiple mode requires 'files' field");
340
+ }
341
+ const editResults = await processMultiFileEdits(parsed.data.files, parsed.data.failFast);
342
+ const output = formatMultiFileEditResults(editResults);
343
+ return {
344
+ content: [{ type: "text", text: output }],
345
+ };
346
+ }
347
+ else {
348
+ throw new Error(`Invalid mode: ${mode}`);
349
+ }
154
350
  }
155
351
  case "write_multiple_files": {
156
352
  const parsed = WriteMultipleFilesArgsSchema.safeParse(args);
@@ -192,17 +192,66 @@ export const EditOperation = z.object({
192
192
  "Set to a specific number to validate the replacement count. " +
193
193
  "The tool will fail if actual occurrences don't match this number."),
194
194
  });
195
- export const EditFileArgsSchema = z.object({
196
- path: z.string(),
195
+ // Single file edit request (for multi-file operations)
196
+ export const EditFileRequestSchema = z.object({
197
+ path: z.string().describe("Path to the file to edit"),
197
198
  edits: z
198
199
  .array(EditOperation)
199
200
  .min(1, "At least one edit must be provided")
200
- .describe("Array of edits to apply sequentially"),
201
+ .describe("Array of edits to apply to this file"),
202
+ matchingStrategy: z
203
+ .enum(["exact", "flexible", "fuzzy", "auto"])
204
+ .optional()
205
+ .default("auto")
206
+ .describe("Matching strategy for this file:\n" +
207
+ "- 'exact': Strict character-for-character match (fastest, safest)\n" +
208
+ "- 'flexible': Whitespace-insensitive line-by-line matching\n" +
209
+ "- 'fuzzy': Token-based regex matching (most permissive)\n" +
210
+ "- 'auto': Try exact → flexible → fuzzy (recommended, default)"),
201
211
  dryRun: z
202
212
  .boolean()
203
213
  .optional()
204
214
  .default(false)
205
- .describe("Preview changes using git-style diff format without writing"),
215
+ .describe("Preview changes for this file without writing"),
216
+ failOnAmbiguous: z
217
+ .boolean()
218
+ .optional()
219
+ .default(true)
220
+ .describe("If true, fail when oldText matches multiple locations (unless expectedOccurrences > 1). " +
221
+ "If false, replace first occurrence only and warn about ambiguity."),
222
+ });
223
+ // Enhanced edit file schema (supports both single and multi-file with explicit mode)
224
+ export const EditFileArgsSchema = z
225
+ .object({
226
+ // Mode discriminator
227
+ mode: z
228
+ .enum(["single", "multiple"])
229
+ .optional()
230
+ .default("single")
231
+ .describe("Edit mode: 'single' for one file, 'multiple' for batch editing"),
232
+ // Single file fields (required when mode is "single")
233
+ path: z
234
+ .string()
235
+ .optional()
236
+ .describe("Path to file (required for single mode)"),
237
+ edits: z
238
+ .array(EditOperation)
239
+ .min(1)
240
+ .optional()
241
+ .describe("Array of edits to apply"),
242
+ // Multi-file fields (required when mode is "multiple")
243
+ files: z
244
+ .array(EditFileRequestSchema)
245
+ .min(1, "At least one file must be provided")
246
+ .max(50, "Maximum 50 files per operation")
247
+ .optional()
248
+ .describe("Array of file edit requests (required for multiple mode)"),
249
+ failFast: z
250
+ .boolean()
251
+ .optional()
252
+ .default(true)
253
+ .describe("Stop processing on first file failure (true) or continue with remaining files (false)"),
254
+ // Global options
206
255
  matchingStrategy: z
207
256
  .enum(["exact", "flexible", "fuzzy", "auto"])
208
257
  .optional()
@@ -212,13 +261,31 @@ export const EditFileArgsSchema = z.object({
212
261
  "- 'flexible': Whitespace-insensitive line-by-line matching\n" +
213
262
  "- 'fuzzy': Token-based regex matching (most permissive)\n" +
214
263
  "- 'auto': Try exact → flexible → fuzzy (recommended, default)"),
264
+ dryRun: z
265
+ .boolean()
266
+ .optional()
267
+ .default(false)
268
+ .describe("Preview changes without writing"),
215
269
  failOnAmbiguous: z
216
270
  .boolean()
217
271
  .optional()
218
272
  .default(true)
219
273
  .describe("If true, fail when oldText matches multiple locations (unless expectedOccurrences > 1). " +
220
274
  "If false, replace first occurrence only and warn about ambiguity."),
221
- });
275
+ })
276
+ .refine((data) => {
277
+ // Validate based on mode
278
+ if (data.mode === "single") {
279
+ return data.path && data.edits;
280
+ }
281
+ else if (data.mode === "multiple") {
282
+ return data.files;
283
+ }
284
+ return false;
285
+ }, {
286
+ message: "Invalid configuration: 'path' and 'edits' required for single mode, 'files' required for multiple mode",
287
+ })
288
+ .describe("Edit files with intelligent matching. Supports single file or batch multi-file operations.");
222
289
  export const MakeDirectoryArgsSchema = z.object({
223
290
  paths: z
224
291
  .union([z.string(), z.array(z.string())])
package/dist/utils/lib.js CHANGED
@@ -518,7 +518,7 @@ function applyEditWithStrategy(content, edit, strategy, failOnAmbiguous) {
518
518
  }
519
519
  return result;
520
520
  }
521
- export async function applyFileEdits(filePath, edits, dryRun = false, matchingStrategy = "auto", failOnAmbiguous = true) {
521
+ export async function applyFileEdits(filePath, edits, dryRun = false, matchingStrategy = "auto", failOnAmbiguous = true, returnMetadata) {
522
522
  // Read file content and detect original line ending
523
523
  const rawContent = await fs.readFile(filePath, "utf-8");
524
524
  const originalLineEnding = detectLineEnding(rawContent);
@@ -561,6 +561,12 @@ export async function applyFileEdits(filePath, edits, dryRun = false, matchingSt
561
561
  throw error;
562
562
  }
563
563
  }
564
+ if (returnMetadata) {
565
+ return {
566
+ diff: formattedDiff,
567
+ metadata: editResults
568
+ };
569
+ }
564
570
  return formattedDiff;
565
571
  }
566
572
  // Memory-efficient implementation to get the last N lines of a file
package/package.json CHANGED
@@ -1,17 +1,42 @@
1
1
  {
2
2
  "name": "@n0zer0d4y/vulcan-file-ops",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "MCP server that gives Claude Desktop and other AI assistants filesystem superpowers—read, write, edit, and manage files like AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "Lloyd Barcatan",
7
7
  "homepage": "https://github.com/n0zer0d4y/vulcan-file-ops",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/n0zer0d4y/vulcan-file-ops.git"
10
+ "url": "git+https://github.com/n0zer0d4y/vulcan-file-ops.git"
11
11
  },
12
12
  "bugs": {
13
13
  "url": "https://github.com/n0zer0d4y/vulcan-file-ops/issues"
14
14
  },
15
+ "keywords": [
16
+ "mcp",
17
+ "mcp-server",
18
+ "model-context-protocol",
19
+ "context-engineering",
20
+ "ai",
21
+ "ai-tools",
22
+ "claude",
23
+ "claude-desktop",
24
+ "filesystem",
25
+ "file-operations",
26
+ "file-management",
27
+ "read-files",
28
+ "write-files",
29
+ "edit-files",
30
+ "ai-assistant",
31
+ "llm",
32
+ "llm-tools",
33
+ "pdf",
34
+ "docx",
35
+ "document-parsing",
36
+ "code-editor",
37
+ "typescript",
38
+ "ai-assisted-coding"
39
+ ],
15
40
  "type": "module",
16
41
  "bin": {
17
42
  "vulcan-file-ops": "dist/cli.js"