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:
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user