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
+66 -2
View File
@@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Core.Engine;
public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecutor
public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecutor, Mcp.IMcpToolHost
{
private readonly IChatCompletionClient _client;
private readonly ToolRegistry _toolRegistry;
@@ -1473,7 +1473,8 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
CancellationToken ct,
string runId,
string? source,
DelegationChain chain)
DelegationChain chain,
bool external = false)
{
var toolName = toolCall.Function.Name;
var arguments = toolCall.Function.Arguments ?? "";
@@ -1515,6 +1516,12 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
var context = BuildToolContext(agentConfig, instanceId, toolName, ct, chain, runId);
// #713: What happens inside a foreign runtime, no check of ours sees. Whatever it
// sends through the MCP server comes from the less trusted tier — the way up is
// the one a reader of foreign content has (#715).
if (external)
context = context with { Trust = ExternalTrust(context.Trust) };
// #579: Was das Werkzeug ohnehin verbieten wuerde, kommt gar nicht erst in die
// Freigabeliste. Vorher lag dort z.B. ein Loeschvorschlag eines Agenten, der
// nur lesen darf - der Mensch entschied ueber etwas, das nie passieren konnte.
@@ -1670,6 +1677,63 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
+ "nicht wiederholen - den Inhalt auf mehrere kleinere Aufrufe verteilen.";
}
/// <summary>#713: The trust of a call from a foreign runtime — never the standard tier.</summary>
private static AgentTrust ExternalTrust(AgentTrust trust)
=> new(TrustTier.ReadsForeignContent, [.. trust.Sources, "external-runtime"]);
/// <summary>
/// #713: The tools an agent has — what the MCP server offers it. The same list a native
/// run of the agent gets, implicit ones included; nothing beyond it.
/// </summary>
public IReadOnlyList<IAgentTool> ToolsFor(string agentId)
=> _agentConfigProvider?.Invoke().FirstOrDefault(a => string.Equals(a.AgentId, agentId, StringComparison.OrdinalIgnoreCase)) is { } agent
? _toolRegistry.GetForAgent(agent)
: [];
/// <summary>
/// #713: A tool call from outside the model loop — a foreign runtime or a human's client,
/// through the MCP server. It takes the same way as a call of the agent's own model:
/// permission gate, preflight, tool rights, staging, audit, framing. No shortcut, and
/// the caller counts as reading foreign content.
/// </summary>
public async Task<(string Text, bool IsError)> ExecuteExternalToolCallAsync(
string agentId, string runId, string toolName, string argumentsJson, CancellationToken ct)
{
var agent = _agentConfigProvider?.Invoke()
.FirstOrDefault(a => string.Equals(a.AgentId, agentId, StringComparison.OrdinalIgnoreCase));
if (agent is null)
return ($"Agent '{agentId}' does not exist (anymore).", true);
var call = new ToolCall
{
Id = "mcp_" + Guid.NewGuid().ToString("N")[..12],
Type = "function",
Function = new ToolCallFunction { Name = toolName, Arguments = argumentsJson }
};
var logger = _loggerFactory.CreateLogger("ClawdDotNet.Core.Mcp");
var text = await ExecuteToolCallAsync(
call, agent, _instanceId, _toolRegistry.GetForAgent(agent), logger, ct, runId,
AuditSource.Mcp, DelegationChain.Root(agent.AgentId), external: true);
return (text, IsErrorResult(text));
}
/// <summary>Errors come back as <c>{"error": …}</c> — everything else is a result.</summary>
private static bool IsErrorResult(string text)
{
if (!text.TrimStart().StartsWith('{'))
return false;
try
{
using var doc = JsonDocument.Parse(text);
return doc.RootElement.ValueKind == JsonValueKind.Object && doc.RootElement.TryGetProperty("error", out _);
}
catch (JsonException)
{
return false;
}
}
private AgentToolContext BuildToolContext(
AgentConfig agentConfig, string instanceId, string toolName, CancellationToken ct,
DelegationChain? chain = null, string? runId = null)