feat: lokaler MCP-Server, Phase 1 (Infrastruktur + Read-Tools)
Erlaubt einem lokalen KI-Client (z.B. Claude Desktop) strukturierten Lesezugriff auf Schüler, Klausuren, Noten, Stundenplan und Zeiterfassung. Neuer LehrerApp.McpBridge-Prozess reicht stdio-JSON-RPC über eine Named Pipe an einen In-Process-MCP-Server im Avalonia-Hauptprozess durch (ModelContextProtocol.Core, StreamServerTransport direkt auf der Pipe). Standardmäßig deaktiviert, Opt-in über neuen Einstellungen-Tab. Dokumentationstypen (Gesprächsnotizen/Vorfälle/Förderpläne) sind auf Code-Ebene nie erreichbar (McpToolScope, analog PlainEventStore.Allowed). Write-Tools, Bestätigungsdialog-UI, Worksheets/Lesson-Plans-Tools und macOS-Packaging folgen in späteren Phasen (siehe TODO.md 4.5.25). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,9 @@
|
|||||||
<!-- API -->
|
<!-- API -->
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||||
|
|
||||||
|
<!-- MCP-Server (lokal, siehe TODO.md) -->
|
||||||
|
<PackageVersion Include="ModelContextProtocol.Core" Version="2.2.0" />
|
||||||
|
|
||||||
<!-- Tests -->
|
<!-- Tests -->
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LehrerApp.Core.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Named-Pipe-Konvention zwischen dem Avalonia-Hauptprozess (Pipe-Server, siehe
|
||||||
|
/// LehrerApp.Desktop/Services/Mcp) und LehrerApp.McpBridge (Pipe-Client). Bewusst als einzelne
|
||||||
|
/// geteilte Konstante statt eigener Bibliothek — die Bridge reicht JSON-RPC-Nachrichten unverändert
|
||||||
|
/// durch und braucht sonst keine gemeinsamen Typen mit der App (siehe Planungsdokument).
|
||||||
|
/// </summary>
|
||||||
|
public static class McpPipeConstants
|
||||||
|
{
|
||||||
|
public const string PipeName = "LehrerApp.Mcp";
|
||||||
|
}
|
||||||
@@ -31,6 +31,14 @@ public static class TestSupport
|
|||||||
new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
|
new HttpClient(), new FakeLessons(), new FakeGroups([]), new FakeSubjects([]),
|
||||||
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
|
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
|
||||||
|
|
||||||
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||||
|
public static McpSettingsService BuildMcpSettingsService()
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(Path.GetTempPath(), $"lehrerapp-mcpsettings-tests-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
return new McpSettingsService(tempPath);
|
||||||
|
}
|
||||||
|
|
||||||
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
/// Analog zu <see cref="BuildAiSettingsService"/>, eigenes Temp-Verzeichnis je Aufruf.
|
||||||
public static WebUntisSettingsService BuildWebUntisSettingsService()
|
public static WebUntisSettingsService BuildWebUntisSettingsService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.Services.Mcp;
|
||||||
|
using LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class McpToolsTests
|
||||||
|
{
|
||||||
|
// ── McpToolScope ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowedReadTools_EnthaeltGenauDieFuenfPhase1Tools()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { "get_exams", "get_grades", "get_schedule", "get_students", "get_time_entries" },
|
||||||
|
McpToolScope.AllowedReadTools.OrderBy(n => n, StringComparer.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowedReadTools_EnthaeltKeineDokumentationstypen()
|
||||||
|
{
|
||||||
|
// Gesprächsnotizen/Vorfälle/Förderpläne dürfen technisch nie über MCP erreichbar sein
|
||||||
|
// (siehe Planungsdokument) - die Namenskonvention "documentation"/"vorgang" darf nie auftauchen.
|
||||||
|
Assert.DoesNotContain(McpToolScope.AllowedReadTools, n =>
|
||||||
|
n.Contains("documentation", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
n.Contains("vorgang", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── StudentTools ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStudents_OhneFilter_LiefertNurAktiveSchueler()
|
||||||
|
{
|
||||||
|
var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true };
|
||||||
|
var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false };
|
||||||
|
var tool = new StudentTools(new FakeStudents([active, inactive]));
|
||||||
|
|
||||||
|
var result = tool.GetStudents();
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(active.Id, result[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStudents_IncludeInactive_LiefertAuchInaktive()
|
||||||
|
{
|
||||||
|
var active = new Student { FirstName = "Anna", LastName = "Aktiv", IsActive = true };
|
||||||
|
var inactive = new Student { FirstName = "Ida", LastName = "Inaktiv", IsActive = false };
|
||||||
|
var tool = new StudentTools(new FakeStudents([active, inactive]));
|
||||||
|
|
||||||
|
var result = tool.GetStudents(includeInactive: true);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ExamTools ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExams_OhneIncludeResults_LiefertKeineErgebnisse()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var exam = new Exam { GroupId = groupId, Title = "Klausur 1" };
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = Guid.NewGuid(), TotalPoints = 10 });
|
||||||
|
var tool = new ExamTools(new FakeExams([exam]), results);
|
||||||
|
|
||||||
|
var dto = Assert.Single(tool.GetExams(groupId));
|
||||||
|
|
||||||
|
Assert.Null(dto.Results);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetExams_MitIncludeResults_LiefertErgebnisseJeSchueler()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var exam = new Exam { GroupId = groupId, Title = "Klausur 1" };
|
||||||
|
var results = new FakeResults();
|
||||||
|
results.Add(new ExamResult { ExamId = exam.Id, StudentId = studentId, TotalPoints = 12.5, Grade = "2+" });
|
||||||
|
var tool = new ExamTools(new FakeExams([exam]), results);
|
||||||
|
|
||||||
|
var dto = Assert.Single(tool.GetExams(groupId, includeResults: true));
|
||||||
|
|
||||||
|
var result = Assert.Single(dto.Results!);
|
||||||
|
Assert.Equal(studentId, result.StudentId);
|
||||||
|
Assert.Equal("2+", result.Grade);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GradeTools ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetGrades_MitStudentId_FiltertAufEinenSchueler()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentA = Guid.NewGuid();
|
||||||
|
var studentB = Guid.NewGuid();
|
||||||
|
var grades = new FakeGrades();
|
||||||
|
grades.Add(new Grade { GroupId = groupId, StudentId = studentA, Value = "2" });
|
||||||
|
grades.Add(new Grade { GroupId = groupId, StudentId = studentB, Value = "3" });
|
||||||
|
var tool = new GradeTools(grades);
|
||||||
|
|
||||||
|
var dto = Assert.Single(tool.GetGrades(groupId, studentA));
|
||||||
|
|
||||||
|
Assert.Equal(studentA, dto.StudentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ScheduleTools ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSchedule_MitGroupId_FiltertNachGruppe()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var slots = new FakeTimetableSlots();
|
||||||
|
slots.Add(new TimetableSlot { GroupId = groupId, Weekday = DayOfWeek.Monday, PeriodNumber = 1 });
|
||||||
|
slots.Add(new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 2 });
|
||||||
|
var tool = new ScheduleTools(slots);
|
||||||
|
|
||||||
|
var dto = Assert.Single(tool.GetSchedule(groupId));
|
||||||
|
|
||||||
|
Assert.Equal(groupId, dto.GroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TimeEntryTools ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTimeEntries_FiltertAufDenAngegebenenZeitraum()
|
||||||
|
{
|
||||||
|
var entries = new FakeTimeEntries();
|
||||||
|
entries.Add(new TimeEntry { Date = new DateOnly(2026, 1, 5), DurationMinutes = 30 });
|
||||||
|
entries.Add(new TimeEntry { Date = new DateOnly(2026, 2, 1), DurationMinutes = 45 });
|
||||||
|
var tool = new TimeEntryTools(entries);
|
||||||
|
|
||||||
|
var dto = Assert.Single(tool.GetTimeEntries(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31)));
|
||||||
|
|
||||||
|
Assert.Equal(30, dto.DurationMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ public sealed class SettingsViewModelTests
|
|||||||
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
|
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
|
||||||
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
|
||||||
new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||||
|
TestSupport.BuildMcpSettingsService(),
|
||||||
TestSupport.BuildWebUntisSettingsService(),
|
TestSupport.BuildWebUntisSettingsService(),
|
||||||
TestSupport.BuildAnnualPlanSettingsService(),
|
TestSupport.BuildAnnualPlanSettingsService(),
|
||||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
|
||||||
@@ -341,6 +342,7 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
|
||||||
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||||
|
TestSupport.BuildMcpSettingsService(),
|
||||||
TestSupport.BuildWebUntisSettingsService(),
|
TestSupport.BuildWebUntisSettingsService(),
|
||||||
TestSupport.BuildAnnualPlanSettingsService(),
|
TestSupport.BuildAnnualPlanSettingsService(),
|
||||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||||
@@ -369,6 +371,7 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||||
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||||
|
TestSupport.BuildMcpSettingsService(),
|
||||||
TestSupport.BuildWebUntisSettingsService(),
|
TestSupport.BuildWebUntisSettingsService(),
|
||||||
TestSupport.BuildAnnualPlanSettingsService(),
|
TestSupport.BuildAnnualPlanSettingsService(),
|
||||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||||
@@ -401,6 +404,7 @@ public sealed class SettingsViewModelTests
|
|||||||
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
|
||||||
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
|
||||||
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
new FakeSupervisionDuties(), new TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
|
||||||
|
TestSupport.BuildMcpSettingsService(),
|
||||||
TestSupport.BuildWebUntisSettingsService(),
|
TestSupport.BuildWebUntisSettingsService(),
|
||||||
TestSupport.BuildAnnualPlanSettingsService(),
|
TestSupport.BuildAnnualPlanSettingsService(),
|
||||||
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ public class App : Application
|
|||||||
_exitHandlerAttached = true;
|
_exitHandlerAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCP-Server (lokal, Phase 1): Start ist ohne Wirkung, falls in den Einstellungen nicht
|
||||||
|
// aktiviert (siehe McpServerHostedService.Start). Kein Live-Reload beim Umschalten des
|
||||||
|
// Opt-in - ein Neustart der App richtet den Pipe-Listener neu ein.
|
||||||
|
Services.GetRequiredService<Services.Mcp.McpServerHostedService>().Start();
|
||||||
|
|
||||||
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
|
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
|
||||||
WireCallbacks(mainVm);
|
WireCallbacks(mainVm);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using LehrerApp.Core.Services;
|
|||||||
using LehrerApp.Data;
|
using LehrerApp.Data;
|
||||||
using LehrerApp.Data.Repositories;
|
using LehrerApp.Data.Repositories;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
|
using LehrerApp.Desktop.Services.Mcp;
|
||||||
|
using LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
using LehrerApp.Desktop.ViewModels.ClassTeacher;
|
||||||
using LehrerApp.Desktop.ViewModels.Exams;
|
using LehrerApp.Desktop.ViewModels.Exams;
|
||||||
@@ -213,6 +215,15 @@ public static class AppBootstrapper
|
|||||||
});
|
});
|
||||||
services.AddSingleton<AiPlanningService>();
|
services.AddSingleton<AiPlanningService>();
|
||||||
|
|
||||||
|
// ── MCP-Server (lokal, Phase 1 – siehe Planungsdokument, optional per Opt-in) ─────────
|
||||||
|
services.AddSingleton(_ => new McpSettingsService(appData));
|
||||||
|
services.AddSingleton<StudentTools>();
|
||||||
|
services.AddSingleton<ExamTools>();
|
||||||
|
services.AddSingleton<GradeTools>();
|
||||||
|
services.AddSingleton<ScheduleTools>();
|
||||||
|
services.AddSingleton<TimeEntryTools>();
|
||||||
|
services.AddSingleton<McpServerHostedService>();
|
||||||
|
|
||||||
// ── WebUntis-iCal-Abgleich (optional – nur wenn URL hinterlegt und aktiviert) ─────────
|
// ── WebUntis-iCal-Abgleich (optional – nur wenn URL hinterlegt und aktiviert) ─────────
|
||||||
var untisSettings = new WebUntisSettingsService(appData);
|
var untisSettings = new WebUntisSettingsService(appData);
|
||||||
services.AddSingleton(untisSettings);
|
services.AddSingleton(untisSettings);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="QuestPDF" />
|
<PackageReference Include="QuestPDF" />
|
||||||
|
<PackageReference Include="ModelContextProtocol.Core" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System.IO.Pipes;
|
||||||
|
using LehrerApp.Core.Mcp;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
using ModelContextProtocol.Protocol;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-Process-MCP-Server (Phase 1, siehe Planungsdokument). Lauscht auf der Named Pipe
|
||||||
|
/// <see cref="McpPipeConstants.PipeName"/> und bedient jede eingehende Verbindung (eine je
|
||||||
|
/// LehrerApp.McpBridge-Instanz) als eigene MCP-Session über <see cref="StreamServerTransport"/> —
|
||||||
|
/// ein <see cref="NamedPipeServerStream"/> ist ein normaler <see cref="Stream"/> und kann direkt
|
||||||
|
/// als Ein-/Ausgabe der Session übergeben werden, ohne eigenes JSON-RPC-Parsing.
|
||||||
|
///
|
||||||
|
/// Nur aktiv, wenn <see cref="McpSettingsService.Enabled"/> — sonst tut <see cref="Start"/> nichts.
|
||||||
|
/// Repositories sind im DI-Container Singletons (siehe AppBootstrapper), deshalb reicht es, die
|
||||||
|
/// Tool-Instanzen und die daraus gebaute <see cref="McpServerOptions"/> einmalig zu bauen und für
|
||||||
|
/// alle Sessions zu teilen.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpServerHostedService : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly McpSettingsService _settings;
|
||||||
|
private readonly AppLogger _logger;
|
||||||
|
private readonly McpServerOptions _serverOptions;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private Task? _acceptLoop;
|
||||||
|
|
||||||
|
public McpServerHostedService(
|
||||||
|
McpSettingsService settings, AppLogger logger,
|
||||||
|
StudentTools studentTools, ExamTools examTools, GradeTools gradeTools,
|
||||||
|
ScheduleTools scheduleTools, TimeEntryTools timeEntryTools)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
_logger = logger;
|
||||||
|
_serverOptions = BuildServerOptions(studentTools, examTools, gradeTools, scheduleTools, timeEntryTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Setzt die Pipe-Server-Accept-Loop auf, falls aktiviert. Ohne Wirkung, falls
|
||||||
|
/// bereits gestartet oder in den Einstellungen deaktiviert (dann bleibt keine Pipe offen).</summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (!_settings.Enabled || _cts is not null) return;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_acceptLoop = Task.Run(() => AcceptLoopAsync(_cts.Token));
|
||||||
|
_logger.Info("MCP-Server gestartet, lauscht auf Pipe '" + McpPipeConstants.PipeName + "'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var pipe = new NamedPipeServerStream(
|
||||||
|
McpPipeConstants.PipeName, PipeDirection.InOut,
|
||||||
|
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||||
|
PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await pipe.WaitForConnectionAsync(ct);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
await pipe.DisposeAsync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error("MCP: Fehler beim Warten auf eine Bridge-Verbindung.", ex);
|
||||||
|
await pipe.DisposeAsync();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nicht awaiten: die Accept-Loop muss sofort weiterlaufen, damit mehrere gleichzeitige
|
||||||
|
// Bridge-Instanzen (mehrere KI-Client-Sitzungen) unabhängig bedient werden.
|
||||||
|
_ = RunSessionAsync(pipe, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunSessionAsync(NamedPipeServerStream pipe, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var transport = new StreamServerTransport(pipe, pipe, "LehrerApp");
|
||||||
|
await using var server = McpServer.Create(transport, _serverOptions, loggerFactory: null, serviceProvider: null);
|
||||||
|
await server.RunAsync(ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.Warn($"MCP: Session beendet ({ex.Message}).");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await pipe.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_cts is null) return;
|
||||||
|
await _cts.CancelAsync();
|
||||||
|
if (_acceptLoop is not null)
|
||||||
|
{
|
||||||
|
try { await _acceptLoop; }
|
||||||
|
catch { /* Beenden über Cancellation ist der Normalfall hier */ }
|
||||||
|
}
|
||||||
|
_cts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static McpServerOptions BuildServerOptions(
|
||||||
|
StudentTools studentTools, ExamTools examTools, GradeTools gradeTools,
|
||||||
|
ScheduleTools scheduleTools, TimeEntryTools timeEntryTools)
|
||||||
|
{
|
||||||
|
var toolCollection = new McpServerPrimitiveCollection<McpServerTool>();
|
||||||
|
|
||||||
|
void AddTool(Delegate handler, string name, string description)
|
||||||
|
{
|
||||||
|
toolCollection.Add(McpServerTool.Create(handler, new McpServerToolCreateOptions
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
ReadOnly = true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTool(studentTools.GetStudents, "get_students",
|
||||||
|
"Listet Schüler, optional gefiltert nach Lerngruppe.");
|
||||||
|
AddTool(examTools.GetExams, "get_exams",
|
||||||
|
"Listet Klausuren, optional gefiltert nach Lerngruppe.");
|
||||||
|
AddTool(gradeTools.GetGrades, "get_grades",
|
||||||
|
"Listet Noten einer Lerngruppe, optional gefiltert auf einen Schüler.");
|
||||||
|
AddTool(scheduleTools.GetSchedule, "get_schedule",
|
||||||
|
"Listet Stundenplan-Einträge, optional gefiltert nach Lerngruppe.");
|
||||||
|
AddTool(timeEntryTools.GetTimeEntries, "get_time_entries",
|
||||||
|
"Listet eigene Zeiterfassungs-Einträge in einem Datumsbereich.");
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(
|
||||||
|
toolCollection.Select(t => t.ProtocolTool.Name).OrderBy(n => n)
|
||||||
|
.SequenceEqual(McpToolScope.AllowedReadTools.OrderBy(n => n)),
|
||||||
|
"Registrierte MCP-Tools weichen von McpToolScope.AllowedReadTools ab.");
|
||||||
|
|
||||||
|
return new McpServerOptions
|
||||||
|
{
|
||||||
|
ServerInfo = new Implementation { Name = "LehrerApp", Version = "1.0.0" },
|
||||||
|
Capabilities = new ServerCapabilities { Tools = new ToolsCapability() },
|
||||||
|
ToolCollection = toolCollection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace LehrerApp.Desktop.Services.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allowlist der über MCP exponierten Tool-Namen. Dieselbe Absicherung wie
|
||||||
|
/// LehrerApp.Api/PlainEventStore.cs (dort für den Klartext-Sync-Kanal): Gesprächsnotizen, Vorfälle
|
||||||
|
/// und Förderpläne (Documentation/Vorgang) sind hier bewusst nie aufgeführt und werden von keiner
|
||||||
|
/// Tool-Klasse referenziert — ein KI-Client kann diese Daten technisch nicht erreichen, unabhängig
|
||||||
|
/// davon, wie vertrauenswürdig der lokale Modell-Client erscheint oder wie die Tool-Liste künftig
|
||||||
|
/// wächst. <see cref="McpServerHostedService"/> registriert nur exakt diese Namen.
|
||||||
|
/// </summary>
|
||||||
|
public static class McpToolScope
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyCollection<string> AllowedReadTools =
|
||||||
|
[
|
||||||
|
"get_students",
|
||||||
|
"get_exams",
|
||||||
|
"get_grades",
|
||||||
|
"get_schedule",
|
||||||
|
"get_time_entries",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
// Schlanke, bewusst nicht 1:1 zu den LiteDB-Entities gehaltene Rückgabetypen: verhindert, dass ein
|
||||||
|
// später zum Modell hinzugefügtes Feld (z.B. ein neues personenbezogenes Attribut) unbeabsichtigt
|
||||||
|
// über ein MCP-Tool nach außen dringt, nur weil es Teil der Entity-Klasse ist.
|
||||||
|
|
||||||
|
public record StudentDto(Guid Id, string FirstName, string LastName, bool IsActive);
|
||||||
|
|
||||||
|
public record ExamResultDto(Guid StudentId, double TotalPoints, string? Grade, bool Absent);
|
||||||
|
|
||||||
|
public record ExamDto(
|
||||||
|
Guid Id, Guid GroupId, string Title, DateOnly Date, ExamStatus Status, Niveau? Niveau,
|
||||||
|
List<ExamResultDto>? Results);
|
||||||
|
|
||||||
|
public record GradeDto(
|
||||||
|
Guid Id, Guid StudentId, Guid GroupId, GradeCategory Category, string Value, DateOnly Date,
|
||||||
|
double Weight, string? Note);
|
||||||
|
|
||||||
|
public record TimetableSlotDto(Guid Id, Guid GroupId, DayOfWeek Weekday, int PeriodNumber, string? Room);
|
||||||
|
|
||||||
|
public record TimeEntryDto(
|
||||||
|
Guid Id, Guid? TaskId, string Category, Guid? GroupId, DateOnly Date,
|
||||||
|
TimeOnly? StartTime, TimeOnly? EndTime, int DurationMinutes, string? Description);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>MCP-Read-Tool "get_exams" (Phase 1, siehe Planungsdokument).</summary>
|
||||||
|
public class ExamTools(IExamRepository exams, IExamResultRepository examResults)
|
||||||
|
{
|
||||||
|
[Description("Listet Klausuren, optional gefiltert nach Lerngruppe.")]
|
||||||
|
public List<ExamDto> GetExams(
|
||||||
|
[Description("Optionale Lerngruppen-ID zum Filtern.")] Guid? groupId = null,
|
||||||
|
[Description("Ergebnisse je Schüler mitliefern (Standard: nein, hält die Antwort klein).")] bool includeResults = false)
|
||||||
|
{
|
||||||
|
var list = groupId is { } id ? exams.GetByGroup(id) : exams.GetAll();
|
||||||
|
return list.Select(e => new ExamDto(
|
||||||
|
e.Id, e.GroupId, e.Title, e.Date, e.Status, e.Niveau,
|
||||||
|
includeResults
|
||||||
|
? examResults.GetByExam(e.Id)
|
||||||
|
.Select(r => new ExamResultDto(r.StudentId, r.TotalPoints, r.Grade, r.Absent))
|
||||||
|
.ToList()
|
||||||
|
: null)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>MCP-Read-Tool "get_grades" (Phase 1, siehe Planungsdokument).</summary>
|
||||||
|
public class GradeTools(IGradeRepository grades)
|
||||||
|
{
|
||||||
|
[Description("Listet Noten einer Lerngruppe, optional gefiltert auf einen einzelnen Schüler.")]
|
||||||
|
public List<GradeDto> GetGrades(
|
||||||
|
[Description("Lerngruppen-ID.")] Guid groupId,
|
||||||
|
[Description("Optionale Schüler-ID zum Filtern auf einen einzelnen Schüler.")] Guid? studentId = null)
|
||||||
|
{
|
||||||
|
var list = studentId is { } sid ? grades.GetByStudentAndGroup(sid, groupId) : grades.GetByGroup(groupId);
|
||||||
|
return list.Select(g => new GradeDto(
|
||||||
|
g.Id, g.StudentId, g.GroupId, g.Category, g.Value, g.Date, g.Weight, g.Note)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>MCP-Read-Tool "get_schedule" (Phase 1, siehe Planungsdokument).</summary>
|
||||||
|
public class ScheduleTools(ITimetableSlotRepository slots)
|
||||||
|
{
|
||||||
|
[Description("Listet Stundenplan-Einträge (Wochenraster), optional gefiltert nach Lerngruppe.")]
|
||||||
|
public List<TimetableSlotDto> GetSchedule(
|
||||||
|
[Description("Optionale Lerngruppen-ID zum Filtern.")] Guid? groupId = null)
|
||||||
|
{
|
||||||
|
var list = groupId is { } id ? slots.GetByGroup(id) : slots.GetAll();
|
||||||
|
return list.Select(s => new TimetableSlotDto(s.Id, s.GroupId, s.Weekday, s.PeriodNumber, s.Room)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>MCP-Read-Tool "get_students" (Phase 1, siehe Planungsdokument). Reine Lesezugriffe auf
|
||||||
|
/// die bestehenden Repositories, keine eigene Datenzugriffslogik.</summary>
|
||||||
|
public class StudentTools(IStudentRepository students)
|
||||||
|
{
|
||||||
|
[Description("Listet Schüler, optional gefiltert nach Lerngruppe. Enthält standardmäßig nur aktive Schüler.")]
|
||||||
|
public List<StudentDto> GetStudents(
|
||||||
|
[Description("Optionale Lerngruppen-ID zum Filtern.")] Guid? groupId = null,
|
||||||
|
[Description("Auch inaktive/ausgeschiedene Schüler einbeziehen.")] bool includeInactive = false)
|
||||||
|
{
|
||||||
|
var list = groupId is { } id ? students.GetByGroup(id) : students.GetAll(includeInactive);
|
||||||
|
if (groupId is not null && !includeInactive)
|
||||||
|
list = list.Where(s => s.IsActive).ToList();
|
||||||
|
return list.Select(s => new StudentDto(s.Id, s.FirstName, s.LastName, s.IsActive)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>MCP-Read-Tool "get_time_entries" (Phase 1, siehe Planungsdokument). Der Zeitraum ist
|
||||||
|
/// Pflicht (nicht optional), damit eine unbedachte Anfrage nicht die gesamte Zeiterfassungshistorie
|
||||||
|
/// zurückgibt.</summary>
|
||||||
|
public class TimeEntryTools(ITimeEntryRepository timeEntries)
|
||||||
|
{
|
||||||
|
[Description("Listet eigene Zeiterfassungs-Einträge in einem Datumsbereich.")]
|
||||||
|
public List<TimeEntryDto> GetTimeEntries(
|
||||||
|
[Description("Startdatum (einschließlich), Format YYYY-MM-DD.")] DateOnly from,
|
||||||
|
[Description("Enddatum (einschließlich), Format YYYY-MM-DD.")] DateOnly to) =>
|
||||||
|
timeEntries.GetByDateRange(from, to).Select(t => new TimeEntryDto(
|
||||||
|
t.Id, t.TaskId, t.Category, t.GroupId, t.Date, t.StartTime, t.EndTime,
|
||||||
|
t.DurationMinutes, t.Description)).ToList();
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Services;
|
||||||
|
|
||||||
|
internal class McpSettingsConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opt-in-Schalter für den lokalen MCP-Server (siehe Planungsdokument, Phase 1). Anders als
|
||||||
|
/// <see cref="AiSettingsService"/> braucht Phase 1 kein Login/Token — die Named Pipe selbst ist die
|
||||||
|
/// Vertrauensgrenze (lokaler Prozess, gleiche Windows-Session bzw. Unix-Dateirechte), siehe
|
||||||
|
/// Begründung im Planungsdokument.
|
||||||
|
/// </summary>
|
||||||
|
public class McpSettingsService
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private McpSettingsConfig _config;
|
||||||
|
|
||||||
|
public bool Enabled => _config.Enabled;
|
||||||
|
|
||||||
|
public McpSettingsService(string appDataPath)
|
||||||
|
{
|
||||||
|
_configPath = Path.Combine(appDataPath, "mcp-settings.json");
|
||||||
|
_config = Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_config.Enabled = enabled;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save() => File.WriteAllText(_configPath, JsonSerializer.Serialize(_config));
|
||||||
|
|
||||||
|
private McpSettingsConfig Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(_configPath))
|
||||||
|
return JsonSerializer.Deserialize<McpSettingsConfig>(File.ReadAllText(_configPath))
|
||||||
|
?? new McpSettingsConfig();
|
||||||
|
}
|
||||||
|
catch { /* beschädigte Konfiguration -> Standardwert */ }
|
||||||
|
return new McpSettingsConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using LehrerApp.Desktop.Services;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
|
||||||
|
public partial class SettingsViewModel
|
||||||
|
{
|
||||||
|
// ── MCP-Server (lokal, Phase 1) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _mcpEnabled;
|
||||||
|
|
||||||
|
private void LoadMcpSettings() => McpEnabled = _mcpSettings.Enabled;
|
||||||
|
|
||||||
|
// Kein Live-Reload (siehe McpServerHostedService/Planungsdokument) - der Pipe-Listener wird
|
||||||
|
// nur beim App-Start eingerichtet, deshalb wirkt eine Änderung hier erst nach Neustart.
|
||||||
|
partial void OnMcpEnabledChanged(bool value) => _mcpSettings.SetEnabled(value);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ public enum SettingsTab
|
|||||||
WebUntis = 13,
|
WebUntis = 13,
|
||||||
Appearance = 14,
|
Appearance = 14,
|
||||||
Trash = 15,
|
Trash = 15,
|
||||||
|
Mcp = 16,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Haupt-ViewModel ───────────────────────────────────────────────────────────
|
// ── Haupt-ViewModel ───────────────────────────────────────────────────────────
|
||||||
@@ -73,6 +74,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly ISupervisionDutyRepository _supervisionDuties;
|
private readonly ISupervisionDutyRepository _supervisionDuties;
|
||||||
private readonly AiSettingsService _aiSettings;
|
private readonly AiSettingsService _aiSettings;
|
||||||
private readonly AiPlanningService _aiPlanning;
|
private readonly AiPlanningService _aiPlanning;
|
||||||
|
private readonly McpSettingsService _mcpSettings;
|
||||||
private readonly WebUntisSettingsService _untisSettings;
|
private readonly WebUntisSettingsService _untisSettings;
|
||||||
private readonly WebUntisIntegrationService? _untisIntegration;
|
private readonly WebUntisIntegrationService? _untisIntegration;
|
||||||
private readonly UntisSyncService? _untisSync;
|
private readonly UntisSyncService? _untisSync;
|
||||||
@@ -99,7 +101,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
|
IShorthandCodeRepository shorthandCodes, ISchoolHolidayRepository schoolHolidays,
|
||||||
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
||||||
ISupervisionDutyRepository supervisionDuties, TemplateStore letterTemplates,
|
ISupervisionDutyRepository supervisionDuties, TemplateStore letterTemplates,
|
||||||
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
AiSettingsService aiSettings, AiPlanningService aiPlanning, McpSettingsService mcpSettings,
|
||||||
WebUntisSettingsService untisSettings,
|
WebUntisSettingsService untisSettings,
|
||||||
AnnualPlanSettingsService annualPlanSettings,
|
AnnualPlanSettingsService annualPlanSettings,
|
||||||
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
|
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
|
||||||
@@ -137,6 +139,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
_letterTemplates = letterTemplates;
|
_letterTemplates = letterTemplates;
|
||||||
_aiSettings = aiSettings;
|
_aiSettings = aiSettings;
|
||||||
_aiPlanning = aiPlanning;
|
_aiPlanning = aiPlanning;
|
||||||
|
_mcpSettings = mcpSettings;
|
||||||
_untisSettings = untisSettings;
|
_untisSettings = untisSettings;
|
||||||
_untisIntegration = untisIntegration;
|
_untisIntegration = untisIntegration;
|
||||||
_untisSync = untisSync;
|
_untisSync = untisSync;
|
||||||
@@ -164,6 +167,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
LoadSupervisionDuties();
|
LoadSupervisionDuties();
|
||||||
LoadLetterTemplates();
|
LoadLetterTemplates();
|
||||||
LoadAiSettings();
|
LoadAiSettings();
|
||||||
|
LoadMcpSettings();
|
||||||
LoadUntisSettings();
|
LoadUntisSettings();
|
||||||
LoadAnnualPlanSettings();
|
LoadAnnualPlanSettings();
|
||||||
LoadSyncSettings();
|
LoadSyncSettings();
|
||||||
|
|||||||
@@ -1142,6 +1142,21 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: MCP-Server (lokal, Phase 1) -->
|
||||||
|
<ContentPage Header="MCP-Server">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="460">
|
||||||
|
|
||||||
|
<TextBlock Text="MCP-Server" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Erlaubt einem lokalen KI-Client (z.B. Claude Desktop) strukturierten Lesezugriff auf Schüler, Klausuren, Noten, Stundenplan und Zeiterfassung dieses Geräts über eine lokale Named Pipe. Es verlassen keine Daten das Gerät; Gesprächsnotizen, Vorfälle und Förderpläne sind nicht erreichbar. Eine Änderung wirkt erst nach einem Neustart der App."/>
|
||||||
|
|
||||||
|
<CheckBox Content="MCP-Server aktivieren" IsChecked="{Binding McpEnabled}"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LehrerApp.Core\LehrerApp.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using LehrerApp.Core.Mcp;
|
||||||
|
|
||||||
|
// Zustandsloser Bridge-Prozess: reicht die vom KI-Client über stdin/stdout gesprochene
|
||||||
|
// JSON-RPC-Verbindung unverändert an die Named Pipe des laufenden LehrerApp-Hauptprozesses durch.
|
||||||
|
// Kein eigenes JSON-RPC-Verständnis nötig, außer im Fehlerfall (siehe unten) — stdout ist
|
||||||
|
// ausschließlich für den durchgereichten Protokollstrom reserviert, jede Diagnose geht nach stderr.
|
||||||
|
|
||||||
|
const int ConnectTimeoutMs = 3000;
|
||||||
|
|
||||||
|
await using var pipe = new NamedPipeClientStream(
|
||||||
|
".", McpPipeConstants.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await pipe.ConnectAsync(ConnectTimeoutMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Console.Error.WriteLineAsync(
|
||||||
|
$"LehrerApp.McpBridge: Verbindung zu LehrerApp fehlgeschlagen ({ex.Message}). " +
|
||||||
|
"Läuft die App und ist der MCP-Server in den Einstellungen aktiviert?");
|
||||||
|
await RespondWithConnectionErrorAsync();
|
||||||
|
Environment.Exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Console.Error.WriteLineAsync("LehrerApp.McpBridge: verbunden.");
|
||||||
|
|
||||||
|
await using var stdin = Console.OpenStandardInput();
|
||||||
|
await using var stdout = Console.OpenStandardOutput();
|
||||||
|
|
||||||
|
var toApp = stdin.CopyToAsync(pipe);
|
||||||
|
var toClient = pipe.CopyToAsync(stdout);
|
||||||
|
|
||||||
|
// Sobald eine Richtung endet (App beendet die Verbindung, oder der KI-Client schließt stdin),
|
||||||
|
// ist die Session vorbei — die andere Kopie hängt sonst an einem offenen Handle.
|
||||||
|
await Task.WhenAny(toApp, toClient);
|
||||||
|
|
||||||
|
// Der Verbindungsaufbau ist fehlgeschlagen, bevor irgendetwas an die App durchgereicht wurde. Der
|
||||||
|
// MCP-Client wartet zu diesem Zeitpunkt bereits auf eine Antwort auf seine erste Anfrage
|
||||||
|
// ("initialize") — eine korrekte JSON-RPC-Fehlerantwort braucht deren "id", also wird diese eine
|
||||||
|
// Zeile noch selbst gelesen und beantwortet, statt den Prozess kommentarlos zu beenden.
|
||||||
|
static async Task RespondWithConnectionErrorAsync()
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
line = await Console.In.ReadLineAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) return;
|
||||||
|
|
||||||
|
JsonNode? requestId = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
requestId = JsonNode.Parse(line)?["id"];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Keine gültige JSON-RPC-Nachricht - ohne "id" ist keine korrekte Fehlerantwort möglich,
|
||||||
|
// der Prozess beendet sich dann einfach mit einem Fehler-Exitcode.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (requestId is null) return;
|
||||||
|
|
||||||
|
var response = new JsonObject
|
||||||
|
{
|
||||||
|
["jsonrpc"] = "2.0",
|
||||||
|
["id"] = requestId.DeepClone(),
|
||||||
|
["error"] = new JsonObject
|
||||||
|
{
|
||||||
|
["code"] = -32001,
|
||||||
|
["message"] = "LehrerApp läuft nicht oder der MCP-Server ist in den Einstellungen nicht aktiviert.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await Console.Out.WriteLineAsync(response.ToJsonString());
|
||||||
|
await Console.Out.FlushAsync();
|
||||||
|
}
|
||||||
+54
-40
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.Templating.Tests"
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.TemplateDesigner.Tests", "LehrerApp.TemplateDesigner.Tests\LehrerApp.TemplateDesigner.Tests.csproj", "{B2000004-0000-0000-0000-000000000004}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.TemplateDesigner.Tests", "LehrerApp.TemplateDesigner.Tests\LehrerApp.TemplateDesigner.Tests.csproj", "{B2000004-0000-0000-0000-000000000004}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LehrerApp.McpBridge", "LehrerApp.McpBridge\LehrerApp.McpBridge.csproj", "{4468C960-EE77-4116-A881-B0BDA0ABDC39}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -42,22 +44,6 @@ Global
|
|||||||
Release|x86 = Release|x86
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{B2000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B2000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B2000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B2000002-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2000002-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B2000002-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B2000002-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B2000003-0000-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2000003-0000-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B2000003-0000-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B2000003-0000-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B2000004-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B2000004-0000-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B2000004-0000-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A1000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000001-0000-0000-0000-000000000001}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{A1000001-0000-0000-0000-000000000001}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -94,6 +80,30 @@ Global
|
|||||||
{A1000003-0000-0000-0000-000000000003}.Release|x64.Build.0 = Release|Any CPU
|
{A1000003-0000-0000-0000-000000000003}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A1000003-0000-0000-0000-000000000003}.Release|x86.ActiveCfg = Release|Any CPU
|
{A1000003-0000-0000-0000-000000000003}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{A1000003-0000-0000-0000-000000000003}.Release|x86.Build.0 = Release|Any CPU
|
{A1000003-0000-0000-0000-000000000003}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000007-0000-0000-0000-000000000007}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A1000008-0000-0000-0000-000000000008}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000004-0000-0000-0000-000000000004}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{A1000004-0000-0000-0000-000000000004}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -178,30 +188,34 @@ Global
|
|||||||
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x64.Build.0 = Release|Any CPU
|
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x86.ActiveCfg = Release|Any CPU
|
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x86.Build.0 = Release|Any CPU
|
{E8152216-11F1-427E-B189-D8CEC9A71C33}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{B2000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|x64.Build.0 = Debug|Any CPU
|
{B2000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{B2000002-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Debug|x86.Build.0 = Debug|Any CPU
|
{B2000002-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B2000002-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B2000002-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|x64.ActiveCfg = Release|Any CPU
|
{B2000003-0000-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|x64.Build.0 = Release|Any CPU
|
{B2000003-0000-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|x86.ActiveCfg = Release|Any CPU
|
{B2000003-0000-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1000007-0000-0000-0000-000000000007}.Release|x86.Build.0 = Release|Any CPU
|
{B2000003-0000-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2000004-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2000004-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{B2000004-0000-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|x64.Build.0 = Debug|Any CPU
|
{B2000004-0000-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Debug|x86.Build.0 = Debug|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|x64.ActiveCfg = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|x64.Build.0 = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|x86.ActiveCfg = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1000008-0000-0000-0000-000000000008}.Release|x86.Build.0 = Release|Any CPU
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4468C960-EE77-4116-A881-B0BDA0ABDC39}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -2461,6 +2461,45 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
|||||||
`WeekCellItem.Date` — letzteres ist nur bei Kopfzeilen gesetzt, nicht bei
|
`WeekCellItem.Date` — letzteres ist nur bei Kopfzeilen gesetzt, nicht bei
|
||||||
Stunden-Kacheln (führte im ersten Testlauf zu einem Bug: die Automatik griff nie).
|
Stunden-Kacheln (führte im ersten Testlauf zu einem Bug: die Automatik griff nie).
|
||||||
|
|
||||||
|
- [x] **4.5.25** Lokaler MCP-Server, Phase 1 (Infrastruktur + Read-Tools), 2026-09-11: erlaubt einem
|
||||||
|
lokalen KI-Client (z.B. Claude Desktop) strukturierten Lesezugriff auf Schüler, Klausuren,
|
||||||
|
Noten, Stundenplan und Zeiterfassung — analog zur bestehenden Regel "LLM nur für Intent, nie
|
||||||
|
für Geometrie" gilt hier "LLM nur für Anfrage/Absicht, nie für direkten Datenbankzugriff":
|
||||||
|
alle Zugriffe laufen über typisierte Tools, kein freier Query-Zugriff.
|
||||||
|
- **Architektur:** neues, eigenständiges `LehrerApp.McpBridge`-Projekt (Konsolenprozess, vom
|
||||||
|
KI-Client per stdio gestartet) reicht JSON-RPC-Nachrichten zeilenweise unverändert über
|
||||||
|
eine Named Pipe (`LehrerApp.Core/Mcp/McpPipeConstants.cs`) an einen In-Process-MCP-Server im
|
||||||
|
laufenden Avalonia-Hauptprozess durch
|
||||||
|
([McpServerHostedService.cs](LehrerApp.Desktop/Services/Mcp/McpServerHostedService.cs)).
|
||||||
|
Grund: LiteDB ist Single-Writer/Embedded ohne Cross-Prozess-Benachrichtigung — ein
|
||||||
|
separater Prozess mit eigener DB-Verbindung hätte Stale-Reads gegenüber der laufenden GUI
|
||||||
|
riskiert. Nutzt `ModelContextProtocol.Core` (`StreamServerTransport` direkt auf dem
|
||||||
|
`NamedPipeServerStream`, kein eigenes JSON-RPC-Parsing nötig) statt eines
|
||||||
|
selbstgebauten Protokolls.
|
||||||
|
- **Tools (5, alle Read-only):** `get_students`, `get_exams`, `get_grades`, `get_schedule`,
|
||||||
|
`get_time_entries` — je eine schlanke Tool-Klasse unter
|
||||||
|
[Services/Mcp/Tools/](LehrerApp.Desktop/Services/Mcp/Tools/), DTOs statt direkt
|
||||||
|
serialisierter LiteDB-Entities. `McpToolScope.cs` dokumentiert die Allowlist der
|
||||||
|
exponierten Tool-Namen nach demselben Muster wie `PlainEventStore.Allowed` (Klartext-
|
||||||
|
Sync-Kanal) — Gesprächsnotizen/Vorfälle/Förderpläne (`Documentation`/`Vorgang`) werden von
|
||||||
|
keiner Tool-Klasse referenziert und sind damit technisch nie erreichbar, nicht nur per
|
||||||
|
Konvention.
|
||||||
|
- **Opt-in:** standardmäßig deaktiviert, `McpSettingsService` + Checkbox im neuen
|
||||||
|
Einstellungen-Tab "MCP-Server" (`SettingsViewModel.McpSettings.cs`). Kein Token/Login nötig
|
||||||
|
(anders als bei 4.5.9) — die Named Pipe selbst ist die Vertrauensgrenze (lokaler Prozess,
|
||||||
|
gleiche Windows-Session bzw. Unix-Dateirechte). Wirkt erst nach Neustart der App (kein
|
||||||
|
Live-Reload des Pipe-Listeners).
|
||||||
|
- **Verifiziert:** End-to-End-Smoke-Test (Bridge-Prozess als echter Kindprozess, `initialize`
|
||||||
|
→ `tools/list` → `tools/call get_students` über die reale Named Pipe) bestätigt die volle
|
||||||
|
Kette; 9 Unit-Tests für Tool-Filterlogik und die Scope-Allowlist in
|
||||||
|
[McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs).
|
||||||
|
- **Bewusst zurückgestellt (spätere Phasen):** Write-Tools samt Bestätigungsdialog-UI,
|
||||||
|
`get_lesson_plans`, Worksheets-Tools (`list_worksheets`/`download_worksheet`/...),
|
||||||
|
macOS-Bundle-Signierung der Bridge-Binary, Named-Pipe-basierte
|
||||||
|
Einzelinstanz-Absicherung (die Spec-Idee dazu funktioniert nicht, da die Pipe nur bei
|
||||||
|
aktiviertem Opt-in existiert — LehrerApp hat ohnehin noch keinen
|
||||||
|
Single-Instance-Mechanismus, unabhängig von MCP).
|
||||||
|
|
||||||
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
||||||
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
||||||
bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`,
|
bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`,
|
||||||
|
|||||||
Reference in New Issue
Block a user