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:
1 parent
1032cd5c63
commit
5f7dec6448
14 files changed
+257
-50
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) : "\"\"");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ''");
|
||||
|
||||
Reference in new issue
Block a user