@n0zer0d4y/vulcan-file-ops 1.2.11 → 1.2.14

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
@@ -9,6 +9,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
 
11
11
 
12
+ ## [1.2.14] - 2026-05-16
13
+
14
+ ### Fixed
15
+
16
+ - **CRITICAL**: Fixed initialization deadlock with `claude-ai` v0.1.0 client (Claude Desktop web client, protocol `2025-11-25`).
17
+ - **Root Cause**: A custom `setRequestHandler(InitializeRequestSchema, ...)` was overriding the SDK's internal `_oninitialize` handler. This broke the SDK's state machine: `_clientCapabilities` and `_clientVersion` were never set, and protocol version negotiation was bypassed — the server echoed back the client's requested version verbatim rather than responding with the highest version it actually supports. The new client's stricter handshake exposed this latent defect.
18
+ - **Fix**: Removed the custom initialize handler and restored the SDK's built-in `_oninitialize`. The server now correctly negotiates `2025-06-18` when a client requests `2025-11-25`, which the client accepts per the MCP spec.
19
+ - **Instructions**: Server instructions (`generateServerDescription()`) are now injected into the SDK's `_instructions` field after directory initialization in `runServer()`, so they remain present in the initialize response without requiring a custom handler.
20
+ - Fixed `oninitialized` callback blocking the MCP handshake. The `listRoots()` call inside `oninitialized` was previously awaited, creating a potential deadlock (client won't respond to `roots/list` until after `initialized` is acknowledged, but the callback blocked acknowledgement). Changed to a detached fire-and-forget promise.
21
+
22
+ ## [1.2.13] - 2026-04-23
23
+
24
+ ### Fixed
25
+
26
+ - Resolved the remaining MCP client tool discovery failure introduced after the SDK upgrade.
27
+ - Tool schemas exposed through `tools/list` are now normalized for stricter MCP clients.
28
+ - Removed client-hostile schema constructs from published tool input schemas, including `$schema`, `$ref`, `anyOf`, `oneOf`, and `allOf`.
29
+ - Replaced the highest-risk wire schemas with explicit compatibility-first object schemas for `attach_image` and `make_directory`.
30
+ - Preserved runtime backward compatibility while tightening the schemas advertised to clients.
31
+ - Fixed residual MCP mode detection drift so stdio mode is triggered reliably in local and `npx` execution paths.
32
+ - Added `zod` as a direct runtime dependency to avoid install-time fragility in clean environments and `npx` usage.
33
+ - Synchronized package metadata so published registry metadata and local package metadata report the same release version.
34
+ - Re-enabled and stabilized the previously skipped PDF-related Jest coverage.
35
+ - Enabled the 10 skipped document tests for PDF read, HTML-to-PDF write, mixed document batches, and PDF round-trip paths.
36
+ - Updated Jest PDF mocks so document tests exercise meaningful content paths instead of placeholder-only buffers.
37
+ - Suppressed expected negative-path test diagnostics inside tests so successful runs no longer look like runtime failures.
38
+ - Added a regression test to prevent incompatible tool schema shapes from reappearing in future releases.
39
+
40
+ ### Changed
41
+
42
+ - Documented the Codex-specific MCP configuration format in the README alongside the JSON-based client examples.
43
+ - Updated `npx` examples to use `-y` explicitly for non-interactive client execution.
44
+
45
+ ## [1.2.12] - 2026-02-21
46
+
47
+ ### Fixed
48
+
49
+ - **CRITICAL**: Fixed MCP server toggle failure and "no tools configured" error caused by Node.js version incompatibility and restrictive MCP detection.
50
+ - **Node.js Compatibility**: Replaced `import ... with { type: "json" }` with manual `fs.readFileSync` for `package.json` to support Node.js versions earlier than 20.10.0 and 18.20.0 (including Node 14/16).
51
+ - **Robust MCP Detection**: Changed TTY detection from AND to OR logic. The server now enters MCP mode (suppressing console output) if *either* stdin or stdout is redirected, improving reliability on Windows when run via `npx`.
52
+ - **Silenced Dotenv**: Added `quiet: true` to `dotenv.config()` to prevent any leakage into the protocol stream.
53
+ - See `local_docs/RCA-MCP-Toggle-Failure-Node-Compatibility-2026-02-21.md` for full details.
54
+
12
55
  ## [1.2.11] - 2026-02-21
13
56
 
14
57
  ### Fixed
package/README.md CHANGED
@@ -124,7 +124,10 @@ This server can be used directly with npx (recommended) or installed globally/lo
124
124
 
125
125
  ### Basic Configuration
126
126
 
127
- Add to your MCP client configuration (e.g., `claude_desktop_config.json`):
127
+ Add to your MCP client configuration.
128
+
129
+ For JSON-based clients such as Claude Desktop and Cursor, use their `mcpServers` JSON format.
130
+ For Codex, use `C:\Users\<username>\.codex\config.toml` and the `mcp_servers` TOML table format shown below.
128
131
 
129
132
  #### Option 1: Using npx (Recommended - No Installation Required)
130
133
 
@@ -133,12 +136,22 @@ Add to your MCP client configuration (e.g., `claude_desktop_config.json`):
133
136
  "mcpServers": {
134
137
  "vulcan-file-ops": {
135
138
  "command": "npx",
136
- "args": ["@n0zer0d4y/vulcan-file-ops"]
139
+ "args": ["-y", "@n0zer0d4y/vulcan-file-ops"]
137
140
  }
138
141
  }
139
142
  }
