fix: GroupId ueber MCP auflösbar (get_groups + get_untis_hub_status)
CI / build-and-test (push) Canceled after 0s

Nutzer-Feedback aus echtem Live-Test der neuen Untis-MCP-Tools: groupId ist ueberall
Pflichtparameter (get_grades, get_schedule, get_lesson_plans, get_untis_absence_rows, ...), war
aber nirgends ueber MCP auflösbar - ein KI-Client kannte bestenfalls den Klarnamen einer
Lerngruppe, nie ihre Id.

- Neues GroupTools.cs mit get_groups (Read): listet Lerngruppen mit Id/Name/Typ/Schuljahr/
  Klassenstufe/SubjectId/IsActive, optional nach Schuljahr gefiltert.
- UntisHubStatusRowDto liefert zusaetzlich GroupId mit (null bei den drei dashboard-weiten
  Zeilen), damit fuer eine dort gelistete Gruppe nicht zusaetzlich get_groups noetig ist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 01:13:13 +02:00
co-authored by Claude Sonnet 5
parent c528bd825f
commit 2c791258a1
8 changed files with 79 additions and 9 deletions
+23 -3
View File
@@ -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]
+1
View File
@@ -228,6 +228,7 @@ public static class AppBootstrapper
services.AddSingleton<GroupMembershipTools>();
services.AddSingleton<LetterTemplateTools>();
services.AddSingleton<CompetencyTools>();
services.AddSingleton<GroupTools>();
services.AddSingleton<UntisComparisonTools>();
services.AddSingleton<McpServerHostedService>();
services.AddSingleton<McpClientRegistrationService>();
@@ -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);
}
/// <summary>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<McpServerTool>();
@@ -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",
@@ -19,6 +19,7 @@ public static class McpToolScope
public static readonly IReadOnlyCollection<string> AllowedReadTools =
[
"get_students",
"get_groups",
"get_exams",
"get_grades",
"get_schedule",
+13 -2
View File
@@ -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);
/// <summary>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.</summary>
/// 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).</summary>
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);
/// <summary>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.</summary>
public record GroupDto(
Guid Id, string Name, GroupType Type, string SchoolYear, int GradeLevel,
Guid? SubjectId, bool IsActive);
/// <summary>Anonymisierte Fehlzeiten-Diskrepanz (siehe UntisComparisonTools.GetUntisAbsenceRows):
/// bewusst KEIN Schülername/keine Klasse - <see cref="RowId"/> ist die einzige Kennung, über die
/// UntisComparisonTools.ApplyUntisAbsenceStatus später zurückordnet.</summary>
@@ -0,0 +1,23 @@
using System.ComponentModel;
using LehrerApp.Core.Interfaces;
namespace LehrerApp.Desktop.Services.Mcp.Tools;
/// <summary>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.</summary>
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<GroupDto> 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();
}
}
@@ -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<UntisHubStatusRowDto> 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:
+11
View File
@@ -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.