feat: MCP-Tools für Mitarbeit-Sitzungen (ParticipationTools)

Mitarbeit-Sitzungen waren bisher komplett MCP-unerreichbar. Neue ParticipationTools-Klasse
exponiert get_participation_sessions (Datum, Kommentar, zugeordnete Kompetenzen) sowie
add_session_competency/remove_session_competency, nach demselben Einzel-Zuordnung-Muster wie
LessonPlanTools. Bewusst ohne die einzelnen Schülerbewertungen (ParticipationEntry) - die bleiben
personenbezogen und MCP-unerreichbar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-19 00:54:19 +02:00
1 parent 845cc5fc4c
commit 51e55e1464
7 files changed
+195 -6

No files matched your search

+92 -2
View File
@@ -17,7 +17,8 @@ public sealed class McpToolsTests
{
"download_lesson_attachment", "get_competency_catalog", "get_competency_plan_versions",
"get_exams", "get_grades",
"get_groups", "get_lesson_plans", "get_named_untis_absence_pattern", "get_schedule",
"get_groups", "get_lesson_plans", "get_named_untis_absence_pattern",
"get_participation_sessions", "get_schedule",
"get_students", "get_subjects", "get_time_entries", "get_untis_absence_rows",
"get_untis_hub_status", "list_letter_templates", "render_letter",
},
@@ -31,10 +32,12 @@ public sealed class McpToolsTests
new[]
{
"add_competency_item", "add_lesson_attachment", "add_lesson_competency", "add_lesson_phase",
"add_session_competency",
"apply_untis_absence_status", "create_competency_domain", "create_competency_plan_version",
"create_grade_entry", "create_lesson",
"create_subject", "create_time_entry", "create_unit", "move_lesson", "remove_competency_item",
"remove_lesson_competency", "remove_lesson_phase", "update_competency_domain",
"remove_lesson_competency", "remove_lesson_phase", "remove_session_competency",
"update_competency_domain",
"update_competency_item", "update_competency_plan_version", "update_group_competency_version",
"update_lesson", "update_lesson_phase", "update_student_group_assignment",
"update_subject", "update_unit",
@@ -1026,4 +1029,91 @@ public sealed class McpToolsTests
Assert.False(result.Applied);
Assert.Equal(0, confirmation.CallCount);
}
// ── ParticipationTools ───────────────────────────────────────────────────────────────────
[Fact]
public void GetParticipationSessions_LiefertDatumKommentarUndKompetenzenDerGruppe()
{
var groupId = Guid.NewGuid();
var item = new CompetencyItem { Code = "K1", Description = "Kompetenz" };
var session = new ParticipationSession
{
GroupId = groupId, Date = new DateOnly(2026, 1, 12), Comment = "Sitzplan",
Competencies = [new CompetencyReference { ItemId = item.Id, Code = item.Code, Description = item.Description }],
};
var sessions = new FakeSessions([session]);
var tool = new ParticipationTools(sessions, new FakeCompetencyDomains(), new FakeMcpConfirmation());
var result = Assert.Single(tool.GetParticipationSessions(groupId));
Assert.Equal(session.Id, result.Id);
Assert.Equal("Sitzplan", result.Comment);
var competency = Assert.Single(result.Competencies);
Assert.Equal(item.Id, competency.ItemId);
Assert.Equal("K1", competency.Code);
}
[Fact]
public async Task AddSessionCompetency_NutzerBestaetigt_OrdnetKompetenzZu()
{
var session = new ParticipationSession { Date = new DateOnly(2026, 1, 12) };
var sessions = new FakeSessions([session]);
var domains = new FakeCompetencyDomains();
var item = new CompetencyItem { Code = "K1", Description = "Kompetenz" };
domains.Add(new CompetencyDomain { Name = "Bereich", Items = [item] });
var tool = new ParticipationTools(sessions, domains, new FakeMcpConfirmation());
var result = await tool.AddSessionCompetency(session.Id, item.Id);
Assert.True(result.Applied);
var stored = Assert.Single(sessions.GetById(session.Id)!.Competencies);
Assert.Equal(item.Id, stored.ItemId);
}
[Fact]
public async Task AddSessionCompetency_UnbekannteId_LiefertFehlerOhneNachfrage()
{
var session = new ParticipationSession { Date = new DateOnly(2026, 1, 12) };
var sessions = new FakeSessions([session]);
var confirmation = new FakeMcpConfirmation();
var tool = new ParticipationTools(sessions, new FakeCompetencyDomains(), confirmation);
var result = await tool.AddSessionCompetency(session.Id, Guid.NewGuid());
Assert.False(result.Applied);
Assert.Equal(0, confirmation.CallCount);
}
[Fact]
public async Task RemoveSessionCompetency_NutzerBestaetigt_EntferntKompetenz()
{
var item = new CompetencyItem { Code = "K1", Description = "Kompetenz" };
var session = new ParticipationSession
{
Date = new DateOnly(2026, 1, 12),
Competencies = [new CompetencyReference { ItemId = item.Id, Code = item.Code, Description = item.Description }],
};
var sessions = new FakeSessions([session]);
var tool = new ParticipationTools(sessions, new FakeCompetencyDomains(), new FakeMcpConfirmation());
var result = await tool.RemoveSessionCompetency(session.Id, item.Id);
Assert.True(result.Applied);
Assert.Empty(sessions.GetById(session.Id)!.Competencies);
}
[Fact]
public async Task RemoveSessionCompetency_NichtZugeordnet_LiefertFehlerOhneNachfrage()
{
var session = new ParticipationSession { Date = new DateOnly(2026, 1, 12) };
var sessions = new FakeSessions([session]);
var confirmation = new FakeMcpConfirmation();
var tool = new ParticipationTools(sessions, new FakeCompetencyDomains(), confirmation);
var result = await tool.RemoveSessionCompetency(session.Id, Guid.NewGuid());
Assert.False(result.Applied);
Assert.Equal(0, confirmation.CallCount);
}
}
+1
View File
@@ -233,6 +233,7 @@ public static class AppBootstrapper
services.AddSingleton<StudentAttendanceCalendarService>();
services.AddSingleton<CompetencyTools>();
services.AddSingleton<GroupTools>();
services.AddSingleton<ParticipationTools>();
services.AddSingleton<UntisComparisonTools>();
services.AddSingleton<McpServerHostedService>();
services.AddSingleton<McpClientRegistrationService>();
@@ -32,13 +32,15 @@ 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, GroupTools groupTools)
CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools,
ParticipationTools participationTools)
{
_settings = settings;
_logger = logger;
_serverOptions = BuildServerOptions(
studentTools, examTools, gradeTools, scheduleTools, timeEntryTools, lessonPlanTools,
groupMembershipTools, letterTemplateTools, competencyTools, untisComparisonTools, groupTools);
groupMembershipTools, letterTemplateTools, competencyTools, untisComparisonTools, groupTools,
participationTools);
}
/// <summary>Setzt die Pipe-Server-Accept-Loop auf, falls aktiviert. Ohne Wirkung, falls
@@ -115,7 +117,8 @@ 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, GroupTools groupTools)
CompetencyTools competencyTools, UntisComparisonTools untisComparisonTools, GroupTools groupTools,
ParticipationTools participationTools)
{
var toolCollection = new McpServerPrimitiveCollection<McpServerTool>();
@@ -187,6 +190,8 @@ public sealed class McpServerHostedService : IAsyncDisposable
"Listet anonymisierte Fehlzeiten-Diskrepanzen einer Lerngruppe gegenüber WebUntis (keine Schülernamen, nur eine row-id je Zeile).");
AddReadTool(untisComparisonTools.GetNamedUntisAbsencePattern, "get_named_untis_absence_pattern",
"Liefert Fehlzeiten MIT Schülername für explizit angegebene Schüler-IDs - Ausnahme von der sonstigen Anonymisierung, erfordert jedes Mal eine gesonderte Nutzerbestätigung.");
AddReadTool(participationTools.GetParticipationSessions, "get_participation_sessions",
"Listet Mitarbeit-Sitzungen einer Lerngruppe (Datum, Kommentar, zugeordnete Kompetenzen), ohne die einzelnen Schülerbewertungen.");
AddWriteTool(timeEntryTools.CreateTimeEntry, "create_time_entry",
"Schlägt einen neuen Zeiterfassungs-Eintrag vor (Bestätigung durch den Nutzer nötig).");
@@ -236,6 +241,10 @@ public sealed class McpServerHostedService : IAsyncDisposable
"Ändert Name/Standardstatus einer Bildungsplan-Version (Bestätigung durch den Nutzer nötig).");
AddWriteTool(groupTools.UpdateGroupCompetencyVersion, "update_group_competency_version",
"Setzt oder löscht die Bildungsplan-Version eines Kurses/einer Klasse (Bestätigung durch den Nutzer nötig).");
AddWriteTool(participationTools.AddSessionCompetency, "add_session_competency",
"Ordnet einer Mitarbeit-Sitzung eine Kompetenz zu (Bestätigung durch den Nutzer nötig).");
AddWriteTool(participationTools.RemoveSessionCompetency, "remove_session_competency",
"Entfernt eine Kompetenz von einer Mitarbeit-Sitzung (Bestätigung durch den Nutzer nötig).");
AddWriteTool(untisComparisonTools.ApplyUntisAbsenceStatus, "apply_untis_absence_status",
"Übernimmt einen Statusvorschlag für eine über get_untis_absence_rows gelieferte row-id (Bestätigung durch den Nutzer nötig, nennt keinen Schülernamen).");
@@ -34,6 +34,7 @@ public static class McpToolScope
"get_untis_hub_status",
"get_untis_absence_rows",
"get_named_untis_absence_pattern",
"get_participation_sessions",
];
/// <summary>Write-Tools (Phase 2+3) — jeder Aufruf läuft über <see cref="IMcpConfirmationService"/>,
@@ -67,6 +68,8 @@ public static class McpToolScope
"create_competency_plan_version",
"update_competency_plan_version",
"update_group_competency_version",
"add_session_competency",
"remove_session_competency",
"apply_untis_absence_status",
];
@@ -39,6 +39,12 @@ public record LessonAttachmentDto(string StorageId, string FileName, long SizeBy
/// Zusatzabfrage lesen kann, auch wenn das Katalog-Item inzwischen gelöscht wurde.</summary>
public record CompetencyReferenceDto(Guid ItemId, string Code, string Description);
/// <summary>Mitarbeit-Sitzung (siehe ParticipationTools) - bewusst ohne die einzelnen
/// Schülerbewertungen (personenbezogen, nicht über MCP erreichbar, siehe Klassenkommentar).</summary>
public record ParticipationSessionDto(
Guid Id, Guid GroupId, DateOnly Date, Guid? LessonId, string? Comment,
List<CompetencyReferenceDto> Competencies);
public record LessonDto(
Guid Id, Guid UnitId, Guid GroupId, DateOnly Date, int? LessonNumber, string Topic,
string? Homework, string? PlanningIdeas, LessonStatus Status, List<CompetencyReferenceDto> Competencies,
@@ -0,0 +1,72 @@
using System.ComponentModel;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
namespace LehrerApp.Desktop.Services.Mcp.Tools;
/// <summary>MCP-Tools für Mitarbeit-Sitzungen (<see cref="ParticipationSession"/>) - Nutzer-Nachtrag.
/// Bewusst nur die Sitzung selbst (Datum, Kommentar, Kompetenzzuordnung), NICHT die einzelnen
/// Schülerbewertungen (<see cref="ParticipationEntry"/>/<see cref="AspectRating"/>): das wären
/// personenbezogene Bewertungsdaten je Schüler, analog zu Documentation/Vorgang bewusst nicht über
/// MCP erreichbar (siehe McpToolScope). "add_session_competency"/"remove_session_competency" folgen
/// demselben Muster wie LessonPlanTools.AddLessonCompetency/RemoveLessonCompetency: einzelne
/// Kompetenz statt ganze Liste ersetzen, um kein "letzter Schreiber gewinnt" zu riskieren, und mit
/// Katalog-Validierung der übergebenen itemId.</summary>
public class ParticipationTools(
IParticipationSessionRepository sessions, ICompetencyDomainRepository competencyDomains,
IMcpConfirmationService confirmation)
{
[Description("Listet Mitarbeit-Sitzungen einer Lerngruppe (Datum, Kommentar, zugeordnete Kompetenzen) - ohne die einzelnen Schülerbewertungen.")]
public List<ParticipationSessionDto> GetParticipationSessions(
[Description("Lerngruppen-ID, aus get_groups.")] Guid groupId) =>
sessions.GetByGroup(groupId).Select(ToDto).ToList();
[Description("Ordnet einer Mitarbeit-Sitzung eine Kompetenz aus dem Katalog zu (siehe get_competency_catalog). Muss der Nutzer erst bestätigen.")]
public async Task<WriteResultDto> AddSessionCompetency(
[Description("ID der Mitarbeit-Sitzung, aus get_participation_sessions.")] Guid sessionId,
[Description("ID der Einzelkompetenz, aus get_competency_catalog.")] Guid itemId,
CancellationToken ct = default)
{
var session = sessions.GetById(sessionId);
if (session is null) return new WriteResultDto(false, null, "Unbekannte Sitzungs-ID.");
var item = FindCompetencyItem(itemId);
if (item is null) return new WriteResultDto(false, null, "Unbekannte Kompetenz-ID.");
if (session.Competencies.Any(c => c.ItemId == itemId))
return new WriteResultDto(true, session.Id, "Kompetenz ist bereits zugeordnet.");
var message = $"Kompetenz „{item.Code}“ zur Mitarbeit-Sitzung vom {session.Date:dd.MM.yyyy} hinzufügen?";
if (!await confirmation.ConfirmAsync("Kompetenz zuordnen?", message, ct))
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
session.Competencies.Add(new CompetencyReference { ItemId = item.Id, Code = item.Code, Description = item.Description });
sessions.Save(session);
return new WriteResultDto(true, session.Id, "Kompetenz zugeordnet.");
}
[Description("Entfernt eine Kompetenz von einer Mitarbeit-Sitzung. Muss der Nutzer erst bestätigen.")]
public async Task<WriteResultDto> RemoveSessionCompetency(
[Description("ID der Mitarbeit-Sitzung.")] Guid sessionId,
[Description("ID der Einzelkompetenz, wie in get_participation_sessions hinterlegt.")] Guid itemId,
CancellationToken ct = default)
{
var session = sessions.GetById(sessionId);
if (session is null) return new WriteResultDto(false, null, "Unbekannte Sitzungs-ID.");
var existing = session.Competencies.FirstOrDefault(c => c.ItemId == itemId);
if (existing is null) return new WriteResultDto(false, null, "Diese Kompetenz ist dieser Sitzung nicht zugeordnet.");
var message = $"Kompetenz „{existing.Code}“ von der Mitarbeit-Sitzung vom {session.Date:dd.MM.yyyy} entfernen?";
if (!await confirmation.ConfirmAsync("Kompetenz entfernen?", message, ct))
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
session.Competencies.Remove(existing);
sessions.Save(session);
return new WriteResultDto(true, session.Id, "Kompetenz entfernt.");
}
private CompetencyItem? FindCompetencyItem(Guid itemId) =>
competencyDomains.GetAll().SelectMany(d => d.Items).FirstOrDefault(i => i.Id == itemId);
private static ParticipationSessionDto ToDto(ParticipationSession s) => new(
s.Id, s.GroupId, s.Date, s.LessonId, s.Comment,
s.Competencies.Select(c => new CompetencyReferenceDto(c.ItemId, c.Code, c.Description)).ToList());
}
+9 -1
View File
@@ -3628,7 +3628,15 @@ alter und neuer Plan parallel, je nachdem, wann ein Kurs/eine Klasse eingeschult
`update_group_competency_version` zum Setzen der Version eines Kurses.
Bewusst nicht Teil dieses Nachtrags: TODO 16.5 (Kompetenz-Referenzen in Unit/Lesson/ExamTask von
Freitext-`Code` auf stabile `CompetencyItem.Id` umstellen) — unabhängiges, vorbestehendes Thema.
Freitext-`Code` auf stabile `CompetencyItem.Id` umstellen) — unabhängiges, vorbestehendes Thema,
mittlerweile eigenständig umgesetzt (siehe dort).
- [x] **8.4.5** MCP-Nachtrag: neue `ParticipationTools`-Klasse exponiert Mitarbeit-Sitzungen
(`get_participation_sessions` — Datum, Kommentar, zugeordnete Kompetenzen, bewusst OHNE die
einzelnen Schülerbewertungen, die bleiben personenbezogen und MCP-unerreichbar) sowie
`add_session_competency`/`remove_session_competency` nach demselben Einzel-Zuordnung-statt-
Listen-Ersetzen-Muster wie `LessonPlanTools`. Vorher gab es für Mitarbeit-Sitzungen gar kein
MCP-Tool.
---