140
143
  ```
141
144
 
145
+ **Codex (`config.toml`)**
146
+
147
+ ```toml
148
+ [mcp_servers.vulcan_file_ops]
149
+ command = "npx"
150
+ args = ["-y", "@n0zer0d4y/vulcan-file-ops"]
151
+ enabled = true
152
+ startup_timeout_sec = 120.0
153
+ ```
154
+
142
155
  #### Option 2: Using Global Installation
143
156
 
144
157
  After running `npm install -g @n0zer0d4y/vulcan-file-ops`:
@@ -184,14 +197,33 @@ Then configure your MCP client:
184
197
  {
185
198
  "mcpServers": {
186
199
  "vulcan-file-ops": {
187
- "command": "vulcan-file-ops",
188
- "args": ["--approved-folders", "/path/to/your/allowed/directories"]
200
+ "command": "node",
201
+ "args": [
202
+ "/absolute/path/to/vulcan-file-ops/dist/cli.js",
203
+ "--approved-folders",
204
+ "/path/to/your/allowed/directories"
205
+ ]
189
206
  }
190
207
  }
191
208
  }
192
209
  ```
193
210
 
194
- **Note:** The `vulcan-file-ops` command will be available in your PATH after building, or you can use the full path: `./dist/cli.js`
211
+ **Codex (`config.toml`)**
212
+
213
+ ```toml
214
+ [mcp_servers.vulcan_file_ops]
215
+ command = "node"
216
+ args = [
217
+ 'C:\absolute\path\to\vulcan-file-ops\dist\cli.js',
218
+ "--approved-folders",
219
+ 'C:\path\to\your\allowed\directories'
220
+ ]
221
+ cwd = 'C:\absolute\path\to\vulcan-file-ops'
222
+ enabled = true
223
+ startup_timeout_sec = 120.0
224
+ ```
225
+
226
+ **Note:** For local repository execution, prefer `node dist/cli.js` with an absolute path. This works reliably in Codex and avoids PATH ambiguity.
195
227
 
196
228
  ### Advanced Configuration
197
229
 
