ClawdDotNet als MCP-Server: durch dieselben Pruefstellen (#713)

Eine fremde Laufzeit oder der eigene Claude Code des Menschen spricht per MCP
mit der Instanz. Der Server bietet genau die Werkzeuge des Agenten hinter dem
Token an; jeder Aufruf laeuft durch die Engine wie ein Aufruf des eigenen
Modells (Rechtetor, Vorpruefung, Werkzeugrechte, Staging, Audit mit Quelle
mcp, Rahmung), und der Aufrufer gilt als Leser fremder Inhalte (#715).

Eigener Socket mcp.sock statt manager.sock: ueber den Instanz-Socket antwortet
die Verwaltung ohne Token, ueber mcp.sock gibt es nur /mcp und nur mit
MCP-Token. Lauf-Tokens nur im Speicher, enden mit dem Lauf; persoenliche Tokens
(hoechstens 90 Tage, widerrufbar, nur als SHA-256) in der Agentenansicht.
Transporte: HTTP auf mcp.sock, Weboberflaeche und Manager (/mcp/{instanz}),
stdio ueber ClawdDotNet --mcp-bridge (Token nur aus Umgebung oder 0600-Datei).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 17:28:47 +02:00
co-authored by Claude Opus 5.5
parent b5f65b8cf7
commit cb46289bb6
25 changed files with 1672 additions and 12 deletions
@@ -123,6 +123,47 @@ internal sealed class InstanceForwarding(ILogger logger) : IDisposable
}
}
/// <summary>
/// #713: An MCP message for an instance — to its MCP socket, not its instance socket.
///
/// <para>This is the one forwarding that keeps <c>Authorization</c>: the MCP token is
/// what the instance checks, and the manager adds nothing of its own. Through the
/// instance socket the same request would have arrived token-free and with all rights —
/// that is why it never goes there.</para>
/// </summary>
public async Task ForwardMcpAsync(HttpContext context, ManagedInstance ziel, CancellationToken ct)
{
HttpClient client;
lock (_lock)
{
var key = "mcp|" + ziel.Directory;
if (!_clients.TryGetValue(key, out client!))
_clients[key] = client = InstanceEndpoint.CreateMcpClient(ziel.Directory);
}
using var anfrage = new HttpRequestMessage(HttpMethod.Post, "/mcp")
{
Content = new StreamContent(context.Request.Body)
};
anfrage.Content.Headers.TryAddWithoutValidation("Content-Type", "application/json");
if (context.Request.Headers.Authorization.Count > 0)
anfrage.Headers.TryAddWithoutValidation("Authorization", context.Request.Headers.Authorization.ToString());
try
{
using var antwort = await client.SendAsync(anfrage, ct).ConfigureAwait(false);
context.Response.StatusCode = (int)antwort.StatusCode;
if (antwort.Content.Headers.ContentType is { } typ)
context.Response.ContentType = typ.ToString();
await antwort.Content.CopyToAsync(context.Response.Body, ct).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogWarning(ex, "Instanz {Name} war für MCP nicht erreichbar.", ziel.Name);
context.Response.StatusCode = StatusCodes.Status502BadGateway;
}
}
private HttpClient ClientFor(string instanceDirectory)
{
lock (_lock)