bach-filecommander-mcp 1.3.0

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,2037 @@
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.3.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 * as fs from "fs/promises";
19
+ import * as fsSync from "fs";
20
+ import * as path from "path";
21
+ import { exec, spawn } from "child_process";
22
+ import { promisify } from "util";
23
+ const execAsync = promisify(exec);
24
+ // ============================================================================
25
+ // Server Initialization
26
+ // ============================================================================
27
+ const server = new McpServer({
28
+ name: "bach-filecommander-mcp",
29
+ version: "1.3.0"
30
+ });
31
+ const processSessions = new Map();
32
+ let sessionCounter = 0;
33
+ function generateSessionId() {
34
+ return `session_${++sessionCounter}_${Date.now()}`;
35
+ }
36
+ const searchSessions = new Map();
37
+ let searchCounter = 0;
38
+ function generateSearchId() {
39
+ return `search_${++searchCounter}_${Date.now()}`;
40
+ }
41
+ /**
42
+ * Asynchrone rekursive Suche mit AbortController
43
+ */
44
+ async function asyncSearchFiles(session, dirPath) {
45
+ if (!session.isRunning || session.abortController.signal.aborted) {
46
+ return;
47
+ }
48
+ try {
49
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
50
+ session.scannedDirs++;
51
+ for (const entry of entries) {
52
+ if (!session.isRunning || session.abortController.signal.aborted) {
53
+ return;
54
+ }
55
+ const fullPath = path.join(dirPath, entry.name);
56
+ if (entry.isDirectory()) {
57
+ // Skip system directories
58
+ if (!['node_modules', '.git', '$RECYCLE.BIN', 'System Volume Information', 'Windows', 'Program Files', 'Program Files (x86)'].includes(entry.name)) {
59
+ await asyncSearchFiles(session, fullPath);
60
+ }
61
+ }
62
+ else if (session.pattern.test(entry.name)) {
63
+ session.results.push(fullPath);
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ // Ignore permission errors silently
69
+ }
70
+ }
71
+ // ============================================================================
72
+ // Helper Functions
73
+ // ============================================================================
74
+ /**
75
+ * Normalisiert Pfade für Windows/Unix Kompatibilität
76
+ */
77
+ function normalizePath(inputPath) {
78
+ return path.normalize(inputPath);
79
+ }
80
+ /**
81
+ * Prüft ob ein Pfad existiert
82
+ */
83
+ async function pathExists(targetPath) {
84
+ try {
85
+ await fs.access(targetPath);
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ /**
93
+ * Formatiert Dateigröße menschenlesbar
94
+ */
95
+ function formatFileSize(bytes) {
96
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
97
+ let unitIndex = 0;
98
+ let size = bytes;
99
+ while (size >= 1024 && unitIndex < units.length - 1) {
100
+ size /= 1024;
101
+ unitIndex++;
102
+ }
103
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
104
+ }
105
+ /**
106
+ * Prüft ob ein Pfad Windows-Sonderzeichen enthält die Probleme machen
107
+ */
108
+ function hasWindowsSpecialChars(inputPath) {
109
+ return /[&^%$#@!]/.test(inputPath);
110
+ }
111
+ /**
112
+ * Escaped einen Pfad für PowerShell-Nutzung
113
+ * Behandelt & und andere Sonderzeichen korrekt
114
+ */
115
+ function escapeForPowerShell(inputPath) {
116
+ // In PowerShell: & ist der Call-Operator, muss in Quotes oder escaped werden
117
+ // Backtick (`) ist das Escape-Zeichen in PowerShell
118
+ return inputPath
119
+ .replace(/`/g, '``') // Backtick zuerst escapen
120
+ .replace(/\$/g, '`$') // Dollar-Zeichen
121
+ .replace(/"/g, '`"') // Anführungszeichen
122
+ .replace(/'/g, "''"); // Single quotes verdoppeln
123
+ }
124
+ /**
125
+ * Escaped einen Pfad für cmd.exe-Nutzung
126
+ * Behandelt & ^ % und andere Sonderzeichen
127
+ */
128
+ function escapeForCmd(inputPath) {
129
+ // In cmd.exe: ^ ist das Escape-Zeichen
130
+ return inputPath
131
+ .replace(/([&^%!<>|])/g, '^$1'); // Sonderzeichen mit ^ escapen
132
+ }
133
+ /**
134
+ * Ermittelt den PowerShell-Pfad auf Windows
135
+ * Fallback-Kette: pwsh -> powershell.exe im System32 -> cmd.exe
136
+ */
137
+ function getWindowsShell() {
138
+ const systemRoot = process.env.SystemRoot || 'C:\\Windows';
139
+ const ps7Path = `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
140
+ // Prüfe ob PowerShell im System32 existiert
141
+ try {
142
+ if (fsSync.existsSync(ps7Path)) {
143
+ return ps7Path;
144
+ }
145
+ }
146
+ catch {
147
+ // Ignore
148
+ }
149
+ // Fallback zu cmd.exe
150
+ return `${systemRoot}\\System32\\cmd.exe`;
151
+ }
152
+ /**
153
+ * Führt einen Befehl aus - auf Windows mit korrektem Escaping für Sonderzeichen
154
+ * Erkennt automatisch & und andere problematische Zeichen in Pfaden
155
+ */
156
+ async function executeCommand(command, options = {}) {
157
+ const isWindows = process.platform === 'win32';
158
+ const cwd = options.cwd;
159
+ // Auf Windows bei Sonderzeichen im Pfad oder Befehl: Spezielles Handling
160
+ const cwdHasSpecialChars = cwd && hasWindowsSpecialChars(cwd);
161
+ const cmdHasSpecialChars = hasWindowsSpecialChars(command);
162
+ if (isWindows && (cwdHasSpecialChars || cmdHasSpecialChars)) {
163
+ const windowsShell = getWindowsShell();
164
+ if (windowsShell.includes('powershell')) {
165
+ // PowerShell: Nutze -LiteralPath für Pfade mit Sonderzeichen
166
+ const escapedCwd = cwd ? escapeForPowerShell(cwd) : '';
167
+ // Befehl für PowerShell vorbereiten
168
+ // Bei Pfaden mit & den Call-Operator nutzen: & "pfad mit &"
169
+ let psCommand;
170
+ if (cwd) {
171
+ psCommand = `Set-Location -LiteralPath '${escapedCwd}'; ${command}`;
172
+ }
173
+ else {
174
+ psCommand = command;
175
+ }
176
+ // Wenn der Befehl selbst Pfade mit & enthält, diese in Quotes wrappen
177
+ // Erkennt Muster wie: python "C:\path\with & special\script.py"
178
+ psCommand = psCommand.replace(/(?<!["`'])([A-Za-z]:\\[^"'`\n]*&[^"'`\n]*?)(?=\s|$)/g, '"$1"');
179
+ return execAsync(`"${windowsShell}" -Command "${psCommand.replace(/"/g, '\\"')}"`, {
180
+ timeout: options.timeout
181
+ });
182
+ }
183
+ else {
184
+ // cmd.exe: Escape mit ^
185
+ const escapedCmd = escapeForCmd(command);
186
+ const escapedCwd = cwd ? escapeForCmd(cwd) : undefined;
187
+ return execAsync(escapedCmd, {
188
+ ...options,
189
+ cwd: escapedCwd || cwd,
190
+ shell: windowsShell
191
+ });
192
+ }
193
+ }
194
+ return execAsync(command, options);
195
+ }
196
+ async function listDirectoryRecursive(dirPath, maxDepth, currentDepth = 0) {
197
+ const results = [];
198
+ if (currentDepth > maxDepth)
199
+ return results;
200
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
201
+ for (const entry of entries) {
202
+ const fullPath = path.join(dirPath, entry.name);
203
+ const indent = " ".repeat(currentDepth);
204
+ if (entry.isDirectory()) {
205
+ results.push(`${indent}📁 ${entry.name}/`);
206
+ if (currentDepth < maxDepth) {
207
+ const subEntries = await listDirectoryRecursive(fullPath, maxDepth, currentDepth + 1);
208
+ results.push(...subEntries);
209
+ }
210
+ }
211
+ else {
212
+ results.push(`${indent}📄 ${entry.name}`);
213
+ }
214
+ }
215
+ return results;
216
+ }
217
+ /**
218
+ * Rekursive Dateisuche
219
+ */
220
+ async function searchFilesRecursive(dirPath, pattern, maxResults, results = []) {
221
+ if (results.length >= maxResults)
222
+ return results;
223
+ try {
224
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
225
+ for (const entry of entries) {
226
+ if (results.length >= maxResults)
227
+ break;
228
+ const fullPath = path.join(dirPath, entry.name);
229
+ if (entry.isDirectory()) {
230
+ // Skip system directories
231
+ if (!['node_modules', '.git', '$RECYCLE.BIN', 'System Volume Information'].includes(entry.name)) {
232
+ await searchFilesRecursive(fullPath, pattern, maxResults, results);
233
+ }
234
+ }
235
+ else if (pattern.test(entry.name)) {
236
+ results.push(fullPath);
237
+ }
238
+ }
239
+ }
240
+ catch {
241
+ // Ignore permission errors
242
+ }
243
+ return results;
244
+ }
245
+ // ============================================================================
246
+ // Tool: Read File
247
+ // ============================================================================
248
+ server.registerTool("fc_read_file", {
249
+ title: "Datei lesen",
250
+ description: `Liest den Inhalt einer Datei.
251
+
252
+ Args:
253
+ - path (string): Vollständiger Pfad zur Datei
254
+ - encoding (string, optional): Zeichenkodierung (default: utf-8)
255
+ - max_lines (number, optional): Maximale Anzahl Zeilen (0 = alle)
256
+
257
+ Returns:
258
+ - Dateiinhalt als Text
259
+ - Bei Binärdateien: Base64-kodierter Inhalt
260
+
261
+ Beispiele:
262
+ - path: "C:\\Users\\User\\test.txt"
263
+ - path: "/home/user/config.json"`,
264
+ inputSchema: {
265
+ path: z.string().min(1).describe("Vollständiger Pfad zur Datei"),
266
+ encoding: z.string().default("utf-8").describe("Zeichenkodierung"),
267
+ max_lines: z.number().int().min(0).default(0).describe("Max Zeilen (0 = alle)")
268
+ },
269
+ annotations: {
270
+ readOnlyHint: true,
271
+ destructiveHint: false,
272
+ idempotentHint: true,
273
+ openWorldHint: false
274
+ }
275
+ }, async (params) => {
276
+ try {
277
+ const filePath = normalizePath(params.path);
278
+ if (!await pathExists(filePath)) {
279
+ return {
280
+ isError: true,
281
+ content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }]
282
+ };
283
+ }
284
+ const stats = await fs.stat(filePath);
285
+ if (stats.isDirectory()) {
286
+ return {
287
+ isError: true,
288
+ content: [{ type: "text", text: `❌ Pfad ist ein Verzeichnis: ${filePath}. Nutze fc_list_directory.` }]
289
+ };
290
+ }
291
+ let content = await fs.readFile(filePath, params.encoding);
292
+ if (params.max_lines > 0) {
293
+ const lines = content.split('\n');
294
+ content = lines.slice(0, params.max_lines).join('\n');
295
+ if (lines.length > params.max_lines) {
296
+ content += `\n\n... (${lines.length - params.max_lines} weitere Zeilen)`;
297
+ }
298
+ }
299
+ return {
300
+ content: [{
301
+ type: "text",
302
+ text: `📄 **${path.basename(filePath)}** (${formatFileSize(stats.size)})\n\n${content}`
303
+ }]
304
+ };
305
+ }
306
+ catch (error) {
307
+ const errorMsg = error instanceof Error ? error.message : String(error);
308
+ return {
309
+ isError: true,
310
+ content: [{ type: "text", text: `❌ Fehler beim Lesen: ${errorMsg}` }]
311
+ };
312
+ }
313
+ });
314
+ // ============================================================================
315
+ // Tool: Write File
316
+ // ============================================================================
317
+ server.registerTool("fc_write_file", {
318
+ title: "Datei schreiben",
319
+ description: `Schreibt Inhalt in eine Datei. Erstellt die Datei falls nicht vorhanden.
320
+
321
+ Args:
322
+ - path (string): Vollständiger Pfad zur Datei
323
+ - content (string): Zu schreibender Inhalt
324
+ - append (boolean, optional): An Datei anhängen statt überschreiben
325
+ - create_dirs (boolean, optional): Fehlende Verzeichnisse erstellen
326
+
327
+ Returns:
328
+ - Bestätigung mit Dateigröße
329
+
330
+ ⚠️ ACHTUNG: Überschreibt existierende Dateien ohne Warnung wenn append=false!`,
331
+ inputSchema: {
332
+ path: z.string().min(1).describe("Vollständiger Pfad zur Datei"),
333
+ content: z.string().describe("Zu schreibender Inhalt"),
334
+ append: z.boolean().default(false).describe("An Datei anhängen"),
335
+ create_dirs: z.boolean().default(true).describe("Fehlende Verzeichnisse erstellen")
336
+ },
337
+ annotations: {
338
+ readOnlyHint: false,
339
+ destructiveHint: true,
340
+ idempotentHint: false,
341
+ openWorldHint: false
342
+ }
343
+ }, async (params) => {
344
+ try {
345
+ const filePath = normalizePath(params.path);
346
+ const dirPath = path.dirname(filePath);
347
+ if (params.create_dirs && !await pathExists(dirPath)) {
348
+ await fs.mkdir(dirPath, { recursive: true });
349
+ }
350
+ if (params.append) {
351
+ await fs.appendFile(filePath, params.content, "utf-8");
352
+ }
353
+ else {
354
+ await fs.writeFile(filePath, params.content, "utf-8");
355
+ }
356
+ const stats = await fs.stat(filePath);
357
+ const action = params.append ? "erweitert" : "geschrieben";
358
+ return {
359
+ content: [{
360
+ type: "text",
361
+ text: `✅ Datei ${action}: ${filePath}\n📊 Größe: ${formatFileSize(stats.size)}`
362
+ }]
363
+ };
364
+ }
365
+ catch (error) {
366
+ const errorMsg = error instanceof Error ? error.message : String(error);
367
+ return {
368
+ isError: true,
369
+ content: [{ type: "text", text: `❌ Fehler beim Schreiben: ${errorMsg}` }]
370
+ };
371
+ }
372
+ });
373
+ // ============================================================================
374
+ // Tool: List Directory
375
+ // ============================================================================
376
+ server.registerTool("fc_list_directory", {
377
+ title: "Verzeichnis auflisten",
378
+ description: `Listet Dateien und Unterverzeichnisse auf.
379
+
380
+ Args:
381
+ - path (string): Pfad zum Verzeichnis
382
+ - depth (number, optional): Maximale Tiefe für rekursive Auflistung (default: 1)
383
+ - show_hidden (boolean, optional): Versteckte Dateien anzeigen
384
+
385
+ Returns:
386
+ - Formatierte Liste aller Einträge mit Icons (📁/📄)`,
387
+ inputSchema: {
388
+ path: z.string().min(1).describe("Pfad zum Verzeichnis"),
389
+ depth: z.number().int().min(0).max(10).default(1).describe("Rekursionstiefe"),
390
+ show_hidden: z.boolean().default(false).describe("Versteckte Dateien anzeigen")
391
+ },
392
+ annotations: {
393
+ readOnlyHint: true,
394
+ destructiveHint: false,
395
+ idempotentHint: true,
396
+ openWorldHint: false
397
+ }
398
+ }, async (params) => {
399
+ try {
400
+ const dirPath = normalizePath(params.path);
401
+ if (!await pathExists(dirPath)) {
402
+ return {
403
+ isError: true,
404
+ content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }]
405
+ };
406
+ }
407
+ const stats = await fs.stat(dirPath);
408
+ if (!stats.isDirectory()) {
409
+ return {
410
+ isError: true,
411
+ content: [{ type: "text", text: `❌ Pfad ist keine Verzeichnis: ${dirPath}. Nutze fc_read_file.` }]
412
+ };
413
+ }
414
+ const entries = await listDirectoryRecursive(dirPath, params.depth);
415
+ // Filter hidden files if needed
416
+ const filteredEntries = params.show_hidden
417
+ ? entries
418
+ : entries.filter(e => !e.trim().startsWith('📁 .') && !e.trim().startsWith('📄 .'));
419
+ return {
420
+ content: [{
421
+ type: "text",
422
+ text: `📂 **${dirPath}**\n\n${filteredEntries.join('\n') || '(Verzeichnis ist leer)'}`
423
+ }]
424
+ };
425
+ }
426
+ catch (error) {
427
+ const errorMsg = error instanceof Error ? error.message : String(error);
428
+ return {
429
+ isError: true,
430
+ content: [{ type: "text", text: `❌ Fehler beim Auflisten: ${errorMsg}` }]
431
+ };
432
+ }
433
+ });
434
+ // ============================================================================
435
+ // Tool: Create Directory
436
+ // ============================================================================
437
+ server.registerTool("fc_create_directory", {
438
+ title: "Verzeichnis erstellen",
439
+ description: `Erstellt ein neues Verzeichnis (inkl. Elternverzeichnisse).
440
+
441
+ Args:
442
+ - path (string): Pfad zum neuen Verzeichnis
443
+
444
+ Returns:
445
+ - Bestätigung der Erstellung`,
446
+ inputSchema: {
447
+ path: z.string().min(1).describe("Pfad zum neuen Verzeichnis")
448
+ },
449
+ annotations: {
450
+ readOnlyHint: false,
451
+ destructiveHint: false,
452
+ idempotentHint: true,
453
+ openWorldHint: false
454
+ }
455
+ }, async (params) => {
456
+ try {
457
+ const dirPath = normalizePath(params.path);
458
+ if (await pathExists(dirPath)) {
459
+ return {
460
+ content: [{ type: "text", text: `ℹ️ Verzeichnis existiert bereits: ${dirPath}` }]
461
+ };
462
+ }
463
+ await fs.mkdir(dirPath, { recursive: true });
464
+ return {
465
+ content: [{ type: "text", text: `✅ Verzeichnis erstellt: ${dirPath}` }]
466
+ };
467
+ }
468
+ catch (error) {
469
+ const errorMsg = error instanceof Error ? error.message : String(error);
470
+ return {
471
+ isError: true,
472
+ content: [{ type: "text", text: `❌ Fehler beim Erstellen: ${errorMsg}` }]
473
+ };
474
+ }
475
+ });
476
+ // ============================================================================
477
+ // Tool: Delete File
478
+ // ============================================================================
479
+ server.registerTool("fc_delete_file", {
480
+ title: "Datei löschen",
481
+ description: `Löscht eine Datei.
482
+
483
+ Args:
484
+ - path (string): Pfad zur Datei
485
+
486
+ ⚠️ ACHTUNG: Unwiderruflich! Keine Papierkorb-Funktion.`,
487
+ inputSchema: {
488
+ path: z.string().min(1).describe("Pfad zur Datei")
489
+ },
490
+ annotations: {
491
+ readOnlyHint: false,
492
+ destructiveHint: true,
493
+ idempotentHint: true,
494
+ openWorldHint: false
495
+ }
496
+ }, async (params) => {
497
+ try {
498
+ const filePath = normalizePath(params.path);
499
+ if (!await pathExists(filePath)) {
500
+ return {
501
+ isError: true,
502
+ content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }]
503
+ };
504
+ }
505
+ const stats = await fs.stat(filePath);
506
+ if (stats.isDirectory()) {
507
+ return {
508
+ isError: true,
509
+ content: [{ type: "text", text: `❌ Pfad ist ein Verzeichnis. Nutze fc_delete_directory.` }]
510
+ };
511
+ }
512
+ await fs.unlink(filePath);
513
+ return {
514
+ content: [{ type: "text", text: `✅ Datei gelöscht: ${filePath}` }]
515
+ };
516
+ }
517
+ catch (error) {
518
+ const errorMsg = error instanceof Error ? error.message : String(error);
519
+ return {
520
+ isError: true,
521
+ content: [{ type: "text", text: `❌ Fehler beim Löschen: ${errorMsg}` }]
522
+ };
523
+ }
524
+ });
525
+ // ============================================================================
526
+ // Tool: Delete Directory
527
+ // ============================================================================
528
+ server.registerTool("fc_delete_directory", {
529
+ title: "Verzeichnis löschen",
530
+ description: `Löscht ein Verzeichnis.
531
+
532
+ Args:
533
+ - path (string): Pfad zum Verzeichnis
534
+ - recursive (boolean): Auch nicht-leere Verzeichnisse löschen
535
+
536
+ ⚠️ ACHTUNG: Mit recursive=true werden ALLE Inhalte unwiderruflich gelöscht!`,
537
+ inputSchema: {
538
+ path: z.string().min(1).describe("Pfad zum Verzeichnis"),
539
+ recursive: z.boolean().default(false).describe("Rekursiv löschen")
540
+ },
541
+ annotations: {
542
+ readOnlyHint: false,
543
+ destructiveHint: true,
544
+ idempotentHint: true,
545
+ openWorldHint: false
546
+ }
547
+ }, async (params) => {
548
+ try {
549
+ const dirPath = normalizePath(params.path);
550
+ if (!await pathExists(dirPath)) {
551
+ return {
552
+ isError: true,
553
+ content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }]
554
+ };
555
+ }
556
+ const stats = await fs.stat(dirPath);
557
+ if (!stats.isDirectory()) {
558
+ return {
559
+ isError: true,
560
+ content: [{ type: "text", text: `❌ Pfad ist keine Verzeichnis. Nutze fc_delete_file.` }]
561
+ };
562
+ }
563
+ await fs.rm(dirPath, { recursive: params.recursive });
564
+ return {
565
+ content: [{ type: "text", text: `✅ Verzeichnis gelöscht: ${dirPath}` }]
566
+ };
567
+ }
568
+ catch (error) {
569
+ const errorMsg = error instanceof Error ? error.message : String(error);
570
+ if (errorMsg.includes('ENOTEMPTY')) {
571
+ return {
572
+ isError: true,
573
+ content: [{ type: "text", text: `❌ Verzeichnis nicht leer. Setze recursive=true zum Löschen aller Inhalte.` }]
574
+ };
575
+ }
576
+ return {
577
+ isError: true,
578
+ content: [{ type: "text", text: `❌ Fehler beim Löschen: ${errorMsg}` }]
579
+ };
580
+ }
581
+ });
582
+ // ============================================================================
583
+ // Tool: Move/Rename
584
+ // ============================================================================
585
+ server.registerTool("fc_move", {
586
+ title: "Verschieben/Umbenennen",
587
+ description: `Verschiebt oder benennt eine Datei/Verzeichnis um.
588
+
589
+ Args:
590
+ - source (string): Quellpfad
591
+ - destination (string): Zielpfad
592
+
593
+ Beispiele:
594
+ - Umbenennen: source="test.txt", destination="test_neu.txt"
595
+ - Verschieben: source="C:\\a\\test.txt", destination="C:\\b\\test.txt"`,
596
+ inputSchema: {
597
+ source: z.string().min(1).describe("Quellpfad"),
598
+ destination: z.string().min(1).describe("Zielpfad")
599
+ },
600
+ annotations: {
601
+ readOnlyHint: false,
602
+ destructiveHint: true,
603
+ idempotentHint: false,
604
+ openWorldHint: false
605
+ }
606
+ }, async (params) => {
607
+ try {
608
+ const sourcePath = normalizePath(params.source);
609
+ const destPath = normalizePath(params.destination);
610
+ if (!await pathExists(sourcePath)) {
611
+ return {
612
+ isError: true,
613
+ content: [{ type: "text", text: `❌ Quelle nicht gefunden: ${sourcePath}` }]
614
+ };
615
+ }
616
+ // Create destination directory if needed
617
+ const destDir = path.dirname(destPath);
618
+ if (!await pathExists(destDir)) {
619
+ await fs.mkdir(destDir, { recursive: true });
620
+ }
621
+ await fs.rename(sourcePath, destPath);
622
+ return {
623
+ content: [{ type: "text", text: `✅ Verschoben:\n 📤 ${sourcePath}\n 📥 ${destPath}` }]
624
+ };
625
+ }
626
+ catch (error) {
627
+ const errorMsg = error instanceof Error ? error.message : String(error);
628
+ return {
629
+ isError: true,
630
+ content: [{ type: "text", text: `❌ Fehler beim Verschieben: ${errorMsg}` }]
631
+ };
632
+ }
633
+ });
634
+ // ============================================================================
635
+ // Tool: Copy
636
+ // ============================================================================
637
+ server.registerTool("fc_copy", {
638
+ title: "Kopieren",
639
+ description: `Kopiert eine Datei oder ein Verzeichnis.
640
+
641
+ Args:
642
+ - source (string): Quellpfad
643
+ - destination (string): Zielpfad
644
+ - recursive (boolean): Verzeichnisse rekursiv kopieren`,
645
+ inputSchema: {
646
+ source: z.string().min(1).describe("Quellpfad"),
647
+ destination: z.string().min(1).describe("Zielpfad"),
648
+ recursive: z.boolean().default(true).describe("Rekursiv kopieren")
649
+ },
650
+ annotations: {
651
+ readOnlyHint: false,
652
+ destructiveHint: false,
653
+ idempotentHint: false,
654
+ openWorldHint: false
655
+ }
656
+ }, async (params) => {
657
+ try {
658
+ const sourcePath = normalizePath(params.source);
659
+ const destPath = normalizePath(params.destination);
660
+ if (!await pathExists(sourcePath)) {
661
+ return {
662
+ isError: true,
663
+ content: [{ type: "text", text: `❌ Quelle nicht gefunden: ${sourcePath}` }]
664
+ };
665
+ }
666
+ // Create destination directory if needed
667
+ const destDir = path.dirname(destPath);
668
+ if (!await pathExists(destDir)) {
669
+ await fs.mkdir(destDir, { recursive: true });
670
+ }
671
+ const stats = await fs.stat(sourcePath);
672
+ if (stats.isDirectory()) {
673
+ await fs.cp(sourcePath, destPath, { recursive: params.recursive });
674
+ }
675
+ else {
676
+ await fs.copyFile(sourcePath, destPath);
677
+ }
678
+ return {
679
+ content: [{ type: "text", text: `✅ Kopiert:\n 📤 ${sourcePath}\n 📥 ${destPath}` }]
680
+ };
681
+ }
682
+ catch (error) {
683
+ const errorMsg = error instanceof Error ? error.message : String(error);
684
+ return {
685
+ isError: true,
686
+ content: [{ type: "text", text: `❌ Fehler beim Kopieren: ${errorMsg}` }]
687
+ };
688
+ }
689
+ });
690
+ // ============================================================================
691
+ // Tool: File Info
692
+ // ============================================================================
693
+ server.registerTool("fc_file_info", {
694
+ title: "Datei-Informationen",
695
+ description: `Zeigt detaillierte Informationen zu einer Datei/Verzeichnis.
696
+
697
+ Args:
698
+ - path (string): Pfad zur Datei/Verzeichnis
699
+
700
+ Returns:
701
+ - Größe, Typ, Erstellungs-/Änderungsdatum, Berechtigungen`,
702
+ inputSchema: {
703
+ path: z.string().min(1).describe("Pfad zur Datei/Verzeichnis")
704
+ },
705
+ annotations: {
706
+ readOnlyHint: true,
707
+ destructiveHint: false,
708
+ idempotentHint: true,
709
+ openWorldHint: false
710
+ }
711
+ }, async (params) => {
712
+ try {
713
+ const targetPath = normalizePath(params.path);
714
+ if (!await pathExists(targetPath)) {
715
+ return {
716
+ isError: true,
717
+ content: [{ type: "text", text: `❌ Pfad nicht gefunden: ${targetPath}` }]
718
+ };
719
+ }
720
+ const stats = await fs.stat(targetPath);
721
+ const type = stats.isDirectory() ? "Verzeichnis" : stats.isFile() ? "Datei" : "Sonstiges";
722
+ const info = [
723
+ `📋 **Informationen: ${path.basename(targetPath)}**`,
724
+ ``,
725
+ `| Eigenschaft | Wert |`,
726
+ `|-------------|------|`,
727
+ `| Typ | ${type} |`,
728
+ `| Größe | ${formatFileSize(stats.size)} |`,
729
+ `| Erstellt | ${stats.birthtime.toLocaleString('de-DE')} |`,
730
+ `| Geändert | ${stats.mtime.toLocaleString('de-DE')} |`,
731
+ `| Zugegriffen | ${stats.atime.toLocaleString('de-DE')} |`,
732
+ `| Pfad | ${targetPath} |`
733
+ ];
734
+ return {
735
+ content: [{ type: "text", text: info.join('\n') }]
736
+ };
737
+ }
738
+ catch (error) {
739
+ const errorMsg = error instanceof Error ? error.message : String(error);
740
+ return {
741
+ isError: true,
742
+ content: [{ type: "text", text: `❌ Fehler: ${errorMsg}` }]
743
+ };
744
+ }
745
+ });
746
+ // ============================================================================
747
+ // Tool: Search Files
748
+ // ============================================================================
749
+ server.registerTool("fc_search_files", {
750
+ title: "Dateien suchen",
751
+ description: `Sucht Dateien nach Name/Muster in einem Verzeichnis.
752
+
753
+ Args:
754
+ - directory (string): Startverzeichnis für die Suche
755
+ - pattern (string): Suchmuster (unterstützt * und ? Wildcards)
756
+ - max_results (number, optional): Maximale Ergebnisse (default: 50)
757
+
758
+ Beispiele:
759
+ - pattern: "*.txt" - Alle Textdateien
760
+ - pattern: "test*" - Dateien die mit "test" beginnen
761
+ - pattern: "*.py" - Alle Python-Dateien`,
762
+ inputSchema: {
763
+ directory: z.string().min(1).describe("Startverzeichnis"),
764
+ pattern: z.string().min(1).describe("Suchmuster mit Wildcards"),
765
+ max_results: z.number().int().min(1).max(500).default(50).describe("Max Ergebnisse")
766
+ },
767
+ annotations: {
768
+ readOnlyHint: true,
769
+ destructiveHint: false,
770
+ idempotentHint: true,
771
+ openWorldHint: false
772
+ }
773
+ }, async (params) => {
774
+ try {
775
+ const dirPath = normalizePath(params.directory);
776
+ if (!await pathExists(dirPath)) {
777
+ return {
778
+ isError: true,
779
+ content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }]
780
+ };
781
+ }
782
+ // Convert wildcard pattern to regex
783
+ const regexPattern = params.pattern
784
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
785
+ .replace(/\*/g, '.*')
786
+ .replace(/\?/g, '.');
787
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
788
+ const results = await searchFilesRecursive(dirPath, regex, params.max_results);
789
+ if (results.length === 0) {
790
+ return {
791
+ content: [{ type: "text", text: `🔍 Keine Dateien gefunden für: "${params.pattern}"` }]
792
+ };
793
+ }
794
+ const output = [
795
+ `🔍 **Suchergebnisse für "${params.pattern}"**`,
796
+ `📁 In: ${dirPath}`,
797
+ `📊 Gefunden: ${results.length} ${results.length >= params.max_results ? `(Maximum erreicht)` : ''}`,
798
+ ``,
799
+ ...results.map(r => ` 📄 ${r}`)
800
+ ];
801
+ return {
802
+ content: [{ type: "text", text: output.join('\n') }]
803
+ };
804
+ }
805
+ catch (error) {
806
+ const errorMsg = error instanceof Error ? error.message : String(error);
807
+ return {
808
+ isError: true,
809
+ content: [{ type: "text", text: `❌ Fehler bei Suche: ${errorMsg}` }]
810
+ };
811
+ }
812
+ });
813
+ // ============================================================================
814
+ // Tool: Start Async Search
815
+ // ============================================================================
816
+ server.registerTool("fc_start_search", {
817
+ title: "Asynchrone Suche starten",
818
+ description: `Startet eine Suche im Hintergrund. Claude kann währenddessen andere Aufgaben erledigen.
819
+
820
+ Args:
821
+ - directory (string): Startverzeichnis
822
+ - pattern (string): Suchmuster (Wildcards: * und ?)
823
+
824
+ Returns:
825
+ - Search-ID für fc_get_search_results, fc_stop_search
826
+
827
+ Beispiel:
828
+ Suche starten: fc_start_search("C:\\Users", "*.pdf")
829
+ Später Ergebnisse holen: fc_get_search_results(search_id)`,
830
+ inputSchema: {
831
+ directory: z.string().min(1).describe("Startverzeichnis"),
832
+ pattern: z.string().min(1).describe("Suchmuster mit Wildcards")
833
+ },
834
+ annotations: {
835
+ readOnlyHint: true,
836
+ destructiveHint: false,
837
+ idempotentHint: false,
838
+ openWorldHint: false
839
+ }
840
+ }, async (params) => {
841
+ try {
842
+ const dirPath = normalizePath(params.directory);
843
+ if (!await pathExists(dirPath)) {
844
+ return {
845
+ isError: true,
846
+ content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }]
847
+ };
848
+ }
849
+ // Convert wildcard pattern to regex
850
+ const regexPattern = params.pattern
851
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
852
+ .replace(/\*/g, '.*')
853
+ .replace(/\?/g, '.');
854
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
855
+ const searchId = generateSearchId();
856
+ const abortController = new AbortController();
857
+ const session = {
858
+ id: searchId,
859
+ directory: dirPath,
860
+ pattern: regex,
861
+ patternString: params.pattern,
862
+ results: [],
863
+ isRunning: true,
864
+ startTime: new Date(),
865
+ scannedDirs: 0,
866
+ abortController
867
+ };
868
+ searchSessions.set(searchId, session);
869
+ // Start search in background (don't await)
870
+ asyncSearchFiles(session, dirPath).then(() => {
871
+ session.isRunning = false;
872
+ }).catch(() => {
873
+ session.isRunning = false;
874
+ });
875
+ return {
876
+ content: [{
877
+ type: "text",
878
+ text: `🔍 **Suche gestartet**\n\n| | |\n|---|---|\n| Search-ID | \`${searchId}\` |\n| Verzeichnis | ${dirPath} |\n| Muster | ${params.pattern} |\n\nNutze \`fc_get_search_results\` um Ergebnisse abzurufen.`
879
+ }]
880
+ };
881
+ }
882
+ catch (error) {
883
+ const errorMsg = error instanceof Error ? error.message : String(error);
884
+ return {
885
+ isError: true,
886
+ content: [{ type: "text", text: `❌ Fehler beim Starten der Suche: ${errorMsg}` }]
887
+ };
888
+ }
889
+ });
890
+ // ============================================================================
891
+ // Tool: Get Search Results
892
+ // ============================================================================
893
+ server.registerTool("fc_get_search_results", {
894
+ title: "Suchergebnisse abrufen",
895
+ description: `Ruft Ergebnisse einer laufenden oder beendeten Suche ab.
896
+
897
+ Args:
898
+ - search_id (string): Search-ID von fc_start_search
899
+ - offset (number, optional): Ab welchem Ergebnis (für Paginierung)
900
+ - limit (number, optional): Maximale Anzahl Ergebnisse (default: 50)
901
+
902
+ Returns:
903
+ - Status der Suche und gefundene Dateien`,
904
+ inputSchema: {
905
+ search_id: z.string().min(1).describe("Search-ID"),
906
+ offset: z.number().int().min(0).default(0).describe("Start-Offset"),
907
+ limit: z.number().int().min(1).max(200).default(50).describe("Max Ergebnisse")
908
+ },
909
+ annotations: {
910
+ readOnlyHint: true,
911
+ destructiveHint: false,
912
+ idempotentHint: true,
913
+ openWorldHint: false
914
+ }
915
+ }, async (params) => {
916
+ const session = searchSessions.get(params.search_id);
917
+ if (!session) {
918
+ return {
919
+ isError: true,
920
+ content: [{ type: "text", text: `❌ Suche nicht gefunden: ${params.search_id}\n\nNutze fc_list_searches für aktive Suchen.` }]
921
+ };
922
+ }
923
+ const status = session.isRunning ? '🔄 Läuft' : '✅ Abgeschlossen';
924
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
925
+ const totalResults = session.results.length;
926
+ const paginatedResults = session.results.slice(params.offset, params.offset + params.limit);
927
+ const hasMore = totalResults > params.offset + params.limit;
928
+ const output = [
929
+ `🔍 **Suchergebnisse** (${status})`,
930
+ ``,
931
+ `| | |`,
932
+ `|---|---|`,
933
+ `| Muster | ${session.patternString} |`,
934
+ `| Verzeichnis | ${session.directory} |`,
935
+ `| Gescannte Ordner | ${session.scannedDirs} |`,
936
+ `| Gefunden | ${totalResults} Dateien |`,
937
+ `| Laufzeit | ${runtime}s |`,
938
+ ``,
939
+ `**Ergebnisse ${params.offset + 1}-${Math.min(params.offset + params.limit, totalResults)} von ${totalResults}:**`,
940
+ ``,
941
+ ...paginatedResults.map(r => ` 📄 ${r}`)
942
+ ];
943
+ if (hasMore) {
944
+ output.push(``, `📌 Weitere Ergebnisse: \`fc_get_search_results("${params.search_id}", offset=${params.offset + params.limit})\``);
945
+ }
946
+ return {
947
+ content: [{ type: "text", text: output.join('\n') }]
948
+ };
949
+ });
950
+ // ============================================================================
951
+ // Tool: Stop Search
952
+ // ============================================================================
953
+ server.registerTool("fc_stop_search", {
954
+ title: "Suche stoppen",
955
+ description: `Stoppt eine laufende Hintergrund-Suche.
956
+
957
+ Args:
958
+ - search_id (string): Search-ID`,
959
+ inputSchema: {
960
+ search_id: z.string().min(1).describe("Search-ID")
961
+ },
962
+ annotations: {
963
+ readOnlyHint: false,
964
+ destructiveHint: false,
965
+ idempotentHint: true,
966
+ openWorldHint: false
967
+ }
968
+ }, async (params) => {
969
+ const session = searchSessions.get(params.search_id);
970
+ if (!session) {
971
+ return {
972
+ isError: true,
973
+ content: [{ type: "text", text: `❌ Suche nicht gefunden: ${params.search_id}` }]
974
+ };
975
+ }
976
+ if (!session.isRunning) {
977
+ return {
978
+ content: [{ type: "text", text: `ℹ️ Suche bereits beendet. ${session.results.length} Ergebnisse gefunden.` }]
979
+ };
980
+ }
981
+ session.isRunning = false;
982
+ session.abortController.abort();
983
+ return {
984
+ content: [{ type: "text", text: `⏹️ Suche gestoppt: ${params.search_id}\n📊 ${session.results.length} Ergebnisse bis hierhin gefunden.` }]
985
+ };
986
+ });
987
+ // ============================================================================
988
+ // Tool: List Searches
989
+ // ============================================================================
990
+ server.registerTool("fc_list_searches", {
991
+ title: "Aktive Suchen auflisten",
992
+ description: `Listet alle aktiven und beendeten Hintergrund-Suchen auf.`,
993
+ inputSchema: {},
994
+ annotations: {
995
+ readOnlyHint: true,
996
+ destructiveHint: false,
997
+ idempotentHint: true,
998
+ openWorldHint: false
999
+ }
1000
+ }, async () => {
1001
+ if (searchSessions.size === 0) {
1002
+ return {
1003
+ content: [{ type: "text", text: `📋 Keine Suchen aktiv.\n\nStarte eine neue mit \`fc_start_search\`.` }]
1004
+ };
1005
+ }
1006
+ const rows = [];
1007
+ for (const [id, session] of searchSessions) {
1008
+ const status = session.isRunning ? '🔄' : '✅';
1009
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
1010
+ rows.push(`| ${status} | \`${id}\` | ${session.patternString} | ${session.results.length} | ${runtime}s |`);
1011
+ }
1012
+ const output = [
1013
+ `📋 **Suchen** (${searchSessions.size})`,
1014
+ ``,
1015
+ `| Status | Search-ID | Muster | Ergebnisse | Laufzeit |`,
1016
+ `|--------|-----------|--------|------------|----------|`,
1017
+ ...rows
1018
+ ];
1019
+ return {
1020
+ content: [{ type: "text", text: output.join('\n') }]
1021
+ };
1022
+ });
1023
+ // ============================================================================
1024
+ // Tool: Clear Search
1025
+ // ============================================================================
1026
+ server.registerTool("fc_clear_search", {
1027
+ title: "Suche entfernen",
1028
+ description: `Entfernt eine beendete Suche aus der Liste und gibt Speicher frei.
1029
+
1030
+ Args:
1031
+ - search_id (string): Search-ID (oder "all" für alle beendeten)`,
1032
+ inputSchema: {
1033
+ search_id: z.string().min(1).describe("Search-ID oder 'all'")
1034
+ },
1035
+ annotations: {
1036
+ readOnlyHint: false,
1037
+ destructiveHint: false,
1038
+ idempotentHint: true,
1039
+ openWorldHint: false
1040
+ }
1041
+ }, async (params) => {
1042
+ if (params.search_id === "all") {
1043
+ let count = 0;
1044
+ for (const [id, session] of searchSessions) {
1045
+ if (!session.isRunning) {
1046
+ searchSessions.delete(id);
1047
+ count++;
1048
+ }
1049
+ }
1050
+ return {
1051
+ content: [{ type: "text", text: `🧹 ${count} beendete Suchen entfernt.` }]
1052
+ };
1053
+ }
1054
+ const session = searchSessions.get(params.search_id);
1055
+ if (!session) {
1056
+ return {
1057
+ isError: true,
1058
+ content: [{ type: "text", text: `❌ Suche nicht gefunden: ${params.search_id}` }]
1059
+ };
1060
+ }
1061
+ if (session.isRunning) {
1062
+ return {
1063
+ isError: true,
1064
+ content: [{ type: "text", text: `⚠️ Suche läuft noch. Nutze erst fc_stop_search.` }]
1065
+ };
1066
+ }
1067
+ searchSessions.delete(params.search_id);
1068
+ return {
1069
+ content: [{ type: "text", text: `✅ Suche entfernt: ${params.search_id}` }]
1070
+ };
1071
+ });
1072
+ // ============================================================================
1073
+ // Tool: Safe Delete (Papierkorb)
1074
+ // ============================================================================
1075
+ server.registerTool("fc_safe_delete", {
1076
+ title: "Sicher löschen (Papierkorb)",
1077
+ description: `Verschiebt Dateien/Verzeichnisse in den Papierkorb statt sie zu löschen.
1078
+
1079
+ Args:
1080
+ - path (string): Pfad zur Datei/Verzeichnis
1081
+
1082
+ ✅ SICHER: Kann aus dem Papierkorb wiederhergestellt werden!
1083
+
1084
+ Hinweis: Nutzt Windows-Papierkorb oder erstellt Backup auf anderen Systemen.`,
1085
+ inputSchema: {
1086
+ path: z.string().min(1).describe("Pfad zur Datei/Verzeichnis")
1087
+ },
1088
+ annotations: {
1089
+ readOnlyHint: false,
1090
+ destructiveHint: false, // Nicht destructive weil wiederherstellbar!
1091
+ idempotentHint: true,
1092
+ openWorldHint: false
1093
+ }
1094
+ }, async (params) => {
1095
+ try {
1096
+ const targetPath = normalizePath(params.path);
1097
+ if (!await pathExists(targetPath)) {
1098
+ return {
1099
+ isError: true,
1100
+ content: [{ type: "text", text: `❌ Pfad nicht gefunden: ${targetPath}` }]
1101
+ };
1102
+ }
1103
+ const stats = await fs.stat(targetPath);
1104
+ const itemType = stats.isDirectory() ? "Verzeichnis" : "Datei";
1105
+ const isWindows = process.platform === 'win32';
1106
+ // Windows: PowerShell mit VisualBasic für echten Papierkorb
1107
+ if (isWindows) {
1108
+ // Windows: PowerShell mit VisualBasic für echten Papierkorb
1109
+ const escapedPath = targetPath.replace(/'/g, "''");
1110
+ const deleteMethod = stats.isDirectory() ? 'DeleteDirectory' : 'DeleteFile';
1111
+ const psCommand = `Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${deleteMethod}('${escapedPath}', 'OnlyErrorDialogs', 'SendToRecycleBin')`;
1112
+ const windowsShell = getWindowsShell();
1113
+ if (windowsShell.includes('powershell')) {
1114
+ await execAsync(`"${windowsShell}" -Command "${psCommand}"`);
1115
+ }
1116
+ else {
1117
+ // cmd.exe kann kein PowerShell - normales Löschen als Fallback
1118
+ if (stats.isDirectory()) {
1119
+ await fs.rm(targetPath, { recursive: true });
1120
+ }
1121
+ else {
1122
+ await fs.unlink(targetPath);
1123
+ }
1124
+ }
1125
+ return {
1126
+ content: [{
1127
+ type: "text",
1128
+ text: `🗑️ **In Papierkorb verschoben**\n\n| | |\n|---|---|\n| Typ | ${itemType} |\n| Pfad | ${targetPath} |\n\n✅ Kann aus dem Papierkorb wiederhergestellt werden.`
1129
+ }]
1130
+ };
1131
+ }
1132
+ else {
1133
+ // Unix/Mac: Verschiebe in ~/.Trash oder erstelle Backup
1134
+ const trashDir = path.join(process.env.HOME || '/tmp', '.Trash');
1135
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1136
+ const baseName = path.basename(targetPath);
1137
+ const trashPath = path.join(trashDir, `${baseName}_${timestamp}`);
1138
+ try {
1139
+ await fs.access(trashDir);
1140
+ }
1141
+ catch {
1142
+ await fs.mkdir(trashDir, { recursive: true });
1143
+ }
1144
+ await fs.rename(targetPath, trashPath);
1145
+ return {
1146
+ content: [{
1147
+ type: "text",
1148
+ text: `🗑️ **In Papierkorb verschoben**\n\n| | |\n|---|---|\n| Typ | ${itemType} |\n| Original | ${targetPath} |\n| Papierkorb | ${trashPath} |\n\n✅ Kann wiederhergestellt werden.`
1149
+ }]
1150
+ };
1151
+ }
1152
+ }
1153
+ catch (error) {
1154
+ const errorMsg = error instanceof Error ? error.message : String(error);
1155
+ return {
1156
+ isError: true,
1157
+ content: [{ type: "text", text: `❌ Fehler beim Verschieben in Papierkorb: ${errorMsg}` }]
1158
+ };
1159
+ }
1160
+ });
1161
+ // ============================================================================
1162
+ // Tool: Execute Command
1163
+ // ============================================================================
1164
+ server.registerTool("fc_execute_command", {
1165
+ title: "Befehl ausführen",
1166
+ description: `Führt einen Shell-Befehl aus und gibt die Ausgabe zurück.
1167
+
1168
+ Args:
1169
+ - command (string): Auszuführender Befehl
1170
+ - cwd (string, optional): Arbeitsverzeichnis
1171
+ - timeout (number, optional): Timeout in Millisekunden (default: 30000)
1172
+
1173
+ ⚠️ ACHTUNG: Befehle werden mit Benutzerrechten ausgeführt!
1174
+
1175
+ Beispiele:
1176
+ - command: "dir" (Windows)
1177
+ - command: "ls -la" (Unix)
1178
+ - command: "python --version"`,
1179
+ inputSchema: {
1180
+ command: z.string().min(1).describe("Auszuführender Befehl"),
1181
+ cwd: z.string().optional().describe("Arbeitsverzeichnis"),
1182
+ timeout: z.number().int().min(1000).max(300000).default(30000).describe("Timeout in ms")
1183
+ },
1184
+ annotations: {
1185
+ readOnlyHint: false,
1186
+ destructiveHint: true,
1187
+ idempotentHint: false,
1188
+ openWorldHint: true
1189
+ }
1190
+ }, async (params) => {
1191
+ try {
1192
+ const options = {
1193
+ timeout: params.timeout
1194
+ };
1195
+ if (params.cwd) {
1196
+ options.cwd = normalizePath(params.cwd);
1197
+ }
1198
+ const { stdout, stderr } = await executeCommand(params.command, options);
1199
+ const output = [`⚡ **Befehl:** \`${params.command}\``];
1200
+ if (stdout.trim()) {
1201
+ output.push(`\n**Ausgabe:**\n\`\`\`\n${stdout.trim()}\n\`\`\``);
1202
+ }
1203
+ if (stderr.trim()) {
1204
+ output.push(`\n**Fehlerausgabe:**\n\`\`\`\n${stderr.trim()}\n\`\`\``);
1205
+ }
1206
+ if (!stdout.trim() && !stderr.trim()) {
1207
+ output.push(`\n✅ Befehl ausgeführt (keine Ausgabe)`);
1208
+ }
1209
+ return {
1210
+ content: [{ type: "text", text: output.join('') }]
1211
+ };
1212
+ }
1213
+ catch (error) {
1214
+ const errorMsg = error instanceof Error ? error.message : String(error);
1215
+ return {
1216
+ isError: true,
1217
+ content: [{ type: "text", text: `❌ Fehler bei Befehlsausführung:\n${errorMsg}` }]
1218
+ };
1219
+ }
1220
+ });
1221
+ // ============================================================================
1222
+ // Tool: Start Process
1223
+ // ============================================================================
1224
+ server.registerTool("fc_start_process", {
1225
+ title: "Prozess starten",
1226
+ description: `Startet einen Prozess im Hintergrund (non-blocking).
1227
+
1228
+ Args:
1229
+ - program (string): Programm/Executable
1230
+ - args (array, optional): Argumente als Array
1231
+ - cwd (string, optional): Arbeitsverzeichnis
1232
+
1233
+ Beispiele:
1234
+ - program: "notepad.exe", args: ["test.txt"]
1235
+ - program: "python", args: ["script.py"]
1236
+ - program: "code", args: ["."] (VS Code öffnen)`,
1237
+ inputSchema: {
1238
+ program: z.string().min(1).describe("Programm/Executable"),
1239
+ args: z.array(z.string()).default([]).describe("Argumente"),
1240
+ cwd: z.string().optional().describe("Arbeitsverzeichnis")
1241
+ },
1242
+ annotations: {
1243
+ readOnlyHint: false,
1244
+ destructiveHint: false,
1245
+ idempotentHint: false,
1246
+ openWorldHint: true
1247
+ }
1248
+ }, async (params) => {
1249
+ try {
1250
+ const options = {
1251
+ detached: true,
1252
+ stdio: 'ignore'
1253
+ };
1254
+ if (params.cwd) {
1255
+ options.cwd = normalizePath(params.cwd);
1256
+ }
1257
+ const child = spawn(params.program, params.args, options);
1258
+ child.unref();
1259
+ const argsStr = params.args.length > 0 ? ` ${params.args.join(' ')}` : '';
1260
+ return {
1261
+ content: [{
1262
+ type: "text",
1263
+ text: `🚀 Prozess gestartet: ${params.program}${argsStr}\n📋 PID: ${child.pid}`
1264
+ }]
1265
+ };
1266
+ }
1267
+ catch (error) {
1268
+ const errorMsg = error instanceof Error ? error.message : String(error);
1269
+ return {
1270
+ isError: true,
1271
+ content: [{ type: "text", text: `❌ Fehler beim Starten: ${errorMsg}` }]
1272
+ };
1273
+ }
1274
+ });
1275
+ // ============================================================================
1276
+ // Tool: Get Current Time
1277
+ // ============================================================================
1278
+ server.registerTool("fc_get_time", {
1279
+ title: "Aktuelle Zeit",
1280
+ description: `Gibt die aktuelle Systemzeit zurück.
1281
+
1282
+ Returns:
1283
+ - Datum, Uhrzeit, Wochentag, Zeitzone`,
1284
+ inputSchema: {},
1285
+ annotations: {
1286
+ readOnlyHint: true,
1287
+ destructiveHint: false,
1288
+ idempotentHint: false,
1289
+ openWorldHint: false
1290
+ }
1291
+ }, async () => {
1292
+ const now = new Date();
1293
+ const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
1294
+ const output = [
1295
+ `🕐 **Aktuelle Systemzeit**`,
1296
+ ``,
1297
+ `| | |`,
1298
+ `|---|---|`,
1299
+ `| Datum | ${now.toLocaleDateString('de-DE')} |`,
1300
+ `| Uhrzeit | ${now.toLocaleTimeString('de-DE')} |`,
1301
+ `| Wochentag | ${days[now.getDay()]} |`,
1302
+ `| ISO | ${now.toISOString()} |`,
1303
+ `| Zeitzone | ${Intl.DateTimeFormat().resolvedOptions().timeZone} |`
1304
+ ];
1305
+ return {
1306
+ content: [{ type: "text", text: output.join('\n') }]
1307
+ };
1308
+ });
1309
+ // ============================================================================
1310
+ // Tool: Read Multiple Files
1311
+ // ============================================================================
1312
+ server.registerTool("fc_read_multiple_files", {
1313
+ title: "Mehrere Dateien lesen",
1314
+ description: `Liest mehrere Dateien auf einmal und gibt deren Inhalte zurück.
1315
+
1316
+ Args:
1317
+ - paths (array): Array von Dateipfaden
1318
+ - max_lines_per_file (number, optional): Max Zeilen pro Datei (0 = alle)
1319
+
1320
+ Returns:
1321
+ - Inhalte aller Dateien mit Trennzeichen
1322
+
1323
+ Beispiel:
1324
+ paths: ["C:\\config.json", "C:\\readme.md"]`,
1325
+ inputSchema: {
1326
+ paths: z.array(z.string().min(1)).min(1).max(20).describe("Array von Dateipfaden"),
1327
+ max_lines_per_file: z.number().int().min(0).default(0).describe("Max Zeilen pro Datei")
1328
+ },
1329
+ annotations: {
1330
+ readOnlyHint: true,
1331
+ destructiveHint: false,
1332
+ idempotentHint: true,
1333
+ openWorldHint: false
1334
+ }
1335
+ }, async (params) => {
1336
+ const results = [];
1337
+ let successCount = 0;
1338
+ let errorCount = 0;
1339
+ for (const filePath of params.paths) {
1340
+ const normalizedPath = normalizePath(filePath);
1341
+ try {
1342
+ if (!await pathExists(normalizedPath)) {
1343
+ results.push(`\n❌ **${path.basename(normalizedPath)}** - Nicht gefunden\n`);
1344
+ errorCount++;
1345
+ continue;
1346
+ }
1347
+ const stats = await fs.stat(normalizedPath);
1348
+ if (stats.isDirectory()) {
1349
+ results.push(`\n❌ **${path.basename(normalizedPath)}** - Ist ein Verzeichnis\n`);
1350
+ errorCount++;
1351
+ continue;
1352
+ }
1353
+ let content = await fs.readFile(normalizedPath, "utf-8");
1354
+ if (params.max_lines_per_file > 0) {
1355
+ const lines = content.split('\n');
1356
+ content = lines.slice(0, params.max_lines_per_file).join('\n');
1357
+ if (lines.length > params.max_lines_per_file) {
1358
+ content += `\n... (${lines.length - params.max_lines_per_file} weitere Zeilen)`;
1359
+ }
1360
+ }
1361
+ results.push(`\n📄 **${normalizedPath}** (${formatFileSize(stats.size)})\n${'─'.repeat(60)}\n${content}\n`);
1362
+ successCount++;
1363
+ }
1364
+ catch (error) {
1365
+ const errorMsg = error instanceof Error ? error.message : String(error);
1366
+ results.push(`\n❌ **${path.basename(normalizedPath)}** - ${errorMsg}\n`);
1367
+ errorCount++;
1368
+ }
1369
+ }
1370
+ const summary = `📊 **Ergebnis:** ${successCount} gelesen, ${errorCount} Fehler\n${'═'.repeat(60)}`;
1371
+ return {
1372
+ content: [{ type: "text", text: summary + results.join('') }]
1373
+ };
1374
+ });
1375
+ // ============================================================================
1376
+ // Tool: Edit File (Zeilenbasiert)
1377
+ // ============================================================================
1378
+ server.registerTool("fc_edit_file", {
1379
+ title: "Datei bearbeiten (Zeilen)",
1380
+ description: `Bearbeitet eine Datei zeilenbasiert: ersetzen, einfügen oder löschen.
1381
+
1382
+ Args:
1383
+ - path (string): Pfad zur Datei
1384
+ - operation (string): "replace" | "insert" | "delete"
1385
+ - start_line (number): Startzeile (1-basiert)
1386
+ - end_line (number, optional): Endzeile für replace/delete
1387
+ - content (string, optional): Neuer Inhalt für replace/insert
1388
+
1389
+ Beispiele:
1390
+ - Zeilen 5-10 ersetzen: operation="replace", start_line=5, end_line=10, content="neuer text"
1391
+ - Nach Zeile 3 einfügen: operation="insert", start_line=3, content="neue zeile"
1392
+ - Zeilen 7-9 löschen: operation="delete", start_line=7, end_line=9`,
1393
+ inputSchema: {
1394
+ path: z.string().min(1).describe("Pfad zur Datei"),
1395
+ operation: z.enum(["replace", "insert", "delete"]).describe("Operation"),
1396
+ start_line: z.number().int().min(1).describe("Startzeile (1-basiert)"),
1397
+ end_line: z.number().int().min(1).optional().describe("Endzeile"),
1398
+ content: z.string().optional().describe("Neuer Inhalt")
1399
+ },
1400
+ annotations: {
1401
+ readOnlyHint: false,
1402
+ destructiveHint: true,
1403
+ idempotentHint: false,
1404
+ openWorldHint: false
1405
+ }
1406
+ }, async (params) => {
1407
+ try {
1408
+ const filePath = normalizePath(params.path);
1409
+ if (!await pathExists(filePath)) {
1410
+ return {
1411
+ isError: true,
1412
+ content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }]
1413
+ };
1414
+ }
1415
+ const originalContent = await fs.readFile(filePath, "utf-8");
1416
+ const lines = originalContent.split('\n');
1417
+ const totalLines = lines.length;
1418
+ const startIdx = params.start_line - 1;
1419
+ const endIdx = params.end_line ? params.end_line - 1 : startIdx;
1420
+ if (startIdx < 0 || startIdx >= totalLines) {
1421
+ return {
1422
+ isError: true,
1423
+ content: [{ type: "text", text: `❌ Startzeile ${params.start_line} ungültig. Datei hat ${totalLines} Zeilen.` }]
1424
+ };
1425
+ }
1426
+ if (endIdx < startIdx || endIdx >= totalLines) {
1427
+ return {
1428
+ isError: true,
1429
+ content: [{ type: "text", text: `❌ Endzeile ${params.end_line} ungültig.` }]
1430
+ };
1431
+ }
1432
+ let newLines;
1433
+ let actionDesc;
1434
+ switch (params.operation) {
1435
+ case "replace":
1436
+ if (!params.content) {
1437
+ return {
1438
+ isError: true,
1439
+ content: [{ type: "text", text: `❌ 'content' erforderlich für replace-Operation.` }]
1440
+ };
1441
+ }
1442
+ const replacementLines = params.content.split('\n');
1443
+ newLines = [
1444
+ ...lines.slice(0, startIdx),
1445
+ ...replacementLines,
1446
+ ...lines.slice(endIdx + 1)
1447
+ ];
1448
+ actionDesc = `Zeilen ${params.start_line}-${endIdx + 1} ersetzt durch ${replacementLines.length} Zeilen`;
1449
+ break;
1450
+ case "insert":
1451
+ if (!params.content) {
1452
+ return {
1453
+ isError: true,
1454
+ content: [{ type: "text", text: `❌ 'content' erforderlich für insert-Operation.` }]
1455
+ };
1456
+ }
1457
+ const insertLines = params.content.split('\n');
1458
+ newLines = [
1459
+ ...lines.slice(0, startIdx + 1),
1460
+ ...insertLines,
1461
+ ...lines.slice(startIdx + 1)
1462
+ ];
1463
+ actionDesc = `${insertLines.length} Zeilen nach Zeile ${params.start_line} eingefügt`;
1464
+ break;
1465
+ case "delete":
1466
+ newLines = [
1467
+ ...lines.slice(0, startIdx),
1468
+ ...lines.slice(endIdx + 1)
1469
+ ];
1470
+ actionDesc = `Zeilen ${params.start_line}-${endIdx + 1} gelöscht`;
1471
+ break;
1472
+ default:
1473
+ return {
1474
+ isError: true,
1475
+ content: [{ type: "text", text: `❌ Unbekannte Operation: ${params.operation}` }]
1476
+ };
1477
+ }
1478
+ await fs.writeFile(filePath, newLines.join('\n'), "utf-8");
1479
+ return {
1480
+ content: [{
1481
+ type: "text",
1482
+ text: `✅ **${path.basename(filePath)}** bearbeitet\n📝 ${actionDesc}\n📊 ${totalLines} → ${newLines.length} Zeilen`
1483
+ }]
1484
+ };
1485
+ }
1486
+ catch (error) {
1487
+ const errorMsg = error instanceof Error ? error.message : String(error);
1488
+ return {
1489
+ isError: true,
1490
+ content: [{ type: "text", text: `❌ Fehler beim Bearbeiten: ${errorMsg}` }]
1491
+ };
1492
+ }
1493
+ });
1494
+ // ============================================================================
1495
+ // Tool: String Replace in File
1496
+ // ============================================================================
1497
+ server.registerTool("fc_str_replace", {
1498
+ title: "String in Datei ersetzen",
1499
+ description: `Ersetzt einen eindeutigen String in einer Datei durch einen anderen.
1500
+
1501
+ Args:
1502
+ - path (string): Pfad zur Datei
1503
+ - old_str (string): Zu ersetzender String (muss genau 1x vorkommen)
1504
+ - new_str (string): Neuer String (leer = löschen)
1505
+
1506
+ Returns:
1507
+ - Bestätigung mit Kontext
1508
+
1509
+ ⚠️ WICHTIG: old_str muss EXAKT 1x in der Datei vorkommen!
1510
+ Bei 0 oder >1 Vorkommen wird ein Fehler ausgegeben.
1511
+
1512
+ Beispiele:
1513
+ - Funktionsname ändern: old_str="def old_name", new_str="def new_name"
1514
+ - Import hinzufügen: old_str="import os", new_str="import os\\nimport sys"
1515
+ - Zeile löschen: old_str="# TODO: remove this\\n", new_str=""`,
1516
+ inputSchema: {
1517
+ path: z.string().min(1).describe("Pfad zur Datei"),
1518
+ old_str: z.string().min(1).describe("Zu ersetzender String (muss eindeutig sein)"),
1519
+ new_str: z.string().default("").describe("Neuer String (leer = löschen)")
1520
+ },
1521
+ annotations: {
1522
+ readOnlyHint: false,
1523
+ destructiveHint: true,
1524
+ idempotentHint: false,
1525
+ openWorldHint: false
1526
+ }
1527
+ }, async (params) => {
1528
+ try {
1529
+ const filePath = normalizePath(params.path);
1530
+ if (!await pathExists(filePath)) {
1531
+ return {
1532
+ isError: true,
1533
+ content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }]
1534
+ };
1535
+ }
1536
+ const stats = await fs.stat(filePath);
1537
+ if (stats.isDirectory()) {
1538
+ return {
1539
+ isError: true,
1540
+ content: [{ type: "text", text: `❌ Pfad ist ein Verzeichnis: ${filePath}` }]
1541
+ };
1542
+ }
1543
+ const content = await fs.readFile(filePath, "utf-8");
1544
+ // Count occurrences
1545
+ const occurrences = content.split(params.old_str).length - 1;
1546
+ if (occurrences === 0) {
1547
+ // Show a snippet of the file to help debug
1548
+ const preview = content.length > 500 ? content.substring(0, 500) + "..." : content;
1549
+ return {
1550
+ isError: true,
1551
+ content: [{
1552
+ type: "text",
1553
+ text: `❌ String nicht gefunden in ${path.basename(filePath)}.\n\n**Gesucht:**\n\`\`\`\n${params.old_str}\n\`\`\`\n\n**Datei-Anfang:**\n\`\`\`\n${preview}\n\`\`\``
1554
+ }]
1555
+ };
1556
+ }
1557
+ if (occurrences > 1) {
1558
+ return {
1559
+ isError: true,
1560
+ content: [{
1561
+ type: "text",
1562
+ text: `❌ String kommt ${occurrences}x vor (muss eindeutig sein).\n\n**Gesucht:**\n\`\`\`\n${params.old_str}\n\`\`\`\n\n💡 Tipp: Erweitere den Suchstring um mehr Kontext.`
1563
+ }]
1564
+ };
1565
+ }
1566
+ // Perform replacement
1567
+ const newContent = content.replace(params.old_str, params.new_str);
1568
+ await fs.writeFile(filePath, newContent, "utf-8");
1569
+ // Calculate change info
1570
+ const oldLines = params.old_str.split('\n').length;
1571
+ const newLines = params.new_str.split('\n').length;
1572
+ const lineChange = newLines - oldLines;
1573
+ const lineInfo = lineChange === 0 ? "gleiche Zeilenanzahl" :
1574
+ lineChange > 0 ? `+${lineChange} Zeilen` : `${lineChange} Zeilen`;
1575
+ // Show context around the change
1576
+ const changeIndex = content.indexOf(params.old_str);
1577
+ const contextStart = Math.max(0, changeIndex - 50);
1578
+ const contextEnd = Math.min(content.length, changeIndex + params.old_str.length + 50);
1579
+ const beforeContext = content.substring(contextStart, changeIndex);
1580
+ const afterContext = content.substring(changeIndex + params.old_str.length, contextEnd);
1581
+ return {
1582
+ content: [{
1583
+ type: "text",
1584
+ text: `✅ **${path.basename(filePath)}** - String ersetzt\n\n| | |\n|---|---|\n| Änderung | ${lineInfo} |\n| Datei | ${filePath} |\n\n**Kontext:**\n\`\`\`\n...${beforeContext}▶${params.new_str}◀${afterContext}...\n\`\`\``
1585
+ }]
1586
+ };
1587
+ }
1588
+ catch (error) {
1589
+ const errorMsg = error instanceof Error ? error.message : String(error);
1590
+ return {
1591
+ isError: true,
1592
+ content: [{ type: "text", text: `❌ Fehler beim Ersetzen: ${errorMsg}` }]
1593
+ };
1594
+ }
1595
+ });
1596
+ // ============================================================================
1597
+ // Tool: List Processes
1598
+ // ============================================================================
1599
+ server.registerTool("fc_list_processes", {
1600
+ title: "Prozesse auflisten",
1601
+ description: `Listet laufende Systemprozesse auf.
1602
+
1603
+ Args:
1604
+ - filter (string, optional): Filter nach Prozessname
1605
+
1606
+ Returns:
1607
+ - Liste der Prozesse mit PID, Name, Speicher
1608
+
1609
+ Hinweis: Nutzt 'tasklist' (Windows) oder 'ps' (Unix)`,
1610
+ inputSchema: {
1611
+ filter: z.string().optional().describe("Filter nach Prozessname")
1612
+ },
1613
+ annotations: {
1614
+ readOnlyHint: true,
1615
+ destructiveHint: false,
1616
+ idempotentHint: true,
1617
+ openWorldHint: true
1618
+ }
1619
+ }, async (params) => {
1620
+ try {
1621
+ const isWindows = process.platform === 'win32';
1622
+ let command;
1623
+ if (isWindows) {
1624
+ command = params.filter
1625
+ ? `tasklist /FI "IMAGENAME eq ${params.filter}*" /FO CSV /NH`
1626
+ : `tasklist /FO CSV /NH`;
1627
+ }
1628
+ else {
1629
+ command = params.filter
1630
+ ? `ps aux | grep -i "${params.filter}" | grep -v grep`
1631
+ : `ps aux --sort=-%mem | head -50`;
1632
+ }
1633
+ const { stdout } = await execAsync(command);
1634
+ if (!stdout.trim()) {
1635
+ return {
1636
+ content: [{ type: "text", text: `🔍 Keine Prozesse gefunden${params.filter ? ` für "${params.filter}"` : ''}.` }]
1637
+ };
1638
+ }
1639
+ let output;
1640
+ if (isWindows) {
1641
+ // Parse CSV output from tasklist
1642
+ const lines = stdout.trim().split('\n').filter(l => l.trim());
1643
+ const processes = lines.map(line => {
1644
+ const parts = line.split('","').map(p => p.replace(/"/g, ''));
1645
+ return `| ${parts[0] || '-'} | ${parts[1] || '-'} | ${parts[4] || '-'} |`;
1646
+ });
1647
+ output = [
1648
+ `📋 **Laufende Prozesse**${params.filter ? ` (Filter: ${params.filter})` : ''}`,
1649
+ ``,
1650
+ `| Name | PID | Speicher |`,
1651
+ `|------|-----|----------|`,
1652
+ ...processes.slice(0, 50)
1653
+ ].join('\n');
1654
+ }
1655
+ else {
1656
+ output = [
1657
+ `📋 **Laufende Prozesse**${params.filter ? ` (Filter: ${params.filter})` : ''}`,
1658
+ ``,
1659
+ '```',
1660
+ stdout.trim(),
1661
+ '```'
1662
+ ].join('\n');
1663
+ }
1664
+ return {
1665
+ content: [{ type: "text", text: output }]
1666
+ };
1667
+ }
1668
+ catch (error) {
1669
+ const errorMsg = error instanceof Error ? error.message : String(error);
1670
+ return {
1671
+ isError: true,
1672
+ content: [{ type: "text", text: `❌ Fehler beim Auflisten: ${errorMsg}` }]
1673
+ };
1674
+ }
1675
+ });
1676
+ // ============================================================================
1677
+ // Tool: Kill Process
1678
+ // ============================================================================
1679
+ server.registerTool("fc_kill_process", {
1680
+ title: "Prozess beenden",
1681
+ description: `Beendet einen Prozess nach PID oder Name.
1682
+
1683
+ Args:
1684
+ - pid (number, optional): Prozess-ID
1685
+ - name (string, optional): Prozessname
1686
+ - force (boolean): Erzwungenes Beenden
1687
+
1688
+ ⚠️ ACHTUNG: Kann zu Datenverlust führen!`,
1689
+ inputSchema: {
1690
+ pid: z.number().int().optional().describe("Prozess-ID"),
1691
+ name: z.string().optional().describe("Prozessname"),
1692
+ force: z.boolean().default(false).describe("Erzwingen")
1693
+ },
1694
+ annotations: {
1695
+ readOnlyHint: false,
1696
+ destructiveHint: true,
1697
+ idempotentHint: true,
1698
+ openWorldHint: true
1699
+ }
1700
+ }, async (params) => {
1701
+ if (!params.pid && !params.name) {
1702
+ return {
1703
+ isError: true,
1704
+ content: [{ type: "text", text: `❌ Entweder 'pid' oder 'name' muss angegeben werden.` }]
1705
+ };
1706
+ }
1707
+ try {
1708
+ const isWindows = process.platform === 'win32';
1709
+ let command;
1710
+ if (isWindows) {
1711
+ if (params.pid) {
1712
+ command = params.force
1713
+ ? `taskkill /F /PID ${params.pid}`
1714
+ : `taskkill /PID ${params.pid}`;
1715
+ }
1716
+ else {
1717
+ command = params.force
1718
+ ? `taskkill /F /IM "${params.name}"`
1719
+ : `taskkill /IM "${params.name}"`;
1720
+ }
1721
+ }
1722
+ else {
1723
+ if (params.pid) {
1724
+ command = params.force
1725
+ ? `kill -9 ${params.pid}`
1726
+ : `kill ${params.pid}`;
1727
+ }
1728
+ else {
1729
+ command = params.force
1730
+ ? `pkill -9 "${params.name}"`
1731
+ : `pkill "${params.name}"`;
1732
+ }
1733
+ }
1734
+ const { stdout, stderr } = await execAsync(command);
1735
+ const target = params.pid ? `PID ${params.pid}` : `"${params.name}"`;
1736
+ return {
1737
+ content: [{
1738
+ type: "text",
1739
+ text: `✅ Prozess beendet: ${target}\n${stdout || stderr || ''}`.trim()
1740
+ }]
1741
+ };
1742
+ }
1743
+ catch (error) {
1744
+ const errorMsg = error instanceof Error ? error.message : String(error);
1745
+ return {
1746
+ isError: true,
1747
+ content: [{ type: "text", text: `❌ Fehler beim Beenden: ${errorMsg}` }]
1748
+ };
1749
+ }
1750
+ });
1751
+ // ============================================================================
1752
+ // Tool: Start Interactive Process (Session)
1753
+ // ============================================================================
1754
+ server.registerTool("fc_start_session", {
1755
+ title: "Interaktive Session starten",
1756
+ description: `Startet einen interaktiven Prozess als Session (für fc_read_output und fc_send_input).
1757
+
1758
+ Args:
1759
+ - command (string): Befehl/Programm
1760
+ - args (array, optional): Argumente
1761
+ - cwd (string, optional): Arbeitsverzeichnis
1762
+
1763
+ Returns:
1764
+ - Session-ID für weitere Interaktion
1765
+
1766
+ Beispiele:
1767
+ - Python REPL: command="python"
1768
+ - Node REPL: command="node"
1769
+ - PowerShell: command="powershell"`,
1770
+ inputSchema: {
1771
+ command: z.string().min(1).describe("Befehl/Programm"),
1772
+ args: z.array(z.string()).default([]).describe("Argumente"),
1773
+ cwd: z.string().optional().describe("Arbeitsverzeichnis")
1774
+ },
1775
+ annotations: {
1776
+ readOnlyHint: false,
1777
+ destructiveHint: false,
1778
+ idempotentHint: false,
1779
+ openWorldHint: true
1780
+ }
1781
+ }, async (params) => {
1782
+ try {
1783
+ const sessionId = generateSessionId();
1784
+ const cwd = params.cwd ? normalizePath(params.cwd) : process.cwd();
1785
+ const proc = spawn(params.command, params.args, {
1786
+ cwd,
1787
+ shell: true,
1788
+ stdio: ['pipe', 'pipe', 'pipe']
1789
+ });
1790
+ const session = {
1791
+ id: sessionId,
1792
+ process: proc,
1793
+ command: params.command,
1794
+ args: params.args,
1795
+ cwd,
1796
+ startTime: new Date(),
1797
+ output: [],
1798
+ isRunning: true
1799
+ };
1800
+ // Capture output
1801
+ proc.stdout?.on('data', (data) => {
1802
+ session.output.push(data.toString());
1803
+ // Keep only last 1000 lines
1804
+ if (session.output.length > 1000) {
1805
+ session.output = session.output.slice(-500);
1806
+ }
1807
+ });
1808
+ proc.stderr?.on('data', (data) => {
1809
+ session.output.push(`[stderr] ${data.toString()}`);
1810
+ });
1811
+ proc.on('close', (code) => {
1812
+ session.isRunning = false;
1813
+ session.output.push(`\n[Prozess beendet mit Code ${code}]`);
1814
+ });
1815
+ proc.on('error', (err) => {
1816
+ session.isRunning = false;
1817
+ session.output.push(`\n[Fehler: ${err.message}]`);
1818
+ });
1819
+ processSessions.set(sessionId, session);
1820
+ return {
1821
+ content: [{
1822
+ type: "text",
1823
+ text: `🚀 **Session gestartet**\n\n| | |\n|---|---|\n| Session-ID | \`${sessionId}\` |\n| Befehl | ${params.command} ${params.args.join(' ')} |\n| PID | ${proc.pid} |\n| Verzeichnis | ${cwd} |\n\nNutze \`fc_read_output\` und \`fc_send_input\` zur Interaktion.`
1824
+ }]
1825
+ };
1826
+ }
1827
+ catch (error) {
1828
+ const errorMsg = error instanceof Error ? error.message : String(error);
1829
+ return {
1830
+ isError: true,
1831
+ content: [{ type: "text", text: `❌ Fehler beim Starten: ${errorMsg}` }]
1832
+ };
1833
+ }
1834
+ });
1835
+ // ============================================================================
1836
+ // Tool: Read Process Output
1837
+ // ============================================================================
1838
+ server.registerTool("fc_read_output", {
1839
+ title: "Session-Output lesen",
1840
+ description: `Liest die Ausgabe einer laufenden Session.
1841
+
1842
+ Args:
1843
+ - session_id (string): Session-ID von fc_start_session
1844
+ - clear (boolean, optional): Output nach dem Lesen löschen
1845
+
1846
+ Returns:
1847
+ - Gesammelter Output seit Start/letztem Clear`,
1848
+ inputSchema: {
1849
+ session_id: z.string().min(1).describe("Session-ID"),
1850
+ clear: z.boolean().default(false).describe("Output löschen")
1851
+ },
1852
+ annotations: {
1853
+ readOnlyHint: true,
1854
+ destructiveHint: false,
1855
+ idempotentHint: true,
1856
+ openWorldHint: false
1857
+ }
1858
+ }, async (params) => {
1859
+ const session = processSessions.get(params.session_id);
1860
+ if (!session) {
1861
+ return {
1862
+ isError: true,
1863
+ content: [{ type: "text", text: `❌ Session nicht gefunden: ${params.session_id}\n\nNutze fc_list_sessions für aktive Sessions.` }]
1864
+ };
1865
+ }
1866
+ const output = session.output.join('');
1867
+ const status = session.isRunning ? '🟢 Läuft' : '🔴 Beendet';
1868
+ if (params.clear) {
1869
+ session.output = [];
1870
+ }
1871
+ return {
1872
+ content: [{
1873
+ type: "text",
1874
+ text: `📤 **Session Output** (${status})\n\`\`\`\n${output || '(kein Output)'}\n\`\`\``
1875
+ }]
1876
+ };
1877
+ });
1878
+ // ============================================================================
1879
+ // Tool: Send Input to Process
1880
+ // ============================================================================
1881
+ server.registerTool("fc_send_input", {
1882
+ title: "Input an Session senden",
1883
+ description: `Sendet Input an eine laufende Session.
1884
+
1885
+ Args:
1886
+ - session_id (string): Session-ID
1887
+ - input (string): Zu sendender Input
1888
+ - newline (boolean, optional): Zeilenumbruch anhängen (default: true)
1889
+
1890
+ Beispiele:
1891
+ - Python: input="print('Hello')"
1892
+ - Shell: input="ls -la"`,
1893
+ inputSchema: {
1894
+ session_id: z.string().min(1).describe("Session-ID"),
1895
+ input: z.string().describe("Zu sendender Input"),
1896
+ newline: z.boolean().default(true).describe("Zeilenumbruch anhängen")
1897
+ },
1898
+ annotations: {
1899
+ readOnlyHint: false,
1900
+ destructiveHint: false,
1901
+ idempotentHint: false,
1902
+ openWorldHint: true
1903
+ }
1904
+ }, async (params) => {
1905
+ const session = processSessions.get(params.session_id);
1906
+ if (!session) {
1907
+ return {
1908
+ isError: true,
1909
+ content: [{ type: "text", text: `❌ Session nicht gefunden: ${params.session_id}` }]
1910
+ };
1911
+ }
1912
+ if (!session.isRunning) {
1913
+ return {
1914
+ isError: true,
1915
+ content: [{ type: "text", text: `❌ Session ist beendet. Starte eine neue mit fc_start_session.` }]
1916
+ };
1917
+ }
1918
+ try {
1919
+ const inputText = params.newline ? params.input + '\n' : params.input;
1920
+ session.process.stdin?.write(inputText);
1921
+ return {
1922
+ content: [{
1923
+ type: "text",
1924
+ text: `📥 Input gesendet an ${params.session_id}:\n\`\`\`\n${params.input}\n\`\`\`\nNutze \`fc_read_output\` um die Antwort zu lesen.`
1925
+ }]
1926
+ };
1927
+ }
1928
+ catch (error) {
1929
+ const errorMsg = error instanceof Error ? error.message : String(error);
1930
+ return {
1931
+ isError: true,
1932
+ content: [{ type: "text", text: `❌ Fehler beim Senden: ${errorMsg}` }]
1933
+ };
1934
+ }
1935
+ });
1936
+ // ============================================================================
1937
+ // Tool: List Sessions
1938
+ // ============================================================================
1939
+ server.registerTool("fc_list_sessions", {
1940
+ title: "Sessions auflisten",
1941
+ description: `Listet alle aktiven und beendeten Sessions auf.
1942
+
1943
+ Returns:
1944
+ - Tabelle aller Sessions mit Status`,
1945
+ inputSchema: {},
1946
+ annotations: {
1947
+ readOnlyHint: true,
1948
+ destructiveHint: false,
1949
+ idempotentHint: true,
1950
+ openWorldHint: false
1951
+ }
1952
+ }, async () => {
1953
+ if (processSessions.size === 0) {
1954
+ return {
1955
+ content: [{ type: "text", text: `📋 Keine Sessions vorhanden.\n\nStarte eine neue mit \`fc_start_session\`.` }]
1956
+ };
1957
+ }
1958
+ const rows = [];
1959
+ for (const [id, session] of processSessions) {
1960
+ const status = session.isRunning ? '🟢' : '🔴';
1961
+ const runtime = Math.round((Date.now() - session.startTime.getTime()) / 1000);
1962
+ rows.push(`| ${status} | \`${id}\` | ${session.command} | ${session.process.pid || '-'} | ${runtime}s |`);
1963
+ }
1964
+ const output = [
1965
+ `📋 **Aktive Sessions** (${processSessions.size})`,
1966
+ ``,
1967
+ `| Status | Session-ID | Befehl | PID | Laufzeit |`,
1968
+ `|--------|------------|--------|-----|----------|`,
1969
+ ...rows
1970
+ ];
1971
+ return {
1972
+ content: [{ type: "text", text: output.join('\n') }]
1973
+ };
1974
+ });
1975
+ // ============================================================================
1976
+ // Tool: Close Session
1977
+ // ============================================================================
1978
+ server.registerTool("fc_close_session", {
1979
+ title: "Session beenden",
1980
+ description: `Beendet eine laufende Session und entfernt sie aus der Liste.
1981
+
1982
+ Args:
1983
+ - session_id (string): Session-ID
1984
+ - force (boolean, optional): Erzwungenes Beenden`,
1985
+ inputSchema: {
1986
+ session_id: z.string().min(1).describe("Session-ID"),
1987
+ force: z.boolean().default(false).describe("Erzwingen")
1988
+ },
1989
+ annotations: {
1990
+ readOnlyHint: false,
1991
+ destructiveHint: true,
1992
+ idempotentHint: true,
1993
+ openWorldHint: false
1994
+ }
1995
+ }, async (params) => {
1996
+ const session = processSessions.get(params.session_id);
1997
+ if (!session) {
1998
+ return {
1999
+ isError: true,
2000
+ content: [{ type: "text", text: `❌ Session nicht gefunden: ${params.session_id}` }]
2001
+ };
2002
+ }
2003
+ try {
2004
+ if (session.isRunning) {
2005
+ if (params.force) {
2006
+ session.process.kill('SIGKILL');
2007
+ }
2008
+ else {
2009
+ session.process.kill('SIGTERM');
2010
+ }
2011
+ }
2012
+ processSessions.delete(params.session_id);
2013
+ return {
2014
+ content: [{ type: "text", text: `✅ Session beendet und entfernt: ${params.session_id}` }]
2015
+ };
2016
+ }
2017
+ catch (error) {
2018
+ const errorMsg = error instanceof Error ? error.message : String(error);
2019
+ return {
2020
+ isError: true,
2021
+ content: [{ type: "text", text: `❌ Fehler beim Beenden: ${errorMsg}` }]
2022
+ };
2023
+ }
2024
+ });
2025
+ // ============================================================================
2026
+ // Server Startup
2027
+ // ============================================================================
2028
+ async function main() {
2029
+ const transport = new StdioServerTransport();
2030
+ await server.connect(transport);
2031
+ console.error("🚀 BACH FileCommander MCP Server gestartet");
2032
+ }
2033
+ main().catch((error) => {
2034
+ console.error("Fatal error:", error);
2035
+ process.exit(1);
2036
+ });
2037
+ //# sourceMappingURL=index.js.map