Generalize MCP servers, add model refresh button, add sonnet-5

- MCP: replace fixed single Unity field with a generic list of servers
  ({name, url, token?}); dynamic mcp-config + allowlist glob mcp__*<name>*
  per entry; new mcp-servers-ui.js editor (no data migration)
- Models: frontend refresh button wired to existing POST /api/models/refresh,
  reloads optgroups and restores selection
- Model: add sonnet-5 alias + dropdown option (bare "sonnet" stays 4.6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-07-02 08:51:20 +02:00
1 parent 1032cd5c63
commit 5f7dec6448
14 files changed
+257 -50

No files matched your search

+24 -19
View File
@@ -35,8 +35,7 @@ public partial class AnthropicService
string sessionId = null,
string agentId = null,
RemoteConnectionManager remoteMgr = null,
string unityMcpUrl = null,
string unityMcpToken = null,
string mcpServers = null,
bool useAutoContext = true,
string effort = null,
TokenUsage seedUsage = null,
@@ -74,7 +73,8 @@ public partial class AnthropicService
var remoteConn = remoteMgr != null && !string.IsNullOrEmpty(sessionId) ? remoteMgr.GetBySessionId(sessionId) : null;
var hasRemote = remoteConn != null;
var remoteOs = remoteConn?.OsVersion ?? "";
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, unityMcpUrl, unityMcpToken, remoteOs, modelId, webSearch);
var mcpServerList = ClaudeCliService.ParseMcpServers(mcpServers);
mcpConfigPath = GenerateMcpConfig(sessionId, agentId, hasRemote, mcpServerList, remoteOs, modelId, webSearch);
// Python-Verfügbarkeit prüfen und ggf. warnen
if (FindPythonCommand() == null)
@@ -305,7 +305,7 @@ public partial class AnthropicService
ultracode: ultracode,
webSearch: webSearch,
mcpConfigPath: mcpConfigPath,
hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl),
mcpServerNames: mcpServerList.Select(s => s.Name).ToList(),
effort: effort,
sessionId: sessionId,
chainId: chainId,
@@ -907,7 +907,7 @@ public partial class AnthropicService
/// 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, string agentId = null, bool hasRemote = false, string unityMcpUrl = null, string unityMcpToken = null, string remoteOs = null, string modelId = null, bool webSearch = false)
private string GenerateMcpConfig(string sessionId, string agentId = null, bool hasRemote = false, List<McpServerDef> userMcpServers = null, string remoteOs = null, string modelId = null, bool webSearch = false)
{
// Port aus Konfiguration oder Default
var port = 19000;
@@ -946,25 +946,30 @@ public partial class AnthropicService
if (pythonCmd == null)
Console.WriteLine("[MCP] ⚠ Python nicht gefunden (python3/python/py) – MCP-Tools werden nicht funktionieren!");
// Unity MCP Server (pro Session konfigurierbar)
// Claude CLI erfordert expliziten "type" – "http" für streamableHttp, "sse" für SSE
if (!string.IsNullOrEmpty(unityMcpUrl))
// Benutzerdefinierte MCP-Server (pro Session, URL-basiert: http/sse + optionaler Token).
// Claude CLI erfordert expliziten "type" – "http" für streamableHttp, "sse" für SSE.
// Namen sind in ParseMcpServers bereits gesäubert + von reservierten Namen befreit.
if (userMcpServers != null)
{
// URL-basierte Typ-Erkennung: /sse → SSE-Transport, sonst HTTP (streamableHttp)
var mcpType = unityMcpUrl.TrimEnd('/').EndsWith("/sse", StringComparison.OrdinalIgnoreCase) ? "sse" : "http";
var unityConfig = new Dictionary<string, object>
foreach (var srv in userMcpServers)
{
["type"] = mcpType,
["url"] = unityMcpUrl
};
if (!string.IsNullOrEmpty(unityMcpToken))
{
unityConfig["headers"] = new Dictionary<string, string>
if (srv == null || string.IsNullOrWhiteSpace(srv.Url)) continue;
// URL-basierte Typ-Erkennung: /sse → SSE-Transport, sonst HTTP (streamableHttp)
var mcpType = srv.Url.TrimEnd('/').EndsWith("/sse", StringComparison.OrdinalIgnoreCase) ? "sse" : "http";
var srvConfig = new Dictionary<string, object>
{
["Authorization"] = $"Bearer {unityMcpToken}"
["type"] = mcpType,
["url"] = srv.Url
};
if (!string.IsNullOrEmpty(srv.Token))
{
srvConfig["headers"] = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {srv.Token}"
};
}
mcpServers[srv.Name] = srvConfig;
}
mcpServers["unity"] = unityConfig;
}
// z.ai Web-Search-Prime-MCP: GLM-Modelle können Anthropics native WebSearch nicht nutzen.
+1
View File
@@ -68,6 +68,7 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig
"sonnet" => "claude-sonnet-4-6",
"sonnet-4-5" => "claude-sonnet-4-5",
"sonnet-4-6" => "claude-sonnet-4-6",
"sonnet-5" => "claude-sonnet-5",
"opus" => "claude-opus-4-8",
"opus-4-5" => "claude-opus-4-5",
"opus-4-6" => "claude-opus-4-6",
+75 -5
View File
@@ -11,6 +11,16 @@ using Microsoft.Extensions.Logging;
namespace Gott.Services;
/// <summary>
/// Benutzerdefinierter MCP-Server (pro Session). URL-basiert (http/sse), optionaler Bearer-Token.
/// </summary>
public class McpServerDef
{
public string Name { get; set; }
public string Url { get; set; }
public string Token { get; set; }
}
/// <summary>
/// Service für die Kommunikation mit der Claude Code CLI.
/// Verwendet die CLI als Backend für OAuth-basierte Chats (kein API-Key nötig).
@@ -238,6 +248,61 @@ public class ClaudeCliService
return null;
}
/// <summary>
/// Reservierte MCP-Server-Namen (gott-intern) – dürfen von benutzerdefinierten Servern
/// nicht überschrieben werden, sonst würde die interne Config/Allowlist kollidieren.
/// </summary>
private static readonly HashSet<string> _reservedMcpNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"gott-remote", "web-search-prime"
};
/// <summary>
/// Parst die in der Session hinterlegte MCP-Server-Liste (JSON-Array von {name,url,token}).
/// Säubert die Namen (glob-/config-tauglich), filtert reservierte Namen, Duplikate und
/// Einträge ohne URL. Wirft nie – ungültige Eingaben ergeben eine leere Liste.
/// </summary>
public static List<McpServerDef> ParseMcpServers(string json)
{
var result = new List<McpServerDef>();
if (string.IsNullOrWhiteSpace(json)) return result;
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != JsonValueKind.Array) return result;
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var el in doc.RootElement.EnumerateArray())
{
if (el.ValueKind != JsonValueKind.Object) continue;
var rawName = el.TryGetProperty("name", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString() : null;
var url = el.TryGetProperty("url", out var u) && u.ValueKind == JsonValueKind.String ? u.GetString() : null;
var token = el.TryGetProperty("token", out var t) && t.ValueKind == JsonValueKind.String ? t.GetString() : null;
var name = SanitizeMcpName(rawName);
if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(url)) continue;
if (_reservedMcpNames.Contains(name)) continue;
if (!seen.Add(name)) continue; // doppelter Name → ignorieren
result.Add(new McpServerDef { Name = name, Url = url.Trim(), Token = string.IsNullOrWhiteSpace(token) ? null : token.Trim() });
}
}
catch { /* ungültiges JSON → leere Liste */ }
return result;
}
/// <summary>
/// Reduziert einen MCP-Server-Namen auf glob-/config-taugliche Zeichen (Buchstaben, Ziffern,
/// '_' und '-'). Andere Zeichen (inkl. Leerzeichen) werden entfernt.
/// </summary>
private static string SanitizeMcpName(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var sb = new StringBuilder();
foreach (var c in raw.Trim())
{
if (char.IsLetterOrDigit(c) || c == '_' || c == '-') sb.Append(c);
}
return sb.Length > 0 ? sb.ToString() : null;
}
/// <summary>
/// Streaming-Chat über die Claude CLI mit Event-basiertem Callback.
/// Unterstützt Text-Streaming und Tool-Use Events.
@@ -253,7 +318,7 @@ public class ClaudeCliService
bool ultracode = false,
bool webSearch = false,
string mcpConfigPath = null,
bool hasUnityMcp = false,
List<string> mcpServerNames = null,
string effort = null,
string sessionId = null,
string chainId = null,
@@ -344,11 +409,16 @@ public class ClaudeCliService
tools.Add("mcp__gott-remote__memory_write");
tools.Add("mcp__gott-remote__memory_list");
}
// Unity MCP Tools erlauben (alle Tools vom unity MCP-Server)
if (hasUnityMcp)
// Benutzerdefinierte MCP-Server: alle Tools je Server erlauben.
// Glob mcp__*<name>* ist bewusst locker (fängt Casing-/Präfix-Varianten ab,
// z.B. Servername "unity" vs. tatsächlicher Tool-Präfix "UnityMCP").
if (mcpServerNames != null)
{
tools.Add("mcp__unity__*");
tools.Add("mcp__UnityMCP__*");
foreach (var mcpName in mcpServerNames)
{
if (!string.IsNullOrWhiteSpace(mcpName))
tools.Add($"mcp__*{mcpName}*");
}
}
args.Add("--allowed-tools");
args.Add(tools.Count > 0 ? string.Join(" ", tools) : "\"\"");
+1
View File
@@ -163,6 +163,7 @@ public partial class DatabaseService
if (body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == System.Text.Json.JsonValueKind.Number) { sets.Add("moa_layers = @moaLayers"); p.Add("moaLayers", mol.GetInt32()); }
if (body.TryGetProperty("unityMcpUrl", out var umu)) { sets.Add("unity_mcp_url = @unityMcpUrl"); p.Add("unityMcpUrl", umu.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umu.GetString()); }
if (body.TryGetProperty("unityMcpToken", out var umt)) { sets.Add("unity_mcp_token = @unityMcpToken"); p.Add("unityMcpToken", umt.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umt.GetString()); }
if (body.TryGetProperty("mcpServers", out var mcps)) { sets.Add("mcp_servers = @mcpServers"); p.Add("mcpServers", mcps.ValueKind == System.Text.Json.JsonValueKind.Null ? null : mcps.GetRawText()); }
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
}
+1
View File
@@ -196,6 +196,7 @@ public partial class DatabaseService
TryAddColumn(conn, "sessions", "preparer_agent_id", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_url", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_token", "TEXT");
TryAddColumn(conn, "sessions", "mcp_servers", "TEXT");
TryAddColumn(conn, "messages", "tag", "TEXT");
TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''");
TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''");