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:
MelekiandClaude Opus 4.6 committed 2026-03-18 09:00:50 +01:00
1 parent ee0b817f25
commit 3b068a7866
14 files changed
+576 -6

No files matched your search

+1 -1
View File
@@ -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
{
+73
View File
@@ -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);
}
}
}
+2 -1
View File
@@ -26,7 +26,8 @@ public class AuthMiddleware
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
var isRemoteWs = path == "/api/remote";
var isRemoteScript = path == "/remote.ps1";
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript)
var isInternal = path.StartsWith("/api/internal/");
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript || isInternal)
{
await _next(ctx);
return;
+126
View File
@@ -0,0 +1,126 @@
# Plan: MCP-Server für `remote_execute` in der CLI
## Übersicht
Ein eigener MCP-Server (Model Context Protocol, stdio-basiert) der `remote_execute` als Tool für die Claude Code CLI exposed. Die CLI erkennt MCP-Tools nativ und kann sie wie Built-in-Tools verwenden.
## Architektur
```
Claude CLI ←→ MCP-Server (stdio) ←→ gott HTTP-API ←→ RemoteEndpoint → SSH/Exec
```
## Komponenten
### 1. MCP-Server: `scripts/mcp-server` (oder `Services/McpServer.cs`)
**Variante A: Standalone Script (Python/Node/Bash+jq)**
- Leichtgewichtig, unabhängig von .NET Runtime
- Kommuniziert über stdin/stdout (JSON-RPC 2.0)
- Ruft gott's interne HTTP-API auf
**Variante B: .NET Console App**
- Kann ToolService/RemoteService direkt referenzieren
- Gleiche Codebase, keine API-Zwischenschicht nötig
- Aber: Eigener Prozess, muss separat gebaut werden
**Empfehlung: Variante A** – ein einfaches Script das die gott-API aufruft. Minimaler Overhead, keine Build-Abhängigkeit.
### 2. Interner API-Endpoint
```
POST /api/internal/remote-execute
Authorization: (interner Token oder localhost-only)
Content-Type: application/json
{
"command": "docker ps",
"sessionId": "abc123", // für Zuordnung zur Chat-Session
"serverId": "optional" // falls mehrere Server
}
```
**Response:**
```json
{
"exitCode": 0,
"stdout": "CONTAINER ID IMAGE ...",
"stderr": "",
"durationMs": 234
}
```
### 3. MCP-Server Konfiguration
In `.claude/settings.json` oder per `--mcp-config`:
```json
{
"mcpServers": {
"gott-remote": {
"command": "/app/gott/scripts/mcp-remote-execute",
"type": "stdio",
"env": {
"GOTT_API_URL": "http://localhost:5000",
"GOTT_SESSION_ID": "${SESSION_ID}"
}
}
}
}
```
### 4. MCP-Protokoll (stdio, JSON-RPC 2.0)
**Tool-Definition (tools/list Response):**
```json
{
"name": "remote_execute",
"description": "Führt einen Befehl auf dem Remote-Server aus (via SSH oder lokal)",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Der auszuführende Befehl"
}
},
"required": ["command"]
}
}
```
**Tool-Call (tools/call):**
- MCP-Server empfängt JSON-RPC Request
- Ruft `POST /api/internal/remote-execute` auf
- Gibt stdout/stderr als Text-Content zurück
### 5. Integration in ClaudeCliService
- MCP-Server-Config wird beim CLI-Start übergeben
- `--mcp-config /tmp/gott-mcp-{sessionId}.json` oder globale Config
- SessionId/ServerId werden als Environment-Variablen an den MCP-Server übergeben
### 6. AnthropicService: CLI-Aufruf anpassen
In `StreamChatViaCli` den MCP-Config-Pfad generieren und an `ChatStreamingAsync` übergeben:
```csharp
var mcpConfig = GenerateMcpConfig(sessionId, serverId);
var mcpConfigPath = WriteTempMcpConfig(mcpConfig);
// CLI-Aufruf mit --mcp-config mcpConfigPath
```
## Dateien
| Datei | Aktion | Beschreibung |
|-------|--------|-------------|
| `scripts/mcp-remote-execute` | **NEU** | MCP-Server Script (stdio) |
| `Endpoints/InternalEndpoint.cs` | **NEU** | Interner API-Endpoint für remote-execute |
| `Services/ClaudeCliService.cs` | **EDIT** | MCP-Config übergeben |
| `Services/AnthropicService.cs` | **EDIT** | MCP-Config generieren |
| `.claude/settings.json` | **EDIT** | MCP-Server registrieren (oder dynamisch per --mcp-config) |
## Offene Fragen
1. **Script-Sprache**: Python (weit verbreitet), Node (npm-Ökosystem), oder Shell+curl+jq (keine Runtime-Abhängigkeit)?
2. **Auth für internen Endpoint**: Nur localhost erlauben? Oder interner Token?
3. **Server-Auswahl**: Wenn mehrere Remote-Server konfiguriert sind – soll der MCP-Server alle als separate Tools exposen oder einen Default verwenden?
4. **Tool-Ergebnis-Mapping**: Wie sollen remote_execute-Results in der gott-UI angezeigt werden? Als normales Tool-Result oder speziell formatiert?
+1
View File
@@ -179,6 +179,7 @@ app.MapApiKeysEndpoints();
app.MapOpenClawEndpoints();
app.MapRemoteEndpoints();
app.MapDatabaseExplorerEndpoints();
app.MapInternalEndpoints();
var port = app.Configuration.GetValue<int>("Port", 19000);
app.Run($"http://0.0.0.0:{port}");
+60 -1
View File
@@ -1733,6 +1733,49 @@ public class AnthropicService
}
}
/// <summary>
/// Generiert eine temporäre MCP-Config-Datei für die Claude CLI.
/// Enthält den MCP-Server für remote_execute.
/// </summary>
private string GenerateMcpConfig(string sessionId)
{
// Port aus Konfiguration oder Default
var port = 19000;
var scriptPath = Path.Combine(AppContext.BaseDirectory, "scripts", "mcp-remote-execute.py");
// Fallback: Quell-Verzeichnis prüfen (für Dev-Modus)
if (!File.Exists(scriptPath))
{
// wwwroot ist typischerweise /app/gott/wwwroot → eine Ebene hoch = Projekt-Root
var projectRoot = Path.GetDirectoryName(AppPaths.WwwRoot) ?? AppContext.BaseDirectory;
var srcPath = Path.Combine(projectRoot, "scripts", "mcp-remote-execute.py");
if (File.Exists(srcPath)) scriptPath = srcPath;
}
var config = new
{
mcpServers = new Dictionary<string, object>
{
["gott-remote"] = new
{
command = "python3",
args = new[] { scriptPath },
env = new Dictionary<string, string>
{
["GOTT_API_URL"] = $"http://localhost:{port}",
["GOTT_SESSION_ID"] = sessionId,
["GOTT_AUTH_TOKEN"] = Endpoints.InternalEndpoint.InternalToken
}
}
}
};
var json = System.Text.Json.JsonSerializer.Serialize(config, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
var tempPath = Path.Combine(Path.GetTempPath(), $"gott-mcp-{sessionId}.json");
File.WriteAllText(tempPath, json);
return tempPath;
}
/// <summary>
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
/// </summary>
@@ -1886,6 +1929,7 @@ public class AnthropicService
bool planMode = false,
bool webSearch = false,
string sessionId = null,
RemoteConnectionManager remoteMgr = null,
CancellationToken workCt = default,
CancellationToken sseCt = default)
{
@@ -1900,6 +1944,7 @@ public class AnthropicService
var usage = new TokenUsage();
var fullText = new StringBuilder();
var tempImagePaths = new List<string>();
string mcpConfigPath = null;
var chain = new ChainRecord
{
@@ -1910,6 +1955,12 @@ public class AnthropicService
try
{
// MCP-Config für remote_execute generieren (wenn Remote-Verbindung vorhanden)
if (remoteMgr != null && !string.IsNullOrEmpty(sessionId) && remoteMgr.GetBySessionId(sessionId) != null)
{
mcpConfigPath = GenerateMcpConfig(sessionId);
}
// System-Prompt als Text extrahieren
var systemText = new StringBuilder();
if (systemPrompt != null)
@@ -2079,6 +2130,7 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
hasWorkspace: !string.IsNullOrEmpty(workspace),
planMode: planMode,
webSearch: webSearch,
mcpConfigPath: mcpConfigPath,
cancellationToken: workCt
);
@@ -2181,6 +2233,12 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
{
// Temp-Bilddateien aufräumen
CleanupTempImages(tempImagePaths);
// MCP-Config Temp-Datei aufräumen
if (mcpConfigPath != null)
{
try { File.Delete(mcpConfigPath); } catch { }
}
}
}
@@ -2200,7 +2258,8 @@ Danach STOPPE die Textgenerierung sofort. Nutze dies sparsam — nur wenn die En
"WebSearch" => "web_search",
"WebFetch" => "web_fetch",
"NotebookEdit" => "notebook_edit",
_ => cliName
"mcp__gott-remote__remote_execute" => "remote_execute",
_ => cliName?.StartsWith("mcp__") == true ? cliName.Split("__").LastOrDefault() ?? cliName : cliName
};
}
+9
View File
@@ -82,6 +82,7 @@ public class ClaudeCliService
bool hasWorkspace = false,
bool planMode = false,
bool webSearch = false,
string mcpConfigPath = null,
CancellationToken cancellationToken = default)
{
if (!_isAvailable)
@@ -111,6 +112,8 @@ public class ClaudeCliService
tools.AddRange(new[] { "Bash(*)", "Read", "Write", "Edit", "Glob", "Grep" });
}
if (webSearch) { tools.Add("WebFetch"); tools.Add("WebSearch"); }
// MCP-Tools erlauben (wenn MCP-Config vorhanden)
if (!string.IsNullOrEmpty(mcpConfigPath)) { tools.Add("mcp__gott-remote__remote_execute"); }
args.Add("--allowed-tools");
args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\"");
@@ -132,6 +135,12 @@ public class ClaudeCliService
args.Add(systemPrompt);
}
if (!string.IsNullOrEmpty(mcpConfigPath))
{
args.Add("--mcp-config");
args.Add(mcpConfigPath);
}
return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken);
}
+4
View File
@@ -47,6 +47,10 @@
<Content Include="Templates/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Content Include="scripts/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.11.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
MCP-Server (Model Context Protocol) für remote_execute.
Kommuniziert über stdin/stdout (JSON-RPC 2.0) mit der Claude CLI.
Ruft die gott-interne HTTP-API auf um Befehle auf dem Remote-PC auszuführen.
Umgebungsvariablen:
GOTT_API_URL - URL der gott-API (z.B. http://localhost:19000)
GOTT_SESSION_ID - Session-ID für die Remote-Verbindungszuordnung
GOTT_AUTH_TOKEN - Auth-Token für die gott-API (optional, für nicht-localhost)
"""
import json
import sys
import urllib.request
import urllib.error
import os
API_URL = os.environ.get("GOTT_API_URL", "http://localhost:19000")
SESSION_ID = os.environ.get("GOTT_SESSION_ID", "")
AUTH_TOKEN = os.environ.get("GOTT_AUTH_TOKEN", "")
TOOL_NAME = "remote_execute"
TOOL_DESCRIPTION = (
"Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. "
"Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs "
"(z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden."
)
def send_response(response):
"""Sendet eine JSON-RPC Response über stdout."""
data = json.dumps(response)
sys.stdout.write(data + "\n")
sys.stdout.flush()
def handle_initialize(req_id, params):
"""MCP initialize handshake."""
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "gott-remote",
"version": "1.0.0"
}
}
}
def handle_tools_list(req_id):
"""Gibt die verfügbaren Tools zurück."""
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"tools": [
{
"name": TOOL_NAME,
"description": TOOL_DESCRIPTION,
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "PowerShell-Befehl zum Ausführen auf dem Remote-PC"
}
},
"required": ["command"]
}
}
]
}
}
def handle_tools_call(req_id, params):
"""Führt ein Tool aus."""
tool_name = params.get("name", "")
arguments = params.get("arguments", {})
if tool_name != TOOL_NAME:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"Unbekanntes Tool: {tool_name}"}],
"isError": True
}
}
command = arguments.get("command", "")
if not command:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": "Fehler: Kein Befehl angegeben"}],
"isError": True
}
}
# API-Call an gott
try:
url = f"{API_URL}/api/internal/remote-execute"
payload = json.dumps({
"command": command,
"sessionId": SESSION_ID
}).encode("utf-8")
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
if AUTH_TOKEN:
req.add_header("X-Internal-Token", AUTH_TOKEN)
with urllib.request.urlopen(req, timeout=200) as resp:
result = json.loads(resp.read().decode("utf-8"))
# Ergebnis formatieren
output_parts = []
if result.get("stdout"):
output_parts.append(result["stdout"])
if result.get("stderr"):
output_parts.append(f"[stderr] {result['stderr']}")
if result.get("exitCode", 0) != 0:
output_parts.append(f"[Exit Code: {result['exitCode']}]")
text = "\n".join(output_parts) if output_parts else "(keine Ausgabe)"
is_error = result.get("exitCode", 0) != 0
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": text}],
"isError": is_error
}
}
except urllib.error.HTTPError as e:
body = ""
try:
body = e.read().decode("utf-8")
except Exception:
pass
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"API-Fehler {e.code}: {body}"}],
"isError": True
}
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": f"Fehler: {str(e)}"}],
"isError": True
}
}
def handle_notifications_initialized(params):
"""Handle notifications/initialized - no response needed."""
pass
def main():
"""Hauptloop: Liest JSON-RPC Requests von stdin, verarbeitet sie."""
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
except json.JSONDecodeError:
continue
method = request.get("method", "")
req_id = request.get("id")
params = request.get("params", {})
# Notifications (kein id) brauchen keine Response
if req_id is None:
if method == "notifications/initialized":
handle_notifications_initialized(params)
continue
if method == "initialize":
response = handle_initialize(req_id, params)
elif method == "tools/list":
response = handle_tools_list(req_id)
elif method == "tools/call":
response = handle_tools_call(req_id, params)
else:
response = {
"jsonrpc": "2.0",
"id": req_id,
"error": {
"code": -32601,
"message": f"Method not found: {method}"
}
}
send_response(response)
if __name__ == "__main__":
main()
+8
View File
@@ -1280,6 +1280,14 @@
color: var(--muted);
}
.inline-choice-block {
margin: var(--gap) 0;
padding: var(--gap);
border-left: 3px solid var(--primary);
background: var(--f1-bg);
border-radius: var(--border-radius);
}
/* ── OpenClaw Import Badge ── */
.openclaw-badge {
+28
View File
@@ -402,6 +402,14 @@ async function sendMessage() {
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive));
}
// ── Direct Message (für Inline-Choice-Buttons) ──
async function sendMessageDirect(text) {
if (isStreaming || !text) return;
addUserBubble(text, true);
scrollDownForce();
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
}
// ── Choice Click Handler ──
async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
@@ -419,3 +427,23 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal, _planModeActive));
}
// ── Inline Choice Blocks (Fallback aus Markdown-Rendering) ──
// Event-Delegation für .inline-choice-btn Buttons die von renderMarkdown() erzeugt wurden
document.addEventListener('click', function(e) {
var btn = e.target.closest('.inline-choice-btn');
if (!btn) return;
var block = btn.closest('.inline-choice-block');
if (!block || block.classList.contains('choice-answered')) return;
var choice = btn.dataset.choice;
// Buttons deaktivieren + markieren
block.querySelectorAll('.inline-choice-btn').forEach(function(b) {
b.disabled = true;
b.classList.add(b === btn ? 'choice-selected' : 'choice-unselected');
});
block.classList.add('choice-answered');
// Inline-Choice als normale User-Nachricht senden (kein tool_use in DB vorhanden)
sendMessageDirect(choice);
});
+28
View File
@@ -6,6 +6,18 @@ function renderMarkdown(text) {
if (!text) return '';
var src = text.trim();
// 0) :::ASK_USER_CHOICE::: Blöcke erkennen und als Platzhalter speichern (vor Markdown-Parsing)
var choiceBlocks = [];
src = src.replace(/:::ASK_USER_CHOICE:::\s*\n?([\s\S]*?)\n?\s*:::END_ASK_USER_CHOICE:::/g, function(match, jsonStr) {
try {
var data = JSON.parse(jsonStr.trim());
choiceBlocks.push(data);
return '%%CHOICE' + (choiceBlocks.length - 1) + '%%';
} catch(e) {
return match; // JSON-Fehler → als Text belassen
}
});
// 1) LaTeX-Ausdrücke VOR dem Markdown-Parsing schützen
// Reihenfolge: zuerst Block ($$...$$), dann Inline ($...$)
var mathBlocks = [];
@@ -95,6 +107,22 @@ function renderMarkdown(text) {
}
});
// 8) Choice-Platzhalter durch interaktive Buttons ersetzen
html = html.replace(/%%CHOICE(\d+)%%/g, function(m, idx) {
var data = choiceBlocks[parseInt(idx)];
var q = data.question || '';
var choices = data.choices || [];
var jsonAttr = esc(JSON.stringify(data));
var out = '<div class="inline-choice-block" data-choice-json="' + jsonAttr + '">';
out += '<div class="choice-question">' + esc(q) + '</div>';
out += '<div class="choice-buttons">';
for (var i = 0; i < choices.length; i++) {
out += '<button class="choice-btn inline-choice-btn" data-choice="' + esc(choices[i]) + '">' + esc(choices[i]) + '</button>';
}
out += '</div></div>';
return out;
});
return html;
}
+10 -1
View File
@@ -15,7 +15,16 @@ function startStreamBubble() {
function appendToStream(text) {
if (!_streamDiv) startStreamBubble();
_streamText += text;
_streamDiv.innerHTML = renderMarkdown(_streamText);
// Wenn ein :::ASK_USER_CHOICE::: Block begonnen aber noch nicht abgeschlossen wurde,
// nur den Text VOR dem Marker rendern (Marker-Block verstecken bis er komplett ist)
var displayText = _streamText;
var choiceStart = displayText.indexOf(':::ASK_USER_CHOICE:::');
if (choiceStart >= 0 && displayText.indexOf(':::END_ASK_USER_CHOICE:::') < 0) {
displayText = displayText.substring(0, choiceStart);
}
_streamDiv.innerHTML = renderMarkdown(displayText);
// Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht
const cursor = document.createElement('span');
cursor.className = 'cursor';
+9 -2
View File
@@ -10,12 +10,19 @@ const CLI_TOOL_NAME_MAP = {
'WebSearch': 'web_search',
'WebFetch': 'web_fetch',
'NotebookEdit': 'notebook_edit',
'ToolSearch': 'tool_search'
'ToolSearch': 'tool_search',
'mcp__gott-remote__remote_execute': 'remote_execute'
};
/** Normalize CLI tool name to gott convention (Edit → edit_file, etc.) */
function normalizeToolName(name) {
return CLI_TOOL_NAME_MAP[name] || name;
if (CLI_TOOL_NAME_MAP[name]) return CLI_TOOL_NAME_MAP[name];
// Generisch: mcp__server__tool → tool
if (name && name.startsWith('mcp__')) {
var parts = name.split('__');
return parts[parts.length - 1] || name;
}
return name;
}
/** Normalize CLI tool input fields (file_path → path, old_string → old_text, etc.) */