Entwurfs-Panel mit MCP-Tool write_panel

- Andockbares Panel im Terminal-Fenster: Markdown-Rendering, Copy-Button,
  normale Selektion; in eigenes Fenster ablösbar und wieder andockbar
  (Splitter, panel-detached/panel-attached).
- MCP-stdio-Server (app --mcp-panel) mit Tool write_panel; schreibt in die
  Datei aus AI_CONTROL_PANEL, Watcher zieht sie ins Panel.
- Provisionierung je Pool: Server-Registrierung in .claude.json (alwaysLoad),
  Freigabe mcp__text-panel__write_panel in settings.json, Panel-Skill; bei
  App-Start (alle Pools) und Pool-Anlage. Alte tee-Freigaben werden entfernt.
- Capability deckt panel-*-Fenster ab.
This commit is contained in:
marcus.hinz
2026-07-11 12:44:51 +02:00
parent c39330bffb
commit 8aa758ae64
13 changed files with 823 additions and 8 deletions
+109
View File
@@ -0,0 +1,109 @@
//! Minimaler MCP-stdio-Server (`app --mcp-panel`), den claude als Tool-Server
//! startet. Er stellt genau ein Tool bereit: `write_panel(text)` schreibt den
//! Text in die Panel-Datei aus `AI_CONTROL_PANEL` (dieselbe Env, die der
//! Terminal-Prozess der PTY mitgibt und die claude an seine MCP-Kinder vererbt).
//! Der bestehende Datei-Watcher im Terminal-Prozess zieht den Inhalt ins Panel.
//!
//! Protokoll: JSON-RPC 2.0, newline-getrennt (ein Objekt pro Zeile).
use serde_json::{json, Value};
use std::io::{BufRead, Write};
pub fn run_mcp_panel() {
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let mut reader = stdin.lock();
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
break; // stdin geschlossen -> claude hat den Server beendet
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Ok(req) = serde_json::from_str::<Value>(trimmed) else {
continue;
};
let id = req.get("id").cloned();
let method = req.get("method").and_then(Value::as_str).unwrap_or("");
let result = handle(method, &req);
// Nur Requests (mit id) bekommen eine Antwort; Notifications nicht.
let (Some(id), Some(result)) = (id, result) else {
continue;
};
let resp = json!({ "jsonrpc": "2.0", "id": id, "result": result });
if writeln!(stdout, "{resp}").is_err() || stdout.flush().is_err() {
break;
}
}
}
/// Liefert das `result` für einen Request, oder None für Notifications/Unbekanntes.
fn handle(method: &str, req: &Value) -> Option<Value> {
match method {
"initialize" => {
// Vom Client vorgeschlagene Protokollversion übernehmen.
let protocol = req["params"]["protocolVersion"]
.as_str()
.unwrap_or("2024-11-05");
Some(json!({
"protocolVersion": protocol,
"capabilities": { "tools": {} },
"serverInfo": { "name": "text-panel", "version": env!("CARGO_PKG_VERSION") },
}))
}
"tools/list" => Some(json!({
"tools": [{
"name": "write_panel",
"description":
"Legt einen längeren Entwurf (ADR, E-Mail, Dokument, Spezifikation, \
Commit-Message, Textbaustein) im ai-control-Panel neben dem Terminal \
ab, wo er mit der Maus selektierbar und über einen Button kopierbar \
ist. Statt den Text zusätzlich als Fließtext auszugeben, dieses Tool \
aufrufen und im Chat nur kurz bestätigen.",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Vollständiger Entwurf als Markdown-Rohtext.",
}
},
"required": ["text"],
},
}],
})),
"tools/call" => Some(call_tool(req)),
"ping" => Some(json!({})),
_ => None,
}
}
fn call_tool(req: &Value) -> Value {
let name = req["params"]["name"].as_str().unwrap_or("");
if name != "write_panel" {
return json!({
"content": [{ "type": "text", "text": format!("Unbekanntes Tool: {name}") }],
"isError": true,
});
}
let text = req["params"]["arguments"]["text"].as_str().unwrap_or("");
match std::env::var("AI_CONTROL_PANEL") {
Ok(path) => match std::fs::write(&path, text) {
Ok(()) => json!({ "content": [{ "type": "text", "text": "Entwurf ins Panel geschrieben." }] }),
Err(e) => json!({
"content": [{ "type": "text", "text": format!("Panel-Datei nicht schreibbar: {e}") }],
"isError": true,
}),
},
Err(_) => json!({
"content": [{ "type": "text", "text": "AI_CONTROL_PANEL nicht gesetzt (Terminal außerhalb ai-control)." }],
"isError": true,
}),
}
}