@n0zer0d4y/vulcan-file-ops 1.0.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/dist/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // CRITICAL: Detect MCP mode and suppress console output BEFORE any imports
3
+ // MCP servers use stdin/stdout for JSON-RPC via stdio transport
4
+ // Detection: stdin/stdout are NOT TTY (piped/redirected) = MCP mode
5
+ const isMCP = (!process.stdin.isTTY && !process.stdout.isTTY) ||
6
+ process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio"));
7
+ if (isMCP) {
8
+ // Suppress all console methods (but NOT stdout/stderr streams - MCP SDK needs those)
9
+ const noop = () => { };
10
+ console.log = noop;
11
+ console.error = noop;
12
+ console.warn = noop;
13
+ console.info = noop;
14
+ console.debug = noop;
15
+ // NOTE: Do NOT redirect process.stdout.write or process.stderr.write
16
+ // as the MCP SDK uses those for JSON-RPC protocol communication
17
+ }
18
+ import { runServer } from "./server/index.js";
19
+ // Run the server and handle any fatal errors
20
+ runServer().catch((error) => {
21
+ // Only show errors when not running under MCP (to avoid protocol corruption)
22
+ if (!isMCP) {
23
+ // Restore console.error temporarily for fatal errors
24
+ const originalError = console.error;
25
+ if (originalError.toString() !== "() => {}") {
26
+ originalError("Fatal error running server:", error);
27
+ }
28
+ }
29
+ process.exit(1);
30
+ });
31
+ //# sourceMappingURL=cli.js.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Main entry point for Vulcan File Ops MCP Server
2
+ export { runServer } from "./server/index.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema, PingRequestSchema, RootsListChangedNotificationSchema, LATEST_PROTOCOL_VERSION, } from "@modelcontextprotocol/sdk/types.js";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+ import dotenv from "dotenv";
8
+ import packageJson from "../../package.json" with { type: "json" };
9
+ import { normalizePath, expandHome } from "../utils/path-utils.js";
10
+ import { getValidRootDirectories } from "../utils/roots-utils.js";
11
+ import { setAllowedDirectories, getAllowedDirectories, setIgnoredFolders, setEnabledTools, } from "../utils/lib.js";
12
+ // Single source of truth for version - imported from package.json
13
+ const VERSION = packageJson.version;
14
+ // Import tool handlers
15
+ import { getReadTools } from "../tools/read-tools.js";
16
+ import { getWriteTools } from "../tools/write-tools.js";
17
+ import { getFileSystemTools } from "../tools/filesystem-tools.js";
18
+ import { getSearchTools } from "../tools/search-tools.js";
19
+ import { initializeShellTool, getShellTools } from "../tools/shell-tool.js";
20
+ // Configuration storage
21
+ let allowedDirectories = [];
22
+ let approvedFoldersFromArgs = [];
23
+ let approvedCommandsFromArgs = [];
24
+ let ignoredFolders = [];
25
+ let enabledToolCategories = [];
26
+ let enabledTools = [];
27
+ // Command line argument parsing
28
+ function parseArguments() {
29
+ const args = process.argv.slice(2);
30
+ const directories = [];
31
+ let parsingIgnoredFolders = false;
32
+ let parsingEnabledToolCategories = false;
33
+ let parsingEnabledTools = false;
34
+ let parsingApprovedFolders = false;
35
+ let parsingApprovedCommands = false;
36
+ // Handle help flag
37
+ if (args.includes("--help") || args.includes("-h")) {
38
+ console.error(`Vulcan File Ops MCP Server v${VERSION}`);
39
+ console.error("");
40
+ console.error("Usage: vulcan-file-ops [options]");
41
+ console.error("");
42
+ console.error("Options:");
43
+ console.error(" --approved-folders <dirs...> Pre-configure allowed directories (comma-separated)");
44
+ console.error(" --ignored-folders <dirs...> Exclude directories from listings (comma-separated)");
45
+ console.error(" --enabled-tool-categories <cats...> Enable specific tool categories (comma-separated)");
46
+ console.error(" --enabled-tools <tools...> Enable specific tools (comma-separated)");
47
+ console.error(" --approved-commands <cmds...> Allow specific shell commands (comma-separated)");
48
+ console.error(" --help, -h Show this help message");
49
+ console.error(" --version, -v Show version information");
50
+ console.error("");
51
+ console.error("Legacy usage (deprecated):");
52
+ console.error(" vulcan-file-ops <directory1> <directory2> ...");
53
+ console.error("");
54
+ console.error("For MCP configuration, use your client's MCP settings to specify approved folders.");
55
+ process.exit(0);
56
+ }
57
+ // Handle version flag
58
+ if (args.includes("--version") || args.includes("-v")) {
59
+ console.error(`vulcan-file-ops v${VERSION}`);
60
+ process.exit(0);
61
+ }
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+ if (arg === "--approved-folders") {
65
+ parsingApprovedFolders = true;
66
+ parsingIgnoredFolders = false;
67
+ parsingEnabledToolCategories = false;
68
+ parsingEnabledTools = false;
69
+ parsingApprovedCommands = false;
70
+ // Look ahead to collect all non-flag arguments
71
+ const folders = [];
72
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
73
+ folders.push(args[i + 1]);
74
+ i++;
75
+ }
76
+ // Also support comma-separated format for backward compatibility
77
+ approvedFoldersFromArgs = folders.flatMap((f) => f
78
+ .split(",")
79
+ .map((dir) => dir.trim())
80
+ .filter((dir) => dir.length > 0));
81
+ continue;
82
+ }
83
+ if (arg === "--ignored-folders") {
84
+ parsingIgnoredFolders = true;
85
+ parsingApprovedFolders = false;
86
+ parsingEnabledToolCategories = false;
87
+ parsingEnabledTools = false;
88
+ parsingApprovedCommands = false;
89
+ // Look ahead to collect all non-flag arguments
90
+ const folders = [];
91
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
92
+ folders.push(args[i + 1]);
93
+ i++;
94
+ }
95
+ // Also support comma-separated format for backward compatibility
96
+ ignoredFolders = folders.flatMap((f) => f
97
+ .split(",")
98
+ .map((dir) => dir.trim())
99
+ .filter((dir) => dir.length > 0));
100
+ continue;
101
+ }
102
+ if (arg === "--enabled-tool-categories") {
103
+ parsingEnabledToolCategories = true;
104
+ parsingIgnoredFolders = false;
105
+ parsingApprovedFolders = false;
106
+ parsingEnabledTools = false;
107
+ parsingApprovedCommands = false;
108
+ // Look ahead to collect all non-flag arguments
109
+ const categories = [];
110
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
111
+ categories.push(args[i + 1]);
112
+ i++;
113
+ }
114
+ // Also support comma-separated format for backward compatibility
115
+ enabledToolCategories = categories.flatMap((c) => c
116
+ .split(",")
117
+ .map((cat) => cat.trim())
118
+ .filter((cat) => cat.length > 0));
119
+ continue;
120
+ }
121
+ if (arg === "--enabled-tools") {
122
+ parsingEnabledTools = true;
123
+ parsingIgnoredFolders = false;
124
+ parsingApprovedFolders = false;
125
+ parsingEnabledToolCategories = false;
126
+ parsingApprovedCommands = false;
127
+ // Look ahead to collect all non-flag arguments
128
+ const tools = [];
129
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
130
+ tools.push(args[i + 1]);
131
+ i++;
132
+ }
133
+ // Also support comma-separated format for backward compatibility
134
+ enabledTools = tools.flatMap((t) => t
135
+ .split(",")
136
+ .map((tool) => tool.trim())
137
+ .filter((tool) => tool.length > 0));
138
+ continue;
139
+ }
140
+ if (arg === "--approved-commands") {
141
+ parsingApprovedCommands = true;
142
+ parsingIgnoredFolders = false;
143
+ parsingApprovedFolders = false;
144
+ parsingEnabledToolCategories = false;
145
+ parsingEnabledTools = false;
146
+ // Look ahead to collect all non-flag arguments
147
+ const commands = [];
148
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
149
+ commands.push(args[i + 1]);
150
+ i++;
151
+ }
152
+ // Also support comma-separated format for backward compatibility
153
+ approvedCommandsFromArgs = commands.flatMap((c) => c
154
+ .split(",")
155
+ .map((cmd) => cmd.trim())
156
+ .filter((cmd) => cmd.length > 0));
157
+ continue;
158
+ }
159
+ // If we're not parsing a flag value, this might be an unrecognized argument
160
+ if (!parsingIgnoredFolders &&
161
+ !parsingApprovedFolders &&
162
+ !parsingEnabledToolCategories &&
163
+ !parsingEnabledTools &&
164
+ !parsingApprovedCommands) {
165
+ // Check if this looks like an unrecognized flag
166
+ if (arg.startsWith("-")) {
167
+ console.error(`Error: Unrecognized option '${arg}'`);
168
+ console.error("Run with --help for usage information.");
169
+ process.exit(1);
170
+ }
171
+ // For backward compatibility, treat non-flag arguments as directories
172
+ // But log a warning that this usage is deprecated
173
+ console.error(`Warning: Treating '${arg}' as a directory. This usage is deprecated.`);
174
+ console.error("Use --approved-folders instead for better MCP client compatibility.");
175
+ directories.push(arg);
176
+ }
177
+ // Reset parsing flags
178
+ parsingIgnoredFolders = false;
179
+ parsingApprovedFolders = false;
180
+ parsingEnabledToolCategories = false;
181
+ parsingEnabledTools = false;
182
+ parsingApprovedCommands = false;
183
+ }
184
+ return directories;
185
+ }
186
+ const directoryArgs = parseArguments();
187
+ // Async initialization function to be called in runServer()
188
+ async function initializeDirectories() {
189
+ // Detect MCP mode: stdin/stdout are NOT TTY (piped) = MCP mode
190
+ const isMCP = (!process.stdin.isTTY && !process.stdout.isTTY) ||
191
+ process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio"));
192
+ // During MCP operation, suppress ALL console output to prevent protocol corruption
193
+ if (isMCP) {
194
+ const noop = () => { };
195
+ console.error = noop;
196
+ console.log = noop;
197
+ console.warn = noop;
198
+ console.info = noop;
199
+ console.debug = noop;
200
+ }
201
+ if (!isMCP &&
202
+ (approvedFoldersFromArgs.length > 0 ||
203
+ directoryArgs.length > 0 ||
204
+ ignoredFolders.length > 0 ||
205
+ enabledToolCategories.length > 0 ||
206
+ enabledTools.length > 0)) {
207
+ console.error("Configuration:");
208
+ if (approvedFoldersFromArgs.length > 0) {
209
+ console.error(` Approved folders: ${approvedFoldersFromArgs.join(", ")}`);
210
+ }
211
+ if (directoryArgs.length > 0) {
212
+ console.error(` Directories: ${directoryArgs.join(", ")}`);
213
+ }
214
+ if (ignoredFolders.length > 0) {
215
+ console.error(` Ignored folders: ${ignoredFolders.join(", ")}`);
216
+ }
217
+ if (enabledToolCategories.length > 0) {
218
+ console.error(` Enabled tool categories: ${enabledToolCategories.join(", ")}`);
219
+ }
220
+ if (enabledTools.length > 0) {
221
+ console.error(` Enabled tools: ${enabledTools.join(", ")}`);
222
+ }
223
+ console.error("");
224
+ }
225
+ // Process approved folders from CLI arguments (--approved-folders)
226
+ if (approvedFoldersFromArgs.length > 0) {
227
+ // Store approved directories in normalized and resolved form
228
+ allowedDirectories = await Promise.all(approvedFoldersFromArgs.map(async (dir) => {
229
+ const expanded = expandHome(dir);
230
+ const absolute = path.resolve(expanded);
231
+ try {
232
+ // Security: Resolve symlinks in allowed directories during startup
233
+ // This ensures we know the real paths and can validate against them later
234
+ const resolved = await fs.realpath(absolute);
235
+ return normalizePath(resolved);
236
+ }
237
+ catch (error) {
238
+ // If we can't resolve (doesn't exist), use the normalized absolute path
239
+ // This allows configuring allowed dirs that will be created later
240
+ return normalizePath(absolute);
241
+ }
242
+ }));
243
+ // Validate that all approved directories exist and are accessible
244
+ await Promise.all(allowedDirectories.map(async (dir) => {
245
+ try {
246
+ const stats = await fs.stat(dir);
247
+ if (!stats.isDirectory()) {
248
+ const errorMsg = `Error: Approved folder ${dir} is not a directory`;
249
+ console.error(errorMsg);
250
+ // In MCP mode, don't exit - just skip invalid directories
251
+ if (!isMCP) {
252
+ process.exit(1);
253
+ }
254
+ }
255
+ }
256
+ catch (error) {
257
+ const errorMsg = `Error accessing approved folder ${dir}: ${error}`;
258
+ console.error(errorMsg);
259
+ // In MCP mode, don't exit - just skip invalid directories
260
+ if (!isMCP) {
261
+ process.exit(1);
262
+ }
263
+ }
264
+ }));
265
+ // Only log initialization if not running under MCP
266
+ if (!isMCP) {
267
+ console.error(`Initialized with ${allowedDirectories.length} approved ${allowedDirectories.length === 1 ? "directory" : "directories"}`);
268
+ }
269
+ }
270
+ // Handle legacy positional directory arguments (backward compatibility)
271
+ // Note: --approved-folders is the preferred method for specifying directories
272
+ if (directoryArgs.length > 0) {
273
+ const legacyDirectories = await Promise.all(directoryArgs.map(async (dir) => {
274
+ const expanded = expandHome(dir);
275
+ const absolute = path.resolve(expanded);
276
+ try {
277
+ // Security: Resolve symlinks in allowed directories during startup
278
+ const resolved = await fs.realpath(absolute);
279
+ return normalizePath(resolved);
280
+ }
281
+ catch (error) {
282
+ // If we can't resolve (doesn't exist), use the normalized absolute path
283
+ return normalizePath(absolute);
284
+ }
285
+ }));
286
+ // Validate legacy directories
287
+ await Promise.all(legacyDirectories.map(async (dir) => {
288
+ try {
289
+ const stats = await fs.stat(dir);
290
+ if (!stats.isDirectory()) {
291
+ const errorMsg = `Error: Legacy directory ${dir} is not a directory`;
292
+ console.error(errorMsg);
293
+ // In MCP mode, don't exit - just skip invalid directories
294
+ if (!isMCP) {
295
+ process.exit(1);
296
+ }
297
+ }
298
+ }
299
+ catch (error) {
300
+ const errorMsg = `Error accessing legacy directory ${dir}: ${error}`;
301
+ console.error(errorMsg);
302
+ // In MCP mode, don't exit - just skip invalid directories
303
+ if (!isMCP) {
304
+ process.exit(1);
305
+ }
306
+ }
307
+ }));
308
+ // Merge with approved folders (avoid duplicates)
309
+ for (const dir of legacyDirectories) {
310
+ if (!allowedDirectories.includes(dir)) {
311
+ allowedDirectories.push(dir);
312
+ }
313
+ }
314
+ }
315
+ // Initialize the global configuration in lib.ts
316
+ setAllowedDirectories(allowedDirectories);
317
+ setIgnoredFolders(ignoredFolders);
318
+ // Set individual enabled tools (categories are combined dynamically in tool handlers)
319
+ setEnabledTools(enabledTools);
320
+ // Load shell command configuration
321
+ let finalApprovedCommands = [];
322
+ // Priority 1: --approved-commands from CLI (supersedes .env)
323
+ if (approvedCommandsFromArgs.length > 0) {
324
+ finalApprovedCommands = approvedCommandsFromArgs;
325
+ if (!isMCP) {
326
+ console.error(` Approved commands (from CLI): ${finalApprovedCommands.join(", ")}`);
327
+ }
328
+ }
329
+ else {
330
+ // Priority 2: Load from .env file
331
+ try {
332
+ const envPath = path.join(process.cwd(), ".env");
333
+ dotenv.config({ path: envPath });
334
+ if (process.env.APPROVED_COMMANDS) {
335
+ finalApprovedCommands = process.env.APPROVED_COMMANDS.split(",")
336
+ .map((c) => c.trim())
337
+ .filter((c) => c.length > 0);
338
+ if (!isMCP) {
339
+ console.error(` Approved commands (from .env): ${finalApprovedCommands.join(", ")}`);
340
+ }
341
+ }
342
+ }
343
+ catch (error) {
344
+ if (!isMCP) {
345
+ console.error(" Note: Could not load .env file (this is okay if using CLI args)");
346
+ }
347
+ }
348
+ }
349
+ // Initialize shell tool with approved commands
350
+ if (finalApprovedCommands.length > 0) {
351
+ initializeShellTool(finalApprovedCommands);
352
+ if (!isMCP) {
353
+ console.error(`Initialized shell tool with ${finalApprovedCommands.length} approved command(s)`);
354
+ }
355
+ }
356
+ else {
357
+ initializeShellTool([]);
358
+ // Only log shell initialization if not running under MCP
359
+ if (!isMCP) {
360
+ console.error("Shell tool initialized with no pre-approved commands (all commands require approval)");
361
+ }
362
+ }
363
+ }
364
+ // Generate dynamic server description
365
+ function generateServerDescription() {
366
+ const baseDescription = "A configurable Model Context Protocol server for secure filesystem operations that absolutely rocks. " +
367
+ "Enables AI assistants to dynamically access and manage file system resources with runtime directory registration and selective tool activation.";
368
+ const currentDirs = getAllowedDirectories();
369
+ if (currentDirs.length === 0) {
370
+ return (baseDescription +
371
+ "\n\nNO DIRECTORIES CURRENTLY ACCESSIBLE. Use register_directory tool to grant access, or restart server with --approved-folders argument.");
372
+ }
373
+ const dirList = currentDirs.map((dir) => ` - ${dir}`).join("\n");
374
+ return `${baseDescription}\n\nIMMEDIATELY ACCESSIBLE DIRECTORIES (pre-approved, no registration needed):\n${dirList}\n\nIMPORTANT: These directories are already accessible to all filesystem tools. Do NOT use register_directory for these paths.\n\nTo add additional directories at runtime, use the register_directory tool or MCP Roots protocol.`;
375
+ }
376
+ // Server setup
377
+ const server = new Server({
378
+ name: "vulcan-file-ops",
379
+ version: VERSION,
380
+ }, {
381
+ capabilities: {
382
+ tools: {
383
+ listChanged: true,
384
+ },
385
+ resources: {},
386
+ prompts: {},
387
+ },
388
+ });
389
+ // Initialize handler - required for MCP protocol
390
+ server.setRequestHandler(InitializeRequestSchema, async (request) => {
391
+ const clientCapabilities = request.params.capabilities;
392
+ return {
393
+ protocolVersion: LATEST_PROTOCOL_VERSION,
394
+ capabilities: {
395
+ tools: {
396
+ listChanged: true,
397
+ },
398
+ resources: {},
399
+ prompts: {},
400
+ },
401
+ serverInfo: {
402
+ name: "vulcan-file-ops",
403
+ version: VERSION,
404
+ },
405
+ instructions: generateServerDescription(),
406
+ };
407
+ });
408
+ // Ping handler - for health checks
409
+ server.setRequestHandler(PingRequestSchema, async () => {
410
+ return {};
411
+ });
412
+ // Tool registry for selective activation
413
+ const TOOL_REGISTRY = {
414
+ // Read tools
415
+ read_file: () => getReadTools().find((t) => t.name === "read_file"),
416
+ attach_image: () => getReadTools().find((t) => t.name === "attach_image"),
417
+ read_multiple_files: () => getReadTools().find((t) => t.name === "read_multiple_files"),
418
+ // Write tools
419
+ write_file: () => getWriteTools().find((t) => t.name === "write_file"),
420
+ edit_file: () => getWriteTools().find((t) => t.name === "edit_file"),
421
+ write_multiple_files: () => getWriteTools().find((t) => t.name === "write_multiple_files"),
422
+ // Filesystem tools
423
+ make_directory: () => getFileSystemTools().find((t) => t.name === "make_directory"),
424
+ list_directory: () => getFileSystemTools().find((t) => t.name === "list_directory"),
425
+ move_file: () => getFileSystemTools().find((t) => t.name === "move_file"),
426
+ file_operations: () => getFileSystemTools().find((t) => t.name === "file_operations"),
427
+ delete_files: () => getFileSystemTools().find((t) => t.name === "delete_files"),
428
+ get_file_info: () => getFileSystemTools().find((t) => t.name === "get_file_info"),
429
+ register_directory: () => getFileSystemTools().find((t) => t.name === "register_directory"),
430
+ list_allowed_directories: () => getFileSystemTools().find((t) => t.name === "list_allowed_directories"),
431
+ // Search tools
432
+ glob_files: () => getSearchTools().find((t) => t.name === "glob_files"),
433
+ grep_files: () => getSearchTools().find((t) => t.name === "grep_files"),
434
+ // Shell tool
435
+ execute_shell: () => getShellTools().find((t) => t.name === "execute_shell"),
436
+ };
437
+ // Tool categories for easier configuration
438
+ const TOOL_CATEGORIES = {
439
+ read: ["read_file", "attach_image", "read_multiple_files"],
440
+ write: ["write_file", "edit_file", "write_multiple_files"],
441
+ filesystem: [
442
+ "make_directory",
443
+ "list_directory",
444
+ "move_file",
445
+ "file_operations",
446
+ "delete_files",
447
+ "get_file_info",
448
+ "register_directory",
449
+ "list_allowed_directories",
450
+ ],
451
+ search: ["glob_files", "grep_files"],
452
+ shell: ["execute_shell"],
453
+ all: [
454
+ "read_file",
455
+ "read_text_file",
456
+ "read_media_file",
457
+ "read_multiple_files",
458
+ "write_file",
459
+ "edit_file",
460
+ "write_multiple_files",
461
+ "make_directory",
462
+ "list_directory",
463
+ "move_file",
464
+ "file_operations",
465
+ "delete_files",
466
+ "get_file_info",
467
+ "register_directory",
468
+ "list_allowed_directories",
469
+ "glob_files",
470
+ "grep_files",
471
+ "execute_shell",
472
+ ],
473
+ };
474
+ // Function to expand tool categories and validate tool names
475
+ function expandEnabledTools(requestedTools) {
476
+ const expandedTools = new Set();
477
+ for (const tool of requestedTools) {
478
+ if (tool === "all") {
479
+ // Add all tools
480
+ TOOL_CATEGORIES.all.forEach((t) => expandedTools.add(t));
481
+ }
482
+ else if (TOOL_CATEGORIES[tool]) {
483
+ // Expand category
484
+ TOOL_CATEGORIES[tool].forEach((t) => expandedTools.add(t));
485
+ }
486
+ else if (TOOL_REGISTRY[tool]) {
487
+ // Individual tool
488
+ expandedTools.add(tool);
489
+ }
490
+ else {
491
+ console.warn(`Unknown tool or category: ${tool}`);
492
+ }
493
+ }
494
+ return Array.from(expandedTools);
495
+ }
496
+ // Function to combine and validate enabled tools from both categories and individual tools
497
+ function combineEnabledTools() {
498
+ const allRequestedTools = [...enabledToolCategories, ...enabledTools];
499
+ if (allRequestedTools.length === 0) {
500
+ // If no tools specified, enable all by default
501
+ return TOOL_CATEGORIES.all;
502
+ }
503
+ return expandEnabledTools(allRequestedTools);
504
+ }
505
+ // Tool handlers
506
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
507
+ let tools = [
508
+ ...getReadTools(),
509
+ ...getWriteTools(),
510
+ ...getFileSystemTools(),
511
+ ...getSearchTools(),
512
+ ...getShellTools(),
513
+ ];
514
+ // Filter tools if selective activation is configured
515
+ if (enabledToolCategories.length > 0 || enabledTools.length > 0) {
516
+ const combinedEnabledTools = combineEnabledTools();
517
+ tools = tools.filter((tool) => combinedEnabledTools.includes(tool.name));
518
+ }
519
+ return { tools };
520
+ });
521
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
522
+ try {
523
+ const { name, arguments: args } = request.params;
524
+ // Check if tool is enabled (if selective activation is configured)
525
+ if (enabledToolCategories.length > 0 || enabledTools.length > 0) {
526
+ const combinedEnabledTools = combineEnabledTools();
527
+ if (!combinedEnabledTools.includes(name)) {
528
+ throw new Error(`Tool '${name}' is not enabled. Enabled tools: ${combinedEnabledTools.join(", ")}`);
529
+ }
530
+ }
531
+ // Import the tool handler dynamically based on the tool name
532
+ // This keeps the main server file clean and modular
533
+ switch (name) {
534
+ // Read tools
535
+ case "read_file":
536
+ case "attach_image":
537
+ case "read_multiple_files": {
538
+ const { handleReadTool } = await import("../tools/read-tools.js");
539
+ return await handleReadTool(name, args);
540
+ }
541
+ // Write tools
542
+ case "write_file":
543
+ case "edit_file":
544
+ case "write_multiple_files": {
545
+ const { handleWriteTool } = await import("../tools/write-tools.js");
546
+ return await handleWriteTool(name, args);
547
+ }
548
+ // Filesystem tools
549
+ case "make_directory":
550
+ case "list_directory":
551
+ case "move_file":
552
+ case "file_operations":
553
+ case "delete_files":
554
+ case "get_file_info":
555
+ case "register_directory":
556
+ case "list_allowed_directories": {
557
+ const { handleFileSystemTool } = await import("../tools/filesystem-tools.js");
558
+ return await handleFileSystemTool(name, args);
559
+ }
560
+ // Search tools
561
+ case "glob_files":
562
+ case "grep_files": {
563
+ const { handleSearchTool } = await import("../tools/search-tools.js");
564
+ return await handleSearchTool(name, args);
565
+ }
566
+ // Shell tool
567
+ case "execute_shell": {
568
+ const { handleShellTool } = await import("../tools/shell-tool.js");
569
+ return await handleShellTool(name, args);
570
+ }
571
+ default:
572
+ throw new Error(`Unknown tool: ${name}`);
573
+ }
574
+ }
575
+ catch (error) {
576
+ const errorMessage = error instanceof Error ? error.message : String(error);
577
+ return {
578
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
579
+ isError: true,
580
+ };
581
+ }
582
+ });
583
+ // Updates allowed directories based on MCP client roots
584
+ async function updateAllowedDirectoriesFromRoots(requestedRoots) {
585
+ const validatedRootDirs = await getValidRootDirectories(requestedRoots);
586
+ if (validatedRootDirs.length > 0) {
587
+ // Note: MCP Roots replaces ALL allowed directories, including those specified via --approved-folders
588
+ // This is the expected behavior per MCP protocol - Roots provides runtime workspace context
589
+ allowedDirectories = [...validatedRootDirs];
590
+ setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts
591
+ console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`);
592
+ console.error("Note: MCP Roots replaces all previously configured directories (including --approved-folders)");
593
+ }
594
+ else {
595
+ console.error("No valid root directories provided by client");
596
+ }
597
+ }
598
+ // Handles dynamic roots updates during runtime, when client sends "roots/list_changed" notification, server fetches the updated roots and replaces all allowed directories with the new roots.
599
+ server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
600
+ try {
601
+ // Request the updated roots list from the client
602
+ const response = await server.listRoots();
603
+ if (response && "roots" in response) {
604
+ await updateAllowedDirectoriesFromRoots(response.roots);
605
+ }
606
+ }
607
+ catch (error) {
608
+ console.error("Failed to request roots from client:", error instanceof Error ? error.message : String(error));
609
+ }
610
+ });
611
+ // Handles post-initialization setup, specifically checking for and fetching MCP roots.
612
+ server.oninitialized = async () => {
613
+ const clientCapabilities = server.getClientCapabilities();
614
+ if (clientCapabilities?.roots) {
615
+ try {
616
+ const response = await server.listRoots();
617
+ if (response && "roots" in response) {
618
+ await updateAllowedDirectoriesFromRoots(response.roots);
619
+ }
620
+ }
621
+ catch (error) {
622
+ // Silently handle errors - dynamic access will work via register_directory tool
623
+ }
624
+ }
625
+ };
626
+ // Start server
627
+ export async function runServer() {
628
+ // Initialize directories before starting server
629
+ // BUT: Don't exit on errors during MCP mode - just log and continue
630
+ try {
631
+ await initializeDirectories();
632
+ }
633
+ catch (error) {
634
+ // In MCP mode, don't crash the server on init errors
635
+ // Just continue with empty configuration
636
+ const isMCP = (!process.stdin.isTTY && !process.stdout.isTTY) ||
637
+ process.argv.some((arg) => arg.includes("mcp") || arg.includes("stdio"));
638
+ if (!isMCP) {
639
+ // In non-MCP mode, we can show errors and exit
640
+ throw error;
641
+ }
642
+ // In MCP mode, silently continue - server can work without approved folders
643
+ }
644
+ const transport = new StdioServerTransport();
645
+ await server.connect(transport);
646
+ // Minimal logging to avoid issues with MCP clients
647
+ }
648
+ //# sourceMappingURL=index.js.map