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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/SECURITY.md +49 -0
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2037 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
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
|