ellmos-filecommander-mcp 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3726 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BACH FileCommander MCP Server
4
+ *
5
+ * A comprehensive MCP server for filesystem access, process management,
6
+ * interactive sessions, and async file search.
7
+ *
8
+ * Copyright (c) 2025-2026 Lukas (BACH). Licensed under MIT License.
9
+ * See LICENSE file for details.
10
+ *
11
+ * @author Lukas (BACH)
12
+ * @version 1.7.0
13
+ * @license MIT
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+ import { t, setLanguage, getLanguage } from './i18n/index.js';
19
+ import * as fs from "fs/promises";
20
+ import * as fsSync from "fs";
21
+ import * as path from "path";
22
+ import * as crypto from "crypto";
23
+ import { createHash } from "crypto";
24
+ import * as os from "os";
25
+ import { exec, execSync, spawn } from "child_process";
26
+ import { promisify } from "util";
27
+ import * as yaml from 'js-yaml';
28
+ import * as toml from 'smol-toml';
29
+ import { XMLParser, XMLBuilder } from 'fast-xml-parser';
30
+ import AdmZip from 'adm-zip';
31
+ import { encode as toonEncode, decode as toonDecode } from '@toon-format/toon';
32
+ const execAsync = promisify(exec);
33
+ // ============================================================================
34
+ // Server Initialization
35
+ // ============================================================================
36
+ const server = new McpServer({
37
+ name: "bach-filecommander-mcp",
38
+ version: "1.7.0"
39
+ });
40
+ const processSessions = new Map();
41
+ let sessionCounter = 0;
42
+ function generateSessionId() {
43
+ return `session_${++sessionCounter}_${Date.now()}`;
44
+ }
45
+ const searchSessions = new Map();
46
+ let searchCounter = 0;
47
+ function generateSearchId() {
48
+ return `search_${++searchCounter}_${Date.now()}`;
49
+ }
50
+ // ============================================================================
51
+ // Safe Mode (global toggle)
52
+ // ============================================================================
53
+ let safeMode = false;
54
+ /**
55
+ * Asynchrone rekursive Suche mit AbortController
56
+ */
57
+ async function asyncSearchFiles(session, dirPath) {
58
+ if (!session.isRunning || session.abortController.signal.aborted) {
59
+ return;
60
+ }
61
+ try {
62
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
63
+ session.scannedDirs++;
64
+ for (const entry of entries) {
65
+ if (!session.isRunning || session.abortController.signal.aborted) {
66
+ return;
67
+ }
68
+ const fullPath = path.join(dirPath, entry.name);
69
+ if (entry.isDirectory()) {
70
+ // Skip system directories
71
+ if (!['node_modules', '.git', '$RECYCLE.BIN', 'System Volume Information', 'Windows', 'Program Files', 'Program Files (x86)'].includes(entry.name)) {
72
+ await asyncSearchFiles(session, fullPath);
73
+ }
74
+ }
75
+ else if (session.pattern.test(entry.name)) {
76
+ session.results.push(fullPath);
77
+ }
78
+ }
79
+ }
80
+ catch {
81
+ // Ignore permission errors silently
82
+ }
83
+ }
84
+ // ============================================================================
85
+ // Helper Functions
86
+ // ============================================================================
87
+ /**
88
+ * Verschiebt eine Datei/Verzeichnis in den Papierkorb (Windows) oder ~/.Trash (Unix).
89
+ * Wiederverwendbare Funktion fuer fc_safe_delete und Safe Mode.
90
+ */
91
+ async function moveToTrash(targetPath) {
92
+ const stats = await fs.stat(targetPath);
93
+ const isWindows = process.platform === 'win32';
94
+ if (isWindows) {
95
+ const escapedPath = targetPath.replace(/'/g, "''");
96
+ const deleteMethod = stats.isDirectory() ? 'DeleteDirectory' : 'DeleteFile';
97
+ const psCommand = `Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${deleteMethod}('${escapedPath}', 'OnlyErrorDialogs', 'SendToRecycleBin')`;
98
+ const windowsShell = getWindowsShell();
99
+ if (windowsShell.includes('powershell')) {
100
+ await execAsync(`"${windowsShell}" -Command "${psCommand}"`);
101
+ }
102
+ else {
103
+ // cmd.exe kann kein PowerShell - normales Loeschen als Fallback
104
+ if (stats.isDirectory()) {
105
+ await fs.rm(targetPath, { recursive: true });
106
+ }
107
+ else {
108
+ await fs.unlink(targetPath);
109
+ }
110
+ }
111
+ return ''; // Windows Papierkorb, kein expliziter Pfad
112
+ }
113
+ else {
114
+ const trashDir = path.join(process.env.HOME || '/tmp', '.Trash');
115
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
116
+ const baseName = path.basename(targetPath);
117
+ const trashPath = path.join(trashDir, `${baseName}_${timestamp}`);
118
+ try {
119
+ await fs.access(trashDir);
120
+ }
121
+ catch {
122
+ await fs.mkdir(trashDir, { recursive: true });
123
+ }
124
+ await fs.rename(targetPath, trashPath);
125
+ return trashPath;
126
+ }
127
+ }
128
+ /**
129
+ * Normalisiert Pfade für Windows/Unix Kompatibilität
130
+ */
131
+ function normalizePath(inputPath) {
132
+ return path.normalize(inputPath);
133
+ }
134
+ /**
135
+ * Prüft ob ein Pfad existiert
136
+ */
137
+ async function pathExists(targetPath) {
138
+ try {
139
+ await fs.access(targetPath);
140
+ return true;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ /**
147
+ * Formatiert Dateigröße menschenlesbar
148
+ */
149
+ function formatFileSize(bytes) {
150
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
151
+ let unitIndex = 0;
152
+ let size = bytes;
153
+ while (size >= 1024 && unitIndex < units.length - 1) {
154
+ size /= 1024;
155
+ unitIndex++;
156
+ }
157
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
158
+ }
159
+ /**
160
+ * Prüft ob ein Pfad Windows-Sonderzeichen enthält die Probleme machen
161
+ */
162
+ function hasWindowsSpecialChars(inputPath) {
163
+ return /[&^%$#@!]/.test(inputPath);
164
+ }
165
+ /**
166
+ * Escaped einen Pfad für PowerShell-Nutzung
167
+ * Behandelt & und andere Sonderzeichen korrekt
168
+ */
169
+ function escapeForPowerShell(inputPath) {
170
+ // In PowerShell: & ist der Call-Operator, muss in Quotes oder escaped werden
171
+ // Backtick (`) ist das Escape-Zeichen in PowerShell
172
+ return inputPath
173
+ .replace(/`/g, '``') // Backtick zuerst escapen
174
+ .replace(/\$/g, '`$') // Dollar-Zeichen
175
+ .replace(/"/g, '`"') // Anführungszeichen
176
+ .replace(/'/g, "''"); // Single quotes verdoppeln
177
+ }
178
+ /**
179
+ * Escaped einen Pfad für cmd.exe-Nutzung
180
+ * Behandelt & ^ % und andere Sonderzeichen
181
+ */
182
+ function escapeForCmd(inputPath) {
183
+ // In cmd.exe: ^ ist das Escape-Zeichen
184
+ return inputPath
185
+ .replace(/([&^%!<>|])/g, '^$1'); // Sonderzeichen mit ^ escapen
186
+ }
187
+ /**
188
+ * Ermittelt den PowerShell-Pfad auf Windows
189
+ * Fallback-Kette: pwsh -> powershell.exe im System32 -> cmd.exe
190
+ */
191
+ function getWindowsShell() {
192
+ const systemRoot = process.env.SystemRoot || 'C:\\Windows';
193
+ const ps7Path = `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
194
+ // Prüfe ob PowerShell im System32 existiert
195
+ try {
196
+ if (fsSync.existsSync(ps7Path)) {
197
+ return ps7Path;
198
+ }
199
+ }
200
+ catch {
201
+ // Ignore
202
+ }
203
+ // Fallback zu cmd.exe
204
+ return `${systemRoot}\\System32\\cmd.exe`;
205
+ }
206
+ /**
207
+ * Führt einen Befehl aus - auf Windows mit korrektem Escaping für Sonderzeichen
208
+ * Erkennt automatisch & und andere problematische Zeichen in Pfaden
209
+ */
210
+ async function executeCommand(command, options = {}) {
211
+ const isWindows = process.platform === 'win32';
212
+ const cwd = options.cwd;
213
+ // Auf Windows bei Sonderzeichen im Pfad oder Befehl: Spezielles Handling
214
+ const cwdHasSpecialChars = cwd && hasWindowsSpecialChars(cwd);
215
+ const cmdHasSpecialChars = hasWindowsSpecialChars(command);
216
+ if (isWindows && (cwdHasSpecialChars || cmdHasSpecialChars)) {
217
+ const windowsShell = getWindowsShell();
218
+ if (windowsShell.includes('powershell')) {
219
+ // PowerShell: Nutze -LiteralPath für Pfade mit Sonderzeichen
220
+ const escapedCwd = cwd ? escapeForPowerShell(cwd) : '';
221
+ // Befehl für PowerShell vorbereiten
222
+ // Bei Pfaden mit & den Call-Operator nutzen: & "pfad mit &"
223
+ let psCommand;
224
+ if (cwd) {
225
+ psCommand = `Set-Location -LiteralPath '${escapedCwd}'; ${command}`;
226
+ }
227
+ else {
228
+ psCommand = command;
229
+ }
230
+ // Wenn der Befehl selbst Pfade mit & enthält, diese in Quotes wrappen
231
+ // Erkennt Muster wie: python "C:\path\with & special\script.py"
232
+ psCommand = psCommand.replace(/(?<!["`'])([A-Za-z]:\\[^"'`\n]*&[^"'`\n]*?)(?=\s|$)/g, '"$1"');
233
+ return execAsync(`"${windowsShell}" -Command "${psCommand.replace(/"/g, '\\"')}"`, {
234
+ timeout: options.timeout
235
+ });
236
+ }
237
+ else {
238
+ // cmd.exe: Escape mit ^
239
+ const escapedCmd = escapeForCmd(command);
240
+ const escapedCwd = cwd ? escapeForCmd(cwd) : undefined;
241
+ return execAsync(escapedCmd, {
242
+ ...options,
243
+ cwd: escapedCwd || cwd,
244
+ shell: windowsShell
245
+ });
246
+ }
247
+ }
248
+ return execAsync(command, options);
249
+ }
250
+ async function listDirectoryRecursive(dirPath, maxDepth, currentDepth = 0) {
251
+ const results = [];
252
+ if (currentDepth > maxDepth)
253
+ return results;
254
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
255
+ for (const entry of entries) {
256
+ const fullPath = path.join(dirPath, entry.name);
257
+ const indent = " ".repeat(currentDepth);
258
+ if (entry.isDirectory()) {
259
+ results.push(`${indent}📁 ${entry.name}/`);
260
+ if (currentDepth < maxDepth) {
261
+ const subEntries = await listDirectoryRecursive(fullPath, maxDepth, currentDepth + 1);
262
+ results.push(...subEntries);
263
+ }
264
+ }
265
+ else {
266
+ results.push(`${indent}📄 ${entry.name}`);
267
+ }
268
+ }
269
+ return results;
270
+ }
271
+ /**
272
+ * Rekursive Dateisuche
273
+ */
274
+ async function searchFilesRecursive(dirPath, pattern, maxResults, results = []) {
275
+ if (results.length >= maxResults)
276
+ return results;
277
+ try {
278
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
279
+ for (const entry of entries) {
280
+ if (results.length >= maxResults)
281
+ break;
282
+ const fullPath = path.join(dirPath, entry.name);
283
+ if (entry.isDirectory()) {
284
+ // Skip system directories
285
+ if (!['node_modules', '.git', '$RECYCLE.BIN', 'System Volume Information'].includes(entry.name)) {
286
+ await searchFilesRecursive(fullPath, pattern, maxResults, results);
287
+ }
288
+ }
289
+ else if (pattern.test(entry.name)) {
290
+ results.push(fullPath);
291
+ }
292
+ }
293
+ }
294
+ catch {
295
+ // Ignore permission errors
296
+ }
297
+ return results;
298
+ }
299
+ // ============================================================================
300
+ // Tool: Read File
301
+ // ============================================================================
302
+ server.registerTool("fc_read_file", {
303
+ title: "Read File",
304
+ description: `Reads the content of a file.
305
+
306
+ Args:
307
+ - path (string): Full path to the file
308
+ - encoding (string, optional): Character encoding (default: utf-8)
309
+ - max_lines (number, optional): Maximum number of lines (0 = all)
310
+
311
+ Returns:
312
+ - File content as text
313
+ - For binary files: Base64-encoded content
314
+
315
+ Examples:
316
+ - path: "C:\\Users\\User\\test.txt"
317
+ - path: "/home/user/config.json"`,
318
+ inputSchema: {
319
+ path: z.string().min(1).describe("Full path to the file"),
320
+ encoding: z.string().default("utf-8").describe("Character encoding"),
321
+ max_lines: z.number().int().min(0).default(0).describe("Max lines (0 = all)")
322
+ },
323
+ annotations: {
324
+ readOnlyHint: true,
325
+ destructiveHint: false,
326
+ idempotentHint: true,
327
+ openWorldHint: false
328
+ }
329
+ }, async (params) => {
330
+ try {
331
+ const filePath = normalizePath(params.path);
332
+ if (!await pathExists(filePath)) {
333
+ return {
334
+ isError: true,
335
+ content: [{ type: "text", text: t().common.fileNotFound(filePath) }]
336
+ };
337
+ }
338
+ const stats = await fs.stat(filePath);
339
+ if (stats.isDirectory()) {
340
+ return {
341
+ isError: true,
342
+ content: [{ type: "text", text: t().common.pathIsDirectoryUseListDir(filePath) }]
343
+ };
344
+ }
345
+ let content = await fs.readFile(filePath, params.encoding);
346
+ if (params.max_lines > 0) {
347
+ const lines = content.split('\n');
348
+ content = lines.slice(0, params.max_lines).join('\n');
349
+ if (lines.length > params.max_lines) {
350
+ content += t().fc_read_file.moreLines(lines.length - params.max_lines);
351
+ }
352
+ }
353
+ return {
354
+ content: [{
355
+ type: "text",
356
+ text: `${t().fc_read_file.fileHeader(path.basename(filePath), formatFileSize(stats.size))}\n\n${content}`
357
+ }]
358
+ };
359
+ }
360
+ catch (error) {
361
+ const errorMsg = error instanceof Error ? error.message : String(error);
362
+ return {
363
+ isError: true,
364
+ content: [{ type: "text", text: t().fc_read_file.readError(errorMsg) }]
365
+ };
366
+ }
367
+ });
368
+ // ============================================================================
369
+ // Tool: Write File
370
+ // ============================================================================
371
+ server.registerTool("fc_write_file", {
372
+ title: "Write File",
373
+ description: `Writes content to a file. Creates the file if it does not exist.
374
+
375
+ Args:
376
+ - path (string): Full path to the file
377
+ - content (string): Content to write
378
+ - append (boolean, optional): Append to file instead of overwriting
379
+ - create_dirs (boolean, optional): Create missing directories
380
+
381
+ Returns:
382
+ - Confirmation with file size
383
+
384
+ Warning: Overwrites existing files without warning when append=false!`,
385
+ inputSchema: {
386
+ path: z.string().min(1).describe("Full path to the file"),
387
+ content: z.string().describe("Content to write"),
388
+ append: z.boolean().default(false).describe("Append to file"),
389
+ create_dirs: z.boolean().default(true).describe("Create missing directories")
390
+ },
391
+ annotations: {
392
+ readOnlyHint: false,
393
+ destructiveHint: true,
394
+ idempotentHint: false,
395
+ openWorldHint: false
396
+ }
397
+ }, async (params) => {
398
+ try {
399
+ const filePath = normalizePath(params.path);
400
+ const dirPath = path.dirname(filePath);
401
+ if (params.create_dirs && !await pathExists(dirPath)) {
402
+ await fs.mkdir(dirPath, { recursive: true });
403
+ }
404
+ if (params.append) {
405
+ await fs.appendFile(filePath, params.content, "utf-8");
406
+ }
407
+ else {
408
+ await fs.writeFile(filePath, params.content, "utf-8");
409
+ }
410
+ const stats = await fs.stat(filePath);
411
+ const action = params.append ? t().fc_write_file.actionAppended : t().fc_write_file.actionWritten;
412
+ return {
413
+ content: [{
414
+ type: "text",
415
+ text: `${t().fc_write_file.success(action, filePath)}\n${t().fc_write_file.sizeLabel(formatFileSize(stats.size))}`
416
+ }]
417
+ };
418
+ }
419
+ catch (error) {
420
+ const errorMsg = error instanceof Error ? error.message : String(error);
421
+ return {
422
+ isError: true,
423
+ content: [{ type: "text", text: t().fc_write_file.writeError(errorMsg) }]
424
+ };
425
+ }
426
+ });
427
+ // ============================================================================
428
+ // Tool: List Directory
429
+ // ============================================================================
430
+ server.registerTool("fc_list_directory", {
431
+ title: "List Directory",
432
+ description: `Lists files and subdirectories.
433
+
434
+ Args:
435
+ - path (string): Path to the directory
436
+ - depth (number, optional): Maximum depth for recursive listing (default: 1)
437
+ - show_hidden (boolean, optional): Show hidden files
438
+
439
+ Returns:
440
+ - Formatted list of all entries with icons`,
441
+ inputSchema: {
442
+ path: z.string().min(1).describe("Path to the directory"),
443
+ depth: z.number().int().min(0).max(10).default(1).describe("Recursion depth"),
444
+ show_hidden: z.boolean().default(false).describe("Show hidden files")
445
+ },
446
+ annotations: {
447
+ readOnlyHint: true,
448
+ destructiveHint: false,
449
+ idempotentHint: true,
450
+ openWorldHint: false
451
+ }
452
+ }, async (params) => {
453
+ try {
454
+ const dirPath = normalizePath(params.path);
455
+ if (!await pathExists(dirPath)) {
456
+ return {
457
+ isError: true,
458
+ content: [{ type: "text", text: t().common.dirNotFound(dirPath) }]
459
+ };
460
+ }
461
+ const stats = await fs.stat(dirPath);
462
+ if (!stats.isDirectory()) {
463
+ return {
464
+ isError: true,
465
+ content: [{ type: "text", text: t().common.pathIsNotDirUseReadFile(dirPath) }]
466
+ };
467
+ }
468
+ const entries = await listDirectoryRecursive(dirPath, params.depth);
469
+ // Filter hidden files if needed
470
+ const filteredEntries = params.show_hidden
471
+ ? entries
472
+ : entries.filter(e => !e.trim().startsWith('\uD83D\uDCC1 .') && !e.trim().startsWith('\uD83D\uDCC4 .'));
473
+ return {
474
+ content: [{
475
+ type: "text",
476
+ text: `${t().fc_list_directory.dirHeader(dirPath)}\n\n${filteredEntries.join('\n') || t().fc_list_directory.emptyDir}`
477
+ }]
478
+ };
479
+ }
480
+ catch (error) {
481
+ const errorMsg = error instanceof Error ? error.message : String(error);
482
+ return {
483
+ isError: true,
484
+ content: [{ type: "text", text: t().fc_list_directory.listError(errorMsg) }]
485
+ };
486
+ }
487
+ });
488
+ // ============================================================================
489
+ // Tool: Create Directory
490
+ // ============================================================================
491
+ server.registerTool("fc_create_directory", {
492
+ title: "Create Directory",
493
+ description: `Creates a new directory (including parent directories).
494
+
495
+ Args:
496
+ - path (string): Path to the new directory
497
+
498
+ Returns:
499
+ - Confirmation of creation`,
500
+ inputSchema: {
501
+ path: z.string().min(1).describe("Path to the new directory")
502
+ },
503
+ annotations: {
504
+ readOnlyHint: false,
505
+ destructiveHint: false,
506
+ idempotentHint: true,
507
+ openWorldHint: false
508
+ }
509
+ }, async (params) => {
510
+ try {
511
+ const dirPath = normalizePath(params.path);
512
+ if (await pathExists(dirPath)) {
513
+ return {
514
+ content: [{ type: "text", text: t().fc_create_directory.alreadyExists(dirPath) }]
515
+ };
516
+ }
517
+ await fs.mkdir(dirPath, { recursive: true });
518
+ return {
519
+ content: [{ type: "text", text: t().fc_create_directory.created(dirPath) }]
520
+ };
521
+ }
522
+ catch (error) {
523
+ const errorMsg = error instanceof Error ? error.message : String(error);
524
+ return {
525
+ isError: true,
526
+ content: [{ type: "text", text: t().fc_create_directory.createError(errorMsg) }]
527
+ };
528
+ }
529
+ });
530
+ // ============================================================================
531
+ // Tool: Delete File
532
+ // ============================================================================
533
+ server.registerTool("fc_delete_file", {
534
+ title: "Delete File",
535
+ description: `Deletes a file.
536
+
537
+ Args:
538
+ - path (string): Path to the file
539
+
540
+ Warning: Irreversible! No recycle bin.`,
541
+ inputSchema: {
542
+ path: z.string().min(1).describe("Path to the file")
543
+ },
544
+ annotations: {
545
+ readOnlyHint: false,
546
+ destructiveHint: true,
547
+ idempotentHint: true,
548
+ openWorldHint: false
549
+ }
550
+ }, async (params) => {
551
+ try {
552
+ const filePath = normalizePath(params.path);
553
+ if (!await pathExists(filePath)) {
554
+ return {
555
+ isError: true,
556
+ content: [{ type: "text", text: t().common.fileNotFound(filePath) }]
557
+ };
558
+ }
559
+ const stats = await fs.stat(filePath);
560
+ if (stats.isDirectory()) {
561
+ return {
562
+ isError: true,
563
+ content: [{ type: "text", text: t().common.pathIsDirectoryUseDeleteDir }]
564
+ };
565
+ }
566
+ // Safe Mode: redirect to recycle bin
567
+ if (safeMode) {
568
+ await moveToTrash(filePath);
569
+ return {
570
+ content: [{ type: "text", text: t().fc_set_safe_mode.redirected('fc_delete_file') + `\n${t().fc_safe_delete.propPath}: ${filePath}` }]
571
+ };
572
+ }
573
+ await fs.unlink(filePath);
574
+ return {
575
+ content: [{ type: "text", text: t().fc_delete_file.deleted(filePath) }]
576
+ };
577
+ }
578
+ catch (error) {
579
+ const errorMsg = error instanceof Error ? error.message : String(error);
580
+ return {
581
+ isError: true,
582
+ content: [{ type: "text", text: t().fc_delete_file.deleteError(errorMsg) }]
583
+ };
584
+ }
585
+ });
586
+ // ============================================================================
587
+ // Tool: Delete Directory
588
+ // ============================================================================
589
+ server.registerTool("fc_delete_directory", {
590
+ title: "Delete Directory",
591
+ description: `Deletes a directory.
592
+
593
+ Args:
594
+ - path (string): Path to the directory
595
+ - recursive (boolean): Delete non-empty directories too
596
+
597
+ Warning: With recursive=true ALL contents are irreversibly deleted!`,
598
+ inputSchema: {
599
+ path: z.string().min(1).describe("Path to the directory"),
600
+ recursive: z.boolean().default(false).describe("Delete recursively")
601
+ },
602
+ annotations: {
603
+ readOnlyHint: false,
604
+ destructiveHint: true,
605
+ idempotentHint: true,
606
+ openWorldHint: false
607
+ }
608
+ }, async (params) => {
609
+ try {
610
+ const dirPath = normalizePath(params.path);
611
+ if (!await pathExists(dirPath)) {
612
+ return {
613
+ isError: true,
614
+ content: [{ type: "text", text: t().common.dirNotFound(dirPath) }]
615
+ };
616
+ }
617
+ const stats = await fs.stat(dirPath);
618
+ if (!stats.isDirectory()) {
619
+ return {
620
+ isError: true,
621
+ content: [{ type: "text", text: t().common.pathIsNotDirectory(dirPath) }]
622
+ };
623
+ }
624
+ // Safe Mode: redirect to recycle bin
625
+ if (safeMode) {
626
+ await moveToTrash(dirPath);
627
+ return {
628
+ content: [{ type: "text", text: t().fc_set_safe_mode.redirected('fc_delete_directory') + `\n${t().fc_safe_delete.propPath}: ${dirPath}` }]
629
+ };
630
+ }
631
+ await fs.rm(dirPath, { recursive: params.recursive });
632
+ return {
633
+ content: [{ type: "text", text: t().fc_delete_directory.deleted(dirPath) }]
634
+ };
635
+ }
636
+ catch (error) {
637
+ const errorMsg = error instanceof Error ? error.message : String(error);
638
+ if (errorMsg.includes('ENOTEMPTY')) {
639
+ return {
640
+ isError: true,
641
+ content: [{ type: "text", text: t().fc_delete_directory.notEmpty }]
642
+ };
643
+ }
644
+ return {
645
+ isError: true,
646
+ content: [{ type: "text", text: t().fc_delete_directory.deleteError(errorMsg) }]
647
+ };
648
+ }
649
+ });
650
+ // ============================================================================
651
+ // Tool: Move/Rename
652
+ // ============================================================================
653
+ server.registerTool("fc_move", {
654
+ title: "Move/Rename",
655
+ description: `Moves or renames a file/directory.
656
+
657
+ Args:
658
+ - source (string): Source path
659
+ - destination (string): Destination path
660
+
661
+ Examples:
662
+ - Rename: source="test.txt", destination="test_new.txt"
663
+ - Move: source="C:\\a\\test.txt", destination="C:\\b\\test.txt"`,
664
+ inputSchema: {
665
+ source: z.string().min(1).describe("Source path"),
666
+ destination: z.string().min(1).describe("Destination path")
667
+ },
668
+ annotations: {
669
+ readOnlyHint: false,
670
+ destructiveHint: true,
671
+ idempotentHint: false,
672
+ openWorldHint: false
673
+ }
674
+ }, async (params) => {
675
+ try {
676
+ const sourcePath = normalizePath(params.source);
677
+ const destPath = normalizePath(params.destination);
678
+ if (!await pathExists(sourcePath)) {
679
+ return {
680
+ isError: true,
681
+ content: [{ type: "text", text: t().common.sourceNotFound(sourcePath) }]
682
+ };
683
+ }
684
+ // Create destination directory if needed
685
+ const destDir = path.dirname(destPath);
686
+ if (!await pathExists(destDir)) {
687
+ await fs.mkdir(destDir, { recursive: true });
688
+ }
689
+ await fs.rename(sourcePath, destPath);
690
+ return {
691
+ content: [{ type: "text", text: t().fc_move.moved(sourcePath, destPath) }]
692
+ };
693
+ }
694
+ catch (error) {
695
+ const errorMsg = error instanceof Error ? error.message : String(error);
696
+ return {
697
+ isError: true,
698
+ content: [{ type: "text", text: t().fc_move.moveError(errorMsg) }]
699
+ };
700
+ }
701
+ });
702
+ // ============================================================================
703
+ // Tool: Copy
704
+ // ============================================================================
705
+ server.registerTool("fc_copy", {
706
+ title: "Copy",
707
+ description: `Copies a file or directory.
708
+
709
+ Args:
710
+ - source (string): Source path
711
+ - destination (string): Destination path
712
+ - recursive (boolean): Copy directories recursively`,
713
+ inputSchema: {
714
+ source: z.string().min(1).describe("Source path"),
715
+ destination: z.string().min(1).describe("Destination path"),
716
+ recursive: z.boolean().default(true).describe("Copy recursively")
717
+ },
718
+ annotations: {
719
+ readOnlyHint: false,
720
+ destructiveHint: false,
721
+ idempotentHint: false,
722
+ openWorldHint: false
723
+ }
724
+ }, async (params) => {
725
+ try {
726
+ const sourcePath = normalizePath(params.source);
727
+ const destPath = normalizePath(params.destination);
728
+ if (!await pathExists(sourcePath)) {
729
+ return {
730
+ isError: true,
731
+ content: [{ type: "text", text: t().common.sourceNotFound(sourcePath) }]
732
+ };
733
+ }
734
+ // Create destination directory if needed
735
+ const destDir = path.dirname(destPath);
736
+ if (!await pathExists(destDir)) {
737
+ await fs.mkdir(destDir, { recursive: true });
738
+ }
739
+ const stats = await fs.stat(sourcePath);
740
+ if (stats.isDirectory()) {
741
+ await fs.cp(sourcePath, destPath, { recursive: params.recursive });
742
+ }
743
+ else {
744
+ await fs.copyFile(sourcePath, destPath);
745
+ }
746
+ return {
747
+ content: [{ type: "text", text: t().fc_copy.copied(sourcePath, destPath) }]
748
+ };
749
+ }
750
+ catch (error) {
751
+ const errorMsg = error instanceof Error ? error.message : String(error);
752
+ return {
753
+ isError: true,
754
+ content: [{ type: "text", text: t().fc_copy.copyError(errorMsg) }]
755
+ };
756
+ }
757
+ });
758
+ // ============================================================================
759
+ // Tool: File Info
760
+ // ============================================================================
761
+ server.registerTool("fc_file_info", {
762
+ title: "File Information",
763
+ description: `Shows detailed information about a file/directory.
764
+
765
+ Args:
766
+ - path (string): Path to the file/directory
767
+
768
+ Returns:
769
+ - Size, type, creation/modification date, permissions`,
770
+ inputSchema: {
771
+ path: z.string().min(1).describe("Path to the file/directory")
772
+ },
773
+ annotations: {
774
+ readOnlyHint: true,
775
+ destructiveHint: false,
776
+ idempotentHint: true,
777
+ openWorldHint: false
778
+ }
779
+ }, async (params) => {
780
+ try {
781
+ const targetPath = normalizePath(params.path);
782
+ if (!await pathExists(targetPath)) {
783
+ return {
784
+ isError: true,
785
+ content: [{ type: "text", text: t().common.pathNotFound(targetPath) }]
786
+ };
787
+ }
788
+ const stats = await fs.stat(targetPath);
789
+ const fileType = stats.isDirectory() ? t().fc_file_info.typeDirectory : stats.isFile() ? t().fc_file_info.typeFile : t().fc_file_info.typeOther;
790
+ const locale = getLanguage() === 'de' ? 'de-DE' : 'en-US';
791
+ const info = [
792
+ t().fc_file_info.header(path.basename(targetPath)),
793
+ ``,
794
+ `| ${t().fc_file_info.propType} | ${fileType} |`,
795
+ `|-------------|------|`,
796
+ `| ${t().fc_file_info.propSize} | ${formatFileSize(stats.size)} |`,
797
+ `| ${t().fc_file_info.propCreated} | ${stats.birthtime.toLocaleString(locale)} |`,
798
+ `| ${t().fc_file_info.propModified} | ${stats.mtime.toLocaleString(locale)} |`,
799
+ `| ${t().fc_file_info.propAccessed} | ${stats.atime.toLocaleString(locale)} |`,
800
+ `| ${t().fc_file_info.propPath} | ${targetPath} |`
801
+ ];
802
+ return {
803
+ content: [{ type: "text", text: info.join('\n') }]
804
+ };
805
+ }
806
+ catch (error) {
807
+ const errorMsg = error instanceof Error ? error.message : String(error);
808
+ return {
809
+ isError: true,
810
+ content: [{ type: "text", text: t().common.errorGeneric(errorMsg) }]
811
+ };
812
+ }
813
+ });
814
+ // ============================================================================
815
+ // Tool: Search Files
816
+ // ============================================================================
817
+ server.registerTool("fc_search_files", {
818
+ title: "Search Files",
819
+ description: `Searches files by name/pattern in a directory.
820
+
821
+ Args:
822
+ - directory (string): Start directory for the search
823
+ - pattern (string): Search pattern (supports * and ? wildcards)
824
+ - max_results (number, optional): Maximum results (default: 50)
825
+
826
+ Examples:
827
+ - pattern: "*.txt" - All text files
828
+ - pattern: "test*" - Files starting with "test"
829
+ - pattern: "*.py" - All Python files`,
830
+ inputSchema: {
831
+ directory: z.string().min(1).describe("Start directory"),
832
+ pattern: z.string().min(1).describe("Search pattern with wildcards"),
833
+ max_results: z.number().int().min(1).max(500).default(50).describe("Max results")
834
+ },
835
+ annotations: {
836
+ readOnlyHint: true,
837
+ destructiveHint: false,
838
+ idempotentHint: true,
839
+ openWorldHint: false
840
+ }
841
+ }, async (params) => {
842
+ try {
843
+ const dirPath = normalizePath(params.directory);
844
+ if (!await pathExists(dirPath)) {
845
+ return {
846
+ isError: true,
847
+ content: [{ type: "text", text: t().common.dirNotFound(dirPath) }]
848
+ };
849
+ }
850
+ // Convert wildcard pattern to regex
851
+ const regexPattern = params.pattern
852
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
853
+ .replace(/\*/g, '.*')
854
+ .replace(/\?/g, '.');
855
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
856
+ const results = await searchFilesRecursive(dirPath, regex, params.max_results);
857
+ if (results.length === 0) {
858
+ return {
859
+ content: [{ type: "text", text: t().fc_search_files.noResults(params.pattern) }]
860
+ };
861
+ }
862
+ const output = [
863
+ t().fc_search_files.resultsHeader(params.pattern),
864
+ t().fc_search_files.inDir(dirPath),
865
+ `${t().fc_search_files.found(results.length)} ${results.length >= params.max_results ? t().fc_search_files.maxReached : ''}`,
866
+ ``,
867
+ ...results.map(r => ` \uD83D\uDCC4 ${r}`)
868
+ ];
869
+ return {
870
+ content: [{ type: "text", text: output.join('\n') }]
871
+ };
872
+ }
873
+ catch (error) {
874
+ const errorMsg = error instanceof Error ? error.message : String(error);
875
+ return {
876
+ isError: true,
877
+ content: [{ type: "text", text: t().fc_search_files.searchError(errorMsg) }]
878
+ };
879
+ }
880
+ });
881
+ // ============================================================================
882
+ // Tool: Start Async Search
883
+ // ============================================================================
884
+ server.registerTool("fc_start_search", {
885
+ title: "Start Async Search",
886
+ description: `Starts a background search. Claude can perform other tasks in the meantime.
887
+
888
+ Args:
889
+ - directory (string): Start directory
890
+ - pattern (string): Search pattern (wildcards: * and ?)
891
+
892
+ Returns:
893
+ - Search ID for fc_get_search_results, fc_stop_search
894
+
895
+ Example:
896
+ Start search: fc_start_search("C:\\Users", "*.pdf")
897
+ Get results later: fc_get_search_results(search_id)`,
898
+ inputSchema: {
899
+ directory: z.string().min(1).describe("Start directory"),
900
+ pattern: z.string().min(1).describe("Search pattern with wildcards")
901
+ },
902
+ annotations: {
903
+ readOnlyHint: true,
904
+ destructiveHint: false,
905
+ idempotentHint: false,
906
+ openWorldHint: false
907
+ }
908
+ }, async (params) => {
909
+ try {
910
+ const dirPath = normalizePath(params.directory);
911
+ if (!await pathExists(dirPath)) {
912
+ return {
913
+ isError: true,
914
+ content: [{ type: "text", text: t().common.dirNotFound(dirPath) }]
915
+ };
916
+ }
917
+ // Convert wildcard pattern to regex
918
+ const regexPattern = params.pattern
919
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
920
+ .replace(/\*/g, '.*')
921
+ .replace(/\?/g, '.');
922
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
923
+ const searchId = generateSearchId();
924
+ const abortController = new AbortController();
925
+ const session = {
926
+ id: searchId,
927
+ directory: dirPath,
928
+ pattern: regex,
929
+ patternString: params.pattern,
930
+ results: [],
931
+ isRunning: true,
932
+ startTime: new Date(),
933
+ scannedDirs: 0,
934
+ abortController
935
+ };
936
+ searchSessions.set(searchId, session);
937
+ // Start search in background (don't await)
938
+ asyncSearchFiles(session, dirPath).then(() => {
939
+ session.isRunning = false;
940
+ }).catch(() => {
941
+ session.isRunning = false;
942
+ });
943
+ return {
944
+ content: [{
945
+ type: "text",
946
+ text: `${t().fc_start_search.started(searchId, dirPath, params.pattern)}\n\n${t().fc_start_search.useGetResults}`
947
+ }]
948
+ };
949
+ }
950
+ catch (error) {
951
+ const errorMsg = error instanceof Error ? error.message : String(error);
952
+ return {
953
+ isError: true,
954
+ content: [{ type: "text", text: t().fc_start_search.startError(errorMsg) }]
955
+ };
956
+ }
957
+ });
958
+ // ============================================================================
959
+ // Tool: Get Search Results
960
+ // ============================================================================
961
+ server.registerTool("fc_get_search_results", {
962
+ title: "Get Search Results",
963
+ description: `Retrieves results of a running or completed search.
964
+
965
+ Args:
966
+ - search_id (string): Search ID from fc_start_search
967
+ - offset (number, optional): Start offset for pagination
968
+ - limit (number, optional): Maximum number of results (default: 50)
969
+
970
+ Returns:
971
+ - Search status and found files`,
972
+ inputSchema: {
973
+ search_id: z.string().min(1).describe("Search ID"),
974
+ offset: z.number().int().min(0).default(0).describe("Start offset"),
975
+ limit: z.number().int().min(1).max(200).default(50).describe("Max results")
976
+ },
977
+ annotations: {
978
+ readOnlyHint: true,
979
+ destructiveHint: false,
980
+ idempotentHint: true,
981
+ openWorldHint: false
982
+ }
983
+ }, async (params) => {
984
+ const session = searchSessions.get(params.search_id);
985
+ if (!session) {
986
+ return {
987
+ isError: true,
988
+ content: [{ type: "text", text: `${t().fc_get_search_results.notFound(params.search_id)}\n\n${t().fc_get_search_results.useListSearches}` }]
989
+ };
990
+ }
991
+ const status = session.isRunning ? t().fc_get_search_results.statusRunning : t().fc_get_search_results.statusDone;
992
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
993
+ const totalResults = session.results.length;
994
+ const paginatedResults = session.results.slice(params.offset, params.offset + params.limit);
995
+ const hasMore = totalResults > params.offset + params.limit;
996
+ const output = [
997
+ t().fc_get_search_results.header(status),
998
+ ``,
999
+ `| | |`,
1000
+ `|---|---|`,
1001
+ `| ${t().fc_get_search_results.labelPattern} | ${session.patternString} |`,
1002
+ `| ${t().fc_get_search_results.labelDirectory} | ${session.directory} |`,
1003
+ `| ${t().fc_get_search_results.labelScannedDirs} | ${session.scannedDirs} |`,
1004
+ `| ${t().fc_get_search_results.labelFound(totalResults)} | |`,
1005
+ `| ${t().fc_get_search_results.labelRuntime(runtime)} | |`,
1006
+ ``,
1007
+ t().fc_get_search_results.resultsRange(params.offset + 1, Math.min(params.offset + params.limit, totalResults), totalResults),
1008
+ ``,
1009
+ ...paginatedResults.map(r => ` \uD83D\uDCC4 ${r}`)
1010
+ ];
1011
+ if (hasMore) {
1012
+ output.push(``, t().fc_get_search_results.moreResults(params.search_id, params.offset + params.limit));
1013
+ }
1014
+ return {
1015
+ content: [{ type: "text", text: output.join('\n') }]
1016
+ };
1017
+ });
1018
+ // ============================================================================
1019
+ // Tool: Stop Search
1020
+ // ============================================================================
1021
+ server.registerTool("fc_stop_search", {
1022
+ title: "Stop Search",
1023
+ description: `Stops a running background search.
1024
+
1025
+ Args:
1026
+ - search_id (string): Search ID`,
1027
+ inputSchema: {
1028
+ search_id: z.string().min(1).describe("Search ID")
1029
+ },
1030
+ annotations: {
1031
+ readOnlyHint: false,
1032
+ destructiveHint: false,
1033
+ idempotentHint: true,
1034
+ openWorldHint: false
1035
+ }
1036
+ }, async (params) => {
1037
+ const session = searchSessions.get(params.search_id);
1038
+ if (!session) {
1039
+ return {
1040
+ isError: true,
1041
+ content: [{ type: "text", text: t().fc_stop_search.notFound(params.search_id) }]
1042
+ };
1043
+ }
1044
+ if (!session.isRunning) {
1045
+ return {
1046
+ content: [{ type: "text", text: t().fc_stop_search.alreadyDone(session.results.length) }]
1047
+ };
1048
+ }
1049
+ session.isRunning = false;
1050
+ session.abortController.abort();
1051
+ return {
1052
+ content: [{ type: "text", text: `${t().fc_stop_search.stopped(params.search_id)}\n${t().fc_stop_search.resultsSoFar(session.results.length)}` }]
1053
+ };
1054
+ });
1055
+ // ============================================================================
1056
+ // Tool: List Searches
1057
+ // ============================================================================
1058
+ server.registerTool("fc_list_searches", {
1059
+ title: "List Searches",
1060
+ description: `Lists all active and completed background searches.`,
1061
+ inputSchema: {},
1062
+ annotations: {
1063
+ readOnlyHint: true,
1064
+ destructiveHint: false,
1065
+ idempotentHint: true,
1066
+ openWorldHint: false
1067
+ }
1068
+ }, async () => {
1069
+ if (searchSessions.size === 0) {
1070
+ return {
1071
+ content: [{ type: "text", text: `${t().fc_list_searches.noSearches}\n\n${t().fc_list_searches.useStartSearch}` }]
1072
+ };
1073
+ }
1074
+ const rows = [];
1075
+ for (const [id, session] of searchSessions) {
1076
+ const status = session.isRunning ? '\uD83D\uDD04' : '\u2705';
1077
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
1078
+ rows.push(`| ${status} | \`${id}\` | ${session.patternString} | ${session.results.length} | ${runtime}s |`);
1079
+ }
1080
+ const output = [
1081
+ t().fc_list_searches.header(searchSessions.size),
1082
+ ``,
1083
+ `| ${t().fc_list_searches.colStatus} | ${t().fc_list_searches.colSearchId} | ${t().fc_list_searches.colPattern} | ${t().fc_list_searches.colResults} | ${t().fc_list_searches.colRuntime} |`,
1084
+ `|--------|-----------|--------|------------|----------|`,
1085
+ ...rows
1086
+ ];
1087
+ return {
1088
+ content: [{ type: "text", text: output.join('\n') }]
1089
+ };
1090
+ });
1091
+ // ============================================================================
1092
+ // Tool: Clear Search
1093
+ // ============================================================================
1094
+ server.registerTool("fc_clear_search", {
1095
+ title: "Clear Search",
1096
+ description: `Removes a completed search from the list and frees memory.
1097
+
1098
+ Args:
1099
+ - search_id (string): Search ID (or "all" for all completed)`,
1100
+ inputSchema: {
1101
+ search_id: z.string().min(1).describe("Search ID or 'all'")
1102
+ },
1103
+ annotations: {
1104
+ readOnlyHint: false,
1105
+ destructiveHint: false,
1106
+ idempotentHint: true,
1107
+ openWorldHint: false
1108
+ }
1109
+ }, async (params) => {
1110
+ if (params.search_id === "all") {
1111
+ let count = 0;
1112
+ for (const [id, session] of searchSessions) {
1113
+ if (!session.isRunning) {
1114
+ searchSessions.delete(id);
1115
+ count++;
1116
+ }
1117
+ }
1118
+ return {
1119
+ content: [{ type: "text", text: t().fc_clear_search.cleared(count) }]
1120
+ };
1121
+ }
1122
+ const session = searchSessions.get(params.search_id);
1123
+ if (!session) {
1124
+ return {
1125
+ isError: true,
1126
+ content: [{ type: "text", text: t().fc_clear_search.notFound(params.search_id) }]
1127
+ };
1128
+ }
1129
+ if (session.isRunning) {
1130
+ return {
1131
+ isError: true,
1132
+ content: [{ type: "text", text: t().fc_clear_search.stillRunning }]
1133
+ };
1134
+ }
1135
+ searchSessions.delete(params.search_id);
1136
+ return {
1137
+ content: [{ type: "text", text: t().fc_clear_search.removed(params.search_id) }]
1138
+ };
1139
+ });
1140
+ // ============================================================================
1141
+ // Tool: Safe Delete (Papierkorb)
1142
+ // ============================================================================
1143
+ server.registerTool("fc_safe_delete", {
1144
+ title: "Safe Delete (Recycle Bin)",
1145
+ description: `Moves files/directories to recycle bin instead of deleting them.
1146
+
1147
+ Args:
1148
+ - path (string): Path to the file/directory
1149
+
1150
+ SAFE: Can be restored from the recycle bin!
1151
+
1152
+ Note: Uses Windows recycle bin or creates backup on other systems.`,
1153
+ inputSchema: {
1154
+ path: z.string().min(1).describe("Path to the file/directory")
1155
+ },
1156
+ annotations: {
1157
+ readOnlyHint: false,
1158
+ destructiveHint: false, // Nicht destructive weil wiederherstellbar!
1159
+ idempotentHint: true,
1160
+ openWorldHint: false
1161
+ }
1162
+ }, async (params) => {
1163
+ try {
1164
+ const targetPath = normalizePath(params.path);
1165
+ if (!await pathExists(targetPath)) {
1166
+ return {
1167
+ isError: true,
1168
+ content: [{ type: "text", text: t().common.pathNotFound(targetPath) }]
1169
+ };
1170
+ }
1171
+ const stats = await fs.stat(targetPath);
1172
+ const itemType = stats.isDirectory() ? t().fc_safe_delete.typeDirectory : t().fc_safe_delete.typeFile;
1173
+ const isWindows = process.platform === 'win32';
1174
+ // Windows: PowerShell mit VisualBasic für echten Papierkorb
1175
+ if (isWindows) {
1176
+ // Windows: PowerShell mit VisualBasic für echten Papierkorb
1177
+ const escapedPath = targetPath.replace(/'/g, "''");
1178
+ const deleteMethod = stats.isDirectory() ? 'DeleteDirectory' : 'DeleteFile';
1179
+ const psCommand = `Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${deleteMethod}('${escapedPath}', 'OnlyErrorDialogs', 'SendToRecycleBin')`;
1180
+ const windowsShell = getWindowsShell();
1181
+ if (windowsShell.includes('powershell')) {
1182
+ await execAsync(`"${windowsShell}" -Command "${psCommand}"`);
1183
+ }
1184
+ else {
1185
+ // cmd.exe kann kein PowerShell - normales Löschen als Fallback
1186
+ if (stats.isDirectory()) {
1187
+ await fs.rm(targetPath, { recursive: true });
1188
+ }
1189
+ else {
1190
+ await fs.unlink(targetPath);
1191
+ }
1192
+ }
1193
+ return {
1194
+ content: [{
1195
+ type: "text",
1196
+ text: `${t().fc_safe_delete.movedToTrash}\n\n| | |\n|---|---|\n| ${t().fc_safe_delete.propType} | ${itemType} |\n| ${t().fc_safe_delete.propPath} | ${targetPath} |\n\n${t().fc_safe_delete.canRestore}`
1197
+ }]
1198
+ };
1199
+ }
1200
+ else {
1201
+ // Unix/Mac: Verschiebe in ~/.Trash oder erstelle Backup
1202
+ const trashDir = path.join(process.env.HOME || '/tmp', '.Trash');
1203
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1204
+ const baseName = path.basename(targetPath);
1205
+ const trashPath = path.join(trashDir, `${baseName}_${timestamp}`);
1206
+ try {
1207
+ await fs.access(trashDir);
1208
+ }
1209
+ catch {
1210
+ await fs.mkdir(trashDir, { recursive: true });
1211
+ }
1212
+ await fs.rename(targetPath, trashPath);
1213
+ return {
1214
+ content: [{
1215
+ type: "text",
1216
+ text: `${t().fc_safe_delete.movedToTrash}\n\n| | |\n|---|---|\n| ${t().fc_safe_delete.propType} | ${itemType} |\n| ${t().fc_safe_delete.propOriginal} | ${targetPath} |\n| ${t().fc_safe_delete.propTrash} | ${trashPath} |\n\n${t().fc_safe_delete.canRestore}`
1217
+ }]
1218
+ };
1219
+ }
1220
+ }
1221
+ catch (error) {
1222
+ const errorMsg = error instanceof Error ? error.message : String(error);
1223
+ return {
1224
+ isError: true,
1225
+ content: [{ type: "text", text: t().fc_safe_delete.trashError(errorMsg) }]
1226
+ };
1227
+ }
1228
+ });
1229
+ // ============================================================================
1230
+ // Tool: Execute Command
1231
+ // ============================================================================
1232
+ server.registerTool("fc_execute_command", {
1233
+ title: "Execute Command",
1234
+ description: `Executes a shell command and returns the output.
1235
+
1236
+ Args:
1237
+ - command (string): Command to execute
1238
+ - cwd (string, optional): Working directory
1239
+ - timeout (number, optional): Timeout in milliseconds (default: 30000)
1240
+
1241
+ Warning: Commands are executed with user privileges!
1242
+
1243
+ Examples:
1244
+ - command: "dir" (Windows)
1245
+ - command: "ls -la" (Unix)
1246
+ - command: "python --version"`,
1247
+ inputSchema: {
1248
+ command: z.string().min(1).describe("Command to execute"),
1249
+ cwd: z.string().optional().describe("Working directory"),
1250
+ timeout: z.number().int().min(1000).max(300000).default(30000).describe("Timeout in ms")
1251
+ },
1252
+ annotations: {
1253
+ readOnlyHint: false,
1254
+ destructiveHint: true,
1255
+ idempotentHint: false,
1256
+ openWorldHint: true
1257
+ }
1258
+ }, async (params) => {
1259
+ try {
1260
+ const options = {
1261
+ timeout: params.timeout
1262
+ };
1263
+ if (params.cwd) {
1264
+ options.cwd = normalizePath(params.cwd);
1265
+ }
1266
+ const { stdout, stderr } = await executeCommand(params.command, options);
1267
+ const output = [t().fc_execute_command.commandLabel(params.command)];
1268
+ if (stdout.trim()) {
1269
+ output.push(`\n${t().fc_execute_command.outputLabel}\n\`\`\`\n${stdout.trim()}\n\`\`\``);
1270
+ }
1271
+ if (stderr.trim()) {
1272
+ output.push(`\n${t().fc_execute_command.stderrLabel}\n\`\`\`\n${stderr.trim()}\n\`\`\``);
1273
+ }
1274
+ if (!stdout.trim() && !stderr.trim()) {
1275
+ output.push(`\n${t().fc_execute_command.noOutput}`);
1276
+ }
1277
+ return {
1278
+ content: [{ type: "text", text: output.join('') }]
1279
+ };
1280
+ }
1281
+ catch (error) {
1282
+ const errorMsg = error instanceof Error ? error.message : String(error);
1283
+ return {
1284
+ isError: true,
1285
+ content: [{ type: "text", text: t().fc_execute_command.execError(errorMsg) }]
1286
+ };
1287
+ }
1288
+ });
1289
+ // ============================================================================
1290
+ // Tool: Start Process
1291
+ // ============================================================================
1292
+ server.registerTool("fc_start_process", {
1293
+ title: "Start Process",
1294
+ description: `Starts a process in the background (non-blocking).
1295
+
1296
+ Args:
1297
+ - program (string): Program/Executable
1298
+ - args (array, optional): Arguments as array
1299
+ - cwd (string, optional): Working directory
1300
+
1301
+ Examples:
1302
+ - program: "notepad.exe", args: ["test.txt"]
1303
+ - program: "python", args: ["script.py"]
1304
+ - program: "code", args: ["."] (open VS Code)`,
1305
+ inputSchema: {
1306
+ program: z.string().min(1).describe("Program/Executable"),
1307
+ args: z.array(z.string()).default([]).describe("Arguments"),
1308
+ cwd: z.string().optional().describe("Working directory")
1309
+ },
1310
+ annotations: {
1311
+ readOnlyHint: false,
1312
+ destructiveHint: false,
1313
+ idempotentHint: false,
1314
+ openWorldHint: true
1315
+ }
1316
+ }, async (params) => {
1317
+ try {
1318
+ const options = {
1319
+ detached: true,
1320
+ stdio: 'ignore'
1321
+ };
1322
+ if (params.cwd) {
1323
+ options.cwd = normalizePath(params.cwd);
1324
+ }
1325
+ const child = spawn(params.program, params.args, options);
1326
+ child.unref();
1327
+ const argsStr = params.args.length > 0 ? ` ${params.args.join(' ')}` : '';
1328
+ return {
1329
+ content: [{
1330
+ type: "text",
1331
+ text: `${t().fc_start_process.started(params.program, argsStr)}\n${t().fc_start_process.pidLabel(child.pid)}`
1332
+ }]
1333
+ };
1334
+ }
1335
+ catch (error) {
1336
+ const errorMsg = error instanceof Error ? error.message : String(error);
1337
+ return {
1338
+ isError: true,
1339
+ content: [{ type: "text", text: t().fc_start_process.startError(errorMsg) }]
1340
+ };
1341
+ }
1342
+ });
1343
+ // ============================================================================
1344
+ // Tool: Get Current Time
1345
+ // ============================================================================
1346
+ server.registerTool("fc_get_time", {
1347
+ title: "Current Time",
1348
+ description: `Returns the current system time.
1349
+
1350
+ Returns:
1351
+ - Date, time, weekday, timezone`,
1352
+ inputSchema: {},
1353
+ annotations: {
1354
+ readOnlyHint: true,
1355
+ destructiveHint: false,
1356
+ idempotentHint: false,
1357
+ openWorldHint: false
1358
+ }
1359
+ }, async () => {
1360
+ const now = new Date();
1361
+ const locale = getLanguage() === 'de' ? 'de-DE' : 'en-US';
1362
+ const output = [
1363
+ t().fc_get_time.header,
1364
+ ``,
1365
+ `| | |`,
1366
+ `|---|---|`,
1367
+ `| ${t().fc_get_time.labelDate} | ${now.toLocaleDateString(locale)} |`,
1368
+ `| ${t().fc_get_time.labelTime} | ${now.toLocaleTimeString(locale)} |`,
1369
+ `| ${t().fc_get_time.labelWeekday} | ${t().common.weekdays[now.getDay()]} |`,
1370
+ `| ${t().fc_get_time.labelISO} | ${now.toISOString()} |`,
1371
+ `| ${t().fc_get_time.labelTimezone} | ${Intl.DateTimeFormat().resolvedOptions().timeZone} |`
1372
+ ];
1373
+ return {
1374
+ content: [{ type: "text", text: output.join('\n') }]
1375
+ };
1376
+ });
1377
+ // ============================================================================
1378
+ // Tool: Read Multiple Files
1379
+ // ============================================================================
1380
+ server.registerTool("fc_read_multiple_files", {
1381
+ title: "Read Multiple Files",
1382
+ description: `Reads multiple files at once and returns their contents.
1383
+
1384
+ Args:
1385
+ - paths (array): Array of file paths
1386
+ - max_lines_per_file (number, optional): Max lines per file (0 = all)
1387
+
1388
+ Returns:
1389
+ - Contents of all files with separators
1390
+
1391
+ Example:
1392
+ paths: ["C:\\config.json", "C:\\readme.md"]`,
1393
+ inputSchema: {
1394
+ paths: z.array(z.string().min(1)).min(1).max(20).describe("Array of file paths"),
1395
+ max_lines_per_file: z.number().int().min(0).default(0).describe("Max lines per file")
1396
+ },
1397
+ annotations: {
1398
+ readOnlyHint: true,
1399
+ destructiveHint: false,
1400
+ idempotentHint: true,
1401
+ openWorldHint: false
1402
+ }
1403
+ }, async (params) => {
1404
+ const results = [];
1405
+ let successCount = 0;
1406
+ let errorCount = 0;
1407
+ for (const filePath of params.paths) {
1408
+ const normalizedPath = normalizePath(filePath);
1409
+ try {
1410
+ if (!await pathExists(normalizedPath)) {
1411
+ results.push(`\n\u274C **${path.basename(normalizedPath)}** - ${t().fc_read_multiple_files.notFound}\n`);
1412
+ errorCount++;
1413
+ continue;
1414
+ }
1415
+ const stats = await fs.stat(normalizedPath);
1416
+ if (stats.isDirectory()) {
1417
+ results.push(`\n\u274C **${path.basename(normalizedPath)}** - ${t().fc_read_multiple_files.isDirectory}\n`);
1418
+ errorCount++;
1419
+ continue;
1420
+ }
1421
+ let content = await fs.readFile(normalizedPath, "utf-8");
1422
+ if (params.max_lines_per_file > 0) {
1423
+ const lines = content.split('\n');
1424
+ content = lines.slice(0, params.max_lines_per_file).join('\n');
1425
+ if (lines.length > params.max_lines_per_file) {
1426
+ content += `\n${t().fc_read_multiple_files.moreLines(lines.length - params.max_lines_per_file)}`;
1427
+ }
1428
+ }
1429
+ results.push(`\n📄 **${normalizedPath}** (${formatFileSize(stats.size)})\n${'─'.repeat(60)}\n${content}\n`);
1430
+ successCount++;
1431
+ }
1432
+ catch (error) {
1433
+ const errorMsg = error instanceof Error ? error.message : String(error);
1434
+ results.push(`\n❌ **${path.basename(normalizedPath)}** - ${errorMsg}\n`);
1435
+ errorCount++;
1436
+ }
1437
+ }
1438
+ const summary = `${t().fc_read_multiple_files.summary(successCount, errorCount)}\n${'═'.repeat(60)}`;
1439
+ return {
1440
+ content: [{ type: "text", text: summary + results.join('') }]
1441
+ };
1442
+ });
1443
+ // ============================================================================
1444
+ // Tool: Edit File (Zeilenbasiert)
1445
+ // ============================================================================
1446
+ server.registerTool("fc_edit_file", {
1447
+ title: "Edit File (Lines)",
1448
+ description: `Edits a file line-based: replace, insert, or delete.
1449
+
1450
+ Args:
1451
+ - path (string): Path to the file
1452
+ - operation (string): "replace" | "insert" | "delete"
1453
+ - start_line (number): Start line (1-based)
1454
+ - end_line (number, optional): End line for replace/delete
1455
+ - content (string, optional): New content for replace/insert
1456
+
1457
+ Examples:
1458
+ - Replace lines 5-10: operation="replace", start_line=5, end_line=10, content="new text"
1459
+ - Insert after line 3: operation="insert", start_line=3, content="new line"
1460
+ - Delete lines 7-9: operation="delete", start_line=7, end_line=9`,
1461
+ inputSchema: {
1462
+ path: z.string().min(1).describe("Path to the file"),
1463
+ operation: z.enum(["replace", "insert", "delete"]).describe("Operation"),
1464
+ start_line: z.number().int().min(1).describe("Start line (1-based)"),
1465
+ end_line: z.number().int().min(1).optional().describe("End line"),
1466
+ content: z.string().optional().describe("New content")
1467
+ },
1468
+ annotations: {
1469
+ readOnlyHint: false,
1470
+ destructiveHint: true,
1471
+ idempotentHint: false,
1472
+ openWorldHint: false
1473
+ }
1474
+ }, async (params) => {
1475
+ try {
1476
+ const filePath = normalizePath(params.path);
1477
+ if (!await pathExists(filePath)) {
1478
+ return {
1479
+ isError: true,
1480
+ content: [{ type: "text", text: t().common.fileNotFound(filePath) }]
1481
+ };
1482
+ }
1483
+ const originalContent = await fs.readFile(filePath, "utf-8");
1484
+ const lines = originalContent.split('\n');
1485
+ const totalLines = lines.length;
1486
+ const startIdx = params.start_line - 1;
1487
+ const endIdx = params.end_line ? params.end_line - 1 : startIdx;
1488
+ if (startIdx < 0 || startIdx >= totalLines) {
1489
+ return {
1490
+ isError: true,
1491
+ content: [{ type: "text", text: t().fc_edit_file.invalidStartLine(params.start_line, totalLines) }]
1492
+ };
1493
+ }
1494
+ if (endIdx < startIdx || endIdx >= totalLines) {
1495
+ return {
1496
+ isError: true,
1497
+ content: [{ type: "text", text: t().fc_edit_file.invalidEndLine(params.end_line) }]
1498
+ };
1499
+ }
1500
+ let newLines;
1501
+ let actionDesc;
1502
+ switch (params.operation) {
1503
+ case "replace":
1504
+ if (!params.content) {
1505
+ return {
1506
+ isError: true,
1507
+ content: [{ type: "text", text: t().fc_edit_file.contentRequired('replace') }]
1508
+ };
1509
+ }
1510
+ const replacementLines = params.content.split('\n');
1511
+ newLines = [
1512
+ ...lines.slice(0, startIdx),
1513
+ ...replacementLines,
1514
+ ...lines.slice(endIdx + 1)
1515
+ ];
1516
+ actionDesc = t().fc_edit_file.replacedLines(params.start_line, endIdx + 1, replacementLines.length);
1517
+ break;
1518
+ case "insert":
1519
+ if (!params.content) {
1520
+ return {
1521
+ isError: true,
1522
+ content: [{ type: "text", text: t().fc_edit_file.contentRequired('insert') }]
1523
+ };
1524
+ }
1525
+ const insertLines = params.content.split('\n');
1526
+ newLines = [
1527
+ ...lines.slice(0, startIdx + 1),
1528
+ ...insertLines,
1529
+ ...lines.slice(startIdx + 1)
1530
+ ];
1531
+ actionDesc = t().fc_edit_file.insertedLines(insertLines.length, params.start_line);
1532
+ break;
1533
+ case "delete":
1534
+ newLines = [
1535
+ ...lines.slice(0, startIdx),
1536
+ ...lines.slice(endIdx + 1)
1537
+ ];
1538
+ actionDesc = t().fc_edit_file.deletedLines(params.start_line, endIdx + 1);
1539
+ break;
1540
+ default:
1541
+ return {
1542
+ isError: true,
1543
+ content: [{ type: "text", text: t().fc_edit_file.unknownOperation(params.operation) }]
1544
+ };
1545
+ }
1546
+ await fs.writeFile(filePath, newLines.join('\n'), "utf-8");
1547
+ return {
1548
+ content: [{
1549
+ type: "text",
1550
+ text: `${t().fc_edit_file.edited(path.basename(filePath))}\n\uD83D\uDCDD ${actionDesc}\n${t().fc_edit_file.lineChange(totalLines, newLines.length)}`
1551
+ }]
1552
+ };
1553
+ }
1554
+ catch (error) {
1555
+ const errorMsg = error instanceof Error ? error.message : String(error);
1556
+ return {
1557
+ isError: true,
1558
+ content: [{ type: "text", text: t().fc_edit_file.editError(errorMsg) }]
1559
+ };
1560
+ }
1561
+ });
1562
+ // ============================================================================
1563
+ // Tool: String Replace in File
1564
+ // ============================================================================
1565
+ server.registerTool("fc_str_replace", {
1566
+ title: "String Replace in File",
1567
+ description: `Replaces a unique string in a file with another.
1568
+
1569
+ Args:
1570
+ - path (string): Path to the file
1571
+ - old_str (string): String to replace (must occur exactly once)
1572
+ - new_str (string): New string (empty = delete)
1573
+
1574
+ Returns:
1575
+ - Confirmation with context
1576
+
1577
+ IMPORTANT: old_str must occur EXACTLY once in the file!
1578
+ An error is returned for 0 or >1 occurrences.
1579
+
1580
+ Examples:
1581
+ - Rename function: old_str="def old_name", new_str="def new_name"
1582
+ - Add import: old_str="import os", new_str="import os\\nimport sys"
1583
+ - Delete line: old_str="# TODO: remove this\\n", new_str=""`,
1584
+ inputSchema: {
1585
+ path: z.string().min(1).describe("Path to the file"),
1586
+ old_str: z.string().min(1).describe("String to replace (must be unique)"),
1587
+ new_str: z.string().default("").describe("New string (empty = delete)")
1588
+ },
1589
+ annotations: {
1590
+ readOnlyHint: false,
1591
+ destructiveHint: true,
1592
+ idempotentHint: false,
1593
+ openWorldHint: false
1594
+ }
1595
+ }, async (params) => {
1596
+ try {
1597
+ const filePath = normalizePath(params.path);
1598
+ if (!await pathExists(filePath)) {
1599
+ return {
1600
+ isError: true,
1601
+ content: [{ type: "text", text: t().common.fileNotFound(filePath) }]
1602
+ };
1603
+ }
1604
+ const stats = await fs.stat(filePath);
1605
+ if (stats.isDirectory()) {
1606
+ return {
1607
+ isError: true,
1608
+ content: [{ type: "text", text: t().fc_str_replace.pathIsDirectory(filePath) }]
1609
+ };
1610
+ }
1611
+ const content = await fs.readFile(filePath, "utf-8");
1612
+ // Count occurrences
1613
+ const occurrences = content.split(params.old_str).length - 1;
1614
+ if (occurrences === 0) {
1615
+ // Show a snippet of the file to help debug
1616
+ const preview = content.length > 500 ? content.substring(0, 500) + "..." : content;
1617
+ return {
1618
+ isError: true,
1619
+ content: [{
1620
+ type: "text",
1621
+ text: `${t().fc_str_replace.notFoundInFile(path.basename(filePath))}\n\n${t().fc_str_replace.searchedFor}\n\`\`\`\n${params.old_str}\n\`\`\`\n\n${t().fc_str_replace.fileStart}\n\`\`\`\n${preview}\n\`\`\``
1622
+ }]
1623
+ };
1624
+ }
1625
+ if (occurrences > 1) {
1626
+ return {
1627
+ isError: true,
1628
+ content: [{
1629
+ type: "text",
1630
+ text: `${t().fc_str_replace.multipleOccurrences(occurrences)}\n\n${t().fc_str_replace.searchedFor}\n\`\`\`\n${params.old_str}\n\`\`\`\n\n${t().fc_str_replace.tip}`
1631
+ }]
1632
+ };
1633
+ }
1634
+ // Perform replacement
1635
+ const newContent = content.replace(params.old_str, params.new_str);
1636
+ await fs.writeFile(filePath, newContent, "utf-8");
1637
+ // Calculate change info
1638
+ const oldLines = params.old_str.split('\n').length;
1639
+ const newLines = params.new_str.split('\n').length;
1640
+ const lineChange = newLines - oldLines;
1641
+ const lineInfo = lineChange === 0 ? t().fc_str_replace.sameLineCount :
1642
+ lineChange > 0 ? t().fc_str_replace.addedLines(lineChange) : t().fc_str_replace.removedLines(lineChange);
1643
+ // Show context around the change
1644
+ const changeIndex = content.indexOf(params.old_str);
1645
+ const contextStart = Math.max(0, changeIndex - 50);
1646
+ const contextEnd = Math.min(content.length, changeIndex + params.old_str.length + 50);
1647
+ const beforeContext = content.substring(contextStart, changeIndex);
1648
+ const afterContext = content.substring(changeIndex + params.old_str.length, contextEnd);
1649
+ return {
1650
+ content: [{
1651
+ type: "text",
1652
+ text: `${t().fc_str_replace.replaced(path.basename(filePath))}\n\n| | |\n|---|---|\n| ${t().fc_str_replace.labelChange} | ${lineInfo} |\n| ${t().fc_str_replace.labelFile} | ${filePath} |\n\n${t().fc_str_replace.contextLabel}\n\`\`\`\n...${beforeContext}\u25B6${params.new_str}\u25C0${afterContext}...\n\`\`\``
1653
+ }]
1654
+ };
1655
+ }
1656
+ catch (error) {
1657
+ const errorMsg = error instanceof Error ? error.message : String(error);
1658
+ return {
1659
+ isError: true,
1660
+ content: [{ type: "text", text: t().fc_str_replace.replaceError(errorMsg) }]
1661
+ };
1662
+ }
1663
+ });
1664
+ // ============================================================================
1665
+ // Tool: List Processes
1666
+ // ============================================================================
1667
+ server.registerTool("fc_list_processes", {
1668
+ title: "List Processes",
1669
+ description: `Lists running system processes.
1670
+
1671
+ Args:
1672
+ - filter (string, optional): Filter by process name
1673
+
1674
+ Returns:
1675
+ - List of processes with PID, name, memory
1676
+
1677
+ Note: Uses 'tasklist' (Windows) or 'ps' (Unix)`,
1678
+ inputSchema: {
1679
+ filter: z.string().optional().describe("Filter by process name")
1680
+ },
1681
+ annotations: {
1682
+ readOnlyHint: true,
1683
+ destructiveHint: false,
1684
+ idempotentHint: true,
1685
+ openWorldHint: true
1686
+ }
1687
+ }, async (params) => {
1688
+ try {
1689
+ const isWindows = process.platform === 'win32';
1690
+ let command;
1691
+ if (isWindows) {
1692
+ command = params.filter
1693
+ ? `tasklist /FI "IMAGENAME eq ${params.filter}*" /FO CSV /NH`
1694
+ : `tasklist /FO CSV /NH`;
1695
+ }
1696
+ else {
1697
+ command = params.filter
1698
+ ? `ps aux | grep -i "${params.filter}" | grep -v grep`
1699
+ : `ps aux --sort=-%mem | head -50`;
1700
+ }
1701
+ const { stdout } = await execAsync(command);
1702
+ if (!stdout.trim()) {
1703
+ return {
1704
+ content: [{ type: "text", text: t().fc_list_processes.noProcesses(params.filter) }]
1705
+ };
1706
+ }
1707
+ let output;
1708
+ if (isWindows) {
1709
+ // Parse CSV output from tasklist
1710
+ const lines = stdout.trim().split('\n').filter(l => l.trim());
1711
+ const processes = lines.map(line => {
1712
+ const parts = line.split('","').map(p => p.replace(/"/g, ''));
1713
+ return `| ${parts[0] || '-'} | ${parts[1] || '-'} | ${parts[4] || '-'} |`;
1714
+ });
1715
+ output = [
1716
+ t().fc_list_processes.header(params.filter),
1717
+ ``,
1718
+ `| ${t().fc_list_processes.colName} | ${t().fc_list_processes.colPid} | ${t().fc_list_processes.colMemory} |`,
1719
+ `|------|-----|----------|`,
1720
+ ...processes.slice(0, 50)
1721
+ ].join('\n');
1722
+ }
1723
+ else {
1724
+ output = [
1725
+ t().fc_list_processes.header(params.filter),
1726
+ ``,
1727
+ '```',
1728
+ stdout.trim(),
1729
+ '```'
1730
+ ].join('\n');
1731
+ }
1732
+ return {
1733
+ content: [{ type: "text", text: output }]
1734
+ };
1735
+ }
1736
+ catch (error) {
1737
+ const errorMsg = error instanceof Error ? error.message : String(error);
1738
+ return {
1739
+ isError: true,
1740
+ content: [{ type: "text", text: t().fc_list_processes.listError(errorMsg) }]
1741
+ };
1742
+ }
1743
+ });
1744
+ // ============================================================================
1745
+ // Tool: Kill Process
1746
+ // ============================================================================
1747
+ server.registerTool("fc_kill_process", {
1748
+ title: "Kill Process",
1749
+ description: `Terminates a process by PID or name.
1750
+
1751
+ Args:
1752
+ - pid (number, optional): Process ID
1753
+ - name (string, optional): Process name
1754
+ - force (boolean): Force termination
1755
+
1756
+ Warning: May cause data loss!`,
1757
+ inputSchema: {
1758
+ pid: z.number().int().optional().describe("Process ID"),
1759
+ name: z.string().optional().describe("Process name"),
1760
+ force: z.boolean().default(false).describe("Force")
1761
+ },
1762
+ annotations: {
1763
+ readOnlyHint: false,
1764
+ destructiveHint: true,
1765
+ idempotentHint: true,
1766
+ openWorldHint: true
1767
+ }
1768
+ }, async (params) => {
1769
+ if (!params.pid && !params.name) {
1770
+ return {
1771
+ isError: true,
1772
+ content: [{ type: "text", text: t().fc_kill_process.pidOrNameRequired }]
1773
+ };
1774
+ }
1775
+ try {
1776
+ const isWindows = process.platform === 'win32';
1777
+ let command;
1778
+ if (isWindows) {
1779
+ if (params.pid) {
1780
+ command = params.force
1781
+ ? `taskkill /F /PID ${params.pid}`
1782
+ : `taskkill /PID ${params.pid}`;
1783
+ }
1784
+ else {
1785
+ command = params.force
1786
+ ? `taskkill /F /IM "${params.name}"`
1787
+ : `taskkill /IM "${params.name}"`;
1788
+ }
1789
+ }
1790
+ else {
1791
+ if (params.pid) {
1792
+ command = params.force
1793
+ ? `kill -9 ${params.pid}`
1794
+ : `kill ${params.pid}`;
1795
+ }
1796
+ else {
1797
+ command = params.force
1798
+ ? `pkill -9 "${params.name}"`
1799
+ : `pkill "${params.name}"`;
1800
+ }
1801
+ }
1802
+ const { stdout, stderr } = await execAsync(command);
1803
+ const target = params.pid ? `PID ${params.pid}` : `"${params.name}"`;
1804
+ return {
1805
+ content: [{
1806
+ type: "text",
1807
+ text: `${t().fc_kill_process.killed(target)}\n${stdout || stderr || ''}`.trim()
1808
+ }]
1809
+ };
1810
+ }
1811
+ catch (error) {
1812
+ const errorMsg = error instanceof Error ? error.message : String(error);
1813
+ return {
1814
+ isError: true,
1815
+ content: [{ type: "text", text: t().fc_kill_process.killError(errorMsg) }]
1816
+ };
1817
+ }
1818
+ });
1819
+ // ============================================================================
1820
+ // Tool: Start Interactive Process (Session)
1821
+ // ============================================================================
1822
+ server.registerTool("fc_start_session", {
1823
+ title: "Start Interactive Session",
1824
+ description: `Starts an interactive process as a session (for fc_read_output and fc_send_input).
1825
+
1826
+ Args:
1827
+ - command (string): Command/Program
1828
+ - args (array, optional): Arguments
1829
+ - cwd (string, optional): Working directory
1830
+
1831
+ Returns:
1832
+ - Session ID for further interaction
1833
+
1834
+ Examples:
1835
+ - Python REPL: command="python"
1836
+ - Node REPL: command="node"
1837
+ - PowerShell: command="powershell"`,
1838
+ inputSchema: {
1839
+ command: z.string().min(1).describe("Command/Program"),
1840
+ args: z.array(z.string()).default([]).describe("Arguments"),
1841
+ cwd: z.string().optional().describe("Working directory")
1842
+ },
1843
+ annotations: {
1844
+ readOnlyHint: false,
1845
+ destructiveHint: false,
1846
+ idempotentHint: false,
1847
+ openWorldHint: true
1848
+ }
1849
+ }, async (params) => {
1850
+ try {
1851
+ const sessionId = generateSessionId();
1852
+ const cwd = params.cwd ? normalizePath(params.cwd) : process.cwd();
1853
+ const proc = spawn(params.command, params.args, {
1854
+ cwd,
1855
+ shell: true,
1856
+ stdio: ['pipe', 'pipe', 'pipe']
1857
+ });
1858
+ const session = {
1859
+ id: sessionId,
1860
+ process: proc,
1861
+ command: params.command,
1862
+ args: params.args,
1863
+ cwd,
1864
+ startTime: new Date(),
1865
+ output: [],
1866
+ isRunning: true
1867
+ };
1868
+ // Capture output
1869
+ proc.stdout?.on('data', (data) => {
1870
+ session.output.push(data.toString());
1871
+ // Keep only last 1000 lines
1872
+ if (session.output.length > 1000) {
1873
+ session.output = session.output.slice(-500);
1874
+ }
1875
+ });
1876
+ proc.stderr?.on('data', (data) => {
1877
+ session.output.push(`[stderr] ${data.toString()}`);
1878
+ });
1879
+ proc.on('close', (code) => {
1880
+ session.isRunning = false;
1881
+ session.output.push(t().fc_start_session.processExited(code));
1882
+ });
1883
+ proc.on('error', (err) => {
1884
+ session.isRunning = false;
1885
+ session.output.push(t().fc_start_session.processError(err.message));
1886
+ });
1887
+ processSessions.set(sessionId, session);
1888
+ return {
1889
+ content: [{
1890
+ type: "text",
1891
+ text: `${t().fc_start_session.started(sessionId, `${params.command} ${params.args.join(' ')}`, proc.pid, cwd)}\n\n${t().fc_start_session.useReadAndSend}`
1892
+ }]
1893
+ };
1894
+ }
1895
+ catch (error) {
1896
+ const errorMsg = error instanceof Error ? error.message : String(error);
1897
+ return {
1898
+ isError: true,
1899
+ content: [{ type: "text", text: t().fc_start_session.startError(errorMsg) }]
1900
+ };
1901
+ }
1902
+ });
1903
+ // ============================================================================
1904
+ // Tool: Read Process Output
1905
+ // ============================================================================
1906
+ server.registerTool("fc_read_output", {
1907
+ title: "Read Session Output",
1908
+ description: `Reads the output of a running session.
1909
+
1910
+ Args:
1911
+ - session_id (string): Session ID from fc_start_session
1912
+ - clear (boolean, optional): Clear output after reading
1913
+
1914
+ Returns:
1915
+ - Collected output since start/last clear`,
1916
+ inputSchema: {
1917
+ session_id: z.string().min(1).describe("Session ID"),
1918
+ clear: z.boolean().default(false).describe("Clear output")
1919
+ },
1920
+ annotations: {
1921
+ readOnlyHint: true,
1922
+ destructiveHint: false,
1923
+ idempotentHint: true,
1924
+ openWorldHint: false
1925
+ }
1926
+ }, async (params) => {
1927
+ const session = processSessions.get(params.session_id);
1928
+ if (!session) {
1929
+ return {
1930
+ isError: true,
1931
+ content: [{ type: "text", text: `${t().fc_read_output.notFound(params.session_id)}\n\n${t().fc_read_output.useListSessions}` }]
1932
+ };
1933
+ }
1934
+ const output = session.output.join('');
1935
+ const status = session.isRunning ? t().fc_read_output.statusRunning : t().fc_read_output.statusEnded;
1936
+ if (params.clear) {
1937
+ session.output = [];
1938
+ }
1939
+ return {
1940
+ content: [{
1941
+ type: "text",
1942
+ text: `${t().fc_read_output.header(status)}\n\`\`\`\n${output || t().fc_read_output.noOutput}\n\`\`\``
1943
+ }]
1944
+ };
1945
+ });
1946
+ // ============================================================================
1947
+ // Tool: Send Input to Process
1948
+ // ============================================================================
1949
+ server.registerTool("fc_send_input", {
1950
+ title: "Send Input to Session",
1951
+ description: `Sends input to a running session.
1952
+
1953
+ Args:
1954
+ - session_id (string): Session ID
1955
+ - input (string): Input to send
1956
+ - newline (boolean, optional): Append newline (default: true)
1957
+
1958
+ Examples:
1959
+ - Python: input="print('Hello')"
1960
+ - Shell: input="ls -la"`,
1961
+ inputSchema: {
1962
+ session_id: z.string().min(1).describe("Session ID"),
1963
+ input: z.string().describe("Input to send"),
1964
+ newline: z.boolean().default(true).describe("Append newline")
1965
+ },
1966
+ annotations: {
1967
+ readOnlyHint: false,
1968
+ destructiveHint: false,
1969
+ idempotentHint: false,
1970
+ openWorldHint: true
1971
+ }
1972
+ }, async (params) => {
1973
+ const session = processSessions.get(params.session_id);
1974
+ if (!session) {
1975
+ return {
1976
+ isError: true,
1977
+ content: [{ type: "text", text: t().fc_send_input.notFound(params.session_id) }]
1978
+ };
1979
+ }
1980
+ if (!session.isRunning) {
1981
+ return {
1982
+ isError: true,
1983
+ content: [{ type: "text", text: t().fc_send_input.sessionEnded }]
1984
+ };
1985
+ }
1986
+ try {
1987
+ const inputText = params.newline ? params.input + '\n' : params.input;
1988
+ session.process.stdin?.write(inputText);
1989
+ return {
1990
+ content: [{
1991
+ type: "text",
1992
+ text: `${t().fc_send_input.sent(params.session_id)}\n\`\`\`\n${params.input}\n\`\`\`\n${t().fc_send_input.useReadOutput}`
1993
+ }]
1994
+ };
1995
+ }
1996
+ catch (error) {
1997
+ const errorMsg = error instanceof Error ? error.message : String(error);
1998
+ return {
1999
+ isError: true,
2000
+ content: [{ type: "text", text: t().fc_send_input.sendError(errorMsg) }]
2001
+ };
2002
+ }
2003
+ });
2004
+ // ============================================================================
2005
+ // Tool: List Sessions
2006
+ // ============================================================================
2007
+ server.registerTool("fc_list_sessions", {
2008
+ title: "List Sessions",
2009
+ description: `Lists all active and ended sessions.
2010
+
2011
+ Returns:
2012
+ - Table of all sessions with status`,
2013
+ inputSchema: {},
2014
+ annotations: {
2015
+ readOnlyHint: true,
2016
+ destructiveHint: false,
2017
+ idempotentHint: true,
2018
+ openWorldHint: false
2019
+ }
2020
+ }, async () => {
2021
+ if (processSessions.size === 0) {
2022
+ return {
2023
+ content: [{ type: "text", text: `${t().fc_list_sessions.noSessions}\n\n${t().fc_list_sessions.useStartSession}` }]
2024
+ };
2025
+ }
2026
+ const rows = [];
2027
+ for (const [id, session] of processSessions) {
2028
+ const status = session.isRunning ? '\uD83D\uDFE2' : '\uD83D\uDD34';
2029
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
2030
+ rows.push(`| ${status} | \`${id}\` | ${session.command} | ${session.process.pid || '-'} | ${runtime}s |`);
2031
+ }
2032
+ const output = [
2033
+ t().fc_list_sessions.header(processSessions.size),
2034
+ ``,
2035
+ `| ${t().fc_list_sessions.colStatus} | ${t().fc_list_sessions.colSessionId} | ${t().fc_list_sessions.colCommand} | ${t().fc_list_sessions.colPid} | ${t().fc_list_sessions.colRuntime} |`,
2036
+ `|--------|------------|--------|-----|----------|`,
2037
+ ...rows
2038
+ ];
2039
+ return {
2040
+ content: [{ type: "text", text: output.join('\n') }]
2041
+ };
2042
+ });
2043
+ // ============================================================================
2044
+ // Tool: Close Session
2045
+ // ============================================================================
2046
+ server.registerTool("fc_close_session", {
2047
+ title: "Close Session",
2048
+ description: `Terminates a running session and removes it from the list.
2049
+
2050
+ Args:
2051
+ - session_id (string): Session ID
2052
+ - force (boolean, optional): Force termination`,
2053
+ inputSchema: {
2054
+ session_id: z.string().min(1).describe("Session ID"),
2055
+ force: z.boolean().default(false).describe("Force")
2056
+ },
2057
+ annotations: {
2058
+ readOnlyHint: false,
2059
+ destructiveHint: true,
2060
+ idempotentHint: true,
2061
+ openWorldHint: false
2062
+ }
2063
+ }, async (params) => {
2064
+ const session = processSessions.get(params.session_id);
2065
+ if (!session) {
2066
+ return {
2067
+ isError: true,
2068
+ content: [{ type: "text", text: t().fc_close_session.notFound(params.session_id) }]
2069
+ };
2070
+ }
2071
+ try {
2072
+ if (session.isRunning) {
2073
+ if (params.force) {
2074
+ session.process.kill('SIGKILL');
2075
+ }
2076
+ else {
2077
+ session.process.kill('SIGTERM');
2078
+ }
2079
+ }
2080
+ processSessions.delete(params.session_id);
2081
+ return {
2082
+ content: [{ type: "text", text: t().fc_close_session.closed(params.session_id) }]
2083
+ };
2084
+ }
2085
+ catch (error) {
2086
+ const errorMsg = error instanceof Error ? error.message : String(error);
2087
+ return {
2088
+ isError: true,
2089
+ content: [{ type: "text", text: t().fc_close_session.closeError(errorMsg) }]
2090
+ };
2091
+ }
2092
+ });
2093
+ // ============================================================================
2094
+ // Tool: Fix JSON
2095
+ // ============================================================================
2096
+ server.registerTool("fc_fix_json", {
2097
+ title: "Fix JSON",
2098
+ description: `Automatically repairs common JSON errors.
2099
+
2100
+ Args:
2101
+ - path (string): Path to the JSON file
2102
+ - dry_run (boolean, optional): Only show problems, do not repair
2103
+ - create_backup (boolean, optional): Create backup before repair
2104
+
2105
+ Repairs: BOM, trailing commas, single quotes, comments, NUL bytes`,
2106
+ inputSchema: {
2107
+ path: z.string().min(1).describe("Path to the JSON file"),
2108
+ dry_run: z.boolean().default(false).describe("Only show problems"),
2109
+ create_backup: z.boolean().default(true).describe("Create backup")
2110
+ },
2111
+ annotations: {
2112
+ readOnlyHint: false,
2113
+ destructiveHint: false,
2114
+ idempotentHint: true,
2115
+ openWorldHint: false
2116
+ }
2117
+ }, async (params) => {
2118
+ try {
2119
+ const filePath = normalizePath(params.path);
2120
+ if (!await pathExists(filePath)) {
2121
+ return { isError: true, content: [{ type: "text", text: t().common.fileNotFound(filePath) }] };
2122
+ }
2123
+ const rawContent = await fs.readFile(filePath, "utf-8");
2124
+ const fixes = [];
2125
+ let content = rawContent;
2126
+ // Remove BOM
2127
+ if (content.charCodeAt(0) === 0xFEFF) {
2128
+ content = content.slice(1);
2129
+ fixes.push(t().fc_fix_json.fixBom);
2130
+ }
2131
+ // Remove NUL bytes
2132
+ if (content.includes('\0')) {
2133
+ content = content.replace(/\0/g, '');
2134
+ fixes.push(t().fc_fix_json.fixNul);
2135
+ }
2136
+ // Remove single-line comments
2137
+ const c1 = content;
2138
+ content = content.replace(/^(\s*)\/\/.*$/gm, '');
2139
+ if (content !== c1)
2140
+ fixes.push(t().fc_fix_json.fixSingleLineComments);
2141
+ // Remove multi-line comments
2142
+ const c2 = content;
2143
+ content = content.replace(/\/\*[\s\S]*?\*\//g, '');
2144
+ if (content !== c2)
2145
+ fixes.push(t().fc_fix_json.fixMultiLineComments);
2146
+ // Fix trailing commas before } or ]
2147
+ const c3 = content;
2148
+ content = content.replace(/,(\s*[}\]])/g, '$1');
2149
+ if (content !== c3)
2150
+ fixes.push(t().fc_fix_json.fixTrailingCommas);
2151
+ // Fix single quotes to double quotes for keys and simple values
2152
+ const c4 = content;
2153
+ content = content.replace(/(\s*)'([^'\\]*(?:\\.[^'\\]*)*)'\s*:/g, '$1"$2":');
2154
+ content = content.replace(/:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g, ': "$1"');
2155
+ if (content !== c4)
2156
+ fixes.push(t().fc_fix_json.fixSingleQuotes);
2157
+ // Try to parse
2158
+ let isValid = false;
2159
+ let parseError = '';
2160
+ try {
2161
+ JSON.parse(content);
2162
+ isValid = true;
2163
+ }
2164
+ catch (e) {
2165
+ parseError = e instanceof Error ? e.message : String(e);
2166
+ }
2167
+ if (fixes.length === 0 && isValid) {
2168
+ return { content: [{ type: "text", text: t().fc_fix_json.alreadyValid(path.basename(filePath)) }] };
2169
+ }
2170
+ if (params.dry_run) {
2171
+ return {
2172
+ content: [{ type: "text", text: [
2173
+ t().fc_fix_json.analysisHeader(path.basename(filePath)), '',
2174
+ fixes.length > 0 ? t().fc_fix_json.foundProblems : t().fc_fix_json.noAutoFixable,
2175
+ ...fixes.map(f => ` - ${f}`), '',
2176
+ isValid ? t().fc_fix_json.afterFixValid : t().fc_fix_json.afterFixInvalid(parseError)
2177
+ ].join('\n') }]
2178
+ };
2179
+ }
2180
+ if (params.create_backup && fixes.length > 0) {
2181
+ await fs.writeFile(filePath + '.bak', rawContent, "utf-8");
2182
+ }
2183
+ if (isValid) {
2184
+ content = JSON.stringify(JSON.parse(content), null, 2);
2185
+ }
2186
+ await fs.writeFile(filePath, content, "utf-8");
2187
+ return {
2188
+ content: [{ type: "text", text: [
2189
+ t().fc_fix_json.repairedHeader(path.basename(filePath)), '',
2190
+ ...fixes.map(f => ` - ${f}`), '',
2191
+ isValid ? t().fc_fix_json.validJson : t().fc_fix_json.stillInvalid(parseError),
2192
+ params.create_backup ? t().fc_fix_json.backupCreated(`${filePath}.bak`) : ''
2193
+ ].join('\n') }]
2194
+ };
2195
+ }
2196
+ catch (error) {
2197
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2198
+ }
2199
+ });
2200
+ // ============================================================================
2201
+ // Tool: Validate JSON
2202
+ // ============================================================================
2203
+ server.registerTool("fc_validate_json", {
2204
+ title: "Validate JSON",
2205
+ description: `Validates a JSON file and shows detailed error information.
2206
+
2207
+ Args:
2208
+ - path (string): Path to the JSON file
2209
+
2210
+ Returns:
2211
+ - Validation status with line/column on errors`,
2212
+ inputSchema: {
2213
+ path: z.string().min(1).describe("Path to the JSON file")
2214
+ },
2215
+ annotations: {
2216
+ readOnlyHint: true,
2217
+ destructiveHint: false,
2218
+ idempotentHint: true,
2219
+ openWorldHint: false
2220
+ }
2221
+ }, async (params) => {
2222
+ try {
2223
+ const filePath = normalizePath(params.path);
2224
+ if (!await pathExists(filePath)) {
2225
+ return { isError: true, content: [{ type: "text", text: t().common.fileNotFound(filePath) }] };
2226
+ }
2227
+ const content = await fs.readFile(filePath, "utf-8");
2228
+ const stats = await fs.stat(filePath);
2229
+ try {
2230
+ const parsed = JSON.parse(content);
2231
+ const keyCount = typeof parsed === 'object' && parsed !== null ? Object.keys(parsed).length : 0;
2232
+ const jsonType = Array.isArray(parsed) ? t().fc_validate_json.typeArray(parsed.length) : typeof parsed === 'object' && parsed !== null ? t().fc_validate_json.typeObject(keyCount) : typeof parsed;
2233
+ return {
2234
+ content: [{ type: "text", text: [
2235
+ t().fc_validate_json.validHeader(path.basename(filePath)), '',
2236
+ `| ${t().fc_validate_json.propType} | ${jsonType} |`, `|---|---|`,
2237
+ `| ${t().fc_validate_json.propSize} | ${formatFileSize(stats.size)} |`,
2238
+ `| ${t().fc_validate_json.propBom} | ${content.charCodeAt(0) === 0xFEFF ? t().fc_validate_json.propBomYes : t().fc_validate_json.propBomNo} |`,
2239
+ `| ${t().fc_validate_json.propEncoding} | UTF-8 |`
2240
+ ].join('\n') }]
2241
+ };
2242
+ }
2243
+ catch (e) {
2244
+ const errorMsg = e instanceof Error ? e.message : String(e);
2245
+ // Extract position from error message
2246
+ const posMatch = errorMsg.match(/position\s+(\d+)/i);
2247
+ let lineInfo = '';
2248
+ if (posMatch) {
2249
+ const pos = parseInt(posMatch[1]);
2250
+ const before = content.substring(0, pos);
2251
+ const line = before.split('\n').length;
2252
+ const col = pos - before.lastIndexOf('\n');
2253
+ const lines = content.split('\n');
2254
+ const contextLines = lines.slice(Math.max(0, line - 3), line + 2);
2255
+ lineInfo = `\n${t().fc_validate_json.errorPosition(line, col)}\n\n\`\`\`\n${contextLines.map((l, i) => `${Math.max(1, line - 2) + i}: ${l}`).join('\n')}\n\`\`\``;
2256
+ }
2257
+ return {
2258
+ content: [{ type: "text", text: `${t().fc_validate_json.invalidHeader(path.basename(filePath))}\n\n${t().fc_validate_json.errorLabel} ${errorMsg}${lineInfo}\n\n${t().fc_validate_json.useFcFixJson}` }]
2259
+ };
2260
+ }
2261
+ }
2262
+ catch (error) {
2263
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2264
+ }
2265
+ });
2266
+ // ============================================================================
2267
+ // Tool: Cleanup File
2268
+ // ============================================================================
2269
+ server.registerTool("fc_cleanup_file", {
2270
+ title: "Cleanup File",
2271
+ description: `Cleans up one or more files from common problems.
2272
+
2273
+ Args:
2274
+ - path (string): Path to file or directory
2275
+ - recursive (boolean, optional): Recursive for directories
2276
+ - extensions (string, optional): Filter file extensions (e.g. ".txt,.json,.py")
2277
+ - remove_bom (boolean): Remove UTF-8 BOM
2278
+ - remove_trailing_whitespace (boolean): Remove trailing whitespace
2279
+ - normalize_line_endings (string, optional): "lf" | "crlf" | null
2280
+ - remove_nul_bytes (boolean): Remove NUL bytes
2281
+ - dry_run (boolean): Preview only
2282
+
2283
+ Cleans: BOM, NUL bytes, trailing whitespace, line endings`,
2284
+ inputSchema: {
2285
+ path: z.string().min(1).describe("Path to file/directory"),
2286
+ recursive: z.boolean().default(false).describe("Recursive"),
2287
+ extensions: z.string().optional().describe("Filter extensions (.txt,.json)"),
2288
+ remove_bom: z.boolean().default(true).describe("Remove BOM"),
2289
+ remove_trailing_whitespace: z.boolean().default(true).describe("Trailing whitespace"),
2290
+ normalize_line_endings: z.enum(["lf", "crlf"]).optional().describe("Line endings"),
2291
+ remove_nul_bytes: z.boolean().default(true).describe("Remove NUL bytes"),
2292
+ dry_run: z.boolean().default(false).describe("Preview only")
2293
+ },
2294
+ annotations: {
2295
+ readOnlyHint: false,
2296
+ destructiveHint: false,
2297
+ idempotentHint: true,
2298
+ openWorldHint: false
2299
+ }
2300
+ }, async (params) => {
2301
+ try {
2302
+ const targetPath = normalizePath(params.path);
2303
+ if (!await pathExists(targetPath)) {
2304
+ return { isError: true, content: [{ type: "text", text: t().common.pathNotFound(targetPath) }] };
2305
+ }
2306
+ const stats = await fs.stat(targetPath);
2307
+ const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
2308
+ // Collect files
2309
+ const files = [];
2310
+ if (stats.isDirectory()) {
2311
+ async function collectFiles(dir) {
2312
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2313
+ for (const entry of entries) {
2314
+ const full = path.join(dir, entry.name);
2315
+ if (entry.isDirectory() && params.recursive) {
2316
+ if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
2317
+ await collectFiles(full);
2318
+ }
2319
+ }
2320
+ else if (entry.isFile()) {
2321
+ if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
2322
+ files.push(full);
2323
+ }
2324
+ }
2325
+ }
2326
+ }
2327
+ await collectFiles(targetPath);
2328
+ }
2329
+ else {
2330
+ files.push(targetPath);
2331
+ }
2332
+ const results = [];
2333
+ let totalFixed = 0;
2334
+ for (const filePath of files) {
2335
+ try {
2336
+ const raw = await fs.readFile(filePath, "utf-8");
2337
+ let content = raw;
2338
+ const fixes = [];
2339
+ if (params.remove_bom && content.charCodeAt(0) === 0xFEFF) {
2340
+ content = content.slice(1);
2341
+ fixes.push("BOM");
2342
+ }
2343
+ if (params.remove_nul_bytes && content.includes('\0')) {
2344
+ content = content.replace(/\0/g, '');
2345
+ fixes.push("NUL");
2346
+ }
2347
+ if (params.remove_trailing_whitespace) {
2348
+ const c = content;
2349
+ content = content.replace(/[ \t]+$/gm, '');
2350
+ if (content !== c)
2351
+ fixes.push("Whitespace");
2352
+ }
2353
+ if (params.normalize_line_endings) {
2354
+ const c = content;
2355
+ content = content.replace(/\r\n/g, '\n');
2356
+ if (params.normalize_line_endings === 'crlf') {
2357
+ content = content.replace(/\n/g, '\r\n');
2358
+ }
2359
+ if (content !== c)
2360
+ fixes.push(params.normalize_line_endings.toUpperCase());
2361
+ }
2362
+ if (fixes.length > 0) {
2363
+ if (!params.dry_run) {
2364
+ await fs.writeFile(filePath, content, "utf-8");
2365
+ }
2366
+ results.push(` ✅ ${path.relative(targetPath, filePath) || path.basename(filePath)} [${fixes.join(', ')}]`);
2367
+ totalFixed++;
2368
+ }
2369
+ }
2370
+ catch {
2371
+ // Skip binary/unreadable files
2372
+ }
2373
+ }
2374
+ if (totalFixed === 0) {
2375
+ return { content: [{ type: "text", text: t().fc_cleanup_file.noCleanupNeeded(files.length) }] };
2376
+ }
2377
+ return {
2378
+ content: [{ type: "text", text: [
2379
+ `${params.dry_run ? t().fc_cleanup_file.previewHeader : t().fc_cleanup_file.cleanedHeader}: ${t().fc_cleanup_file.cleanedCount(totalFixed, files.length)}`, '',
2380
+ ...results
2381
+ ].join('\n') }]
2382
+ };
2383
+ }
2384
+ catch (error) {
2385
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2386
+ }
2387
+ });
2388
+ // ============================================================================
2389
+ // Tool: Fix Encoding
2390
+ // ============================================================================
2391
+ server.registerTool("fc_fix_encoding", {
2392
+ title: "Fix Encoding",
2393
+ description: `Detects and repairs encoding errors (mojibake, double UTF-8).
2394
+
2395
+ Args:
2396
+ - path (string): Path to the file
2397
+ - dry_run (boolean): Only show problems
2398
+ - create_backup (boolean): Create backup
2399
+
2400
+ Repairs common mojibake patterns like:
2401
+ - ä -> ae, ö -> oe, ü -> ue (German umlauts)
2402
+ - ß -> ss, € -> EUR`,
2403
+ inputSchema: {
2404
+ path: z.string().min(1).describe("Path to the file"),
2405
+ dry_run: z.boolean().default(false).describe("Preview only"),
2406
+ create_backup: z.boolean().default(true).describe("Create backup")
2407
+ },
2408
+ annotations: {
2409
+ readOnlyHint: false,
2410
+ destructiveHint: false,
2411
+ idempotentHint: true,
2412
+ openWorldHint: false
2413
+ }
2414
+ }, async (params) => {
2415
+ try {
2416
+ const filePath = normalizePath(params.path);
2417
+ if (!await pathExists(filePath)) {
2418
+ return { isError: true, content: [{ type: "text", text: t().common.fileNotFound(filePath) }] };
2419
+ }
2420
+ const rawContent = await fs.readFile(filePath, "utf-8");
2421
+ // Common Mojibake patterns (UTF-8 decoded as Latin-1 then re-encoded as UTF-8)
2422
+ const mojibakeMap = [
2423
+ [/ä/g, 'ä', 'ä'], [/ö/g, 'ö', 'ö'], [/ü/g, 'ü', 'ü'],
2424
+ [/Ä/g, 'Ä', 'Ä'], [/Ö/g, 'Ö', 'Ö'], [/Ü/g, 'Ü', 'Ü'],
2425
+ [/ß/g, 'ß', 'ß'], [/€/g, '€', '€'],
2426
+ [/é/g, 'é', 'é'], [/è/g, 'è', 'è'],
2427
+ [/à /g, 'à', 'à'], [/á/g, 'á', 'á'],
2428
+ [/î/g, 'î', 'î'], [/ï/g, 'ï', 'ï'],
2429
+ [/ô/g, 'ô', 'ô'], [/ù/g, 'ù', 'ù'],
2430
+ [/ç/g, 'ç', 'ç'], [/ñ/g, 'ñ', 'ñ'],
2431
+ [/\u00e2\u0080\u0093/g, '\u2013', 'en-dash'], [/\u00e2\u0080\u0094/g, '\u2014', 'em-dash'],
2432
+ [/\u00e2\u0080\u009c/g, '\u201C', 'left-dquote'], [/\u00e2\u0080\u009d/g, '\u201D', 'right-dquote'],
2433
+ [/\u00e2\u0080\u0098/g, '\u2018', 'left-squote'], [/\u00e2\u0080\u0099/g, '\u2019', 'right-squote'],
2434
+ [/\u00c2\u00a0/g, ' ', 'NBSP'], [/\u00c2\u00a9/g, '\u00A9', '\u00A9'],
2435
+ [/\u00c2\u00ae/g, '\u00AE', '\u00AE'], [/\u00c2\u00b0/g, '\u00B0', '\u00B0'],
2436
+ ];
2437
+ let content = rawContent;
2438
+ const fixes = [];
2439
+ for (const [pattern, replacement, label] of mojibakeMap) {
2440
+ const before = content;
2441
+ content = content.replace(pattern, replacement);
2442
+ if (content !== before) {
2443
+ const count = (before.match(pattern) || []).length;
2444
+ fixes.push(`${label} (${count}x)`);
2445
+ }
2446
+ }
2447
+ if (fixes.length === 0) {
2448
+ return { content: [{ type: "text", text: t().fc_fix_encoding.noErrors(path.basename(filePath)) }] };
2449
+ }
2450
+ if (params.dry_run) {
2451
+ return {
2452
+ content: [{ type: "text", text: [
2453
+ t().fc_fix_encoding.analysisHeader(path.basename(filePath)), '',
2454
+ t().fc_fix_encoding.foundMojibake,
2455
+ ...fixes.map(f => ` - ${f}`)
2456
+ ].join('\n') }]
2457
+ };
2458
+ }
2459
+ if (params.create_backup) {
2460
+ await fs.writeFile(filePath + '.bak', rawContent, "utf-8");
2461
+ }
2462
+ await fs.writeFile(filePath, content, "utf-8");
2463
+ return {
2464
+ content: [{ type: "text", text: [
2465
+ t().fc_fix_encoding.repairedHeader(path.basename(filePath)), '',
2466
+ ...fixes.map(f => ` - ${f}`),
2467
+ params.create_backup ? `\n${t().fc_fix_encoding.backupCreated(`${filePath}.bak`)}` : ''
2468
+ ].join('\n') }]
2469
+ };
2470
+ }
2471
+ catch (error) {
2472
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2473
+ }
2474
+ });
2475
+ // ============================================================================
2476
+ // Tool: Folder Diff
2477
+ // ============================================================================
2478
+ server.registerTool("fc_folder_diff", {
2479
+ title: "Folder Diff",
2480
+ description: `Compares the current state of a directory with a saved snapshot.
2481
+
2482
+ Args:
2483
+ - path (string): Path to the directory
2484
+ - save_snapshot (boolean): Save current state as new snapshot
2485
+ - extensions (string, optional): Filter file extensions
2486
+
2487
+ Detects: New files, modified files, deleted files
2488
+ Snapshots are saved in %TEMP%/.fc_snapshots/`,
2489
+ inputSchema: {
2490
+ path: z.string().min(1).describe("Path to the directory"),
2491
+ save_snapshot: z.boolean().default(true).describe("Save snapshot"),
2492
+ extensions: z.string().optional().describe("Filter extensions")
2493
+ },
2494
+ annotations: {
2495
+ readOnlyHint: true,
2496
+ destructiveHint: false,
2497
+ idempotentHint: true,
2498
+ openWorldHint: false
2499
+ }
2500
+ }, async (params) => {
2501
+ try {
2502
+ const dirPath = normalizePath(params.path);
2503
+ if (!await pathExists(dirPath)) {
2504
+ return { isError: true, content: [{ type: "text", text: t().common.dirNotFound(dirPath) }] };
2505
+ }
2506
+ const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
2507
+ const snapshotDir = path.join(os.tmpdir(), '.fc_snapshots');
2508
+ const snapshotId = crypto.createHash('md5').update(dirPath).digest('hex');
2509
+ const snapshotFile = path.join(snapshotDir, `${snapshotId}.json`);
2510
+ const currentState = {};
2511
+ async function scanDir(dir) {
2512
+ try {
2513
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2514
+ for (const entry of entries) {
2515
+ const full = path.join(dir, entry.name);
2516
+ if (entry.isDirectory()) {
2517
+ if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
2518
+ await scanDir(full);
2519
+ }
2520
+ }
2521
+ else if (entry.isFile()) {
2522
+ if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
2523
+ const stats = await fs.stat(full);
2524
+ const rel = path.relative(dirPath, full);
2525
+ currentState[rel] = { size: stats.size, mtime: stats.mtimeMs };
2526
+ }
2527
+ }
2528
+ }
2529
+ }
2530
+ catch { /* skip inaccessible dirs */ }
2531
+ }
2532
+ await scanDir(dirPath);
2533
+ // Load previous snapshot
2534
+ let previousState = {};
2535
+ let hasSnapshot = false;
2536
+ try {
2537
+ const data = await fs.readFile(snapshotFile, "utf-8");
2538
+ previousState = JSON.parse(data);
2539
+ hasSnapshot = true;
2540
+ }
2541
+ catch { /* no previous snapshot */ }
2542
+ // Compare
2543
+ const newFiles = [];
2544
+ const modifiedFiles = [];
2545
+ const deletedFiles = [];
2546
+ for (const [rel, entry] of Object.entries(currentState)) {
2547
+ if (!previousState[rel]) {
2548
+ newFiles.push(rel);
2549
+ }
2550
+ else if (entry.size !== previousState[rel].size || Math.abs(entry.mtime - previousState[rel].mtime) > 1000) {
2551
+ modifiedFiles.push(rel);
2552
+ }
2553
+ }
2554
+ for (const rel of Object.keys(previousState)) {
2555
+ if (!currentState[rel]) {
2556
+ deletedFiles.push(rel);
2557
+ }
2558
+ }
2559
+ // Save snapshot
2560
+ if (params.save_snapshot) {
2561
+ await fs.mkdir(snapshotDir, { recursive: true });
2562
+ await fs.writeFile(snapshotFile, JSON.stringify(currentState), "utf-8");
2563
+ }
2564
+ const totalFiles = Object.keys(currentState).length;
2565
+ const totalChanges = newFiles.length + modifiedFiles.length + deletedFiles.length;
2566
+ if (!hasSnapshot) {
2567
+ return {
2568
+ content: [{ type: "text", text: [
2569
+ t().fc_folder_diff.firstSnapshot(path.basename(dirPath)), '',
2570
+ `| | |`, `|---|---|`,
2571
+ `| ${t().fc_folder_diff.labelFiles} | ${totalFiles} |`,
2572
+ `| ${t().fc_folder_diff.labelSnapshot} | ${snapshotFile} |`, '',
2573
+ t().fc_folder_diff.nextCallInfo
2574
+ ].join('\n') }]
2575
+ };
2576
+ }
2577
+ if (totalChanges === 0) {
2578
+ return { content: [{ type: "text", text: t().fc_folder_diff.noChanges(path.basename(dirPath), totalFiles) }] };
2579
+ }
2580
+ const output = [
2581
+ t().fc_folder_diff.diffHeader(path.basename(dirPath)), '',
2582
+ `| | |`, `|---|---|`,
2583
+ `| ${t().fc_folder_diff.catNew} | ${newFiles.length} |`,
2584
+ `| ${t().fc_folder_diff.catModified} | ${modifiedFiles.length} |`,
2585
+ `| ${t().fc_folder_diff.catDeleted} | ${deletedFiles.length} |`,
2586
+ `| ${t().fc_folder_diff.catUnchanged} | ${totalFiles - newFiles.length - modifiedFiles.length} |`
2587
+ ];
2588
+ if (newFiles.length > 0) {
2589
+ output.push('', t().fc_folder_diff.newFiles, ...newFiles.slice(0, 50).map(f => ` \uD83D\uDFE2 ${f}`));
2590
+ if (newFiles.length > 50)
2591
+ output.push(` ${t().fc_folder_diff.andMore(newFiles.length - 50)}`);
2592
+ }
2593
+ if (modifiedFiles.length > 0) {
2594
+ output.push('', t().fc_folder_diff.modifiedFiles, ...modifiedFiles.slice(0, 50).map(f => ` \uD83D\uDFE1 ${f}`));
2595
+ if (modifiedFiles.length > 50)
2596
+ output.push(` ${t().fc_folder_diff.andMore(modifiedFiles.length - 50)}`);
2597
+ }
2598
+ if (deletedFiles.length > 0) {
2599
+ output.push('', t().fc_folder_diff.deletedFiles, ...deletedFiles.slice(0, 50).map(f => ` \uD83D\uDD34 ${f}`));
2600
+ if (deletedFiles.length > 50)
2601
+ output.push(` ${t().fc_folder_diff.andMore(deletedFiles.length - 50)}`);
2602
+ }
2603
+ return { content: [{ type: "text", text: output.join('\n') }] };
2604
+ }
2605
+ catch (error) {
2606
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2607
+ }
2608
+ });
2609
+ // ============================================================================
2610
+ // Tool: Batch Rename
2611
+ // ============================================================================
2612
+ server.registerTool("fc_batch_rename", {
2613
+ title: "Batch Rename",
2614
+ description: `Renames files by pattern: remove prefix/suffix, replace, or auto-detect.
2615
+
2616
+ Args:
2617
+ - directory (string): Directory with the files
2618
+ - mode (string): "remove_prefix" | "remove_suffix" | "replace" | "auto_detect"
2619
+ - pattern (string, optional): Text to remove/replace
2620
+ - replacement (string, optional): Replacement text (for replace mode)
2621
+ - extensions (string, optional): Filter by extensions
2622
+ - dry_run (boolean): Preview only
2623
+
2624
+ Examples:
2625
+ - Remove prefix: mode="remove_prefix", pattern="backup_"
2626
+ - Auto-detect: mode="auto_detect" detects common prefixes`,
2627
+ inputSchema: {
2628
+ directory: z.string().min(1).describe("Directory"),
2629
+ mode: z.enum(["remove_prefix", "remove_suffix", "replace", "auto_detect"]).describe("Mode"),
2630
+ pattern: z.string().optional().describe("Text to remove/replace"),
2631
+ replacement: z.string().default("").describe("Replacement text"),
2632
+ extensions: z.string().optional().describe("Filter extensions"),
2633
+ dry_run: z.boolean().default(true).describe("Preview only")
2634
+ },
2635
+ annotations: {
2636
+ readOnlyHint: false,
2637
+ destructiveHint: true,
2638
+ idempotentHint: false,
2639
+ openWorldHint: false
2640
+ }
2641
+ }, async (params) => {
2642
+ try {
2643
+ const dirPath = normalizePath(params.directory);
2644
+ if (!await pathExists(dirPath)) {
2645
+ return { isError: true, content: [{ type: "text", text: t().common.dirNotFound(dirPath) }] };
2646
+ }
2647
+ const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
2648
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
2649
+ const files = entries.filter(e => e.isFile() && (!extFilter || extFilter.includes(path.extname(e.name).toLowerCase())));
2650
+ if (files.length === 0) {
2651
+ return { content: [{ type: "text", text: t().fc_batch_rename.noMatchingFiles(dirPath) }] };
2652
+ }
2653
+ const renames = [];
2654
+ if (params.mode === 'auto_detect') {
2655
+ // Find common prefix
2656
+ const names = files.map(f => f.name);
2657
+ let commonPrefix = names[0] || '';
2658
+ for (let i = 1; i < names.length; i++) {
2659
+ while (!names[i].startsWith(commonPrefix) && commonPrefix.length > 0) {
2660
+ commonPrefix = commonPrefix.slice(0, -1);
2661
+ }
2662
+ }
2663
+ // Find common suffix (before extension)
2664
+ const stems = files.map(f => path.parse(f.name).name);
2665
+ let commonSuffix = stems[0] || '';
2666
+ for (let i = 1; i < stems.length; i++) {
2667
+ while (!stems[i].endsWith(commonSuffix) && commonSuffix.length > 0) {
2668
+ commonSuffix = commonSuffix.slice(1);
2669
+ }
2670
+ }
2671
+ const detections = [];
2672
+ if (commonPrefix.length >= 3)
2673
+ detections.push(`Prefix: "${commonPrefix}"`);
2674
+ if (commonSuffix.length >= 3)
2675
+ detections.push(`Suffix: "${commonSuffix}"`);
2676
+ if (detections.length === 0) {
2677
+ return { content: [{ type: "text", text: t().fc_batch_rename.noCommonPattern(files.length) }] };
2678
+ }
2679
+ // Use prefix if found
2680
+ if (commonPrefix.length >= 3) {
2681
+ for (const f of files) {
2682
+ const newName = f.name.slice(commonPrefix.length);
2683
+ if (newName.length > 0) {
2684
+ renames.push({ old: f.name, new: newName });
2685
+ }
2686
+ }
2687
+ }
2688
+ return {
2689
+ content: [{ type: "text", text: [
2690
+ t().fc_batch_rename.autoDetectHeader(files.length), '',
2691
+ t().fc_batch_rename.detectedPatterns(detections.join(', ')), '',
2692
+ renames.length > 0 ? t().fc_batch_rename.suggestedRename(commonPrefix) : '',
2693
+ ...renames.slice(0, 30).map(r => ` ${r.old} \u2192 ${r.new}`),
2694
+ renames.length > 30 ? ` ${t().fc_batch_rename.andMore(renames.length - 30)}` : '', '',
2695
+ t().fc_batch_rename.useTip(commonPrefix)
2696
+ ].join('\n') }]
2697
+ };
2698
+ }
2699
+ if (!params.pattern) {
2700
+ return { isError: true, content: [{ type: "text", text: t().fc_batch_rename.patternRequired(params.mode) }] };
2701
+ }
2702
+ for (const f of files) {
2703
+ let newName;
2704
+ switch (params.mode) {
2705
+ case 'remove_prefix':
2706
+ newName = f.name.startsWith(params.pattern) ? f.name.slice(params.pattern.length) : f.name;
2707
+ break;
2708
+ case 'remove_suffix': {
2709
+ const parsed = path.parse(f.name);
2710
+ newName = parsed.name.endsWith(params.pattern) ? parsed.name.slice(0, -params.pattern.length) + parsed.ext : f.name;
2711
+ break;
2712
+ }
2713
+ case 'replace':
2714
+ newName = f.name.replace(new RegExp(params.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), params.replacement);
2715
+ break;
2716
+ default:
2717
+ newName = f.name;
2718
+ }
2719
+ if (newName !== f.name && newName.length > 0) {
2720
+ renames.push({ old: f.name, new: newName });
2721
+ }
2722
+ }
2723
+ if (renames.length === 0) {
2724
+ return { content: [{ type: "text", text: t().fc_batch_rename.noFilesMatchPattern(params.pattern) }] };
2725
+ }
2726
+ if (params.dry_run) {
2727
+ return {
2728
+ content: [{ type: "text", text: [
2729
+ t().fc_batch_rename.previewHeader(renames.length), '',
2730
+ ...renames.map(r => ` ${r.old} \u2192 ${r.new}`), '',
2731
+ t().fc_batch_rename.setDryRunFalse
2732
+ ].join('\n') }]
2733
+ };
2734
+ }
2735
+ let successCount = 0;
2736
+ const errors = [];
2737
+ for (const r of renames) {
2738
+ try {
2739
+ await fs.rename(path.join(dirPath, r.old), path.join(dirPath, r.new));
2740
+ successCount++;
2741
+ }
2742
+ catch (e) {
2743
+ errors.push(`${r.old}: ${e instanceof Error ? e.message : String(e)}`);
2744
+ }
2745
+ }
2746
+ return {
2747
+ content: [{ type: "text", text: [
2748
+ t().fc_batch_rename.renamed(successCount, renames.length),
2749
+ ...errors.map(e => ` \u274C ${e}`)
2750
+ ].join('\n') }]
2751
+ };
2752
+ }
2753
+ catch (error) {
2754
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2755
+ }
2756
+ });
2757
+ // ============================================================================
2758
+ // Tool: Convert Format
2759
+ // ============================================================================
2760
+ server.registerTool("fc_convert_format", {
2761
+ title: "Convert Format",
2762
+ description: `Converts files between different formats.
2763
+
2764
+ Args:
2765
+ - input_path (string): Path to source file
2766
+ - output_path (string): Path to target file
2767
+ - input_format (string): "json" | "csv" | "ini" | "yaml" | "toml" | "xml" | "toon"
2768
+ - output_format (string): "json" | "csv" | "ini" | "yaml" | "toml" | "xml" | "toon"
2769
+ - json_indent (number, optional): JSON indentation (default: 2)
2770
+
2771
+ Supported conversions:
2772
+ - JSON <-> CSV (for arrays of objects)
2773
+ - JSON <-> INI (for flat objects/sections)
2774
+ - JSON <-> YAML
2775
+ - JSON <-> TOML
2776
+ - JSON <-> XML
2777
+ - JSON <-> TOON (hierarchical key-value format)
2778
+ - JSON pretty-print / minify`,
2779
+ inputSchema: {
2780
+ input_path: z.string().min(1).describe("Source file"),
2781
+ output_path: z.string().min(1).describe("Target file"),
2782
+ input_format: z.enum(["json", "csv", "ini", "yaml", "toml", "xml", "toon"]).describe("Input format"),
2783
+ output_format: z.enum(["json", "csv", "ini", "yaml", "toml", "xml", "toon"]).describe("Output format"),
2784
+ json_indent: z.number().int().min(0).max(8).default(2).describe("JSON indentation")
2785
+ },
2786
+ annotations: {
2787
+ readOnlyHint: false,
2788
+ destructiveHint: false,
2789
+ idempotentHint: true,
2790
+ openWorldHint: false
2791
+ }
2792
+ }, async (params) => {
2793
+ try {
2794
+ const inputPath = normalizePath(params.input_path);
2795
+ const outputPath = normalizePath(params.output_path);
2796
+ if (!await pathExists(inputPath)) {
2797
+ return { isError: true, content: [{ type: "text", text: t().fc_convert_format.sourceNotFound(inputPath) }] };
2798
+ }
2799
+ const rawContent = await fs.readFile(inputPath, "utf-8");
2800
+ let data;
2801
+ // Parse input
2802
+ switch (params.input_format) {
2803
+ case 'json':
2804
+ data = JSON.parse(rawContent);
2805
+ break;
2806
+ case 'csv': {
2807
+ const lines = rawContent.trim().split('\n');
2808
+ if (lines.length < 2) {
2809
+ return { isError: true, content: [{ type: "text", text: t().fc_convert_format.csvNeedsRows }] };
2810
+ }
2811
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
2812
+ data = lines.slice(1).map(line => {
2813
+ const vals = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
2814
+ const obj = {};
2815
+ headers.forEach((h, i) => { obj[h] = vals[i] || ''; });
2816
+ return obj;
2817
+ });
2818
+ break;
2819
+ }
2820
+ case 'ini': {
2821
+ const result = {};
2822
+ let currentSection = '_default';
2823
+ result[currentSection] = {};
2824
+ for (const line of rawContent.split('\n')) {
2825
+ const trimmed = line.trim();
2826
+ if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#'))
2827
+ continue;
2828
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
2829
+ if (sectionMatch) {
2830
+ currentSection = sectionMatch[1];
2831
+ result[currentSection] = result[currentSection] || {};
2832
+ }
2833
+ else {
2834
+ const eqIdx = trimmed.indexOf('=');
2835
+ if (eqIdx > 0) {
2836
+ const key = trimmed.substring(0, eqIdx).trim();
2837
+ const val = trimmed.substring(eqIdx + 1).trim();
2838
+ result[currentSection][key] = val;
2839
+ }
2840
+ }
2841
+ }
2842
+ // Remove empty default section
2843
+ if (Object.keys(result._default).length === 0)
2844
+ delete result._default;
2845
+ data = result;
2846
+ break;
2847
+ }
2848
+ case 'yaml':
2849
+ data = yaml.load(rawContent);
2850
+ break;
2851
+ case 'toml':
2852
+ data = toml.parse(rawContent);
2853
+ break;
2854
+ case 'xml': {
2855
+ const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
2856
+ data = xmlParser.parse(rawContent);
2857
+ break;
2858
+ }
2859
+ case 'toon':
2860
+ data = toonDecode(rawContent);
2861
+ break;
2862
+ }
2863
+ // Generate output
2864
+ let output;
2865
+ switch (params.output_format) {
2866
+ case 'json':
2867
+ output = JSON.stringify(data, null, params.json_indent || undefined);
2868
+ break;
2869
+ case 'csv': {
2870
+ if (!Array.isArray(data)) {
2871
+ return { isError: true, content: [{ type: "text", text: t().fc_convert_format.csvNeedsArray }] };
2872
+ }
2873
+ const headers = Object.keys(data[0] || {});
2874
+ const rows = data.map((item) => headers.map(h => {
2875
+ const val = String(item[h] ?? '');
2876
+ return val.includes(',') || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
2877
+ }).join(','));
2878
+ output = [headers.join(','), ...rows].join('\n');
2879
+ break;
2880
+ }
2881
+ case 'ini': {
2882
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
2883
+ return { isError: true, content: [{ type: "text", text: t().fc_convert_format.iniNeedsObject }] };
2884
+ }
2885
+ const lines = [];
2886
+ for (const [section, values] of Object.entries(data)) {
2887
+ if (typeof values === 'object' && values !== null && !Array.isArray(values)) {
2888
+ lines.push(`[${section}]`);
2889
+ for (const [key, val] of Object.entries(values)) {
2890
+ lines.push(`${key} = ${val}`);
2891
+ }
2892
+ lines.push('');
2893
+ }
2894
+ else {
2895
+ lines.push(`${section} = ${values}`);
2896
+ }
2897
+ }
2898
+ output = lines.join('\n');
2899
+ break;
2900
+ }
2901
+ case 'yaml':
2902
+ output = yaml.dump(data, { indent: 2, lineWidth: 120 });
2903
+ break;
2904
+ case 'toml':
2905
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
2906
+ return { isError: true, content: [{ type: "text", text: t().fc_convert_format.unsupportedFormat('TOML requires an object as root') }] };
2907
+ }
2908
+ output = toml.stringify(data);
2909
+ break;
2910
+ case 'xml': {
2911
+ const xmlBuilder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: true });
2912
+ output = xmlBuilder.build(data);
2913
+ break;
2914
+ }
2915
+ case 'toon': {
2916
+ output = toonEncode(data);
2917
+ break;
2918
+ }
2919
+ }
2920
+ // Ensure output directory exists
2921
+ const outDir = path.dirname(outputPath);
2922
+ if (!await pathExists(outDir)) {
2923
+ await fs.mkdir(outDir, { recursive: true });
2924
+ }
2925
+ await fs.writeFile(outputPath, output, "utf-8");
2926
+ const outStats = await fs.stat(outputPath);
2927
+ return {
2928
+ content: [{ type: "text", text: [
2929
+ t().fc_convert_format.converted(params.input_format.toUpperCase(), params.output_format.toUpperCase()), '',
2930
+ `| | |`, `|---|---|`,
2931
+ `| ${t().fc_convert_format.labelSource} | ${inputPath} |`,
2932
+ `| ${t().fc_convert_format.labelTarget} | ${outputPath} |`,
2933
+ `| ${t().fc_convert_format.labelSize} | ${formatFileSize(outStats.size)} |`
2934
+ ].join('\n') }]
2935
+ };
2936
+ }
2937
+ catch (error) {
2938
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
2939
+ }
2940
+ });
2941
+ // ============================================================================
2942
+ // Tool: Detect Duplicates
2943
+ // ============================================================================
2944
+ server.registerTool("fc_detect_duplicates", {
2945
+ title: "Detect Duplicates",
2946
+ description: `Finds file duplicates in a directory using SHA-256 hashes.
2947
+
2948
+ Args:
2949
+ - directory (string): Directory to scan
2950
+ - recursive (boolean): Search recursively
2951
+ - extensions (string, optional): Filter by extensions
2952
+ - min_size (number, optional): Minimum size in bytes (default: 1)
2953
+ - max_size (number, optional): Maximum size in bytes
2954
+
2955
+ Returns:
2956
+ - Groups of duplicates with paths and sizes`,
2957
+ inputSchema: {
2958
+ directory: z.string().min(1).describe("Directory"),
2959
+ recursive: z.boolean().default(true).describe("Recursive"),
2960
+ extensions: z.string().optional().describe("Filter extensions"),
2961
+ min_size: z.number().int().min(0).default(1).describe("Minimum size in bytes"),
2962
+ max_size: z.number().int().optional().describe("Maximum size in bytes")
2963
+ },
2964
+ annotations: {
2965
+ readOnlyHint: true,
2966
+ destructiveHint: false,
2967
+ idempotentHint: true,
2968
+ openWorldHint: false
2969
+ }
2970
+ }, async (params) => {
2971
+ try {
2972
+ const dirPath = normalizePath(params.directory);
2973
+ if (!await pathExists(dirPath)) {
2974
+ return { isError: true, content: [{ type: "text", text: t().common.dirNotFound(dirPath) }] };
2975
+ }
2976
+ const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
2977
+ // Collect files with sizes
2978
+ const files = [];
2979
+ async function collectFiles(dir) {
2980
+ try {
2981
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2982
+ for (const entry of entries) {
2983
+ const full = path.join(dir, entry.name);
2984
+ if (entry.isDirectory() && params.recursive) {
2985
+ if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
2986
+ await collectFiles(full);
2987
+ }
2988
+ }
2989
+ else if (entry.isFile()) {
2990
+ if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
2991
+ const stats = await fs.stat(full);
2992
+ if (stats.size >= params.min_size && (!params.max_size || stats.size <= params.max_size)) {
2993
+ files.push({ path: full, size: stats.size });
2994
+ }
2995
+ }
2996
+ }
2997
+ }
2998
+ }
2999
+ catch { /* skip inaccessible */ }
3000
+ }
3001
+ await collectFiles(dirPath);
3002
+ // Group by size first (quick filter)
3003
+ const sizeGroups = new Map();
3004
+ for (const f of files) {
3005
+ const group = sizeGroups.get(f.size) || [];
3006
+ group.push(f.path);
3007
+ sizeGroups.set(f.size, group);
3008
+ }
3009
+ // Hash only files with matching sizes
3010
+ const hashGroups = new Map();
3011
+ let hashedCount = 0;
3012
+ for (const [size, paths] of sizeGroups) {
3013
+ if (paths.length < 2)
3014
+ continue;
3015
+ for (const filePath of paths) {
3016
+ try {
3017
+ const content = await fs.readFile(filePath);
3018
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
3019
+ hashedCount++;
3020
+ const group = hashGroups.get(hash) || { paths: [], size };
3021
+ group.paths.push(filePath);
3022
+ hashGroups.set(hash, group);
3023
+ }
3024
+ catch { /* skip unreadable */ }
3025
+ }
3026
+ }
3027
+ // Filter to actual duplicates
3028
+ const duplicates = [...hashGroups.values()].filter(g => g.paths.length > 1);
3029
+ const totalDuplicateFiles = duplicates.reduce((sum, g) => sum + g.paths.length - 1, 0);
3030
+ const totalWastedSpace = duplicates.reduce((sum, g) => sum + g.size * (g.paths.length - 1), 0);
3031
+ if (duplicates.length === 0) {
3032
+ return {
3033
+ content: [{ type: "text", text: t().fc_detect_duplicates.noDuplicates(files.length, hashedCount) }]
3034
+ };
3035
+ }
3036
+ const output = [
3037
+ t().fc_detect_duplicates.header, '',
3038
+ `| | |`, `|---|---|`,
3039
+ `| ${t().fc_detect_duplicates.labelChecked} | ${files.length} |`,
3040
+ `| ${t().fc_detect_duplicates.labelGroups} | ${duplicates.length} |`,
3041
+ `| ${t().fc_detect_duplicates.labelDuplicates} | ${totalDuplicateFiles} |`,
3042
+ `| ${t().fc_detect_duplicates.labelWasted} | ${formatFileSize(totalWastedSpace)} |`
3043
+ ];
3044
+ for (let i = 0; i < Math.min(duplicates.length, 20); i++) {
3045
+ const group = duplicates[i];
3046
+ output.push('', t().fc_detect_duplicates.groupHeader(i + 1, formatFileSize(group.size)));
3047
+ for (const p of group.paths) {
3048
+ output.push(` \uD83D\uDCC4 ${path.relative(dirPath, p)}`);
3049
+ }
3050
+ }
3051
+ if (duplicates.length > 20) {
3052
+ output.push('', t().fc_detect_duplicates.andMoreGroups(duplicates.length - 20));
3053
+ }
3054
+ output.push('', t().fc_detect_duplicates.useSafeDelete);
3055
+ return { content: [{ type: "text", text: output.join('\n') }] };
3056
+ }
3057
+ catch (error) {
3058
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
3059
+ }
3060
+ });
3061
+ // ============================================================================
3062
+ // Helper: Browser Detection for PDF generation
3063
+ // ============================================================================
3064
+ function findBrowser() {
3065
+ const candidates = process.platform === 'win32' ? [
3066
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
3067
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
3068
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
3069
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
3070
+ ] : process.platform === 'darwin' ? [
3071
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
3072
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
3073
+ ] : [];
3074
+ for (const p of candidates) {
3075
+ if (fsSync.existsSync(p))
3076
+ return p;
3077
+ }
3078
+ if (process.platform === 'linux') {
3079
+ for (const cmd of ['google-chrome', 'chromium-browser', 'chromium', 'microsoft-edge']) {
3080
+ try {
3081
+ const result = execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
3082
+ if (result)
3083
+ return result;
3084
+ }
3085
+ catch { }
3086
+ }
3087
+ }
3088
+ return null;
3089
+ }
3090
+ // ============================================================================
3091
+ // Tool: Markdown to HTML
3092
+ // ============================================================================
3093
+ server.registerTool("fc_md_to_html", {
3094
+ title: "Markdown to HTML",
3095
+ description: `Converts Markdown to formatted HTML (printable as PDF).
3096
+
3097
+ Args:
3098
+ - input_path (string): Path to Markdown file
3099
+ - output_path (string): Path to HTML output
3100
+ - title (string, optional): Document title
3101
+
3102
+ Generates standalone HTML with CSS styling, printable as PDF via browser.`,
3103
+ inputSchema: {
3104
+ input_path: z.string().min(1).describe("Markdown file"),
3105
+ output_path: z.string().min(1).describe("HTML output"),
3106
+ title: z.string().optional().describe("Document title")
3107
+ },
3108
+ annotations: {
3109
+ readOnlyHint: false,
3110
+ destructiveHint: false,
3111
+ idempotentHint: true,
3112
+ openWorldHint: false
3113
+ }
3114
+ }, async (params) => {
3115
+ try {
3116
+ const inputPath = normalizePath(params.input_path);
3117
+ const outputPath = normalizePath(params.output_path);
3118
+ if (!await pathExists(inputPath)) {
3119
+ return { isError: true, content: [{ type: "text", text: t().common.fileNotFound(inputPath) }] };
3120
+ }
3121
+ const md = await fs.readFile(inputPath, "utf-8");
3122
+ const title = params.title || path.basename(inputPath, '.md');
3123
+ // --- Inline formatting ---
3124
+ const inlineFmt = (text) => {
3125
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
3126
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
3127
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
3128
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
3129
+ text = text.replace(/\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g, '<a href="$3"><img src="$2" alt="$1"></a>');
3130
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
3131
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
3132
+ text = text.replace(/\[x\]/gi, '&#9745;');
3133
+ text = text.replace(/\[ \]/g, '&#9744;');
3134
+ return text;
3135
+ };
3136
+ // --- Table parser ---
3137
+ const parseTable = (tableLines) => {
3138
+ if (tableLines.length < 2)
3139
+ return `<p>${inlineFmt(tableLines[0])}</p>`;
3140
+ const rows = tableLines.map(tl => tl.replace(/^\||\|$/g, '').split('|').map(c => c.trim()));
3141
+ let out = '<table>\n<thead>\n<tr>';
3142
+ for (const cell of rows[0])
3143
+ out += `<th>${inlineFmt(cell)}</th>`;
3144
+ out += '</tr>\n</thead>\n<tbody>\n';
3145
+ for (let r = 2; r < rows.length; r++) {
3146
+ out += '<tr>';
3147
+ for (const cell of rows[r])
3148
+ out += `<td>${inlineFmt(cell)}</td>`;
3149
+ out += '</tr>\n';
3150
+ }
3151
+ out += '</tbody>\n</table>';
3152
+ return out;
3153
+ };
3154
+ // --- List parser (nested, ordered + unordered) ---
3155
+ const parseList = (allLines, start) => {
3156
+ const result = [];
3157
+ const stack = [];
3158
+ let li = start;
3159
+ while (li < allLines.length) {
3160
+ const lline = allLines[li].trimEnd();
3161
+ const lm = lline.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
3162
+ if (!lm)
3163
+ break;
3164
+ const indent = lm[1].length;
3165
+ const marker = lm[2];
3166
+ const content = inlineFmt(lm[3]);
3167
+ const tag = /^\d/.test(marker) ? 'ol' : 'ul';
3168
+ const depth = Math.floor(indent / 2);
3169
+ while (stack.length > depth + 1)
3170
+ result.push(`</${stack.pop()}>`);
3171
+ while (stack.length <= depth) {
3172
+ result.push(`<${tag}>`);
3173
+ stack.push(tag);
3174
+ }
3175
+ result.push(`<li>${content}</li>`);
3176
+ li++;
3177
+ }
3178
+ while (stack.length > 0)
3179
+ result.push(`</${stack.pop()}>`);
3180
+ return [result.join('\n'), li];
3181
+ };
3182
+ // --- Line-by-line parser ---
3183
+ const lines = md.split('\n');
3184
+ const parts = [];
3185
+ let i = 0;
3186
+ const n = lines.length;
3187
+ while (i < n) {
3188
+ const line = lines[i].trimEnd();
3189
+ // Fenced code block
3190
+ if (line.trimStart().startsWith('```')) {
3191
+ const lang = line.trim().slice(3).trim();
3192
+ const codeLines = [];
3193
+ i++;
3194
+ while (i < n && !lines[i].trimEnd().trimStart().startsWith('```')) {
3195
+ codeLines.push(lines[i].trimEnd().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
3196
+ i++;
3197
+ }
3198
+ i++;
3199
+ parts.push(`<pre><code class="language-${lang}">${codeLines.join('\n')}</code></pre>`);
3200
+ continue;
3201
+ }
3202
+ // Table
3203
+ if (line.includes('|') && line.trim().startsWith('|') && line.trim().endsWith('|')) {
3204
+ const tableLines = [];
3205
+ while (i < n && lines[i].includes('|') && lines[i].trim().startsWith('|')) {
3206
+ tableLines.push(lines[i].trim());
3207
+ i++;
3208
+ }
3209
+ parts.push(parseTable(tableLines));
3210
+ continue;
3211
+ }
3212
+ // Blockquote
3213
+ if (line.startsWith('>')) {
3214
+ const bqLines = [];
3215
+ while (i < n && lines[i].trimEnd().startsWith('>')) {
3216
+ bqLines.push(inlineFmt(lines[i].trimEnd().replace(/^>\s*/, '')));
3217
+ i++;
3218
+ }
3219
+ parts.push(`<blockquote><p>${bqLines.join('<br>')}</p></blockquote>`);
3220
+ continue;
3221
+ }
3222
+ // Empty line
3223
+ if (line.trim() === '') {
3224
+ i++;
3225
+ continue;
3226
+ }
3227
+ // Horizontal rule
3228
+ if (/^(-{3,}|={3,}|\*{3,})$/.test(line.trim())) {
3229
+ parts.push('<hr>');
3230
+ i++;
3231
+ continue;
3232
+ }
3233
+ // Header
3234
+ const hm = line.match(/^(#{1,6})\s+(.+)$/);
3235
+ if (hm) {
3236
+ const lvl = hm[1].length;
3237
+ parts.push(`<h${lvl}>${inlineFmt(hm[2])}</h${lvl}>`);
3238
+ i++;
3239
+ continue;
3240
+ }
3241
+ // List (ordered or unordered)
3242
+ if (/^(\s*)([-*]|\d+\.)\s+/.test(line)) {
3243
+ const [listHtml, nextI] = parseList(lines, i);
3244
+ parts.push(listHtml);
3245
+ i = nextI;
3246
+ continue;
3247
+ }
3248
+ // Normal paragraph
3249
+ parts.push(`<p>${inlineFmt(line)}</p>`);
3250
+ i++;
3251
+ }
3252
+ const html = parts.join('\n');
3253
+ const fullHtml = `<!DOCTYPE html>
3254
+ <html lang="de">
3255
+ <head>
3256
+ <meta charset="UTF-8">
3257
+ <title>${title}</title>
3258
+ <style>
3259
+ body { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.7; color: #2c3e50; font-size: 11pt; }
3260
+ h1 { color: #1a252f; border-bottom: 3px solid #3498db; padding-bottom: 12px; font-size: 22pt; }
3261
+ h2 { color: #2c3e50; border-bottom: 1px solid #bdc3c7; padding-bottom: 6px; margin-top: 28px; font-size: 16pt; }
3262
+ h3 { color: #34495e; margin-top: 22px; font-size: 13pt; }
3263
+ h4 { color: #7f8c8d; margin-top: 18px; font-size: 11pt; font-style: italic; }
3264
+ p { margin: 8px 0; }
3265
+ code { background: #f0f3f5; padding: 2px 6px; border-radius: 4px; font-family: 'Cascadia Code', Consolas, 'Courier New', monospace; font-size: 0.9em; color: #c0392b; }
3266
+ pre { background: #1e1e2e; color: #cdd6f4; padding: 16px 20px; border-radius: 8px; overflow-x: auto; font-size: 9.5pt; line-height: 1.5; margin: 14px 0; }
3267
+ pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
3268
+ blockquote { border-left: 4px solid #3498db; margin: 16px 0; padding: 10px 20px; background: #f8f9fa; color: #555; border-radius: 0 6px 6px 0; }
3269
+ blockquote p { margin: 4px 0; }
3270
+ table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 10pt; }
3271
+ th { background: #2c3e50; color: white; padding: 10px 14px; text-align: left; font-weight: 600; }
3272
+ td { border: 1px solid #ddd; padding: 8px 14px; }
3273
+ tr:nth-child(even) { background: #f8f9fa; }
3274
+ ul, ol { margin: 6px 0; padding-left: 24px; }
3275
+ li { margin: 4px 0; }
3276
+ hr { border: none; border-top: 1px solid #e0e0e0; margin: 24px 0; }
3277
+ a { color: #2980b9; text-decoration: none; }
3278
+ a:hover { text-decoration: underline; }
3279
+ img { max-width: 100%; }
3280
+ @media print { body { max-width: none; margin: 0; } @page { margin: 2cm 2.5cm; size: A4; } }
3281
+ </style>
3282
+ </head>
3283
+ <body>
3284
+ ${html}
3285
+ </body>
3286
+ </html>`;
3287
+ const outDir = path.dirname(outputPath);
3288
+ if (!await pathExists(outDir))
3289
+ await fs.mkdir(outDir, { recursive: true });
3290
+ await fs.writeFile(outputPath, fullHtml, "utf-8");
3291
+ const outStats = await fs.stat(outputPath);
3292
+ return {
3293
+ content: [{ type: "text", text: [
3294
+ t().fc_md_to_html.converted(path.basename(outputPath)), '',
3295
+ `| | |`, `|---|---|`,
3296
+ `| ${t().fc_md_to_html.labelSource} | ${inputPath} |`,
3297
+ `| ${t().fc_md_to_html.labelTarget} | ${outputPath} |`,
3298
+ `| ${t().fc_md_to_html.labelSize} | ${formatFileSize(outStats.size)} |`, '',
3299
+ t().fc_md_to_html.openInBrowser
3300
+ ].join('\n') }]
3301
+ };
3302
+ }
3303
+ catch (error) {
3304
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
3305
+ }
3306
+ });
3307
+ // ============================================================================
3308
+ // Tool: Markdown to PDF
3309
+ // ============================================================================
3310
+ server.registerTool("fc_md_to_pdf", {
3311
+ title: "Markdown to PDF",
3312
+ description: `Converts Markdown to PDF using a headless browser (Edge/Chrome).
3313
+
3314
+ Args:
3315
+ - input_path (string): Path to the Markdown file
3316
+ - output_path (string): Path to the PDF output
3317
+ - title (string, optional): Document title
3318
+
3319
+ Uses the same Markdown parser as fc_md_to_html. Requires Edge or Chrome.
3320
+ Falls back to HTML if no browser is found.`,
3321
+ inputSchema: {
3322
+ input_path: z.string().min(1).describe("Markdown file"),
3323
+ output_path: z.string().min(1).describe("PDF output"),
3324
+ title: z.string().optional().describe("Document title")
3325
+ },
3326
+ annotations: {
3327
+ readOnlyHint: false,
3328
+ destructiveHint: false,
3329
+ idempotentHint: true,
3330
+ openWorldHint: false
3331
+ }
3332
+ }, async (params) => {
3333
+ try {
3334
+ const inputPath = normalizePath(params.input_path);
3335
+ const outputPath = normalizePath(params.output_path);
3336
+ if (!await pathExists(inputPath)) {
3337
+ return { isError: true, content: [{ type: "text", text: t().common.fileNotFound(inputPath) }] };
3338
+ }
3339
+ const md = await fs.readFile(inputPath, "utf-8");
3340
+ const title = params.title || path.basename(inputPath, '.md');
3341
+ // --- Inline formatting ---
3342
+ const inlineFmt = (text) => {
3343
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
3344
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
3345
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
3346
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
3347
+ text = text.replace(/\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g, '<a href="$3"><img src="$2" alt="$1"></a>');
3348
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
3349
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
3350
+ text = text.replace(/\[x\]/gi, '&#9745;');
3351
+ text = text.replace(/\[ \]/g, '&#9744;');
3352
+ return text;
3353
+ };
3354
+ // --- Table parser ---
3355
+ const parseTable = (tableLines) => {
3356
+ if (tableLines.length < 2)
3357
+ return `<p>${inlineFmt(tableLines[0])}</p>`;
3358
+ const rows = tableLines.map(tl => tl.replace(/^\||\|$/g, '').split('|').map(c => c.trim()));
3359
+ let out = '<table>\n<thead>\n<tr>';
3360
+ for (const cell of rows[0])
3361
+ out += `<th>${inlineFmt(cell)}</th>`;
3362
+ out += '</tr>\n</thead>\n<tbody>\n';
3363
+ for (let r = 2; r < rows.length; r++) {
3364
+ out += '<tr>';
3365
+ for (const cell of rows[r])
3366
+ out += `<td>${inlineFmt(cell)}</td>`;
3367
+ out += '</tr>\n';
3368
+ }
3369
+ out += '</tbody>\n</table>';
3370
+ return out;
3371
+ };
3372
+ // --- List parser (nested, ordered + unordered) ---
3373
+ const parseList = (allLines, start) => {
3374
+ const result = [];
3375
+ const stack = [];
3376
+ let li = start;
3377
+ while (li < allLines.length) {
3378
+ const lline = allLines[li].trimEnd();
3379
+ const lm = lline.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
3380
+ if (!lm)
3381
+ break;
3382
+ const indent = lm[1].length;
3383
+ const marker = lm[2];
3384
+ const content = inlineFmt(lm[3]);
3385
+ const tag = /^\d/.test(marker) ? 'ol' : 'ul';
3386
+ const depth = Math.floor(indent / 2);
3387
+ while (stack.length > depth + 1)
3388
+ result.push(`</${stack.pop()}>`);
3389
+ while (stack.length <= depth) {
3390
+ result.push(`<${tag}>`);
3391
+ stack.push(tag);
3392
+ }
3393
+ result.push(`<li>${content}</li>`);
3394
+ li++;
3395
+ }
3396
+ while (stack.length > 0)
3397
+ result.push(`</${stack.pop()}>`);
3398
+ return [result.join('\n'), li];
3399
+ };
3400
+ // --- Line-by-line parser ---
3401
+ const lines = md.split('\n');
3402
+ const parts = [];
3403
+ let i = 0;
3404
+ const n = lines.length;
3405
+ while (i < n) {
3406
+ const line = lines[i].trimEnd();
3407
+ if (line.trimStart().startsWith('```')) {
3408
+ const lang = line.trim().slice(3).trim();
3409
+ const codeLines = [];
3410
+ i++;
3411
+ while (i < n && !lines[i].trimEnd().trimStart().startsWith('```')) {
3412
+ codeLines.push(lines[i].trimEnd().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'));
3413
+ i++;
3414
+ }
3415
+ i++;
3416
+ parts.push(`<pre><code class="language-${lang}">${codeLines.join('\n')}</code></pre>`);
3417
+ continue;
3418
+ }
3419
+ if (line.includes('|') && line.trim().startsWith('|') && line.trim().endsWith('|')) {
3420
+ const tableLines = [];
3421
+ while (i < n && lines[i].includes('|') && lines[i].trim().startsWith('|')) {
3422
+ tableLines.push(lines[i].trim());
3423
+ i++;
3424
+ }
3425
+ parts.push(parseTable(tableLines));
3426
+ continue;
3427
+ }
3428
+ if (line.startsWith('>')) {
3429
+ const bqLines = [];
3430
+ while (i < n && lines[i].trimEnd().startsWith('>')) {
3431
+ bqLines.push(inlineFmt(lines[i].trimEnd().replace(/^>\s*/, '')));
3432
+ i++;
3433
+ }
3434
+ parts.push(`<blockquote><p>${bqLines.join('<br>')}</p></blockquote>`);
3435
+ continue;
3436
+ }
3437
+ if (line.trim() === '') {
3438
+ i++;
3439
+ continue;
3440
+ }
3441
+ if (/^(-{3,}|={3,}|\*{3,})$/.test(line.trim())) {
3442
+ parts.push('<hr>');
3443
+ i++;
3444
+ continue;
3445
+ }
3446
+ const hm = line.match(/^(#{1,6})\s+(.+)$/);
3447
+ if (hm) {
3448
+ const lvl = hm[1].length;
3449
+ parts.push(`<h${lvl}>${inlineFmt(hm[2])}</h${lvl}>`);
3450
+ i++;
3451
+ continue;
3452
+ }
3453
+ if (/^(\s*)([-*]|\d+\.)\s+/.test(line)) {
3454
+ const [listHtml, nextI] = parseList(lines, i);
3455
+ parts.push(listHtml);
3456
+ i = nextI;
3457
+ continue;
3458
+ }
3459
+ parts.push(`<p>${inlineFmt(line)}</p>`);
3460
+ i++;
3461
+ }
3462
+ const html = parts.join('\n');
3463
+ const fullHtml = `<!DOCTYPE html>
3464
+ <html lang="de">
3465
+ <head>
3466
+ <meta charset="UTF-8">
3467
+ <title>${title}</title>
3468
+ <style>
3469
+ body { font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.7; color: #2c3e50; font-size: 11pt; }
3470
+ h1 { color: #1a252f; border-bottom: 3px solid #3498db; padding-bottom: 12px; font-size: 22pt; }
3471
+ h2 { color: #2c3e50; border-bottom: 1px solid #bdc3c7; padding-bottom: 6px; margin-top: 28px; font-size: 16pt; }
3472
+ h3 { color: #34495e; margin-top: 22px; font-size: 13pt; }
3473
+ h4 { color: #7f8c8d; margin-top: 18px; font-size: 11pt; font-style: italic; }
3474
+ p { margin: 8px 0; }
3475
+ code { background: #f0f3f5; padding: 2px 6px; border-radius: 4px; font-family: 'Cascadia Code', Consolas, 'Courier New', monospace; font-size: 0.9em; color: #c0392b; }
3476
+ pre { background: #1e1e2e; color: #cdd6f4; padding: 16px 20px; border-radius: 8px; overflow-x: auto; font-size: 9.5pt; line-height: 1.5; margin: 14px 0; }
3477
+ pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
3478
+ blockquote { border-left: 4px solid #3498db; margin: 16px 0; padding: 10px 20px; background: #f8f9fa; color: #555; border-radius: 0 6px 6px 0; }
3479
+ blockquote p { margin: 4px 0; }
3480
+ table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 10pt; }
3481
+ th { background: #2c3e50; color: white; padding: 10px 14px; text-align: left; font-weight: 600; }
3482
+ td { border: 1px solid #ddd; padding: 8px 14px; }
3483
+ tr:nth-child(even) { background: #f8f9fa; }
3484
+ ul, ol { margin: 6px 0; padding-left: 24px; }
3485
+ li { margin: 4px 0; }
3486
+ hr { border: none; border-top: 1px solid #e0e0e0; margin: 24px 0; }
3487
+ a { color: #2980b9; text-decoration: none; }
3488
+ a:hover { text-decoration: underline; }
3489
+ img { max-width: 100%; }
3490
+ @media print { body { max-width: none; margin: 0; } @page { margin: 2cm 2.5cm; size: A4; } }
3491
+ </style>
3492
+ </head>
3493
+ <body>
3494
+ ${html}
3495
+ </body>
3496
+ </html>`;
3497
+ // Write temp HTML
3498
+ const tempHtml = outputPath.replace(/\.pdf$/i, '.tmp.html');
3499
+ const outDir = path.dirname(outputPath);
3500
+ if (!await pathExists(outDir))
3501
+ await fs.mkdir(outDir, { recursive: true });
3502
+ await fs.writeFile(tempHtml, fullHtml, "utf-8");
3503
+ const browser = findBrowser();
3504
+ if (!browser) {
3505
+ // Fallback: save as HTML instead of PDF
3506
+ const htmlFallback = outputPath.replace(/\.pdf$/i, '.html');
3507
+ await fs.rename(tempHtml, htmlFallback);
3508
+ const outStats = await fs.stat(htmlFallback);
3509
+ return { content: [{ type: "text", text: [
3510
+ t().fc_md_to_pdf.converted(path.basename(htmlFallback)), '',
3511
+ `| | |`, `|---|---|`,
3512
+ `| ${t().fc_md_to_pdf.labelSource} | ${inputPath} |`,
3513
+ `| ${t().fc_md_to_pdf.labelTarget} | ${htmlFallback} |`,
3514
+ `| ${t().fc_md_to_pdf.labelSize} | ${formatFileSize(outStats.size)} |`, '',
3515
+ t().fc_md_to_pdf.noBrowser
3516
+ ].join('\n') }] };
3517
+ }
3518
+ try {
3519
+ const fileUrl = `file:///${tempHtml.replace(/\\/g, '/')}`;
3520
+ execSync(`"${browser}" --headless --disable-gpu --print-to-pdf="${outputPath}" --no-pdf-header-footer "${fileUrl}"`, { timeout: 30000 });
3521
+ }
3522
+ catch (browserError) {
3523
+ // If browser fails, keep HTML as fallback
3524
+ const htmlFallback = outputPath.replace(/\.pdf$/i, '.html');
3525
+ await fs.rename(tempHtml, htmlFallback);
3526
+ const outStats = await fs.stat(htmlFallback);
3527
+ return { content: [{ type: "text", text: [
3528
+ t().fc_md_to_pdf.converted(path.basename(htmlFallback)), '',
3529
+ `| | |`, `|---|---|`,
3530
+ `| ${t().fc_md_to_pdf.labelSource} | ${inputPath} |`,
3531
+ `| ${t().fc_md_to_pdf.labelTarget} | ${htmlFallback} |`,
3532
+ `| ${t().fc_md_to_pdf.labelSize} | ${formatFileSize(outStats.size)} |`, '',
3533
+ t().fc_md_to_pdf.noBrowser
3534
+ ].join('\n') }] };
3535
+ }
3536
+ // Clean up temp HTML
3537
+ try {
3538
+ await fs.unlink(tempHtml);
3539
+ }
3540
+ catch { }
3541
+ const outStats = await fs.stat(outputPath);
3542
+ const browserName = path.basename(browser).replace(/\.exe$/i, '');
3543
+ return { content: [{ type: "text", text: [
3544
+ t().fc_md_to_pdf.converted(path.basename(outputPath)), '',
3545
+ `| | |`, `|---|---|`,
3546
+ `| ${t().fc_md_to_pdf.labelSource} | ${inputPath} |`,
3547
+ `| ${t().fc_md_to_pdf.labelTarget} | ${outputPath} |`,
3548
+ `| ${t().fc_md_to_pdf.labelSize} | ${formatFileSize(outStats.size)} |`, '',
3549
+ t().fc_md_to_pdf.browserUsed(browserName)
3550
+ ].join('\n') }] };
3551
+ }
3552
+ catch (error) {
3553
+ return { isError: true, content: [{ type: "text", text: t().common.errorGeneric(error instanceof Error ? error.message : String(error)) }] };
3554
+ }
3555
+ });
3556
+ // ============================================================================
3557
+ // Tool: OCR - Text from Image
3558
+ // ============================================================================
3559
+ server.registerTool("fc_ocr", {
3560
+ title: "OCR - Text from Image/PDF",
3561
+ description: t().fc_ocr.description,
3562
+ inputSchema: {
3563
+ file_path: z.string().min(1).describe("Path to image (jpg/png/bmp/tiff) or PDF file"),
3564
+ language: z.string().default("eng").describe("OCR language (default: eng). Use deu for German, fra for French, etc."),
3565
+ output_path: z.string().optional().describe("Optional: Save extracted text to file"),
3566
+ },
3567
+ annotations: {
3568
+ title: "OCR",
3569
+ readOnlyHint: true,
3570
+ openWorldHint: false
3571
+ }
3572
+ }, async (params) => {
3573
+ let Tesseract;
3574
+ try {
3575
+ Tesseract = await (Function('return import("tesseract.js")')());
3576
+ }
3577
+ catch {
3578
+ return { content: [{ type: "text", text: t().fc_ocr.notInstalled }] };
3579
+ }
3580
+ const filePath = normalizePath(params.file_path);
3581
+ const ext = path.extname(filePath).toLowerCase();
3582
+ if (!['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.pdf'].includes(ext)) {
3583
+ throw new Error(t().fc_ocr.unsupportedFormat(ext));
3584
+ }
3585
+ if (ext === '.pdf') {
3586
+ return { content: [{ type: "text", text: t().fc_ocr.pdfNotYetSupported }] };
3587
+ }
3588
+ const worker = await Tesseract.createWorker(params.language);
3589
+ const result = await worker.recognize(filePath);
3590
+ await worker.terminate();
3591
+ const text = result.data.text;
3592
+ if (params.output_path) {
3593
+ await fs.writeFile(params.output_path, text, 'utf-8');
3594
+ }
3595
+ let response = `${t().fc_ocr.header(path.basename(filePath))}\n`;
3596
+ response += `${t().fc_ocr.labelLanguage}: ${params.language}\n`;
3597
+ response += `${t().fc_ocr.labelConfidence}: ${(result.data.confidence).toFixed(1)}%\n`;
3598
+ response += `${t().fc_ocr.labelChars}: ${text.length}\n`;
3599
+ if (params.output_path)
3600
+ response += `${t().fc_ocr.labelSaved}: ${params.output_path}\n`;
3601
+ response += `\n---\n${text}`;
3602
+ return { content: [{ type: "text", text: response }] };
3603
+ });
3604
+ // ============================================================================
3605
+ // Tool: Archive (ZIP)
3606
+ // ============================================================================
3607
+ server.registerTool("fc_archive", {
3608
+ title: "Archive (ZIP)",
3609
+ description: t().fc_archive.description,
3610
+ inputSchema: {
3611
+ action: z.enum(["create", "extract", "list"]).describe("Action to perform"),
3612
+ archive_path: z.string().min(1).describe("Path to the ZIP archive"),
3613
+ source_paths: z.array(z.string()).optional().describe("Files/folders to add (for create action)"),
3614
+ extract_to: z.string().optional().describe("Extraction target directory (for extract action)"),
3615
+ },
3616
+ annotations: {
3617
+ title: "Archive",
3618
+ readOnlyHint: false,
3619
+ openWorldHint: false
3620
+ }
3621
+ }, async (params) => {
3622
+ const archivePath = normalizePath(params.archive_path);
3623
+ if (params.action === 'create') {
3624
+ if (!params.source_paths || params.source_paths.length === 0)
3625
+ throw new Error('source_paths required for create');
3626
+ const zip = new AdmZip();
3627
+ for (const src of params.source_paths) {
3628
+ const srcPath = normalizePath(src);
3629
+ const stat = await fs.stat(srcPath);
3630
+ if (stat.isDirectory()) {
3631
+ zip.addLocalFolder(srcPath, path.basename(srcPath));
3632
+ }
3633
+ else {
3634
+ zip.addLocalFile(srcPath);
3635
+ }
3636
+ }
3637
+ zip.writeZip(archivePath);
3638
+ const info = await fs.stat(archivePath);
3639
+ return { content: [{ type: "text", text: `${t().fc_archive.created(archivePath)}\n${t().fc_archive.labelSize}: ${(info.size / 1024).toFixed(1)} KB\n${t().fc_archive.labelFiles}: ${params.source_paths.length}` }] };
3640
+ }
3641
+ if (params.action === 'extract') {
3642
+ const target = params.extract_to ? normalizePath(params.extract_to) : path.dirname(archivePath);
3643
+ const zip = new AdmZip(archivePath);
3644
+ zip.extractAllTo(target, true);
3645
+ const entries = zip.getEntries();
3646
+ return { content: [{ type: "text", text: `${t().fc_archive.extracted(archivePath, target)}\n${t().fc_archive.labelFiles}: ${entries.length}` }] };
3647
+ }
3648
+ if (params.action === 'list') {
3649
+ const zip = new AdmZip(archivePath);
3650
+ const entries = zip.getEntries();
3651
+ let listing = `${t().fc_archive.listHeader(archivePath)}\n${t().fc_archive.labelFiles}: ${entries.length}\n\n`;
3652
+ for (const entry of entries) {
3653
+ const size = entry.header.size;
3654
+ listing += `${entry.isDirectory ? '[DIR]' : (size / 1024).toFixed(1) + ' KB'} ${entry.entryName}\n`;
3655
+ }
3656
+ return { content: [{ type: "text", text: listing }] };
3657
+ }
3658
+ throw new Error(`Unknown action: ${params.action}`);
3659
+ });
3660
+ // ============================================================================
3661
+ // Tool: File Checksum
3662
+ // ============================================================================
3663
+ server.registerTool("fc_checksum", {
3664
+ title: "File Checksum",
3665
+ description: t().fc_checksum.description,
3666
+ inputSchema: {
3667
+ file_path: z.string().min(1).describe("Path to file"),
3668
+ algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256").describe("Hash algorithm (default: sha256)"),
3669
+ compare: z.string().optional().describe("Optional: Hash to compare against"),
3670
+ },
3671
+ annotations: {
3672
+ title: "Checksum",
3673
+ readOnlyHint: true,
3674
+ openWorldHint: false
3675
+ }
3676
+ }, async (params) => {
3677
+ const filePath = normalizePath(params.file_path);
3678
+ const content = await fs.readFile(filePath);
3679
+ const hash = createHash(params.algorithm).update(content).digest('hex');
3680
+ let result = `${t().fc_checksum.header(path.basename(filePath))}\n`;
3681
+ result += `${t().fc_checksum.labelAlgorithm}: ${params.algorithm.toUpperCase()}\n`;
3682
+ result += `${t().fc_checksum.labelHash}: ${hash}\n`;
3683
+ if (params.compare) {
3684
+ const match = hash.toLowerCase() === params.compare.toLowerCase();
3685
+ result += `\n${match ? t().fc_checksum.match : t().fc_checksum.mismatch}`;
3686
+ }
3687
+ return { content: [{ type: "text", text: result }] };
3688
+ });
3689
+ // ============================================================================
3690
+ // Tool: Set Safe Mode
3691
+ // ============================================================================
3692
+ server.registerTool("fc_set_safe_mode", {
3693
+ title: "Set Safe Mode",
3694
+ description: t().fc_set_safe_mode.description,
3695
+ inputSchema: {
3696
+ enabled: z.boolean().describe("Enable or disable safe mode"),
3697
+ },
3698
+ annotations: {
3699
+ title: "Safe Mode",
3700
+ readOnlyHint: false,
3701
+ openWorldHint: false
3702
+ }
3703
+ }, async (params) => {
3704
+ safeMode = params.enabled;
3705
+ return { content: [{ type: "text", text: safeMode ? t().fc_set_safe_mode.enabled : t().fc_set_safe_mode.disabled }] };
3706
+ });
3707
+ // ============================================================================
3708
+ // Tool: Set Language
3709
+ // ============================================================================
3710
+ server.tool("fc_set_language", "Set the output language for FileCommander tools", { language: z.enum(["de", "en"]).describe("Language code") }, async ({ language }) => {
3711
+ setLanguage(language);
3712
+ return { content: [{ type: "text", text: t().server.languageSet(language) }] };
3713
+ });
3714
+ // ============================================================================
3715
+ // Server Startup
3716
+ // ============================================================================
3717
+ async function main() {
3718
+ const transport = new StdioServerTransport();
3719
+ await server.connect(transport);
3720
+ console.error(t().server.started);
3721
+ }
3722
+ main().catch((error) => {
3723
+ console.error("Fatal error:", error);
3724
+ process.exit(1);
3725
+ });
3726
+ //# sourceMappingURL=index.js.map