Add MCP server (v1 read-only): toggle, per-pane gate, panel UI

This commit is contained in:
megaproxy 2026-05-25 21:31:49 +01:00
parent 6068522ee3
commit 83d8932c98
15 changed files with 1235 additions and 7 deletions

View file

@ -1,10 +1,14 @@
//! Tauri command surface. Every JS-callable function lives here.
use std::sync::Arc;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use tauri::{AppHandle, Manager};
use tokio::sync::RwLock;
use crate::creds;
use crate::hosts::{self, SshHost, SshHostView};
use crate::mcp::{self, McpMirror, McpServerHandle, McpState, RunningServer};
use crate::pty::{list_wsl_distros, PaneId, PtyManager, SpawnSpec};
const WORKSPACE_FILE: &str = "workspace.json";
@ -17,7 +21,7 @@ pub async fn list_distros() -> Result<Vec<String>, String> {
#[tauri::command]
pub async fn spawn_pane(
app: AppHandle,
manager: tauri::State<'_, PtyManager>,
manager: tauri::State<'_, Arc<PtyManager>>,
spec: SpawnSpec,
cols: u16,
rows: u16,
@ -29,7 +33,7 @@ pub async fn spawn_pane(
/// strings; the frontend encodes before sending).
#[tauri::command]
pub async fn write_to_pane(
manager: tauri::State<'_, PtyManager>,
manager: tauri::State<'_, Arc<PtyManager>>,
id: PaneId,
data_b64: String,
) -> Result<(), String> {
@ -41,7 +45,7 @@ pub async fn write_to_pane(
#[tauri::command]
pub async fn resize_pane(
manager: tauri::State<'_, PtyManager>,
manager: tauri::State<'_, Arc<PtyManager>>,
id: PaneId,
cols: u16,
rows: u16,
@ -51,7 +55,7 @@ pub async fn resize_pane(
#[tauri::command]
pub async fn kill_pane(
manager: tauri::State<'_, PtyManager>,
manager: tauri::State<'_, Arc<PtyManager>>,
id: PaneId,
) -> Result<(), String> {
manager.kill(id).map_err(|e| e.to_string())
@ -137,3 +141,80 @@ pub async fn delete_host_password(host_id: String) -> Result<(), String> {
pub async fn has_host_password(host_id: String) -> Result<bool, String> {
Ok(creds::has(&host_id))
}
// ---- MCP server lifecycle --------------------------------------------------
#[derive(serde::Serialize)]
pub struct McpStatus {
pub running: bool,
pub url: Option<String>,
pub token: Option<String>,
}
fn server_status(handle: &McpServerHandle) -> McpStatus {
let g = handle.0.lock();
match g.as_ref() {
Some(srv) => McpStatus {
running: true,
url: Some(format!("http://{}/mcp", srv.addr)),
token: Some(srv.token.clone()),
},
None => McpStatus {
running: false,
url: None,
token: None,
},
}
}
#[tauri::command]
pub async fn mcp_start(
ptys: tauri::State<'_, Arc<PtyManager>>,
state: tauri::State<'_, Arc<RwLock<McpState>>>,
handle: tauri::State<'_, McpServerHandle>,
) -> Result<McpStatus, String> {
{
let g = handle.0.lock();
if g.is_some() {
return Ok(server_status(&handle));
}
}
let ptys_arc: Arc<PtyManager> = (*ptys).clone();
let state_arc: Arc<RwLock<McpState>> = (*state).clone();
let running: RunningServer = mcp::start_server(ptys_arc, state_arc)
.await
.map_err(|e| e.to_string())?;
{
let mut g = handle.0.lock();
*g = Some(running);
}
Ok(server_status(&handle))
}
#[tauri::command]
pub async fn mcp_stop(
handle: tauri::State<'_, McpServerHandle>,
) -> Result<McpStatus, String> {
mcp::stop_server(&handle);
Ok(server_status(&handle))
}
#[tauri::command]
pub async fn mcp_status(
handle: tauri::State<'_, McpServerHandle>,
) -> Result<McpStatus, String> {
Ok(server_status(&handle))
}
/// Frontend pushes the gated mirror after every tree/host change. Backend
/// caches it for MCP responses — the MCP server only ever sees what the
/// frontend chose to mirror (default-deny per-leaf gate).
#[tauri::command]
pub async fn mcp_update_state(
state: tauri::State<'_, Arc<RwLock<McpState>>>,
mirror: McpMirror,
) -> Result<(), String> {
let mut g = state.write().await;
g.mirror = mirror;
Ok(())
}