feat: lokaler MCP-Server, Phase 4 (Elternbrief-Vorlagen + Claude-Desktop-Registrierung)

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>
This commit is contained in:
2026-09-11 22:53:46 +02:00
co-authored by Claude Sonnet 5
parent dd2e1e7c61
commit 55fba2cadb
17 changed files with 689 additions and 26 deletions
@@ -0,0 +1,41 @@
using LehrerApp.Core.Models;
using LehrerApp.Templating;
namespace LehrerApp.Desktop.Services;
/// <summary>
/// Baut die Standard-Platzhalterwerte für Elternbrief-Vorlagen (Datum, Anrede, Kontaktadresse,
/// Schülerdaten, Brieftext, ...). Ursprünglich Teil von
/// <see cref="ViewModels.Students.CreateLetterDialogViewModel"/>, hierher extrahiert, damit
/// <c>LetterTemplateTools</c> (MCP, siehe Planungsdokument) dieselbe Logik nutzt statt sie zu
/// duplizieren — beide Aufrufer müssen exakt dieselben Platzhalternamen befüllen, sonst driften
/// Dialog-generierte und KI-generierte Briefe unbemerkt auseinander.
/// </summary>
public static class LetterPlaceholderBuilder
{
public static Dictionary<string, PlaceholderValue> BuildStandardValues(
Student student, Contact? contact, LearningGroup? group, DateOnly date,
string letterText, string teacherName)
{
var cityLine = string.Join(" ", new[] { contact?.PostalCode, contact?.City }.Where(x => !string.IsNullOrWhiteSpace(x)));
var address = string.Join(Environment.NewLine, new[] { contact?.Name, contact?.Street, cityLine }.Where(x => !string.IsNullOrWhiteSpace(x)));
return new Dictionary<string, PlaceholderValue>(StringComparer.Ordinal)
{
["Datum"] = new DateValue(date), ["CurrentDate"] = new DateValue(date),
["Empfaenger"] = new TextValue(contact?.Name ?? ""), ["Anrede"] = new TextValue(contact?.LetterSalutation ?? ""),
["Brieftext"] = new MultilineValue(letterText), ["LehrerName"] = new TextValue(teacherName),
["Student.FirstName"] = new TextValue(student.FirstName), ["Student.LastName"] = new TextValue(student.LastName),
["Contact.Name"] = new TextValue(contact?.Name ?? ""), ["Contact.Address"] = new MultilineValue(address),
["Contact.Street"] = new TextValue(contact?.Street ?? ""), ["Contact.PostalCode"] = new TextValue(contact?.PostalCode ?? ""),
["Contact.City"] = new TextValue(contact?.City ?? ""), ["Letter.Salutation"] = new TextValue(contact?.LetterSalutation ?? ""),
["Group.Name"] = new TextValue(group?.Name ?? ""), ["SchoolYear"] = new TextValue(group?.SchoolYear ?? ""),
};
}
public static bool IsEmpty(PlaceholderValue value) => value switch
{
TextValue x => string.IsNullOrWhiteSpace(x.Value),
MultilineValue x => string.IsNullOrWhiteSpace(x.Value),
_ => false,
};
}