diff --git a/LehrerApp.Desktop.Tests/McpToolsTests.cs b/LehrerApp.Desktop.Tests/McpToolsTests.cs
index d12204f..dea423b 100644
--- a/LehrerApp.Desktop.Tests/McpToolsTests.cs
+++ b/LehrerApp.Desktop.Tests/McpToolsTests.cs
@@ -29,12 +29,18 @@ public sealed class McpToolsTests
new[]
{
"add_lesson_attachment", "add_lesson_phase", "create_grade_entry", "create_lesson",
- "create_time_entry", "create_unit", "remove_lesson_phase", "update_lesson",
- "update_lesson_phase", "update_student_group_assignment", "update_unit",
+ "create_time_entry", "create_unit", "move_lesson", "remove_lesson_phase",
+ "update_lesson", "update_lesson_phase", "update_student_group_assignment", "update_unit",
},
McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal));
}
+ [Fact]
+ public void AllowedDestructiveWriteTools_EnthaeltNurDeleteLesson()
+ {
+ Assert.Equal(["delete_lesson"], McpToolScope.AllowedDestructiveWriteTools);
+ }
+
[Fact]
public void AllowedTools_EnthaeltKeineDokumentationstypen()
{
@@ -463,6 +469,110 @@ public sealed class McpToolsTests
Assert.Equal(0, confirmation.CallCount);
}
+ [Fact]
+ public async Task MoveLesson_NutzerBestaetigt_VerschiebtDatum()
+ {
+ var lesson = new Lesson { Topic = "Brechung", Date = new DateOnly(2026, 1, 5) };
+ var lessons = new FakeLessons();
+ lessons.Add(lesson);
+ var tool = BuildLessonPlanTools(lessons: lessons);
+
+ var result = await tool.MoveLesson(lesson.Id, new DateOnly(2026, 1, 12));
+
+ Assert.True(result.Applied);
+ Assert.Equal(new DateOnly(2026, 1, 12), lessons.GetById(lesson.Id)!.Date);
+ }
+
+ [Fact]
+ public async Task MoveLesson_ShiftFollowing_VerschiebtSpaetereStundenDerselbenEinheitMit()
+ {
+ var unitId = Guid.NewGuid();
+ var moved = new Lesson { UnitId = unitId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5) };
+ var later = new Lesson { UnitId = unitId, Topic = "Reflexion", Date = new DateOnly(2026, 1, 7) };
+ var earlier = new Lesson { UnitId = unitId, Topic = "Einstieg", Date = new DateOnly(2026, 1, 1) };
+ var lessons = new FakeLessons();
+ lessons.Add(moved); lessons.Add(later); lessons.Add(earlier);
+ var tool = BuildLessonPlanTools(lessons: lessons);
+
+ // moved: 5.1. -> 12.1. (Delta +7 Tage), later (7.1., danach) muss mitwandern, earlier (1.1., davor) nicht.
+ var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), shiftFollowingLessons: true);
+
+ Assert.True(result.Applied);
+ Assert.Equal(new DateOnly(2026, 1, 14), lessons.GetById(later.Id)!.Date);
+ Assert.Equal(new DateOnly(2026, 1, 1), lessons.GetById(earlier.Id)!.Date);
+ }
+
+ [Fact]
+ public async Task MoveLesson_ZielStundeBelegt_LiefertFehlerNachBestaetigung()
+ {
+ var groupId = Guid.NewGuid();
+ var moved = new Lesson { GroupId = groupId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5), LessonNumber = 1 };
+ var occupying = new Lesson { GroupId = groupId, Topic = "Anderer Kurs", Date = new DateOnly(2026, 1, 12), LessonNumber = 1 };
+ var lessons = new FakeLessons();
+ lessons.Add(moved); lessons.Add(occupying);
+ var confirmation = new FakeMcpConfirmation();
+ var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation);
+
+ var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), newPeriod: 1);
+
+ Assert.False(result.Applied);
+ Assert.Equal(1, confirmation.CallCount); // Bestätigung lief bereits, Konflikt zeigt sich erst beim Speichern.
+ Assert.Equal(new DateOnly(2026, 1, 5), lessons.GetById(moved.Id)!.Date); // unverändert
+ }
+
+ [Fact]
+ public async Task MoveLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage()
+ {
+ var confirmation = new FakeMcpConfirmation();
+ var tool = BuildLessonPlanTools(confirmation: confirmation);
+
+ var result = await tool.MoveLesson(Guid.NewGuid(), new DateOnly(2026, 1, 12));
+
+ Assert.False(result.Applied);
+ Assert.Equal(0, confirmation.CallCount);
+ }
+
+ [Fact]
+ public async Task DeleteLesson_NutzerBestaetigt_LoeschtEndgueltig()
+ {
+ var lesson = new Lesson { Topic = "Brechung" };
+ var lessons = new FakeLessons();
+ lessons.Add(lesson);
+ var tool = BuildLessonPlanTools(lessons: lessons);
+
+ var result = await tool.DeleteLesson(lesson.Id);
+
+ Assert.True(result.Applied);
+ Assert.Null(lessons.GetById(lesson.Id));
+ }
+
+ [Fact]
+ public async Task DeleteLesson_NutzerLehntAb_BleibtErhalten()
+ {
+ var lesson = new Lesson { Topic = "Brechung" };
+ var lessons = new FakeLessons();
+ lessons.Add(lesson);
+ var confirmation = new FakeMcpConfirmation { Response = false };
+ var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation);
+
+ var result = await tool.DeleteLesson(lesson.Id);
+
+ Assert.False(result.Applied);
+ Assert.NotNull(lessons.GetById(lesson.Id));
+ }
+
+ [Fact]
+ public async Task DeleteLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage()
+ {
+ var confirmation = new FakeMcpConfirmation();
+ var tool = BuildLessonPlanTools(confirmation: confirmation);
+
+ var result = await tool.DeleteLesson(Guid.NewGuid());
+
+ Assert.False(result.Applied);
+ Assert.Equal(0, confirmation.CallCount);
+ }
+
// ── GroupMembershipTools ─────────────────────────────────────────────────────────────────
[Fact]
diff --git a/LehrerApp.Desktop/Services/Mcp/AvaloniaMcpConfirmationService.cs b/LehrerApp.Desktop/Services/Mcp/AvaloniaMcpConfirmationService.cs
index 6d0434e..b0b36bd 100644
--- a/LehrerApp.Desktop/Services/Mcp/AvaloniaMcpConfirmationService.cs
+++ b/LehrerApp.Desktop/Services/Mcp/AvaloniaMcpConfirmationService.cs
@@ -2,15 +2,20 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
-using LehrerApp.Desktop.Views.Shared;
+using LehrerApp.Desktop.Views.Mcp;
namespace LehrerApp.Desktop.Services.Mcp;
///
-/// Produktive -Implementierung: zeigt den bestehenden
-/// (Views/Shared) über dem Hauptfenster an. Der aufrufende Tool-Handler
-/// läuft auf einem Hintergrund-Thread (MCP-Pipe-Session in ),
-/// deshalb Marshalling über .
+/// Produktive -Implementierung: zeigt
+/// über dem Hauptfenster an. Der aufrufende Tool-Handler läuft auf einem Hintergrund-Thread
+/// (MCP-Pipe-Session in ), deshalb Marshalling über
+/// .
+///
+/// Nutzer-Feedback: der Dialog fiel zu wenig auf, wenn LehrerApp im Hintergrund lief (naheliegend,
+/// da der Anstoß von einem KI-Client in einem anderen Fenster kommt) — deshalb wird das Hauptfenster
+/// vor dem Anzeigen aus einer möglichen Minimierung geholt und aktiviert, und der Dialog selbst
+/// läuft `Topmost`.
///
/// Ohne Reaktion des Nutzers würde die Pipe-Session (und damit der wartende KI-Client) unbegrenzt
/// hängen bleiben — nach wird der Dialog automatisch geschlossen und die
@@ -31,9 +36,12 @@ public sealed class AvaloniaMcpConfirmationService : IMcpConfirmationService
// sicher aufgerufen werden kann.
return await Dispatcher.UIThread.InvokeAsync(async () =>
{
- var dialog = new ConfirmDialog
+ if (owner.WindowState == WindowState.Minimized) owner.WindowState = WindowState.Normal;
+ owner.Activate();
+
+ var dialog = new McpConfirmDialog
{
- DataContext = new ConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
+ DataContext = new McpConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
};
var dialogTask = dialog.ShowDialog(owner);
var timeoutTask = Task.Delay(Timeout, ct);
diff --git a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs
index 772c163..045cd1e 100644
--- a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs
+++ b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs
@@ -8,7 +8,7 @@ using ModelContextProtocol.Server;
namespace LehrerApp.Desktop.Services.Mcp;
///
-/// In-Process-MCP-Server (Phase 1–3, siehe Planungsdokument). Lauscht auf der Named Pipe
+/// In-Process-MCP-Server (siehe Planungsdokument und TODO.md 4.5.25ff.). Lauscht auf der Named Pipe
/// und bedient jede eingehende Verbindung (eine je
/// LehrerApp.McpBridge-Instanz) als eigene MCP-Session über —
/// ein ist ein normaler und kann direkt
@@ -129,7 +129,8 @@ public sealed class McpServerHostedService : IAsyncDisposable
// Write-Tools schreiben nie direkt - jede Handler-Methode ruft selbst erst
// IMcpConfirmationService auf (siehe die jeweilige Tool-Klasse). ReadOnly bewusst false,
- // Destructive bewusst false (keine der Phase-2-Schreiboperationen löscht etwas).
+ // Destructive bewusst false (keine dieser Schreiboperationen löscht etwas) - für die
+ // Ausnahme siehe AddDestructiveWriteTool/McpToolScope.AllowedDestructiveWriteTools.
void AddWriteTool(Delegate handler, string name, string description)
{
toolCollection.Add(McpServerTool.Create(handler, new McpServerToolCreateOptions
@@ -141,6 +142,17 @@ public sealed class McpServerHostedService : IAsyncDisposable
}));
}
+ void AddDestructiveWriteTool(Delegate handler, string name, string description)
+ {
+ toolCollection.Add(McpServerTool.Create(handler, new McpServerToolCreateOptions
+ {
+ Name = name,
+ Description = description,
+ ReadOnly = false,
+ Destructive = true,
+ }));
+ }
+
AddReadTool(studentTools.GetStudents, "get_students",
"Listet Schüler, optional gefiltert nach Lerngruppe.");
AddReadTool(examTools.GetExams, "get_exams",
@@ -182,10 +194,16 @@ public sealed class McpServerHostedService : IAsyncDisposable
"Entfernt eine Verlaufsplan-Phase aus einer Einzelstunde (Bestätigung durch den Nutzer nötig).");
AddWriteTool(lessonPlanTools.AddLessonAttachment, "add_lesson_attachment",
"Fügt einer Einzelstunde ein neues Material als Base64-kodierten Anhang hinzu (Bestätigung durch den Nutzer nötig).");
+ AddWriteTool(lessonPlanTools.MoveLesson, "move_lesson",
+ "Verschiebt eine Einzelstunde auf ein neues Datum, optional mit Mitverschieben späterer Stunden derselben Einheit (Bestätigung durch den Nutzer nötig).");
+
+ AddDestructiveWriteTool(lessonPlanTools.DeleteLesson, "delete_lesson",
+ "Löscht eine Einzelstunde endgültig, ohne Papierkorb (Bestätigung durch den Nutzer nötig).");
System.Diagnostics.Debug.Assert(
toolCollection.Select(t => t.ProtocolTool.Name).OrderBy(n => n)
- .SequenceEqual(McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools).OrderBy(n => n)),
+ .SequenceEqual(McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools)
+ .Concat(McpToolScope.AllowedDestructiveWriteTools).OrderBy(n => n)),
"Registrierte MCP-Tools weichen von McpToolScope ab.");
return new McpServerOptions
diff --git a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs
index cc14022..ddf1735 100644
--- a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs
+++ b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs
@@ -41,5 +41,16 @@ public static class McpToolScope
"update_lesson_phase",
"remove_lesson_phase",
"add_lesson_attachment",
+ "move_lesson",
+ ];
+
+ /// Löschende Write-Tools — eine bewusste, gezielte Ausnahme von der sonst geltenden
+ /// "v1 ohne Lösch-Tools"-Regel (siehe Planungsdokument), nicht deren Aufhebung. Getrennt von
+ /// aufgeführt, damit diese Ausnahme beim Lesen sofort auffällt.
+ /// registriert diese Tools zusätzlich mit
+ /// Destructive = true.
+ public static readonly IReadOnlyCollection AllowedDestructiveWriteTools =
+ [
+ "delete_lesson",
];
}
diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs
index 870d311..0f33600 100644
--- a/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs
+++ b/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs
@@ -2,6 +2,7 @@ using System.ComponentModel;
using System.Text;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
namespace LehrerApp.Desktop.Services.Mcp.Tools;
@@ -16,7 +17,12 @@ namespace LehrerApp.Desktop.Services.Mcp.Tools;
/// Stunde) und liefern einen für den Bestätigungsdialog tatsächlich lesbaren Diff statt eines
/// kompletten Objekt-Dumps. Ein Tool, das die ganze Stunde überschreibt, ist bewusst NICHT
/// vorgesehen; wo es fehlt, ist die Kombination aus update_lesson (Metadaten) +
-/// add/update/remove_lesson_phase (je eine Phase) der vorgesehene Weg.
+/// add/update/remove_lesson_phase (je eine Phase) der vorgesehene Weg.
+///
+/// "delete_lesson" ist (Stand Nutzer-Nachtrag) das einzige Lösch-Tool im gesamten MCP-Katalog —
+/// eine bewusste, gezielte Ausnahme von der sonst geltenden "v1 ohne Lösch-Tools"-Regel (siehe
+/// Planungsdokument), nicht deren Aufhebung. Entsprechend als "Destructive" annotiert
+/// (siehe McpServerHostedService) und mit besonders deutlicher Bestätigungsnachricht.
public class LessonPlanTools(
IUnitRepository units, ILessonRepository lessons, IGroupRepository groups,
IAttachmentStorage attachments, IMcpConfirmationService confirmation)
@@ -294,6 +300,63 @@ public class LessonPlanTools(
return new WriteResultDto(true, phase.Id, "Phase entfernt.");
}
+ // ── Einzelstunden (Lesson) — Verschieben & Löschen ───────────────────────────────────────
+
+ [Description("Verschiebt eine Einzelstunde auf ein neues Datum (und optional eine neue Stundennummer). Mit shiftFollowingLessons=true verschieben sich alle noch nicht durchgeführten, späteren Stunden derselben Einheit um denselben Tages-Versatz mit — so lässt sich eine Lücke für eine neue Stunde öffnen: diese Stunde auf den Termin der übernächsten verschieben (mit shiftFollowingLessons), dann create_lesson auf das dadurch freigewordene ursprüngliche Datum. Muss der Nutzer erst bestätigen.")]
+ public async Task MoveLesson(
+ [Description("ID der zu verschiebenden Einzelstunde.")] Guid lessonId,
+ [Description("Neues Datum, Format YYYY-MM-DD.")] DateOnly newDate,
+ [Description("Neue Stundennummer im Tagesraster. Unverändert lassen: weglassen.")] int? newPeriod = null,
+ [Description("Alle späteren, noch nicht durchgeführten Stunden derselben Einheit um denselben Tages-Versatz mitverschieben.")] bool shiftFollowingLessons = false,
+ CancellationToken ct = default)
+ {
+ var lesson = lessons.GetById(lessonId);
+ if (lesson is null) return new WriteResultDto(false, null, "Unbekannte Stunden-ID.");
+
+ var oldDate = lesson.Date;
+ var affectedCount = shiftFollowingLessons
+ ? lessons.GetByUnit(lesson.UnitId).Count(l => l.Id != lesson.Id && l.Status != LessonStatus.Conducted && l.Date > oldDate)
+ : 0;
+
+ var message = $"„{lesson.Topic}“ von {oldDate:dd.MM.yyyy} auf {newDate:dd.MM.yyyy} verschieben?" +
+ (newPeriod is not null ? $"\nNeue Stundennummer: {newPeriod}." : "") +
+ (shiftFollowingLessons
+ ? affectedCount > 0
+ ? $"\n{affectedCount} spätere Stunde(n) derselben Einheit verschieben sich um denselben Versatz mit."
+ : "\nKeine späteren, noch offenen Stunden derselben Einheit betroffen."
+ : "");
+
+ if (!await confirmation.ConfirmAsync("Stunde verschieben?", message, ct))
+ return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
+
+ try
+ {
+ new LessonSchedulingService(lessons).Move(lesson, newDate, newPeriod, shiftFollowingLessons);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return new WriteResultDto(false, null, ex.Message);
+ }
+ return new WriteResultDto(true, lesson.Id, "Stunde verschoben.");
+ }
+
+ [Description("Löscht eine Einzelstunde endgültig, inklusive ihrer Anhänge. Anders als die meisten anderen Löschvorgänge in LehrerApp landet eine gelöschte Stunde NICHT im Papierkorb — nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")]
+ public async Task DeleteLesson(
+ [Description("ID der zu löschenden Einzelstunde.")] Guid lessonId,
+ CancellationToken ct = default)
+ {
+ var lesson = lessons.GetById(lessonId);
+ if (lesson is null) return new WriteResultDto(false, null, "Unbekannte Stunden-ID.");
+
+ var message = $"Die Stunde „{lesson.Topic}“ vom {lesson.Date:dd.MM.yyyy} wird endgültig gelöscht. " +
+ "Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Stunden).";
+ if (!await confirmation.ConfirmAsync("Stunde endgültig löschen?", message, ct))
+ return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
+
+ lessons.Delete(lessonId);
+ return new WriteResultDto(true, lessonId, "Stunde gelöscht.");
+ }
+
private static LessonDto ToDto(Lesson l) => new(
l.Id, l.UnitId, l.GroupId, l.Date, l.LessonNumber, l.Topic, l.Homework, l.Status,
l.Phases.Select(p => new LessonPhaseDto(p.Id, p.Name, p.DurationMinutes, p.Activity, p.Material, p.Shorthand)).ToList(),
diff --git a/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml
new file mode 100644
index 0000000..3bac1f1
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml.cs b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml.cs
new file mode 100644
index 0000000..bb2d1a6
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace LehrerApp.Desktop.Views.Mcp;
+
+public partial class McpConfirmDialog : Window
+{
+ public McpConfirmDialog() => InitializeComponent();
+
+ private void OnConfirm(object? sender, RoutedEventArgs e) => Close(true);
+ private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
+}
diff --git a/LehrerApp.Desktop/Views/Mcp/McpConfirmDialogInfo.cs b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialogInfo.cs
new file mode 100644
index 0000000..0c6aaa2
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Mcp/McpConfirmDialogInfo.cs
@@ -0,0 +1,14 @@
+namespace LehrerApp.Desktop.Views.Mcp;
+
+/// DataContext für — bewusst ein eigener, auffälligerer
+/// Dialog statt Wiederverwendung von : Letzterer wird an
+/// vielen Stellen der App für routinemäßige Bestätigungen verwendet, eine Änderung an dessen
+/// Größe/Optik hätte all diese Stellen mitbetroffen. Ein von der KI vorgeschlagener Schreibzugriff
+/// soll sich dagegen bewusst absetzen (Nutzer-Feedback: der ursprüngliche gemeinsame Dialog fiel zu
+/// wenig auf, wenn LehrerApp gerade nicht im Vordergrund war).
+public class McpConfirmDialogInfo
+{
+ public string Title { get; init; } = "";
+ public string Message { get; init; } = "";
+ public string ConfirmText { get; init; } = "Übernehmen";
+}
diff --git a/TODO.md b/TODO.md
index 27d2ae9..c1f7cd6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2650,6 +2650,40 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
ohne erneutes `get_lesson_plans` möglich ist. 4 neue Tests in
[McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 32).
+- [x] **4.5.31** Bestätigungsdialog-Prominenz + `move_lesson`/`delete_lesson` (2026-09-12,
+ Nutzer-Feedback nach erstem Live-Test mit echtem Bridge-Prozess).
+ - **Dialog fiel zu wenig auf:** der bisher wiederverwendete `ConfirmDialog` (Views/Shared,
+ an vielen anderen Stellen der App für Routine-Bestätigungen im Einsatz) ist nur zum
+ Hauptfenster modal — läuft LehrerApp im Hintergrund, während der Anstoß von einem
+ KI-Client in einem anderen Fenster kommt (der Normalfall bei MCP), geht der Dialog leicht
+ unter. Neuer, eigenständiger
+ [McpConfirmDialog](LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml) statt einer
+ Style-Änderung am geteilten `ConfirmDialog` (hätte alle dessen bestehenden Aufrufer
+ mitbetroffen): breiter (520 statt 420), auffälliger Kopfbereich ("Vorschlag deines
+ KI-Assistenten — bitte prüfen"), `Topmost`. `AvaloniaMcpConfirmationService` holt das
+ Hauptfenster zusätzlich aus einer möglichen Minimierung und aktiviert es
+ (`WindowState`/`Activate()`), bevor der Dialog erscheint.
+ - **`move_lesson`:** deckt sowohl "eine Stunde verschieben" als auch "Lücke für eine neue
+ Stunde öffnen" ab, indem es die bereits vorhandene, bislang nur UI-seitig genutzte
+ [LessonSchedulingService.Move](LehrerApp.Core/Services/LessonSchedulingService.cs) kapselt
+ (`shiftFollowingLessons=true` verschiebt alle späteren, noch nicht durchgeführten Stunden
+ derselben Einheit um denselben Tages-Versatz mit — "Lücke öffnen" = die Stunde hinter der
+ Lücke so verschieben, danach `create_lesson` auf das freigewordene Datum). Keine neue
+ Terminlogik geschrieben, nur wiederverwendet.
+ - **`delete_lesson` — bewusste, gezielte Ausnahme von "v1 ohne Lösch-Tools":** explizite
+ Nutzeranforderung. `Lesson` hängt (anders als die meisten anderen Entitäten) nicht am
+ Papierkorb-System — `ILessonRepository.Delete` löscht endgültig, kein `Restore`. Deshalb:
+ eigene `McpToolScope.AllowedDestructiveWriteTools`-Liste getrennt von den normalen
+ Write-Tools (fällt beim Lesen sofort auf), `Destructive = true`-Annotation im
+ MCP-Protokoll, und eine Bestätigungsnachricht, die explizit "NICHT rückgängig zu machen"
+ sagt (Wortlaut an den bestehenden Lösch-Dialog in `PlanningTabView.axaml.cs`
+ angelehnt: "wird endgültig gelöscht").
+ - 8 neue Tests in [McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 40),
+ u.a.: `shiftFollowingLessons` verschiebt nur Stunden NACH der bewegten, nicht davor;
+ ein belegtes Ziel liefert nach bereits erfolgter Bestätigung einen Fehler statt
+ stillschweigend zu überschreiben (die bestehende `EnsureTargetIsFree`-Prüfung in
+ `LessonSchedulingService` greift unverändert).
+
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`,