[appimage] Phase 4: MCP-Hub nativ — Server aus Config laden + UI-Verwaltung
Some checks failed
Build AppImage / build (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-21 14:28:08 +02:00
parent e36209690e
commit fab7e88c44
5 changed files with 210 additions and 1 deletions

View file

@ -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`) - **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`) - **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`) - **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`) - **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`) - **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`) - **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`)

View file

@ -61,7 +61,7 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
| Feature | Datei(en) | Status | | Feature | Datei(en) | Status |
|---------|-----------|--------| |---------|-----------|--------|
| ✅ Projekt-Wechsel | `db.rs`, `SessionList.svelte` | Ein Klick wechselt Projekt (CWD, Context, KB-Filter) | | ✅ 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 | | ✅ 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 | | ✅ 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 | | ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation |

View file

@ -43,6 +43,10 @@ let agentMode = 'solo';
// Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert // Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert
let stickyContext = ''; let stickyContext = '';
// MCP-Server Configs (vom Rust-Backend oder aus .claude.json geladen)
// Format: { "name": { type: "stdio", command: "...", args: [...], env: {...} } }
let mcpServerConfigs = {};
// ============ Orchestrator Prompts ============ // ============ Orchestrator Prompts ============
const ORCHESTRATOR_PROMPTS = { const ORCHESTRATOR_PROMPTS = {
@ -419,6 +423,11 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
queryOptions.resume = resumeSessionId; 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- // 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. // Konfiguration auf Sub-Agents. Es gibt keine saubere Trennung Main vs. Sub.
// Daher: Tool-Preset fuer alle Modi freischalten, Restriktion via System-Prompt. // Daher: Tool-Preset fuer alle Modi freischalten, Restriktion via System-Prompt.
@ -848,6 +857,29 @@ function handleCommand(msg) {
}); });
break; 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': case 'ping':
sendResponse(msg.id, { pong: true }); sendResponse(msg.id, { pong: true });
break; break;

View file

@ -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" => { "agent-started" | "subagent-start" => {
if let Ok(agent) = serde_json::from_value::<AgentEvent>(payload.clone()) { if let Ok(agent) = serde_json::from_value::<AgentEvent>(payload.clone()) {
@ -975,6 +981,172 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
Ok(info) Ok(info)
} }
// ============ MCP-Hub ============
/// MCP-Server Info (für UI-Anzeige)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerInfo {
pub name: String,
#[serde(rename = "type")]
pub server_type: String,
pub command: String,
pub args: Vec<String>,
pub env: std::collections::HashMap<String, String>,
}
/// MCP-Server Configs aus ~/.claude.json laden
fn load_mcp_configs() -> Result<serde_json::Value, String> {
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::<Arc<Mutex<ClaudeState>>>();
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<Vec<McpServerInfo>, 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<String>,
env: std::collections::HashMap<String, String>,
) -> Result<String, String> {
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<String, String> {
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 /// Bridge-Verbindungsstatus abfragen
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct BridgeStatus { pub struct BridgeStatus {

View file

@ -56,6 +56,9 @@ pub fn run() {
claude::init_sticky_context, claude::init_sticky_context,
claude::get_bridge_status, claude::get_bridge_status,
claude::stop_bridge_daemon, claude::stop_bridge_daemon,
claude::list_mcp_servers,
claude::add_mcp_server,
claude::remove_mcp_server,
// Gedächtnis-System // Gedächtnis-System
memory::load_memory, memory::load_memory,
memory::get_sticky_memory_entries, memory::get_sticky_memory_entries,