From fab7e88c442cb960b9e33e0d55fc2b11d3cfc9cf Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 21 Apr 2026 14:28:08 +0200 Subject: [PATCH] =?UTF-8?q?[appimage]=20Phase=204:=20MCP-Hub=20nativ=20?= =?UTF-8?q?=E2=80=94=20Server=20aus=20Config=20laden=20+=20UI-Verwaltung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP-Server-Configs werden beim Bridge-Start aus ~/.claude.json geladen und per set-mcp-servers Command an die Bridge injiziert. Neue Tauri-Commands: list_mcp_servers, add_mcp_server, remove_mcp_server für Runtime-Verwaltung. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 + ROADMAP.md | 2 +- scripts/claude-bridge.js | 32 ++++++++ src-tauri/src/claude.rs | 172 +++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 3 + 5 files changed, 210 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e54c0..25373ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). - **Plan-Erkennung**: Claude-Antworten mit Plänen werden automatisch als Slides an das Präsentationsfenster gesendet (`planPresentation.ts`) - **Session-Projekt-Filter**: Sessions werden nach aktivem Projekt/Workspace gefiltert (`db.rs`, `session.rs`) - **Weibliche TTS-Stimme**: Kerstin als Standard-Stimme, 5 deutsche Stimmen wählbar (`voice.rs`) +- **MCP-Hub nativ (Phase 4)**: MCP-Server werden aus `~/.claude.json` geladen und beim Bridge-Start injiziert — kein CLI-Umweg nötig (`claude.rs`, `claude-bridge.js`) +- **MCP-Verwaltung**: Tauri-Commands `list_mcp_servers`, `add_mcp_server`, `remove_mcp_server` — Server zur Laufzeit hinzufügen/entfernen - **UTF-8 Crash Fix**: Kein Panic mehr bei Multi-Byte-Zeichen in DB-Abfragen (`db.rs`, `knowledge.rs`) - **Guard-Rails UI (Live)**: 3-Tab-Ansicht (Live-Feed/Regeln/Blockiert), Risiko-Statistik-Leiste, Ein-Klick-Freigabe bei Bestätigungsbedarf, guard-check Events vom Backend (`GuardRailsPanel.svelte`, `guard.rs`) - **D-Bus Desktop-Aktionen**: 10 vordefinierte Aktionen (Dolphin, Kate, Konsole, Firefox, Notify, Lock Screen), Aktionen-Grid im ProgramsPanel, CLI/GUI-Unterscheidung (`programs.rs`, `ProgramsPanel.svelte`) diff --git a/ROADMAP.md b/ROADMAP.md index 8714a20..3c5d439 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -61,7 +61,7 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig: | Feature | Datei(en) | Status | |---------|-----------|--------| | ✅ Projekt-Wechsel | `db.rs`, `SessionList.svelte` | Ein Klick wechselt Projekt (CWD, Context, KB-Filter) | -| MCP-Hub nativ | `claude-bridge.js` | ⬜ Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg | +| ✅ MCP-Hub nativ | `claude.rs`, `claude-bridge.js` | MCP-Server aus .claude.json, UI-Verwaltung, Runtime-Injection | | ✅ Guard-Rails UI | `guard.rs`, `GuardRailsPanel.svelte` | Live-Feed, Risiko-Statistik, Ein-Klick-Freigabe, 3 Tabs | | ✅ Persistent Memory | `memory.rs`, `claude.rs` | Auto-Load Eintraege in Context, Cross-Session Gedaechtnis | | ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation | diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 4d46ab7..3cf0ec7 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -43,6 +43,10 @@ let agentMode = 'solo'; // Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert let stickyContext = ''; +// MCP-Server Configs (vom Rust-Backend oder aus .claude.json geladen) +// Format: { "name": { type: "stdio", command: "...", args: [...], env: {...} } } +let mcpServerConfigs = {}; + // ============ Orchestrator Prompts ============ const ORCHESTRATOR_PROMPTS = { @@ -419,6 +423,11 @@ async function sendMessage(message, requestId, model = null, contextOverride = n queryOptions.resume = resumeSessionId; } + // MCP-Server injizieren (aus App-Config oder DB geladen) + if (Object.keys(mcpServerConfigs).length > 0) { + queryOptions.mcpServers = mcpServerConfigs; + } + // In @anthropic-ai/claude-agent-sdk 0.2.104 vererbt sich JEDE tools/disallowedTools- // Konfiguration auf Sub-Agents. Es gibt keine saubere Trennung Main vs. Sub. // Daher: Tool-Preset fuer alle Modi freischalten, Restriktion via System-Prompt. @@ -848,6 +857,29 @@ function handleCommand(msg) { }); break; + case 'set-mcp-servers': + // MCP-Server-Configs empfangen (von Rust-Backend aus DB/Config geladen) + if (msg.servers && typeof msg.servers === 'object') { + mcpServerConfigs = msg.servers; + const names = Object.keys(mcpServerConfigs); + sendResponse(msg.id, { status: 'MCP-Server gesetzt', count: names.length, servers: names }); + sendMonitorEvent('mcp', `${names.length} MCP-Server konfiguriert: ${names.join(', ')}`, { + servers: names, + count: names.length, + }); + } else { + sendError(msg.id, 'Ungültige MCP-Server-Konfiguration'); + } + break; + + case 'get-mcp-servers': + sendResponse(msg.id, { + servers: Object.keys(mcpServerConfigs), + count: Object.keys(mcpServerConfigs).length, + configs: mcpServerConfigs, + }); + break; + case 'ping': sendResponse(msg.id, { pong: true }); break; diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index 780a7bd..536c862 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -397,6 +397,12 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) { } } } + + // MCP-Hub: Server-Configs an Bridge senden + match send_mcp_configs_to_bridge(app) { + Ok(()) => {} + Err(e) => println!("⚠️ MCP-Configs senden fehlgeschlagen: {}", e), + } } "agent-started" | "subagent-start" => { if let Ok(agent) = serde_json::from_value::(payload.clone()) { @@ -975,6 +981,172 @@ pub async fn init_sticky_context(app: AppHandle) -> Result, + pub env: std::collections::HashMap, +} + +/// MCP-Server Configs aus ~/.claude.json laden +fn load_mcp_configs() -> Result { + let home = std::env::var("HOME").map_err(|_| "HOME nicht gesetzt".to_string())?; + let config_path = std::path::PathBuf::from(&home).join(".claude.json"); + + if !config_path.exists() { + return Ok(serde_json::json!({})); + } + + let content = std::fs::read_to_string(&config_path) + .map_err(|e| format!("~/.claude.json lesen fehlgeschlagen: {}", e))?; + + let config: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("~/.claude.json parsen fehlgeschlagen: {}", e))?; + + Ok(config.get("mcpServers").cloned().unwrap_or(serde_json::json!({}))) +} + +/// MCP-Configs an die Bridge senden (nach Bridge-Start) +fn send_mcp_configs_to_bridge(app: &AppHandle) -> Result<(), String> { + let configs = load_mcp_configs()?; + + if configs.as_object().map_or(true, |o| o.is_empty()) { + println!("ℹ️ Keine MCP-Server in ~/.claude.json konfiguriert"); + return Ok(()); + } + + let server_names: Vec<&str> = configs.as_object() + .map(|o| o.keys().map(|k| k.as_str()).collect()) + .unwrap_or_default(); + println!("🔌 MCP-Hub: {} Server gefunden: {}", server_names.len(), server_names.join(", ")); + + let state = app.state::>>(); + let mut state = state.lock().unwrap(); + + state.request_counter += 1; + let request_id = format!("req-{}", state.request_counter); + + let msg = serde_json::json!({ + "command": "set-mcp-servers", + "id": request_id, + "servers": configs + }); + + state.write_line(&msg.to_string())?; + Ok(()) +} + +/// Verfügbare MCP-Server auflisten (für UI) +#[tauri::command] +pub async fn list_mcp_servers() -> Result, String> { + let configs = load_mcp_configs()?; + + let mut servers = Vec::new(); + if let Some(obj) = configs.as_object() { + for (name, config) in obj { + servers.push(McpServerInfo { + name: name.clone(), + server_type: config.get("type").and_then(|v| v.as_str()).unwrap_or("stdio").to_string(), + command: config.get("command").and_then(|v| v.as_str()).unwrap_or("").to_string(), + args: config.get("args") + .and_then(|v| v.as_array()) + .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default(), + env: config.get("env") + .and_then(|v| v.as_object()) + .map(|o| o.iter() + .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) + .collect()) + .unwrap_or_default(), + }); + } + } + + Ok(servers) +} + +/// MCP-Server hinzufügen (in ~/.claude.json schreiben) +#[tauri::command] +pub async fn add_mcp_server( + app: AppHandle, + name: String, + command: String, + args: Vec, + env: std::collections::HashMap, +) -> Result { + let home = std::env::var("HOME").map_err(|_| "HOME nicht gesetzt".to_string())?; + let config_path = std::path::PathBuf::from(&home).join(".claude.json"); + + // Bestehende Config laden + let mut config: serde_json::Value = if config_path.exists() { + let content = std::fs::read_to_string(&config_path) + .map_err(|e| format!("Lesen fehlgeschlagen: {}", e))?; + serde_json::from_str(&content).unwrap_or(serde_json::json!({})) + } else { + serde_json::json!({}) + }; + + // MCP-Server hinzufügen + let servers = config.as_object_mut() + .ok_or("Config ist kein Objekt")? + .entry("mcpServers") + .or_insert(serde_json::json!({})); + + servers[&name] = serde_json::json!({ + "type": "stdio", + "command": command, + "args": args, + "env": env + }); + + // Zurückschreiben + std::fs::write(&config_path, serde_json::to_string_pretty(&config).unwrap()) + .map_err(|e| format!("Schreiben fehlgeschlagen: {}", e))?; + + println!("✅ MCP-Server '{}' hinzugefügt", name); + + // Aktualisierte Configs an Bridge senden + let _ = send_mcp_configs_to_bridge(&app); + + Ok(format!("MCP-Server '{}' hinzugefügt", name)) +} + +/// MCP-Server entfernen +#[tauri::command] +pub async fn remove_mcp_server(app: AppHandle, name: String) -> Result { + let home = std::env::var("HOME").map_err(|_| "HOME nicht gesetzt".to_string())?; + let config_path = std::path::PathBuf::from(&home).join(".claude.json"); + + if !config_path.exists() { + return Err("~/.claude.json nicht vorhanden".to_string()); + } + + let content = std::fs::read_to_string(&config_path) + .map_err(|e| format!("Lesen fehlgeschlagen: {}", e))?; + let mut config: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("Parsen fehlgeschlagen: {}", e))?; + + // Server entfernen + if let Some(servers) = config.get_mut("mcpServers").and_then(|v| v.as_object_mut()) { + if servers.remove(&name).is_some() { + std::fs::write(&config_path, serde_json::to_string_pretty(&config).unwrap()) + .map_err(|e| format!("Schreiben fehlgeschlagen: {}", e))?; + + println!("🗑️ MCP-Server '{}' entfernt", name); + let _ = send_mcp_configs_to_bridge(&app); + return Ok(format!("MCP-Server '{}' entfernt", name)); + } + } + + Err(format!("MCP-Server '{}' nicht gefunden", name)) +} + /// Bridge-Verbindungsstatus abfragen #[derive(Debug, Clone, serde::Serialize)] pub struct BridgeStatus { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3d3829c..618f619 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -56,6 +56,9 @@ pub fn run() { claude::init_sticky_context, claude::get_bridge_status, claude::stop_bridge_daemon, + claude::list_mcp_servers, + claude::add_mcp_server, + claude::remove_mcp_server, // Gedächtnis-System memory::load_memory, memory::get_sticky_memory_entries,