@@ -207,6 +239,7 @@ Pre-configure specific directories for immediate access on server start:
207
239
  "vulcan-file-ops": {
208
240
  "command": "npx",
209
241
  "args": [
242
+ "-y",
210
243
  "@n0zer0d4y/vulcan-file-ops",
211
244
  "--approved-folders",
212
245
  "/Users/username/projects",
@@ -225,6 +258,7 @@ Pre-configure specific directories for immediate access on server start:
225
258
  "vulcan-file-ops": {
226
259
  "command": "npx",
227
260
  "args": [
261
+ "-y",
228
262
  "@n0zer0d4y/vulcan-file-ops",
229
263
  "--approved-folders",
230
264
  "C:/Users/username/projects",
@@ -254,6 +288,22 @@ For users running from a cloned repository (after `npm run build`):
254
288
  }
255
289
  ```
256
290
 
291
+ **Codex with Approved Folders (`config.toml`)**
292
+
293
+ ```toml
294
+ [mcp_servers.vulcan_file_ops]
295
+ command = "node"
296
+ args = [
297
+ 'C:\absolute\path\to\vulcan-file-ops\dist\cli.js',
298
+ "--approved-folders",
299
+ 'C:\Users\username\projects',
300
+ 'C:\Users\username\documents'
301
+ ]
302
+ cwd = 'C:\absolute\path\to\vulcan-file-ops'
303
+ enabled = true
304
+ startup_timeout_sec = 120.0
305
+ ```
306
+
257
307
  **Path Format Note:**
258
308
 
259
309
  - **Windows**: Include drive letter (e.g., `C:/`, `D:/`). Use forward slashes in JSON to avoid escaping backslashes.
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // MCP servers use stdin/stdout for JSON-RPC via stdio transport
4
4
  // Detection: stdin/stdout are NOT TTY (piped/redirected) OR explicit MCP flags are present.
5
5
  // Note: We exclude help/version flags to allow CLI usage even when piped.
6
- const isMCP = ((!process.stdin.isTTY && !process.stdout.isTTY) ||
6
+ const isMCP = ((!process.stdin.isTTY || !process.stdout.isTTY) ||
7
7
  process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio") || arg.includes("inspector"))) &&
8
8
  !process.argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
9
9
  if (isMCP) {
@@ -3,7 +3,7 @@
3
3
  // due to ES Module static import execution order (imports are evaluated BEFORE top-level code).
4
4
  // Detection: stdin/stdout are NOT TTY (piped/redirected) OR explicit MCP flags are present.
5
5
  // Note: We exclude help/version flags to allow CLI usage even when piped.
6
- const _isMCP = ((!process.stdin.isTTY && !process.stdout.isTTY) ||
6
+ const _isMCP = ((!process.stdin.isTTY || !process.stdout.isTTY) ||
7
7
  process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio") || arg.includes("inspector"))) &&
8
8
  !process.argv.some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v");
9
9
  if (_isMCP) {
@@ -16,11 +16,16 @@ if (_isMCP) {
16
16
  }
17
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
- import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema, PingRequestSchema, RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, PingRequestSchema, RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
20
20
  import fs from "fs/promises";
21
+ import { readFileSync } from "fs";
21
22
  import path from "path";
23
+ import { fileURLToPath } from "url";
22
24
  import dotenv from "dotenv";
23
- import packageJson from "../../package.json" with { type: "json" };
25
+ // Manual JSON import for compatibility with older Node.js versions
26
+ // (native 'import ... with { type: "json" }' requires Node 20.10+ or 18.20+)
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+ const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"));
24
29
  import { normalizePath, expandHome } from "../utils/path-utils.js";
25
30
  import { getValidRootDirectories } from "../utils/roots-utils.js";
26
31
  import { setAllowedDirectories, getAllowedDirectories, setIgnoredFolders, setEnabledTools, } from "../utils/lib.js";
@@ -336,7 +341,7 @@ async function initializeDirectories() {
336
341
  // Priority 2: Load from .env file
337
342
  try {
338
343
  const envPath = path.join(process.cwd(), ".env");
339
- dotenv.config({ path: envPath });
344
+ dotenv.config({ path: envPath, quiet: true });
340
345
  if (process.env.APPROVED_COMMANDS) {
341
346
  finalApprovedCommands = process.env.APPROVED_COMMANDS.split(",")
342
347
  .map((c) => c.trim())
@@ -394,26 +399,11 @@ const server = new Server({
394
399
  // receive "Method not found" errors, and fail to enable the toggle
395
400
  },
396
401
  });
397
- // Initialize handler - required for MCP protocol
398
- server.setRequestHandler(InitializeRequestSchema, async (request) => {
399
- const clientCapabilities = request.params.capabilities;
400
- return {
401
- // CRITICAL: Return the protocol version the CLIENT requested, not LATEST
402
- // Claude Desktop only supports 2025-06-18 and will disconnect if we return 2025-11-25
403
- protocolVersion: request.params.protocolVersion,
404
- capabilities: {
405
- tools: {
406
- listChanged: true,
407
- },
408
- // CRITICAL: Do NOT declare resources or prompts - we don't implement them
409
- },
410
- serverInfo: {
411
- name: "vulcan-file-ops",
412
- version: VERSION,
413
- },
414
- instructions: generateServerDescription(),
415
- };
416
- });
402
+ // The SDK's built-in initialize handler (_oninitialize) is left in place.
403
+ // Overriding it via setRequestHandler breaks the SDK's internal state machine:
404
+ // _clientCapabilities and _clientVersion never get set, and protocol version
405
+ // negotiation is bypassed (should respond with max supported ≤ client's requested).
406
+ // Instructions are injected at runtime in runServer() after directories initialize.
417
407
  // Ping handler - for health checks
418
408
  server.setRequestHandler(PingRequestSchema, async () => {
419
409
  return {};
@@ -618,18 +608,19 @@ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
618
608
  }
619
609
  });
620
610
  // Handles post-initialization setup, specifically checking for and fetching MCP roots.
621
- server.oninitialized = async () => {
611
+ // listRoots() is fired detached (no await) so it doesn't block the initialize handshake.
612
+ // Blocking here deadlocks: the client won't respond to roots/list until after initialize
613
+ // completes, but initialize won't complete until this callback returns.
614
+ server.oninitialized = () => {
622
615
  const clientCapabilities = server.getClientCapabilities();
623
616
  if (clientCapabilities?.roots) {
624
- try {
625
- const response = await server.listRoots();
617
+ server.listRoots().then(async (response) => {
626
618
  if (response && "roots" in response) {
627
619
  await updateAllowedDirectoriesFromRoots(response.roots);
628
620
  }
629
- }
630
- catch (error) {
621
+ }).catch(() => {
631
622
  // Silently handle errors - dynamic access will work via register_directory tool
632
- }
623
+ });
633
624
  }
634
625
  };
635
626
  // Start server
@@ -643,14 +634,20 @@ export async function runServer() {
643
634
  catch (error) {
644
635
  // In MCP mode, don't crash the server on init errors
645
636
  // Just continue with empty configuration
646
- const isMCP = (!process.stdin.isTTY && !process.stdout.isTTY) ||
647
- process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio"));
637
+ const isMCP = (!process.stdin.isTTY || !process.stdout.isTTY) ||
638
+ process.argv.some((arg) => arg.includes("mcp") ||
639
+ arg.includes("stdio") ||
640
+ arg.includes("inspector"));
648
641
  if (!isMCP) {
649
642
  // In non-MCP mode, we can show errors and exit
650
643
  throw error;
651
644
  }
652
645
  // In MCP mode, silently continue - server can work without approved folders
653
646
  }
647
+ // Inject instructions now that directories are known. The SDK's built-in
648
+ // _oninitialize reads this._instructions when building the initialize response.
649
+ server._instructions =
650
+ generateServerDescription();
654
651
  const transport = new StdioServerTransport();
655
652
  await server.connect(transport);
656
653
  // Minimal logging to avoid issues with MCP clients
@@ -7,6 +7,7 @@ import { expandHome, normalizePath } from "../utils/path-utils.js";
7
7
  import { isPathWithinAllowedDirectories } from "../utils/path-validation.js";
8
8
  import { MakeDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, RegisterDirectoryArgsSchema, FileOperationsArgsSchema, DeleteFilesArgsSchema, } from "../types/index.js";
9
9
  import { validatePath, getFileStats, formatSize, getAllowedDirectories, setAllowedDirectories, shouldIgnoreFolder, } from "../utils/lib.js";
10
+ import { createEmptyObjectSchema, createPathArraySchema, sanitizeToolInputSchema, } from "../utils/tool-schema.js";
10
11
  const ToolInputSchema = ToolSchema.shape.inputSchema;
11
12
  export function getFileSystemTools() {
12
13
  // Get current allowed directories for dynamic descriptions
@@ -25,7 +26,14 @@ export function getFileSystemTools() {
25
26
  description: "Create single or multiple directories with recursive parent creation " +
26
27
  "(like Unix 'mkdir -p'). Idempotent - won't error if directories exist. " +
27
28
  "Only works within allowed directories.",
28
- inputSchema: zodToJsonSchema(MakeDirectoryArgsSchema),
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ paths: createPathArraySchema("Directory paths to create. For maximum MCP client compatibility, provide an array even when creating a single directory."),
33
+ },
34
+ required: ["paths"],
35
+ additionalProperties: false,
36
+ },
29
37
  },
30
38
  {
31
39
  name: "list_directory",
@@ -34,7 +42,7 @@ export function getFileSystemTools() {
34
42
  "Supports simple listings, detailed views with sizes/timestamps, hierarchical " +
35
43
  "tree display, and structured JSON output. Automatically filters globally " +
36
44
  "configured ignored folders. Only works within allowed directories.",
37
- inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
45
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(ListDirectoryArgsSchema)),
38
46
  },
39
47
  {
40
48
  name: "move_file",
@@ -43,7 +51,7 @@ export function getFileSystemTools() {
43
51
  "Fails safely if the destination path already exists to prevent accidental overwrites. " +
44
52
  "Can also perform simple same-directory renames. " +
45
53
  "Both source and destination must be within allowed directories.",
46
- inputSchema: zodToJsonSchema(MoveFileArgsSchema),
54
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(MoveFileArgsSchema)),
47
55
  },
48
56
  {
49
57
  name: "get_file_info",
@@ -51,7 +59,7 @@ export function getFileSystemTools() {
51
59
  "Provides detailed information including size, timestamps (creation and last modification), permissions, and entry type. " +
52
60
  "Perfect for inspecting file properties and attributes without accessing the actual content. " +
53
61
  "Only works within allowed directories.",
54
- inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
62
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(GetFileInfoArgsSchema)),
55
63
  },
56
64
  {
57
65
  name: "register_directory",
@@ -59,7 +67,7 @@ export function getFileSystemTools() {
59
67
  "to directories specified by the human user during conversation. The directory " +
60
68
  "and all its subdirectories will become accessible for all filesystem operations." +
61
69
  generateApprovedDirsText(),
62
- inputSchema: zodToJsonSchema(RegisterDirectoryArgsSchema),
70
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(RegisterDirectoryArgsSchema)),
63
71
  },
64
72
  {
65
73
  name: "list_allowed_directories",
@@ -67,11 +75,7 @@ export function getFileSystemTools() {
67
75
  "Note that subdirectories within listed paths are implicitly accessible as well. " +
68
76
  "Use this to determine available filesystem scope and plan operations accordingly before attempting file access." +
69
77
  generateApprovedDirsText(),
70
- inputSchema: {
71
- type: "object",
72
- properties: {},
73
- required: [],
74
- },
78
+ inputSchema: createEmptyObjectSchema(),
75
79
  },
76
80
  {
77
81
  name: "file_operations",
@@ -126,7 +130,7 @@ export function getFileSystemTools() {
126
130
  "Operations are processed concurrently for performance. " +
127
131
  "Maximum 100 paths per operation. " +
128
132
  "Only works within allowed directories.",
129
- inputSchema: zodToJsonSchema(DeleteFilesArgsSchema),
133
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(DeleteFilesArgsSchema)),
130
134
  },
131
135
  ];
132
136
  }
@@ -5,6 +5,7 @@ import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { ReadFileArgsSchema, AttachImageArgsSchema, ReadMultipleFilesArgsSchema, } from "../types/index.js";
6
6
  import { validatePath, readFileContent, tailFile, headFile, rangeFile, } from "../utils/lib.js";
7
7
  import { isDocumentFile, parseDocument } from "../utils/document-parser.js";
8
+ import { createPathArraySchema, sanitizeToolInputSchema, } from "../utils/tool-schema.js";
8
9
  const ToolInputSchema = ToolSchema.shape.inputSchema;
9
10
  // Reads a file as a stream of buffers, concatenates them, and then encodes
10
11
  // the result to a Base64 string. This is a memory-efficient way to handle
@@ -34,7 +35,7 @@ export function getReadTools() {
34
35
  "range (lines from startLine to endLine, inclusive, 1-indexed). " +
35
36
  "Document files ignore mode parameters and always return full content. " +
36
37
  "Only works within allowed directories.",
37
- inputSchema: zodToJsonSchema(ReadFileArgsSchema),
38
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(ReadFileArgsSchema)),
38
39
  },
39
40
  {
40
41
  name: "attach_image",
@@ -45,7 +46,14 @@ export function getReadTools() {
45
46
  "Supports PNG, JPEG, GIF, WebP, BMP, and SVG formats. " +
46
47
  "Note: This requires the MCP client to support vision capabilities. " +
47
48
  "Only works within allowed directories.",
48
- inputSchema: zodToJsonSchema(AttachImageArgsSchema),
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ path: createPathArraySchema("Path(s) to image file(s) to attach for AI vision analysis. For maximum MCP client compatibility, provide an array even when attaching a single image."),
53
+ },
54
+ required: ["path"],
55
+ additionalProperties: false,
56
+ },
49
57
  },
50
58
  {
51
59
  name: "read_multiple_files",
@@ -56,7 +64,7 @@ export function getReadTools() {
56
64
  "Document files ignore mode parameters and return full content. " +
57
65
  "Processes files concurrently for performance. Maximum 50 files per operation. " +
58
66
  "Only works within allowed directories.",
59
- inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema),
67
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(ReadMultipleFilesArgsSchema)),
60
68
  },
61
69
  ];
62
70
  }
@@ -2,6 +2,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { SearchFilesArgsSchema, GrepArgsSchema, } from "../types/index.js";
4
4
  import { validatePath, searchFilesWithValidation, grepFilesWithValidation, getAllowedDirectories, getIgnoredFolders, } from "../utils/lib.js";
5
+ import { sanitizeToolInputSchema } from "../utils/tool-schema.js";
5
6
  const ToolInputSchema = ToolSchema.shape.inputSchema;
6
7
  export function getSearchTools() {
7
8
  return [
@@ -12,7 +13,7 @@ export function getSearchTools() {
12
13
  "Use simple patterns like '*.ext' for current directory matches, or '**/*.ext' for deep subdirectory searches. " +
13
14
  "Returns absolute paths to all discovered items. Excellent for locating files when exact paths are unknown. " +
14
15
  "Only searches within allowed directories.",
15
- inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
16
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(SearchFilesArgsSchema)),
16
17
  },
17
18
  {
18
19
  name: "grep_files",
@@ -20,7 +21,7 @@ export function getSearchTools() {
20
21
  "Supports: regex patterns, case-insensitive (-i), context lines (-A/-B/-C), file type filters (type: js/py/ts/etc), glob patterns, multiline mode. " +
21
22
  "Output modes: content (lines+context), files_with_matches (paths only), count (match counts). " +
22
23
  "Respects ignored folders. Use head_limit to cap results. Only searches within allowed directories.",
23
- inputSchema: zodToJsonSchema(GrepArgsSchema),
24
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(GrepArgsSchema)),
24
25
  },
25
26
  ];
26
27
  }
@@ -6,6 +6,7 @@ import { executeShellCommand, } from "../utils/shell-execution.js";
6
6
  import { validatePath, getAllowedDirectories } from "../utils/lib.js";
7
7
  import { extractPathsFromCommand } from "../utils/command-path-extraction.js";
8
8
  import { isPathWithinAllowedDirectories } from "../utils/path-validation.js";
9
+ import { sanitizeToolInputSchema } from "../utils/tool-schema.js";
9
10
  const ToolInputSchema = ToolSchema.shape.inputSchema;
10
11
  // Global state for approved commands
11
12
  let approvedCommands = new Set();
@@ -62,7 +63,7 @@ export function getShellTools() {
62
63
  `If no workdir is specified, the server's current working directory will be used and validated.` +
63
64
  approvedCommandsText +
64
65
  `\n\nIMPORTANT: Always provide a clear description of what the command does and why it's needed.`,
65
- inputSchema: zodToJsonSchema(ShellCommandArgsSchema),
66
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(ShellCommandArgsSchema)),
66
67
  },
67
68
  ];
