feat: MCP-Server für remote_execute + Inline-Choice-UI
- Python MCP-Server (stdio, JSON-RPC 2.0) für remote_execute via Claude CLI - Interner API-Endpoint /api/internal/remote-execute mit Token-Auth - MCP-Config wird automatisch generiert wenn Remote-Verbindung vorhanden - Auth-Middleware: /api/internal/ von Auth ausgenommen - Frontend: :::ASK_USER_CHOICE::: Blöcke als interaktive Buttons rendern - Streaming: Unvollständige Choice-Blöcke werden während des Streams versteckt - tool-ui.js: MCP-Tool-Namen generisch normalisieren (mcp__server__tool → tool) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee0b817f25
commit
3b068a7866
14 files changed
+576
-6
No files matched your search
@@ -245,7 +245,7 @@ public static class ChatEndpoint
|
||||
{
|
||||
if (anthropic.IsCliAvailable)
|
||||
{
|
||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, workCt: workCt, sseCt: sseCt);
|
||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, remoteMgr: remoteMgr, workCt: workCt, sseCt: sseCt);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Interne API-Endpoints für MCP-Server und andere lokale Integrationen.
|
||||
/// Gesichert über einen internen Token (da Reverse-Proxy den Localhost-Check umgeht).
|
||||
/// </summary>
|
||||
public static class InternalEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Interner Auth-Token – wird beim Start generiert und an MCP-Server übergeben.
|
||||
/// </summary>
|
||||
public static string InternalToken { get; } = System.Guid.NewGuid().ToString("N");
|
||||
|
||||
public static void MapInternalEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("/api/internal/remote-execute", HandleRemoteExecute);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleRemoteExecute(HttpContext ctx, RemoteConnectionManager mgr)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Token-Check: Header "X-Internal-Token" muss den internen Token enthalten
|
||||
var token = ctx.Request.Headers["X-Internal-Token"].FirstOrDefault() ?? "";
|
||||
if (token != InternalToken)
|
||||
return Results.StatusCode(403);
|
||||
|
||||
using var reader = new StreamReader(ctx.Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
JsonElement json;
|
||||
try { json = JsonSerializer.Deserialize<JsonElement>(body); }
|
||||
catch { return Results.BadRequest(new { error = "Invalid JSON" }); }
|
||||
|
||||
var command = json.TryGetProperty("command", out var cmd) ? cmd.GetString() ?? "" : "";
|
||||
var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() ?? "" : "";
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
return Results.BadRequest(new { error = "command erforderlich" });
|
||||
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
return Results.BadRequest(new { error = "sessionId erforderlich" });
|
||||
|
||||
var conn = mgr.GetBySessionId(sessionId);
|
||||
if (conn == null)
|
||||
return Results.NotFound(new { error = "Keine Remote-Verbindung für diese Session" });
|
||||
|
||||
using var cts = new CancellationTokenSource(System.TimeSpan.FromSeconds(180));
|
||||
var result = await mgr.ExecuteCommand(conn.Id, command, 180, cts.Token);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
exitCode = result.ExitCode,
|
||||
stdout = result.Output ?? "",
|
||||
stderr = result.Error ?? ""
|
||||
});
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
return Results.Json(new { error = ex.Message, stackTrace = ex.StackTrace }, statusCode: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user