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[] new[]
{ {
"download_lesson_attachment", "get_competency_catalog", "get_exams", "get_grades", "download_lesson_attachment", "get_competency_catalog", "get_exams", "get_grades",
"get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule", "get_students", "get_groups", "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule",
"get_subjects", "get_time_entries", "get_untis_absence_rows", "get_untis_hub_status", "get_students", "get_subjects", "get_time_entries", "get_untis_absence_rows",
"list_letter_templates", "render_letter", "get_untis_hub_status", "list_letter_templates", "render_letter",
}, },
McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal)); McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal));
} }
@@ -59,6 +59,26 @@ public sealed class McpToolsTests
n.Contains("note", StringComparison.OrdinalIgnoreCase)); 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 ───────────────────────────────────────────────────────────────────────── // ── StudentTools ─────────────────────────────────────────────────────────────────────────
[Fact] [Fact]
+1
View File
@@ -228,6 +228,7 @@ public static class AppBootstrapper
services.AddSingleton<GroupMembershipTools>(); services.AddSingleton<GroupMembershipTools>();
services.AddSingleton<LetterTemplateTools>(); services.AddSingleton<LetterTemplateTools>();
services.AddSingleton<CompetencyTools>(); services.AddSingleton<CompetencyTools>();
services.AddSingleton<GroupTools>();
services.AddSingleton<UntisComparisonTools>(); services.AddSingleton<UntisComparisonTools>();
services.AddSingleton<McpServerHostedService>(); services.AddSingleton<McpServerHostedService>();
services.AddSingleton<McpClientRegistrationService>(); services.AddSingleton<McpClientRegistrationService>();
@@ -32,13 +32,13 @@ public sealed class McpServerHostedService : IAsyncDisposable
StudentTools studentTools, ExamTools examTools, GradeTools gradeTools, StudentTools studentTools, ExamTools examTools, GradeTools gradeTools,
ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools, ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools,
GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools, GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools,
CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools) CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools)
{ {
_settings = settings; _settings = settings;
_logger = logger; _logger = logger;
_serverOptions = BuildServerOptions( _serverOptions = BuildServerOptions(
studentTools, examTools, gradeTools, scheduleTools, timeEntryTools, lessonPlanTools, 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 /// <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, StudentTools studentTools, ExamTools examTools, GradeTools gradeTools,
ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools, ScheduleTools scheduleTools, TimeEntryTools timeEntryTools, LessonPlanTools lessonPlanTools,
GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools, GroupMembershipTools groupMembershipTools, LetterTemplateTools letterTemplateTools,
CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools) CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools)
{ {
var toolCollection = new McpServerPrimitiveCollection<McpServerTool>(); var toolCollection = new McpServerPrimitiveCollection<McpServerTool>();
@@ -157,6 +157,8 @@ public sealed class McpServerHostedService : IAsyncDisposable
AddReadTool(studentTools.GetStudents, "get_students", AddReadTool(studentTools.GetStudents, "get_students",
"Listet Schüler, optional gefiltert nach Lerngruppe."); "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", AddReadTool(examTools.GetExams, "get_exams",
"Listet Klausuren, optional gefiltert nach Lerngruppe."); "Listet Klausuren, optional gefiltert nach Lerngruppe.");
AddReadTool(gradeTools.GetGrades, "get_grades", AddReadTool(gradeTools.GetGrades, "get_grades",
@@ -19,6 +19,7 @@ public static class McpToolScope
public static readonly IReadOnlyCollection<string> AllowedReadTools = public static readonly IReadOnlyCollection<string> AllowedReadTools =
[ [
"get_students", "get_students",
"get_groups",
"get_exams", "get_exams",
"get_grades", "get_grades",
"get_schedule", "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); public record LetterRenderResultDto(bool Success, string Message, string? Base64Pdf, string? SuggestedFileName);
/// <summary>Eine Zeile des Untis-Hub (siehe UntisHubService) - "Kind"/"DueState" als Text statt /// <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( 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); 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): /// <summary>Anonymisierte Fehlzeiten-Diskrepanz (siehe UntisComparisonTools.GetUntisAbsenceRows):
/// bewusst KEIN Schülername/keine Klasse - <see cref="RowId"/> ist die einzige Kennung, über die /// bewusst KEIN Schülername/keine Klasse - <see cref="RowId"/> ist die einzige Kennung, über die
/// UntisComparisonTools.ApplyUntisAbsenceStatus später zurückordnet.</summary> /// 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.")] [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() => public List<UntisHubStatusRowDto> GetUntisHubStatus() =>
hub.GetRows().Select(r => new UntisHubStatusRowDto( 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(""" [Description("""
Listet Fehlzeiten-Diskrepanzen einer Lerngruppe gegenüber WebUntis in einem Zeitraum, ANONYMISIERT: 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 (unbekannter/nicht zulässiger Status, unbekannte/abgelaufene row-id, jeweils ohne Bestätigung
erreicht) sind in `UntisComparisonToolsTests.cs` abgedeckt. 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 ### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3 - [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. ("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.