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