[appimage] Phase 4: MCP-Hub nativ — Server aus Config laden + UI-Verwaltung
Some checks failed
Build AppImage / build (push) Has been cancelled
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:
parent
e36209690e
commit
fab7e88c44
5 changed files with 210 additions and 1 deletions
|
|
@ -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`)
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue