Schließt die MCP-Server-Spec ab. "Worksheets" aus der Spec entsprechen im tatsächlichen Datenmodell den .lavorlage-Elternbrief-Vorlagen (LehrerApp.Templating) - es gibt kein separates Arbeitsblatt-Konzept mit Fach/Klassenstufe-Metadaten. Neue Tools list_letter_templates (Read) und render_letter (Read, liefert Base64-PDF, kein DB-Schreibzugriff). upload_worksheet/update_worksheet bewusst nicht umgesetzt: das Seitenlayout ist eine eigene positionsbasierte DSL mit eigenem visuellen Editor (LehrerApp.TemplateDesigner) - ein LLM müsste sie blind erzeugen, mit hohem Risiko für kaputte Layouts. Platzhalter-Logik aus CreateLetterDialogViewModel nach LetterPlaceholderBuilder extrahiert, damit Dialog und MCP-Tool nicht auseinanderdriften. Neuer McpClientRegistrationService trägt den Bridge-Pfad in Claude Desktops claude_desktop_config.json ein (Button in den Einstellungen, nie automatisch), ohne bestehende Fremdeinträge zu verlieren und ohne eine nicht lesbare Konfigurationsdatei zu überschreiben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
158 lines
6.8 KiB
C#
158 lines
6.8 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace LehrerApp.Desktop.Services.Mcp;
|
|
|
|
public record McpRegistrationResult(bool Success, string Message);
|
|
|
|
/// <summary>
|
|
/// 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 (<c>mcpServers</c>-Objekt in <c>claude_desktop_config.json</c>) — 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, <see cref="ResolveBridgePath"/> findet sie dann nicht.
|
|
/// </summary>
|
|
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()) { }
|
|
|
|
/// <summary>Test-Seam: erlaubt, Konfigurations- und Bridge-Pfad ohne echte
|
|
/// Sonderordner/Installation vorzugeben (siehe <see cref="ResolveConfigPath"/>/
|
|
/// <see cref="ResolveBridgePath"/> für das produktive Verhalten).</summary>
|
|
public McpClientRegistrationService(string? configPath, string? bridgePath)
|
|
{
|
|
ConfigPath = configPath;
|
|
_bridgePath = bridgePath;
|
|
}
|
|
|
|
public string? ConfigPath { get; }
|
|
|
|
/// <summary>Ob überhaupt ein Claude-Desktop-Konfigurationsordner existiert — ein Hinweis, ob die
|
|
/// Anwendung installiert ist, unabhängig davon, ob LehrerApp dort schon eingetragen ist.</summary>
|
|
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.
|
|
}
|
|
}
|