68
69
  }
@@ -5,6 +5,7 @@ import { promises as fs } from "fs";
5
5
  import { WriteFileArgsSchema, WriteMultipleFilesArgsSchema, EditFileArgsSchema, } from "../types/index.js";
6
6
  import { validatePath, writeFileContent, readFileContent, applyFileEdits, } from "../utils/lib.js";
7
7
  import { isHTMLContent, convertHTMLToPDF, convertHTMLToDOCX, } from "../utils/html-to-document.js";
8
+ import { sanitizeToolInputSchema } from "../utils/tool-schema.js";
8
9
  const ToolInputSchema = ToolSchema.shape.inputSchema;
9
10
  /**
10
11
  * Helper function to write file content based on file extension
@@ -261,7 +262,7 @@ export function getWriteTools() {
261
262
  "- Correct: Use actual line breaks in your JSON string value\n" +
262
263
  "\n" +
263
264
  "Only works within allowed directories.",
264
- inputSchema: zodToJsonSchema(WriteFileArgsSchema),
265
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(WriteFileArgsSchema)),
265
266
  },
266
267
  {
267
268
  name: "edit_file",
@@ -294,7 +295,7 @@ export function getWriteTools() {
294
295
  "- The MCP/JSON layer handles encoding automatically\n" +
295
296
  "- Using \\n literally will search for/write backslash+n characters (wrong!)\n\n" +
296
297
  "Only works within allowed directories.",
297
- inputSchema: zodToJsonSchema(EditFileArgsSchema),
298
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(EditFileArgsSchema)),
298
299
  },
299
300
  {
300
301
  name: "write_multiple_files",
@@ -310,7 +311,7 @@ export function getWriteTools() {
310
311
  "- Each file's content will be written exactly as provided in the string\n" +
311
312
  "\n" +
312
313
  "Only works within allowed directories.",
313
- inputSchema: zodToJsonSchema(WriteMultipleFilesArgsSchema),
314
+ inputSchema: sanitizeToolInputSchema(zodToJsonSchema(WriteMultipleFilesArgsSchema)),
314
315
  },
315
316
  ];
316
317
  }
@@ -0,0 +1,87 @@
1
+ function cloneValue(value) {
2
+ return JSON.parse(JSON.stringify(value));
3
+ }
4
+ function unescapeJsonPointerSegment(segment) {
5
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
6
+ }
7
+ function resolveLocalRef(root, ref) {
8
+ if (!ref.startsWith("#/")) {
9
+ return undefined;
10
+ }
11
+ const segments = ref
12
+ .slice(2)
13
+ .split("/")
14
+ .map((segment) => unescapeJsonPointerSegment(segment));
15
+ let current = root;
16
+ for (const segment of segments) {
17
+ if (current == null || typeof current !== "object" || !(segment in current)) {
18
+ return undefined;
19
+ }
20
+ current = current[segment];
21
+ }
22
+ return current;
23
+ }
24
+ function sanitizeSchemaNode(node, root) {
25
+ if (Array.isArray(node)) {
26
+ return node.map((item) => sanitizeSchemaNode(item, root));
27
+ }
28
+ if (!node || typeof node !== "object") {
29
+ return node;
30
+ }
31
+ const schema = node;
32
+ if (typeof schema.$ref === "string") {
33
+ const resolved = resolveLocalRef(root, schema.$ref);
34
+ if (resolved && typeof resolved === "object") {
35
+ const { $ref: _ignoredRef, ...siblings } = schema;
36
+ return sanitizeSchemaNode({
37
+ ...cloneValue(resolved),
38
+ ...siblings,
39
+ }, root);
40
+ }
41
+ }
42
+ const result = {};
43
+ for (const [key, value] of Object.entries(schema)) {
44
+ if (key === "$schema" || key === "$defs" || key === "definitions") {
45
+ continue;
46
+ }
47
+ result[key] = sanitizeSchemaNode(value, root);
48
+ }
49
+ return result;
50
+ }
51
+ export function sanitizeToolInputSchema(schema) {
52
+ const cloned = cloneValue(schema);
53
+ const sanitized = sanitizeSchemaNode(cloned, cloned);
54
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
55
+ return {
56
+ type: "object",
57
+ additionalProperties: false,
58
+ };
59
+ }
60
+ const normalized = sanitized;
61
+ if (normalized.type !== "object") {
62
+ normalized.type = "object";
63
+ }
64
+ if (!("additionalProperties" in normalized)) {
65
+ normalized.additionalProperties = false;
66
+ }
67
+ return normalized;
68
+ }
69
+ export function createEmptyObjectSchema() {
70
+ return {
71
+ type: "object",
72
+ properties: {},
73
+ required: [],
74
+ additionalProperties: false,
75
+ };
76
+ }
77
+ export function createPathArraySchema(description) {
78
+ return {
79
+ type: "array",
80
+ items: {
81
+ type: "string",
82
+ },
83
+ minItems: 1,
84
+ description,
85
+ };
86
+ }
87
+ //# sourceMappingURL=tool-schema.js.map
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@n0zer0d4y/vulcan-file-ops",
3
- "version": "1.2.11",
3
+ "version": "1.2.14",
4
4
  "mcpName": "io.github.n0zer0d4y/vulcan-file-ops",
5
5
  "description": "MCP server for AI assistants: read, write, edit, and manage files securely on local filesystem.",
6
6
  "license": "MIT",
7
- "author": "Lloyd Barcatan",
7
+ "author": "n0zer0d4y",
8
8
  "homepage": "https://github.com/n0zer0d4y/vulcan-file-ops",
9
9
  "repository": {
10
10
  "type": "git",
@@ -79,6 +79,7 @@
79
79
  "pdf-lib": "^1.17.1",
80
80
  "pdf-parse": "^2.4.5",
81
81
  "pdfmake": "^0.2.20",
82
+ "zod": "^3.25.76",
82
83
  "zod-to-json-schema": "^3.25.1"
83
84
  },
84
85
  "devDependencies": {