feat(mcp): wire mcp_status/set_mcp_enabled/set_mcp_scope/mcp_access_log (T10.4)

Replaces the four not_implemented() stubs. mcp_status/set_mcp_enabled
reach the process-wide RmcpServer singleton (behind `#[cfg(feature =
"mcp")]`, with a not_implemented fallback for builds without it);
set_mcp_scope/mcp_access_log are plain settings/Store I/O and work in
every build regardless of the `mcp` cargo feature. The token is only
ever returned once, right when set_mcp_enabled mints it -- it is never
re-readable afterwards, same as any other freshly-issued secret.
This commit is contained in:
iamdoubz
2026-07-07 00:26:11 -05:00
parent f99845ef1b
commit 0ecc72f18f
+114 -11
View File
@@ -3135,29 +3135,132 @@ pub async fn set_brief_exposed(_id: String, _exposed: bool) -> WaResult<()> {
}
#[tauri::command]
pub async fn mcp_status() -> WaResult<serde_json::Value> {
Err(not_implemented("mcp_status"))
pub async fn mcp_status(state: State<'_, AppState>, app: AppHandle) -> WaResult<serde_json::Value> {
#[cfg(feature = "mcp")]
{
let settings = load_settings();
let server = crate::mcp::server::instance(state.store.clone(), app);
let running = server.is_running().await;
let transport = crate::mcp::McpTransport::parse(&settings.mcp_transport);
let endpoint = if running {
mcp_endpoint_for(transport, settings.mcp_port)
} else {
String::new()
};
Ok(serde_json::json!({
"enabled": running,
"transport": transport.as_str(),
"endpoint": endpoint,
"tokenSet": crate::mcp::token::is_set(),
"exposeScope": settings.mcp_expose,
}))
}
#[cfg(not(feature = "mcp"))]
{
let _ = (state, app);
Err(not_implemented("mcp_status"))
}
}
/// Builds the endpoint string surfaced by `mcp_status`/`set_mcp_enabled` —
/// shared so both agree on the shape (`http://127.0.0.1:<port>/mcp` for
/// Streamable HTTP, or the `--mcp-stdio` command line the agent should spawn).
#[cfg(feature = "mcp")]
fn mcp_endpoint_for(transport: crate::mcp::McpTransport, port: u16) -> String {
match transport {
crate::mcp::McpTransport::Http => format!("http://127.0.0.1:{port}/mcp"),
crate::mcp::McpTransport::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());
format!("{exe} --mcp-stdio")
}
}
}
/// Enable/disable the loopback MCP server; returns endpoint + token on enable (FR-MCP-1/6).
/// The token is minted fresh on every enable and lives only in the OS
/// credential store (`mcp::token`) — this command's return value is the one
/// time it's ever surfaced, exactly like a newly-created password.
#[tauri::command]
pub async fn set_mcp_enabled(
_enabled: bool,
_transport: Option<String>,
_port: Option<u16>,
state: State<'_, AppState>,
app: AppHandle,
enabled: bool,
transport: Option<String>,
port: Option<u16>,
) -> WaResult<serde_json::Value> {
Err(not_implemented("set_mcp_enabled"))
#[cfg(feature = "mcp")]
{
use crate::mcp::McpServer;
let mut settings = load_settings();
if let Some(t) = &transport {
settings.mcp_transport = t.clone();
}
if let Some(p) = port {
settings.mcp_port = p;
}
settings.mcp_enabled = enabled;
save_settings(&settings)?;
let server = crate::mcp::server::instance(state.store.clone(), app);
if enabled {
let cfg = crate::mcp::McpConfig {
transport: crate::mcp::McpTransport::parse(&settings.mcp_transport),
port: settings.mcp_port,
expose: crate::mcp::ExposeScope::parse(&settings.mcp_expose),
expose_recordings: settings.mcp_expose_recordings,
};
let handle = server
.start(cfg)
.await
.map_err(|e| WaError::new("mcp", e.to_string()))?;
Ok(serde_json::json!({ "endpoint": handle.endpoint, "token": handle.token }))
} else {
server
.stop(crate::mcp::McpHandle {
endpoint: String::new(),
token: String::new(),
})
.await
.map_err(|e| WaError::new("mcp", e.to_string()))?;
Ok(serde_json::json!({ "endpoint": "", "token": "" }))
}
}
#[cfg(not(feature = "mcp"))]
{
let _ = (state, app, enabled, transport, port);
Err(not_implemented("set_mcp_enabled"))
}
}
/// Set exposure scope (none|selected|all) and whether recordings may be served (FR-MCP-3).
/// Set exposure scope (none|selected|all) and whether recordings may be served
/// (FR-MCP-3). Pure settings I/O — every MCP tool handler reads this live
/// (`mcp::handler::WaMcpHandler::current_scope`), so a change here takes
/// effect immediately without restarting the server.
#[tauri::command]
pub async fn set_mcp_scope(_expose: String, _expose_recordings: Option<bool>) -> WaResult<()> {
Err(not_implemented("set_mcp_scope"))
pub async fn set_mcp_scope(expose: String, expose_recordings: Option<bool>) -> WaResult<()> {
let mut settings = load_settings();
settings.mcp_expose = expose;
if let Some(r) = expose_recordings {
settings.mcp_expose_recordings = r;
}
save_settings(&settings)
}
/// Audit trail (FR-MCP-5) — every tool read, allowed or denied.
#[tauri::command]
pub async fn mcp_access_log(_limit: Option<u32>) -> WaResult<Vec<McpAccessEntry>> {
Err(not_implemented("mcp_access_log"))
pub async fn mcp_access_log(
state: State<'_, AppState>,
limit: Option<u32>,
) -> WaResult<Vec<McpAccessEntry>> {
state
.store
.list_mcp_access_log(limit)
.await
.map_err(|e| WaError::new("mcp", e.to_string()))
}
// ---- Agent push / task-tracker handoff (Phase 10c, later) ----