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:
1 parent
845cc5fc4c
commit
51e55e1464
7 files changed
+195
-6
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user