@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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +747 -0
- package/dist/cli.js +31 -0
- package/dist/index.js +3 -0
- package/dist/server/index.js +648 -0
- package/dist/tools/filesystem-tools.js +905 -0
- package/dist/tools/read-tools.js +234 -0
- package/dist/tools/search-tools.js +156 -0
- package/dist/tools/shell-tool.js +191 -0
- package/dist/tools/write-tools.js +256 -0
- package/dist/types/index.js +368 -0
- package/dist/utils/command-path-extraction.js +200 -0
- package/dist/utils/command-validation.js +121 -0
- package/dist/utils/document-parser.js +171 -0
- package/dist/utils/docx-writer.js +30 -0
- package/dist/utils/html-to-document.js +334 -0
- package/dist/utils/lib.js +944 -0
- package/dist/utils/path-utils.js +92 -0
- package/dist/utils/path-validation.js +76 -0
- package/dist/utils/pdf-writer.js +94 -0
- package/dist/utils/roots-utils.js +71 -0
- package/dist/utils/shell-execution.js +77 -0
- package/package.json +73 -0
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,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
|