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:
2026-09-11 19:59:47 +02:00
co-authored by Claude Sonnet 5
parent 455c61c946
commit 98f5573999
24 changed files with 743 additions and 41 deletions
@@ -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>
+84
View File
@@ -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();
}