Feature chore bug 005 #19

Merged
iamdoubz merged 106 commits from feature_chore_bug_005 into main 2026-07-07 22:02:41 -05:00
Showing only changes of commit bd9b5ba3cc - Show all commits
+109
View File
@@ -0,0 +1,109 @@
//! `RmcpServer` — the concrete `McpServer` implementation (T10.4). Owns the
//! one running transport (HTTP listener, if any) so `stop()` can tear it
//! down; a process-wide singleton (`instance`) is what `commands.rs` reaches
//! for, since Tauri command handlers are separate calls with no shared state
//! of their own beyond `AppState`.
use crate::mcp::handler::WaMcpHandler;
use crate::mcp::{http_transport, token, McpConfig, McpError, McpHandle, McpServer, McpToolDescriptor, McpTransport};
use crate::storage::Store;
use async_trait::async_trait;
use std::sync::{Arc, OnceLock};
use tauri::AppHandle;
use tokio::sync::Mutex;
enum Running {
Http(http_transport::HttpServerHandle),
/// stdio has nothing running *in this process* — the agent spawns its
/// own `--mcp-stdio` child (see `mcp::stdio_transport`); this variant
/// just records "enabled" for `mcp_status`.
Stdio,
}
pub struct RmcpServer {
store: Arc<dyn Store>,
app: AppHandle,
running: Mutex<Option<Running>>,
}
impl RmcpServer {
pub fn new(store: Arc<dyn Store>, app: AppHandle) -> Self {
Self {
store,
app,
running: Mutex::new(None),
}
}
async fn stop_running(&self) {
if let Some(running) = self.running.lock().await.take() {
if let Running::Http(handle) = running {
handle.stop().await;
}
}
}
/// `true` once a `start()` has actually taken effect (HTTP listener bound
/// or stdio mode recorded) — used by `mcp_status`.
pub async fn is_running(&self) -> bool {
self.running.lock().await.is_some()
}
}
#[async_trait]
impl McpServer for RmcpServer {
async fn start(&self, cfg: McpConfig) -> Result<McpHandle, McpError> {
// Re-enabling (or switching transport/port) replaces whatever was running.
self.stop_running().await;
let auth_token = token::mint_and_store()?;
match cfg.transport {
McpTransport::Http => {
let listener = http_transport::bind_loopback("127.0.0.1", cfg.port).await?;
let local_addr = listener
.local_addr()
.map_err(|e| McpError::Server(e.to_string()))?;
let handler = WaMcpHandler::new(self.store.clone(), self.app.clone());
let handle = http_transport::serve(listener, auth_token.clone(), handler);
let endpoint = format!("http://{local_addr}/mcp");
*self.running.lock().await = Some(Running::Http(handle));
Ok(McpHandle {
endpoint,
token: auth_token,
})
}
McpTransport::Stdio => {
*self.running.lock().await = Some(Running::Stdio);
let exe = std::env::current_exe()
.ok()
.and_then(|p| p.to_str().map(str::to_string))
.unwrap_or_else(|| "whispassist.exe".to_string());
Ok(McpHandle {
endpoint: format!("{exe} --mcp-stdio"),
token: auth_token,
})
}
}
}
async fn stop(&self, _handle: McpHandle) -> Result<(), McpError> {
self.stop_running().await;
token::delete();
Ok(())
}
fn tools(&self) -> Vec<McpToolDescriptor> {
crate::mcp::TOOL_DESCRIPTORS.to_vec()
}
}
static INSTANCE: OnceLock<Arc<RmcpServer>> = OnceLock::new();
/// The process-wide `RmcpServer`. `store`/`app` are only used on the first
/// call (they're the same `AppState`/`AppHandle` for the process's whole
/// life); later calls just return the existing instance.
pub fn instance(store: Arc<dyn Store>, app: AppHandle) -> Arc<RmcpServer> {
INSTANCE
.get_or_init(|| Arc::new(RmcpServer::new(store, app)))
.clone()
}