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:
+114
-11
@@ -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) ----
|
||||
|
||||
Reference in New Issue
Block a user