From 5f7dec6448b9f8d027dc667923c310b718453d4a Mon Sep 17 00:00:00 2001 From: Meleki Date: Thu, 2 Jul 2026 08:51:20 +0200 Subject: [PATCH] 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__** 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 --- Endpoints/ChatEndpoint.cs | 2 +- Endpoints/SessionEndpoint.cs | 1 + Models/SessionRecord.cs | 1 + Services/AnthropicService.Cli.cs | 43 ++++++++------- Services/ChatService.cs | 1 + Services/ClaudeCliService.cs | 80 ++++++++++++++++++++++++++-- Services/DatabaseService.Sessions.cs | 1 + Services/DatabaseService.cs | 1 + wwwroot/css/form.css | 51 ++++++++++++++++++ wwwroot/index.html | 15 ++++-- wwwroot/js/api.js | 3 ++ wwwroot/js/mcp-servers-ui.js | 48 +++++++++++++++++ wwwroot/js/pages/agent-page.js | 45 +++++++++++----- wwwroot/js/pages/session-page.js | 15 +++--- 14 files changed, 257 insertions(+), 50 deletions(-) create mode 100644 wwwroot/js/mcp-servers-ui.js diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 52dd990..32d316c 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -292,7 +292,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, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, 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, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, mcpServers: ctx.Session?.McpServers, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); } else { diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 2548b89..cec5a79 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -83,6 +83,7 @@ public static class SessionEndpoint s.PreparerAgentId, s.UnityMcpUrl, s.UnityMcpToken, + s.McpServers, s.IsDeleted, s.CreatedAt, s.UpdatedAt, diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index ca48da8..1610463 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -28,6 +28,7 @@ public class SessionRecord public int MoaLayers { get; set; } = 2; // MoA: Anzahl Proposer-Runden vor der Synthese (Fusion ignoriert) public string UnityMcpUrl { get; set; } public string UnityMcpToken { get; set; } + public string McpServers { get; set; } // JSON-Array benutzerdefinierter MCP-Server [{name,url,token}] public string CreatedAt { get; set; } public string UpdatedAt { get; set; } } diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index bb6322a..3f92e99 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -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. /// - 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 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 + foreach (var srv in userMcpServers) { - ["type"] = mcpType, - ["url"] = unityMcpUrl - }; - if (!string.IsNullOrEmpty(unityMcpToken)) - { - unityConfig["headers"] = new Dictionary + 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 { - ["Authorization"] = $"Bearer {unityMcpToken}" + ["type"] = mcpType, + ["url"] = srv.Url }; + if (!string.IsNullOrEmpty(srv.Token)) + { + srvConfig["headers"] = new Dictionary + { + ["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. diff --git a/Services/ChatService.cs b/Services/ChatService.cs index b6675cc..9cf80c2 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -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", diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 59b7030..543ce27 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -11,6 +11,16 @@ using Microsoft.Extensions.Logging; namespace Gott.Services; +/// +/// Benutzerdefinierter MCP-Server (pro Session). URL-basiert (http/sse), optionaler Bearer-Token. +/// +public class McpServerDef +{ + public string Name { get; set; } + public string Url { get; set; } + public string Token { get; set; } +} + /// /// 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; } + /// + /// Reservierte MCP-Server-Namen (gott-intern) – dürfen von benutzerdefinierten Servern + /// nicht überschrieben werden, sonst würde die interne Config/Allowlist kollidieren. + /// + private static readonly HashSet _reservedMcpNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "gott-remote", "web-search-prime" + }; + + /// + /// 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. + /// + public static List ParseMcpServers(string json) + { + var result = new List(); + 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(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; + } + + /// + /// Reduziert einen MCP-Server-Namen auf glob-/config-taugliche Zeichen (Buchstaben, Ziffern, + /// '_' und '-'). Andere Zeichen (inkl. Leerzeichen) werden entfernt. + /// + 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; + } + /// /// 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 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__** 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) : "\"\""); diff --git a/Services/DatabaseService.Sessions.cs b/Services/DatabaseService.Sessions.cs index fa5840d..7c0d6ee 100644 --- a/Services/DatabaseService.Sessions.cs +++ b/Services/DatabaseService.Sessions.cs @@ -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); } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 89da766..b4f37c0 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -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 ''"); diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index 81a5e80..2cb9d38 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -10,6 +10,17 @@ margin-top: var(--box-spacing); } +.form-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--box-spacing); +} + +.form-label-row label { + margin-top: 0; +} + .more { margin: var(--box-spacing) auto; cursor: pointer; @@ -214,6 +225,46 @@ align-self: flex-start; } +.mcp-servers { + display: flex; + flex-direction: column; + gap: var(--badge-spacing); +} + +.mcp-server-item { + display: flex; +} + +.mcp-server-fields { + flex: 1; + display: flex; + gap: var(--badge-spacing); + overflow: hidden; +} + +.mcp-server-fields .mcp-name { + flex: 2; +} + +.mcp-server-fields .mcp-url { + flex: 4; +} + +.mcp-server-fields .mcp-token { + flex: 3; +} + +.mcp-server-item .remove-mcp-btn { + margin-left: var(--badge-spacing); + color: var(--dangerous); + align-self: center; +} + +.mcp-server-item .remove-mcp-btn:hover { + background: var(--dangerous); + color: var(--f1-bg); +} + /* ── Checkbox ── */ label.checkbox-label { diff --git a/wwwroot/index.html b/wwwroot/index.html index fb38378..de7e37e 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -172,10 +172,10 @@
- - - - + +
+ + Pro Eintrag: Name, URL (http/sse) und optionaler Token. Tools werden headless als mcp__*<name>* freigegeben.
Kein Bot zugewiesen
@@ -218,10 +218,14 @@
?
- +
+ + +
+ + +
+ + `; + item.querySelector('.remove-mcp-btn').addEventListener('click', function() { item.remove(); }); + container.appendChild(item); +} + +function getMcpServersFromUI(container) { + var items = container.querySelectorAll('.mcp-server-item'); + var servers = []; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var name = item.querySelector('.mcp-name').value.trim(); + var url = item.querySelector('.mcp-url').value.trim(); + var token = item.querySelector('.mcp-token').value.trim(); + // Nur vollständige Einträge (Name + URL) übernehmen. + if (name && url) servers.push({ name: name, url: url, token: token || null }); + } + return servers.length > 0 ? servers : null; +} + +function loadMcpServersIntoUI(container, mcpServers) { + if (!mcpServers) return; + try { + var list = typeof mcpServers === 'string' ? JSON.parse(mcpServers) : mcpServers; + if (!Array.isArray(list)) return; + for (var i = 0; i < list.length; i++) { + var s = list[i]; + createMcpServerRow(container, s.name || '', s.url || '', s.token || ''); + } + } catch (e) { /* ignore parse errors */ } +} diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 8a89bd2..2ddb0b9 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -23,12 +23,6 @@ registerPage('agent', { const nameEl = document.getElementById('agentName'); const descEl = document.getElementById('agentDescription'); const modelEl = document.getElementById('agentModel'); - const nvidiaOptgroup = document.getElementById('nvidiaOptgroup'); - const pollinationsOptgroup = document.getElementById('pollinationsOptgroup'); - const geminiOptgroup = document.getElementById('geminiOptgroup'); - const openaiOptgroup = document.getElementById('openaiOptgroup'); - const minimaxOptgroup = document.getElementById('minimaxOptgroup'); - const glmOptgroup = document.getElementById('glmOptgroup'); // Modelle dynamisch laden const loadProviderModels = async (url, prefix, optgroup) => { @@ -51,14 +45,39 @@ registerPage('agent', { } }; - await Promise.all([ - loadProviderModels('/api/models/openai', 'openai', openaiOptgroup), - loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup), - loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup), - loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup), - loadProviderModels('/api/models/minimax', 'minimax', minimaxOptgroup), - loadProviderModels('/api/models/glm', 'glm', glmOptgroup) + // Optgroups bei jedem Aufruf frisch abfragen (nach dem ersten Laden können leere entfernt worden sein) + const loadAllModels = () => Promise.all([ + loadProviderModels('/api/models/openai', 'openai', document.getElementById('openaiOptgroup')), + loadProviderModels('/api/models/nvidia', 'nvidia', document.getElementById('nvidiaOptgroup')), + loadProviderModels('/api/models/pollinations', 'pollinations', document.getElementById('pollinationsOptgroup')), + loadProviderModels('/api/models/gemini', 'gemini', document.getElementById('geminiOptgroup')), + loadProviderModels('/api/models/minimax', 'minimax', document.getElementById('minimaxOptgroup')), + loadProviderModels('/api/models/glm', 'glm', document.getElementById('glmOptgroup')) ]); + + await loadAllModels(); + + // Refresh-Button: Modelle live von den Providern neu laden (Backend erneuert seinen Cache) + const refreshModelsBtn = document.getElementById('refreshModelsBtn'); + if (refreshModelsBtn) { + refreshModelsBtn.addEventListener('click', async () => { + const origHtml = refreshModelsBtn.innerHTML; + refreshModelsBtn.innerHTML = ''; + refreshModelsBtn.disabled = true; + const prevValue = modelEl.value; + try { + await apiRefreshModels(); + await loadAllModels(); + modelEl.value = prevValue; // vorherige Auswahl wiederherstellen (falls noch vorhanden) + showToast('Modell-Listen neu geladen'); + } catch (err) { + showToast(err.message || 'Fehler beim Neu-Laden der Modelle', 'error'); + } finally { + refreshModelsBtn.innerHTML = origHtml; + refreshModelsBtn.disabled = false; + } + }); + } const allowUserChoiceEl = document.getElementById('agentAllowUserChoice'); const autoCommitEl = document.getElementById('agentAutoCommit'); const effortEl = document.getElementById('agentEffort'); diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index 5688a10..60b842f 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -38,8 +38,8 @@ registerPage('session', { const deleteBtn = document.getElementById('sessionDeleteBtn'); const commandsList = document.getElementById('customCommandsList'); const addCommandBtn = document.getElementById('addCommandBtn'); - const unityMcpUrlEl = document.getElementById('sessionUnityMcpUrl'); - const unityMcpTokenEl = document.getElementById('sessionUnityMcpToken'); + const mcpServersList = document.getElementById('mcpServersList'); + const addMcpServerBtn = document.getElementById('addMcpServerBtn'); const ownerRow = document.getElementById('sessionOwnerRow'); const userSelectCard = document.getElementById('sessionUserSelectCard'); const userSelect = document.getElementById('sessionUserSelect'); @@ -64,9 +64,8 @@ registerPage('session', { // Custom Commands laden loadCommandsIntoUI(commandsList, session.customCommands); - // Unity MCP - unityMcpUrlEl.value = session.unityMcpUrl || ''; - unityMcpTokenEl.value = session.unityMcpToken || ''; + // MCP-Server + loadMcpServersIntoUI(mcpServersList, session.mcpServers); } else { titleEl.textContent = 'Neue Session'; deleteBtn.hidden = true; @@ -81,6 +80,9 @@ registerPage('session', { // Befehl hinzufügen addCommandBtn.addEventListener('click', () => createCommandRow(commandsList)); + // MCP-Server hinzufügen + addMcpServerBtn.addEventListener('click', () => createMcpServerRow(mcpServersList)); + // Telegram-Section nur für existierende Sessions if (!isNew) { initSessionTelegram(sessionId, session); @@ -217,8 +219,7 @@ registerPage('session', { avatarScale: _sessionAvatarEditor.getScale(), avatarOffsetX: _sessionAvatarEditor.getOffsetX(), avatarOffsetY: _sessionAvatarEditor.getOffsetY(), - unityMcpUrl: unityMcpUrlEl.value.trim() || null, - unityMcpToken: unityMcpTokenEl.value.trim() || null + mcpServers: getMcpServersFromUI(mcpServersList) }; // Session einem anderen Benutzer zuweisen. Wechselt der Besitzer tatsächlich,