diff --git a/LehrerApp.Desktop.Tests/McpToolsTests.cs b/LehrerApp.Desktop.Tests/McpToolsTests.cs index 5ebe3dc..e578491 100644 --- a/LehrerApp.Desktop.Tests/McpToolsTests.cs +++ b/LehrerApp.Desktop.Tests/McpToolsTests.cs @@ -16,9 +16,9 @@ public sealed class McpToolsTests new[] { "download_lesson_attachment", "get_competency_catalog", "get_exams", "get_grades", - "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule", "get_students", - "get_subjects", "get_time_entries", "get_untis_absence_rows", "get_untis_hub_status", - "list_letter_templates", "render_letter", + "get_groups", "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule", + "get_students", "get_subjects", "get_time_entries", "get_untis_absence_rows", + "get_untis_hub_status", "list_letter_templates", "render_letter", }, McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal)); } @@ -59,6 +59,26 @@ public sealed class McpToolsTests n.Contains("note", StringComparison.OrdinalIgnoreCase)); } + // ── GroupTools ─────────────────────────────────────────────────────────────────────────── + + [Fact] + public void GetGroups_LiefertIdNameUndSchuljahrJeGruppe() + { + var group = new LearningGroup + { + Name = "9a", Type = GroupType.Class, SchoolYear = "2025/26", GradeLevel = 9, IsActive = true, + }; + var tool = new GroupTools(new FakeGroups([group])); + + var result = Assert.Single(tool.GetGroups()); + + Assert.Equal(group.Id, result.Id); + Assert.Equal("9a", result.Name); + Assert.Equal("2025/26", result.SchoolYear); + Assert.Equal(9, result.GradeLevel); + Assert.True(result.IsActive); + } + // ── StudentTools ───────────────────────────────────────────────────────────────────────── [Fact] diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index 2a844ac..b28bb9b 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -228,6 +228,7 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs index 2e3f136..f813271 100644 --- a/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs +++ b/LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs @@ -32,13 +32,13 @@ public sealed class McpServerHostedService : IAsyncDisposable StudentTools studentTools, ExamTools examTools, GradeTools gradeTools, ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools, GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools, - CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools) + CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools) { _settings = settings; _logger = logger; _serverOptions = BuildServerOptions( studentTools, examTools, gradeTools, scheduleTools, timeEntryTools, lessonPlanTools, - groupMembershipTools, letterTemplateTools, competencyTools, untisComparisonTools); + groupMembershipTools, letterTemplateTools, competencyTools, untisComparisonTools, groupTools); } /// Setzt die Pipe-Server-Accept-Loop auf, falls aktiviert. Ohne Wirkung, falls @@ -115,7 +115,7 @@ public sealed class McpServerHostedService : IAsyncDisposable StudentTools studentTools, ExamTools examTools, GradeTools gradeTools, ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools, GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools, - CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools) + CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools) { var toolCollection = new McpServerPrimitiveCollection(); @@ -157,6 +157,8 @@ public sealed class McpServerHostedService : IAsyncDisposable AddReadTool(studentTools.GetStudents, "get_students", "Listet Schüler, optional gefiltert nach Lerngruppe."); + AddReadTool(groupTools.GetGroups, "get_groups", + "Listet Lerngruppen (Klassen/Kurse) mit ihrer Id, optional gefiltert nach Schuljahr."); AddReadTool(examTools.GetExams, "get_exams", "Listet Klausuren, optional gefiltert nach Lerngruppe."); AddReadTool(gradeTools.GetGrades, "get_grades", diff --git a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs index 806f7be..020e2c1 100644 --- a/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs +++ b/LehrerApp.Desktop/Services/Mcp/McpToolScope.cs @@ -19,6 +19,7 @@ public static class McpToolScope public static readonly IReadOnlyCollection AllowedReadTools = [ "get_students", + "get_groups", "get_exams", "get_grades", "get_schedule", diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs index 89a4bfb..623dfab 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs @@ -71,11 +71,22 @@ public record LetterTemplateDto(string Id, string Name, string Description, List public record LetterRenderResultDto(bool Success, string Message, string? Base64Pdf, string? SuggestedFileName); /// Eine Zeile des Untis-Hub (siehe UntisHubService) - "Kind"/"DueState" als Text statt -/// Enum-Wert, damit ein KI-Client sie ohne Kenntnis des internen Enums lesen kann. +/// Enum-Wert, damit ein KI-Client sie ohne Kenntnis des internen Enums lesen kann. GroupId ist bei +/// den drei dashboard-weiten Zeilen (offene Stunden, Klassenbuch-/Hausaufgabenabgleich) null - siehe +/// UntisHubJobRow. Enthalten, damit ein KI-Client die Id nicht erst über get_groups nachschlagen +/// muss, um get_untis_absence_rows/get_named_untis_absence_pattern für eine hier gelistete Gruppe +/// aufzurufen (Nutzer-Feedback: die GroupId war nirgends exponiert). public record UntisHubStatusRowDto( - string Kind, string GroupName, string DueState, string DueLabel, + string Kind, Guid? GroupId, string GroupName, string DueState, string DueLabel, DateTime? LastRunAt, string? LastResultSummary); +/// Lerngruppe (Klasse oder Kurs) - siehe GroupTools.GetGroups. Bewusst kein Verweis auf +/// SubjectId->Name aufgelöst (dafür get_subjects), um nicht bei jeder Gruppe implizit einen ganzen +/// Fach-Datensatz mitzuschleppen. +public record GroupDto( + Guid Id, string Name, GroupType Type, string SchoolYear, int GradeLevel, + Guid? SubjectId, bool IsActive); + /// Anonymisierte Fehlzeiten-Diskrepanz (siehe UntisComparisonTools.GetUntisAbsenceRows): /// bewusst KEIN Schülername/keine Klasse - ist die einzige Kennung, über die /// UntisComparisonTools.ApplyUntisAbsenceStatus später zurückordnet. diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs new file mode 100644 index 0000000..8914487 --- /dev/null +++ b/LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using LehrerApp.Core.Interfaces; + +namespace LehrerApp.Desktop.Services.Mcp.Tools; + +/// MCP-Read-Tool "get_groups" (Nutzer-Nachtrag: die GroupId, die praktisch jedes andere +/// Tool als Pflichtparameter verlangt - get_grades, get_schedule, get_lesson_plans, +/// get_untis_absence_rows usw. - war nirgends über MCP auflösbar; ein KI-Client kannte bestenfalls +/// den Klarnamen einer Lerngruppe aus dem Gespräch, nie ihre Id). Reiner Lesezugriff auf das +/// bestehende Repository, keine eigene Datenzugriffslogik. +public class GroupTools(IGroupRepository groups) +{ + [Description("Listet Lerngruppen (Klassen/Kurse) mit ihrer Id - Voraussetzung, um andere Tools (z.B. get_grades, get_schedule, get_untis_absence_rows) für eine bestimmte Gruppe aufzurufen, wenn nur ihr Name bekannt ist. Ohne schoolYear werden alle Schuljahre zurückgegeben.")] + public List GetGroups( + [Description("Optionales Schuljahr zum Filtern, Format \"2025/26\". Ohne Angabe alle Schuljahre.")] string? schoolYear = null, + [Description("Auch inaktive/archivierte Gruppen einbeziehen.")] bool includeInactive = false) + { + var list = schoolYear is null + ? groups.GetAll(includeInactive) + : groups.GetBySchoolYear(schoolYear, includeInactive); + return list.Select(g => new GroupDto(g.Id, g.Name, g.Type, g.SchoolYear, g.GradeLevel, g.SubjectId, g.IsActive)).ToList(); + } +} diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/UntisComparisonTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/UntisComparisonTools.cs index 6bce9d6..33f4f3b 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/UntisComparisonTools.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/UntisComparisonTools.cs @@ -50,7 +50,8 @@ public class UntisComparisonTools( [Description("Listet die Fälligkeit der Untis-Hub-Abgleiche (Fehlzeiten je Lerngruppe, offene Stunden, Klassenbuch-/Hausaufgabenabgleich) - reine Lesefunktion aus der lokalen Fälligkeits-Historie, kein eigener WebUntis-Zugriff.")] public List GetUntisHubStatus() => hub.GetRows().Select(r => new UntisHubStatusRowDto( - r.Kind.ToString(), r.GroupName, r.DueState.ToString(), r.DueLabel, r.LastRunAt, r.LastResultSummary)).ToList(); + r.Kind.ToString(), r.GroupId, r.GroupName, r.DueState.ToString(), r.DueLabel, + r.LastRunAt, r.LastResultSummary)).ToList(); [Description(""" Listet Fehlzeiten-Diskrepanzen einer Lerngruppe gegenüber WebUntis in einem Zeitraum, ANONYMISIERT: diff --git a/TODO.md b/TODO.md index 0747e8f..3bd4845 100644 --- a/TODO.md +++ b/TODO.md @@ -2086,6 +2086,17 @@ vollständig enthält. (unbekannter/nicht zulässiger Status, unbekannte/abgelaufene row-id, jeweils ohne Bestätigung erreicht) sind in `UntisComparisonToolsTests.cs` abgedeckt. + **Nachtrag (aus echtem Live-Test, 2026-09-13):** Nutzer versuchte die neuen Tools tatsächlich zu + benutzen und stieß darauf, dass `groupId` überall Pflichtparameter ist, aber nirgends über MCP + auflösbar war — ein KI-Client kannte bestenfalls den Klarnamen einer Lerngruppe aus dem Gespräch, + nie ihre Id (betrifft nicht nur die Untis-Tools, sondern z.B. auch `get_grades`/`get_schedule`/ + `get_lesson_plans`). Beide vom Nutzer vorgeschlagenen Wege umgesetzt: + - Neues [GroupTools.cs](LehrerApp.Desktop/Services/Mcp/Tools/GroupTools.cs) mit `get_groups` + (Read; Id/Name/Typ/Schuljahr/Klassenstufe/SubjectId/IsActive, optional nach Schuljahr gefiltert). + - `UntisHubStatusRowDto` bekommt zusätzlich `GroupId` (null bei den drei dashboard-weiten + Zeilen), damit ein KI-Client für eine dort gelistete Gruppe nicht zusätzlich `get_groups` + aufrufen muss, nur um den Fehlzeitenabgleich für sie auszulösen. + ### 4.4 Wochen-/Tagesansicht - [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3 ("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.