feat: LehrerApp.McpBridge zu resilientem MCP-Proxy umgebaut

Claude Desktop deaktivierte den ganzen MCP-Server, sobald der Initialize-Handshake beim
Bridge-Start fehlschlug - der Regelfall, wenn LehrerApp.Desktop (z.B. nach Windows-Autostart
von Claude Desktop) noch nicht läuft. Die Bridge ist jetzt selbst ein MCP-Server gegenüber
Claude Desktop und gleichzeitig ein MCP-Client gegenüber dem echten Server in LehrerApp.Desktop:
der Handshake gelingt dadurch immer.

Verbunden spiegelt sie die echte Werkzeugliste 1:1; ohne Verbindung bietet sie nur ein
lehrerapp_status-Werkzeug an, das den Grund erklärt und erneut verbindet. Ein Hintergrund-Loop
versucht unabhängig davon alle 5s zu reconnecten und schaltet per notifications/tools/list_changed
automatisch auf die echten Werkzeuge um, sobald LehrerApp erreichbar ist. Bricht die Verbindung
während eines laufenden Aufrufs ab, kommt ein normales Tool-Fehlerergebnis statt eines
Prozessabsturzes zurück.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 21:45:03 +02:00
co-authored by Claude Sonnet 5
parent 3f8813df51
commit d979a84e7b
7 changed files with 378 additions and 73 deletions
@@ -0,0 +1,36 @@
using LehrerApp.Core.Mcp;
namespace LehrerApp.McpBridge;
/// <summary>
/// Unterscheidet an jeder Stelle, an der die Bridge versucht, sich mit LehrerApp.Desktop zu
/// verbinden, zwischen dem harmlosen Regelfall "LehrerApp läuft gerade nicht" und einem echten
/// Verbindungsfehler. <see cref="NamedPipeClientStream.ConnectAsync"/> wirft bei einer lokalen
/// Named Pipe (kein Netzwerk, kein Handshake) genau dann eine <see cref="TimeoutException"/>, wenn
/// innerhalb der Frist kein Prozess auf den Pipe-Namen lauschte - praktisch immer, weil
/// LehrerApp.Desktop nicht läuft oder der MCP-Server dort in den Einstellungen nicht aktiviert ist.
/// Jede andere Exception (z.B. <see cref="UnauthorizedAccessException"/> bei einem Rechte-/
/// ACL-Mismatch zwischen den Prozessen) deutet auf ein echtes Konfigurationsproblem hin.
/// </summary>
internal static class ConnectionDiagnostics
{
public const int NotRunningErrorCode = -32001;
public const int UnexpectedErrorCode = -32002;
public static (int Code, string ClientMessage, string StderrLine) Describe(Exception ex) => ex switch
{
TimeoutException => (
NotRunningErrorCode,
"LehrerApp läuft nicht oder der MCP-Server ist in den Einstellungen nicht aktiviert.",
"LehrerApp.McpBridge: Kein Kontakt zu LehrerApp (Pipe „" + McpPipeConstants.PipeName +
"“ nicht erreichbar). Das ist normal, wenn LehrerApp.Desktop gerade nicht läuft oder der " +
"MCP-Server dort in den Einstellungen nicht aktiviert ist - kein Hinweis auf eine " +
"fehlerhafte Bridge-/Client-Konfiguration."),
_ => (
UnexpectedErrorCode,
$"Unerwarteter Verbindungsfehler ({ex.GetType().Name}): {ex.Message}",
$"LehrerApp.McpBridge: Unerwarteter Fehler beim Verbindungsaufbau zu LehrerApp " +
$"({ex.GetType().Name}: {ex.Message}). Das ist vermutlich KEIN einfaches \"LehrerApp " +
"läuft noch nicht\", sondern deutet auf ein Konfigurations- oder Rechteproblem hin."),
};
}
@@ -5,5 +5,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LehrerApp.Core\LehrerApp.Core.csproj" /> <ProjectReference Include="..\LehrerApp.Core\LehrerApp.Core.csproj" />
<PackageReference Include="ModelContextProtocol.Core" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+34 -73
View File
@@ -1,84 +1,45 @@
using System.IO.Pipes; using LehrerApp.McpBridge;
using System.Text.Json; using ModelContextProtocol.Protocol;
using System.Text.Json.Nodes; using ModelContextProtocol.Server;
using LehrerApp.Core.Mcp;
// Zustandsloser Bridge-Prozess: reicht die vom KI-Client über stdin/stdout gesprochene // Resilienter Bridge-Prozess (Nutzer-Feedback): meldet sich gegenüber dem KI-Client (z.B. Claude
// JSON-RPC-Verbindung unverändert an die Named Pipe des laufenden LehrerApp-Hauptprozesses durch. // Desktop) IMMER erfolgreich als MCP-Server, unabhängig davon, ob LehrerApp.Desktop beim Start
// Kein eigenes JSON-RPC-Verständnis nötig, außer im Fehlerfall (siehe unten) — stdout ist // dieses Prozesses bereits läuft - Claude Desktop startet oft per Windows-Autostart, bevor der
// ausschließlich für den durchgereichten Protokollstrom reserviert, jede Diagnose geht nach stderr. // Nutzer LehrerApp geöffnet hat. Die eigentliche Weiterleitung an den echten Server sowie das
// Nachladen der echten Werkzeugliste, sobald LehrerApp erreichbar ist, übernimmt
// RealServerBridge - Details und Begründung siehe dort.
const int ConnectTimeoutMs = 3000; var bridge = new RealServerBridge();
await bridge.TryReconnectAsync(CancellationToken.None);
bridge.EnsureFallbackTool();
await using var pipe = new NamedPipeClientStream( using var cts = new CancellationTokenSource();
".", McpPipeConstants.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous); var reconnectLoop = bridge.RunBackgroundReconnectLoopAsync(TimeSpan.FromSeconds(5), cts.Token);
try var serverOptions = new McpServerOptions
{ {
await pipe.ConnectAsync(ConnectTimeoutMs); ServerInfo = new Implementation { Name = "LehrerApp", Version = "1.0.0" },
} // ListChanged = true: Voraussetzung dafür, dass Claude Desktop reagiert, wenn RealServerBridge
catch (Exception ex) // das Status-Werkzeug nachträglich durch die echte Werkzeugliste ersetzt (oder umgekehrt, falls
{ // LehrerApp während der Sitzung beendet wird).
await Console.Error.WriteLineAsync( Capabilities = new ServerCapabilities { Tools = new ToolsCapability { ListChanged = true } },
$"LehrerApp.McpBridge: Verbindung zu LehrerApp fehlgeschlagen ({ex.Message}). " + ToolCollection = bridge.Tools,
"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 stdin = Console.OpenStandardInput();
await using var stdout = Console.OpenStandardOutput(); await using var stdout = Console.OpenStandardOutput();
await using var transport = new StreamServerTransport(stdin, stdout, "LehrerApp.McpBridge");
await using var server = McpServer.Create(transport, serverOptions, loggerFactory: null, serviceProvider: null);
var toApp = stdin.CopyToAsync(pipe); try
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; // Läuft, bis der KI-Client stdin schließt (Sitzungsende) - danach ist nichts mehr zu tun.
try await server.RunAsync(cts.Token);
{ }
line = await Console.In.ReadLineAsync(); finally
} {
catch await cts.CancelAsync();
{ try { await reconnectLoop; }
return; catch { /* Beenden über Cancellation ist der Normalfall hier */ }
} await bridge.DisposeAsync();
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();
} }
+21
View File
@@ -0,0 +1,21 @@
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace LehrerApp.McpBridge;
/// <summary>
/// Reicht einen von LehrerApp.Desktop tatsächlich angebotenen MCP-Tool-Aufruf 1:1 an den echten
/// Server weiter - Name, Beschreibung und Eingabeschema (<see cref="ProtocolTool"/>) stammen
/// unverändert von dort (siehe <see cref="RealServerBridge"/>). Die Bridge muss dadurch kein
/// eigenes Wissen über einzelne LehrerApp-Werkzeuge/-Parameter pflegen; ändert sich ein Tool in
/// LehrerApp.Desktop, übernimmt die Bridge das beim nächsten Verbindungsaufbau automatisch.
/// </summary>
internal sealed class ProxyMcpServerTool(Tool protocolTool, RealServerBridge bridge) : McpServerTool
{
public override Tool ProtocolTool { get; } = protocolTool;
public override IReadOnlyList<object> Metadata { get; } = [];
public override ValueTask<CallToolResult> InvokeAsync(
RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken) =>
new(bridge.CallRealToolAsync(request.Params, cancellationToken));
}
+177
View File
@@ -0,0 +1,177 @@
using System.IO.Pipes;
using LehrerApp.Core.Mcp;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace LehrerApp.McpBridge;
/// <summary>
/// Hält die eigentliche Verbindung zu LehrerApp.Desktop als echter MCP-Client über die Named Pipe
/// (dasselbe ModelContextProtocol-SDK, das der Server in LehrerApp.Desktop nutzt, siehe
/// McpServerHostedService) und spiegelt dessen aktuelle Werkzeugliste 1:1 in <see cref="Tools"/> -
/// der Werkzeugsammlung, die der Bridge-eigene MCP-Server gegenüber Claude Desktop anbietet.
///
/// Kernidee (Nutzer-Feedback): ein MCP-Client wie Claude Desktop führt den Initialize-Handshake nur
/// einmal beim Start des Bridge-Prozesses durch und markiert den ganzen Server als fehlgeschlagen
/// (und deaktiviert ihn für die Sitzung), wenn dieser Handshake nicht klappt - typischerweise, weil
/// LehrerApp.Desktop beim Windows-Systemstart noch nicht läuft (Claude Desktop startet oft per
/// Autostart, LehrerApp nicht). Diese Klasse sorgt dafür, dass der Handshake IMMER klappt: ist
/// LehrerApp nicht erreichbar, bietet die Bridge statt der echten Werkzeuge nur ein einzelnes
/// Status-Werkzeug (<see cref="StatusMcpServerTool"/>) an, das auf Wunsch erneut verbindet und den
/// aktuellen Stand erklärt - der Assistent kann dem Nutzer dadurch mitteilen, dass LehrerApp
/// gestartet werden muss, statt dass der ganze MCP-Server kommentarlos verschwindet. Ein
/// Hintergrund-Loop (<see cref="RunBackgroundReconnectLoopAsync"/>) versucht unabhängig davon
/// regelmäßig, die echte Verbindung wiederherzustellen - sobald das klappt, ersetzt <see cref="Tools"/>
/// das Status-Werkzeug durch die echte, tagesaktuelle Werkzeugliste, und das SDK benachrichtigt
/// Claude Desktop automatisch per "notifications/tools/list_changed"
/// (<see cref="McpServerPrimitiveCollection{T}.DeferChangedEvents"/>).
/// </summary>
internal sealed class RealServerBridge : IAsyncDisposable
{
private const int ConnectTimeoutMs = 3000;
public McpServerPrimitiveCollection<McpServerTool> Tools { get; } = new();
private readonly SemaphoreSlim _gate = new(1, 1);
private McpClient? _client;
private NamedPipeClientStream? _pipe;
/// <summary>Reicht einen Tool-Aufruf an die echte Verbindung weiter (aufgerufen aus
/// <see cref="ProxyMcpServerTool"/>). Bricht die Verbindung erst hier ab, statt sie proaktiv vor
/// jedem Aufruf zu prüfen - ein <see cref="ProxyMcpServerTool"/> existiert per Konstruktion nur,
/// solange <see cref="_client"/> zuletzt verbunden war, ein zusätzlicher Verbindungsversuch davor
/// wäre unnötige Latenz im Erfolgsfall (dem weit überwiegenden Normalfall).</summary>
public async Task<CallToolResult> CallRealToolAsync(CallToolRequestParams requestParams, CancellationToken ct)
{
var client = _client;
if (client is null)
{
EnsureFallbackTool();
return ErrorResult("LehrerApp läuft nicht oder der MCP-Server ist in den Einstellungen nicht aktiviert.");
}
try
{
return await client.CallToolAsync(requestParams, ct);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
var (_, message, stderrLine) = ConnectionDiagnostics.Describe(ex);
await Console.Error.WriteLineAsync(stderrLine);
await DisconnectAsync();
return ErrorResult("Verbindung zu LehrerApp während des Aufrufs verloren. " + message);
}
}
/// <summary>Versucht, sofern noch nicht verbunden, jetzt sofort eine Verbindung aufzubauen -
/// aufgerufen sowohl beim Start der Bridge als auch aus <see cref="StatusMcpServerTool"/>
/// (expliziter Nutzer-/Assistenten-Wunsch, jetzt nachzusehen) und aus dem Hintergrund-Loop.</summary>
public async Task<bool> TryReconnectAsync(CancellationToken ct)
{
if (_client is not null) return true;
await _gate.WaitAsync(ct);
try
{
if (_client is not null) return true; // ein anderer Aufrufer war währenddessen schneller
return await ConnectCoreAsync(ct);
}
finally { _gate.Release(); }
}
private async Task<bool> ConnectCoreAsync(CancellationToken ct)
{
var pipe = new NamedPipeClientStream(
".", McpPipeConstants.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
try
{
await pipe.ConnectAsync(ConnectTimeoutMs, ct);
var transport = new StreamClientTransport(pipe, pipe, loggerFactory: null);
var client = await McpClient.CreateAsync(
transport,
new McpClientOptions { ClientInfo = new Implementation { Name = "LehrerApp.McpBridge", Version = "1.0.0" } },
loggerFactory: null, ct);
var tools = await client.ListToolsAsync(cancellationToken: ct);
using (Tools.DeferChangedEvents())
{
Tools.Clear();
foreach (var tool in tools)
Tools.Add(new ProxyMcpServerTool(tool.ProtocolTool, this));
}
_client = client;
_pipe = pipe;
await Console.Error.WriteLineAsync(
$"LehrerApp.McpBridge: mit LehrerApp verbunden, {tools.Count} Werkzeug(e) übernommen.");
return true;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
await pipe.DisposeAsync();
var (_, _, stderrLine) = ConnectionDiagnostics.Describe(ex);
await Console.Error.WriteLineAsync(stderrLine);
EnsureFallbackTool();
return false;
}
}
/// <summary>Stellt sicher, dass mindestens das Status-Werkzeug angeboten wird - ohne
/// <see cref="Tools"/> anzufassen, wenn bereits etwas darin steht (egal ob Status- oder echte
/// Werkzeuge), um ein versehentliches Überschreiben bei überlappenden Aufrufen zu vermeiden.</summary>
public void EnsureFallbackTool()
{
if (Tools.Count > 0) return;
Tools.Add(new StatusMcpServerTool(this));
}
private async Task DisconnectAsync()
{
await _gate.WaitAsync();
try
{
_client = null;
if (_pipe is not null)
{
await _pipe.DisposeAsync();
_pipe = null;
}
using (Tools.DeferChangedEvents())
{
Tools.Clear();
Tools.Add(new StatusMcpServerTool(this));
}
}
finally { _gate.Release(); }
}
/// <summary>Läuft für die gesamte Prozesslaufzeit mit, unabhängig davon, ob der KI-Client
/// jemals von sich aus "lehrerapp_status" aufruft - sonst würden die echten Werkzeuge erst
/// wieder auftauchen, nachdem jemand aktiv danach gefragt hat.</summary>
public async Task RunBackgroundReconnectLoopAsync(TimeSpan interval, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try { await Task.Delay(interval, ct); }
catch (OperationCanceledException) { return; }
if (_client is null)
{
try { await TryReconnectAsync(ct); }
catch (OperationCanceledException) { return; }
}
}
}
private static CallToolResult ErrorResult(string message) => new()
{
IsError = true,
Content = [new TextContentBlock { Text = message }],
};
public async ValueTask DisposeAsync()
{
if (_pipe is not null) await _pipe.DisposeAsync();
_gate.Dispose();
}
}
@@ -0,0 +1,53 @@
using System.Text.Json;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace LehrerApp.McpBridge;
/// <summary>
/// Einziges Werkzeug, das die Bridge anbietet, solange keine Verbindung zu LehrerApp besteht (siehe
/// <see cref="RealServerBridge"/>). Ohne dieses Werkzeug hätte ein KI-Client keine Möglichkeit zu
/// erfahren, WARUM gerade keine LehrerApp-Werkzeuge da sind, sondern sähe nur eine leere
/// Werkzeugliste - der Assistent könnte dem Nutzer dann nicht erklären, dass LehrerApp erst
/// gestartet werden muss. Versucht bei jedem Aufruf aktiv erneut zu verbinden; klappt das, ersetzt
/// <see cref="RealServerBridge"/> dieses Werkzeug automatisch durch die echte Werkzeugliste, und der
/// Client wird per "notifications/tools/list_changed" benachrichtigt (siehe
/// <see cref="McpServerPrimitiveCollection{T}"/>).
/// </summary>
internal sealed class StatusMcpServerTool(RealServerBridge bridge) : McpServerTool
{
public override Tool ProtocolTool { get; } = new()
{
Name = "lehrerapp_status",
Description = "Prüft, ob LehrerApp erreichbar ist, und versucht bei Bedarf erneut zu " +
"verbinden. Solange keine Verbindung besteht, sind alle anderen LehrerApp-Werkzeuge " +
"(Schüler, Noten, Stundenplanung, Kompetenzen, ...) nicht verfügbar - sie erscheinen erst " +
"nach einer erfolgreichen Verbindung in der Werkzeugliste. Bei einer Fehlermeldung eines " +
"anderen LehrerApp-Werkzeugs, die auf eine fehlende Verbindung hindeutet, dieses Werkzeug " +
"aufrufen, um den Grund zu klären, statt den Nutzer ohne Erklärung zu vertrösten.",
InputSchema = JsonDocument.Parse("""{"type":"object","properties":{}}""").RootElement,
};
public override IReadOnlyList<object> Metadata { get; } = [];
public override async ValueTask<CallToolResult> InvokeAsync(
RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken)
{
var connected = await bridge.TryReconnectAsync(cancellationToken);
return new CallToolResult
{
Content =
[
new TextContentBlock
{
Text = connected
? "Verbindung zu LehrerApp hergestellt. Die eigentlichen LehrerApp-Werkzeuge " +
"sind jetzt verfügbar."
: "LehrerApp läuft nicht oder der MCP-Server ist in den Einstellungen nicht " +
"aktiviert. Bitte den Nutzer bitten, LehrerApp zu starten und den " +
"MCP-Server in den Einstellungen zu aktivieren - dieses Werkzeug danach " +
"erneut aufrufen, um zu prüfen, ob die Verbindung jetzt klappt.",
},
],
};
}
}
+56
View File
@@ -2756,6 +2756,62 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
schrieb und ein reiner No-Op genügte. schrieb und ein reiner No-Op genügte.
- 16 neue Tests in [McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 57). - 16 neue Tests in [McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 57).
- [x] **4.5.34** `LehrerApp.McpBridge` zu einem resilienten MCP-Proxy umgebaut, statt bei
fehlendem Kontakt zu LehrerApp den ganzen Prozess fehlschlagen zu lassen (2026-09-12,
Nutzer-Feedback): Claude Desktop startet per Windows-Autostart oft, bevor LehrerApp.Desktop
läuft. Die Bridge führte den MCP-Initialize-Handshake bisher selbst nur einmal beim
Prozessstart durch und ließ ihn fehlschlagen, wenn die Pipe zu diesem Zeitpunkt nicht
erreichbar war — Claude Desktop markiert einen MCP-Server nach einem fehlgeschlagenen
Handshake als tot und deaktiviert ihn für die Sitzung, ohne erneut zu versuchen. Ein erster
Zwischenschritt (nur die Fehlermeldung nach Ursache unterscheiden, `TimeoutException` vs.
echter Fehler) hätte daran nichts geändert — auf Nachfrage entschieden, stattdessen das
eigentliche Problem zu lösen.
- **Neue Architektur:** die Bridge ist jetzt selbst ein vollwertiger MCP-Server (über
`ModelContextProtocol.Server`, dieselbe SDK-Bausteine wie `McpServerHostedService` in
LehrerApp.Desktop) UND gleichzeitig ein MCP-**Client** gegenüber dem echten Server in
LehrerApp.Desktop (`ModelContextProtocol.Client.McpClient` über
`ModelContextProtocol.Protocol.StreamClientTransport` auf derselben Named Pipe). Der
Initialize-Handshake gegenüber Claude Desktop gelingt dadurch **immer**, unabhängig davon,
ob LehrerApp erreichbar ist.
- Neues [RealServerBridge.cs](LehrerApp.McpBridge/RealServerBridge.cs): hält die Client-
Verbindung zu LehrerApp und spiegelt deren aktuelle Werkzeugliste 1:1 (Name, Beschreibung,
Eingabeschema unverändert übernommen) in die Werkzeugsammlung, die die Bridge Claude
Desktop anbietet — kein eigenes Wissen über einzelne LehrerApp-Werkzeuge nötig, ändert sich
eins in LehrerApp.Desktop, übernimmt die Bridge das automatisch beim nächsten
Verbindungsaufbau. Ist LehrerApp nicht erreichbar, bietet die Bridge statt der echten
Werkzeuge genau ein Werkzeug an,
[StatusMcpServerTool.cs](LehrerApp.McpBridge/StatusMcpServerTool.cs)
(`lehrerapp_status`) — es erklärt der KI, dass LehrerApp gestartet werden muss, und
versucht bei jedem Aufruf aktiv erneut zu verbinden. Ein Hintergrund-Loop
(`RunBackgroundReconnectLoopAsync`, alle 5s) versucht das unabhängig davon ebenfalls
laufend, damit die echten Werkzeuge auch ohne aktives Nachfragen zurückkommen, sobald der
Nutzer LehrerApp startet. Jeder Wechsel zwischen Status-Werkzeug und echter Werkzeugliste
löst automatisch `notifications/tools/list_changed` aus
(`McpServerPrimitiveCollection<T>.DeferChangedEvents`), worauf ein MCP-konformer Client
seine Werkzeugliste neu abruft.
- [ProxyMcpServerTool.cs](LehrerApp.McpBridge/ProxyMcpServerTool.cs) reicht einen Tool-Aufruf
unverändert an den echten Client weiter; bricht die Verbindung während eines Aufrufs ab
(LehrerApp wurde beendet), liefert `RealServerBridge.CallRealToolAsync` ein reguläres
`CallToolResult` mit `IsError = true` und einer erklärenden Nachricht zurück — kein
Transport-/Prozessabbruch, die Sitzung mit Claude Desktop bleibt bestehen, nur dieser eine
Aufruf schlägt sichtbar (für den Assistenten erklärbar) fehl.
- [ConnectionDiagnostics.cs](LehrerApp.McpBridge/ConnectionDiagnostics.cs): dieselbe
Unterscheidung wie im verworfenen Zwischenschritt (`TimeoutException` = LehrerApp läuft
nicht/MCP-Server nicht aktiviert, alles andere = echtes Konfigurations-/Rechteproblem, z.B.
ein ACL-Mismatch zwischen den Prozessen) — jetzt als gemeinsam genutzter Baustein an allen
drei Verbindungsversuchsstellen (initialer Connect, Hintergrund-Loop, Status-Werkzeug)
statt nur im alten Einzel-Connect-Pfad.
- End-to-End mit einer echten, laufenden LehrerApp.Desktop-Instanz verifiziert (manuelles
JSON-RPC über die Bridge): erfolgreicher Connect übernimmt alle 35 registrierten Werkzeuge
mit vollem Schema; `get_students` liefert echte Daten; LehrerApp.Desktop während einer
laufenden Sitzung beendet lässt einen in Arbeit befindlichen Aufruf mit lesbarer
Fehlermeldung statt Absturz fehlschlagen und schaltet auf das Status-Werkzeug um;
LehrerApp.Desktop neu gestartet stellt die volle Werkzeugliste innerhalb des
Hintergrund-Loop-Intervalls automatisch wieder her.
- Kein zusätzliches Feature wie Windows-Autostart für LehrerApp.Desktop selbst oder ein
Tray-Icon (auf Nachfrage bewusst nicht Teil dieser Änderung) — die Bridge löst das Problem
rein auf Protokollebene, unabhängig davon, wann der Nutzer LehrerApp tatsächlich startet.
**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`,