using System.Text.Json; using System.Text.Json.Nodes; namespace LehrerApp.Desktop.Services.Mcp; public record McpRegistrationResult(bool Success, string Message); /// /// Trägt LehrerApp.McpBridge in die MCP-Server-Konfiguration von Claude Desktop ein (siehe /// Planungsdokument, "Registrierungs-Workflow"). Bewusst nur für Claude Desktop: das ist der einzige /// KI-Client, den die Spec konkret benennt und der eine dokumentierte, stabile Config-Datei-Konvention /// hat (mcpServers-Objekt in claude_desktop_config.json) — andere lokale Clients /// (Ollama, LM Studio, ...) haben keine einheitliche Konvention, die sich hier ohne Rätselraten /// unterstützen ließe. /// /// Läuft nur auf explizite Nutzeraktion (Button in den Einstellungen), nie automatisch beim /// App-Start — das Schreiben in die Konfigurationsdatei eines fremden Programms ist ein sichtbarer /// externer Seiteneffekt und gehört nicht in den normalen Startpfad. /// /// Setzt eine gepackte Installation voraus (Bridge liegt neben der Hauptapp-Executable, siehe /// build-macos-app.sh) — bei einem lokalen "dotnet build/run" liegt die Bridge in ihrem eigenen /// separaten bin-Ordner, findet sie dann nicht. /// public class McpClientRegistrationService { private const string ServerName = "lehrerapp"; private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true }; private readonly string? _bridgePath; public McpClientRegistrationService() : this(ResolveConfigPath(), ResolveBridgePath()) { } /// Test-Seam: erlaubt, Konfigurations- und Bridge-Pfad ohne echte /// Sonderordner/Installation vorzugeben (siehe / /// für das produktive Verhalten). public McpClientRegistrationService(string? configPath, string? bridgePath) { ConfigPath = configPath; _bridgePath = bridgePath; } public string? ConfigPath { get; } /// Ob überhaupt ein Claude-Desktop-Konfigurationsordner existiert — ein Hinweis, ob die /// Anwendung installiert ist, unabhängig davon, ob LehrerApp dort schon eingetragen ist. public bool IsClaudeDesktopDetected => ConfigPath is not null && File.Exists(ConfigPath); public bool IsRegistered() => TryLoadRoot(out var root, out _) && root["mcpServers"]?[ServerName] is not null; public McpRegistrationResult Register() { if (ConfigPath is null) return new(false, "Claude Desktop wird auf diesem Betriebssystem nicht unterstützt."); var bridgePath = _bridgePath; if (bridgePath is null || !File.Exists(bridgePath)) return new(false, $"Bridge-Programm nicht gefunden ({bridgePath ?? "unbekannter Pfad"}). " + "Diese Funktion setzt eine gepackte Installation voraus, nicht einen lokalen Entwicklungs-Build."); if (!TryLoadRoot(out var root, out var error)) return new(false, error!); if (root["mcpServers"] is not JsonObject servers) { servers = new JsonObject(); root["mcpServers"] = servers; } servers[ServerName] = new JsonObject { ["command"] = bridgePath, ["args"] = new JsonArray(), }; var detectedBefore = IsClaudeDesktopDetected; var result = WriteRoot(root, "In Claude Desktop eingetragen. Claude Desktop neu starten, damit die Änderung wirkt."); if (result.Success && !detectedBefore) return result with { Message = result.Message + " Hinweis: Es wurde keine bestehende Claude-Desktop-Installation erkannt — die Konfiguration " + "greift erst, sobald Claude Desktop installiert ist.", }; return result; } public McpRegistrationResult Unregister() { if (ConfigPath is null || !File.Exists(ConfigPath)) return new(true, "Nichts einzutragen gefunden."); if (!TryLoadRoot(out var root, out var error)) return new(false, error!); if (root["mcpServers"] is not JsonObject servers || !servers.Remove(ServerName)) return new(true, "War nicht eingetragen."); return WriteRoot(root, "Eintrag entfernt."); } private bool TryLoadRoot(out JsonObject root, out string? error) { error = null; if (ConfigPath is null || !File.Exists(ConfigPath)) { root = new JsonObject(); return true; } try { root = JsonNode.Parse(File.ReadAllText(ConfigPath)) as JsonObject ?? new JsonObject(); return true; } catch (JsonException ex) { // Nicht raten und überschreiben - eine kaputte fremde Konfigurationsdatei bleibt unangetastet, // der Nutzer bekommt stattdessen eine klare Fehlermeldung mit Pfad. root = new JsonObject(); error = $"Bestehende Claude-Desktop-Konfiguration ist kein gültiges JSON ({ex.Message}). " + $"Bitte manuell prüfen: {ConfigPath}"; return false; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Z.B. gesperrt, weil Claude Desktop selbst gerade schreibt - ebenfalls nicht überschreiben. root = new JsonObject(); error = $"Bestehende Claude-Desktop-Konfiguration konnte nicht gelesen werden: {ex.Message}"; return false; } } private McpRegistrationResult WriteRoot(JsonObject root, string successMessage) { try { Directory.CreateDirectory(Path.GetDirectoryName(ConfigPath!)!); File.WriteAllText(ConfigPath!, root.ToJsonString(WriteOptions)); return new(true, successMessage); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return new(false, $"Konfiguration konnte nicht geschrieben werden: {ex.Message}"); } } private static string? ResolveBridgePath() { var bridgeFile = OperatingSystem.IsWindows() ? "LehrerApp.McpBridge.exe" : "LehrerApp.McpBridge"; return Path.Combine(AppContext.BaseDirectory, bridgeFile); } private static string? ResolveConfigPath() { if (OperatingSystem.IsWindows()) return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"); if (OperatingSystem.IsMacOS()) return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"); return null; // Linux: kein offizieller Claude-Desktop-Client bekannt. } }