Merge branch 'worktree-agent-af937df502b61b006' into feature_chore_bug_005

# Conflicts:
#	src-tauri/src/storage/mod.rs
This commit is contained in:
iamdoubz
2026-07-07 01:15:08 -05:00
17 changed files with 1644 additions and 69 deletions
+135 -15
View File
@@ -65,6 +65,11 @@ fn default_settings() -> Settings {
audio_output_device: None,
microphone_enabled: true,
audio_input_device: None,
mcp_enabled: false,
mcp_transport: "http".into(),
mcp_port: 4849,
mcp_expose: "none".into(),
mcp_expose_recordings: false,
}
}
@@ -1735,7 +1740,10 @@ pub(crate) fn serve_recording(
if let Some((start, end)) = parse_byte_range(range, total) {
return base()
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_RANGE, format!("bytes {start}-{end}/{total}"))
.header(
header::CONTENT_RANGE,
format!("bytes {start}-{end}/{total}"),
)
.header(header::CONTENT_LENGTH, (end - start + 1).to_string())
.body(plain[start..=end].to_vec())
.unwrap_or_else(|_| fail(StatusCode::INTERNAL_SERVER_ERROR));
@@ -3233,9 +3241,18 @@ pub async fn list_feature_briefs(
/// index (`docs/03-data-model.md`).
#[tauri::command]
pub async fn get_feature_brief(state: State<'_, AppState>, id: String) -> WaResult<FeatureBrief> {
let row = state
.store
.get_feature_brief_row(&id)
get_feature_brief_core(&state.store, &id).await
}
/// Core of `get_feature_brief`, factored out of the `State`-taking command so
/// the MCP `get_feature_brief` tool (`mcp::handler`, which only holds an
/// `Arc<dyn Store>`, not a live Tauri `State`) can call the same logic.
pub(crate) async fn get_feature_brief_core(
store: &std::sync::Arc<dyn crate::storage::Store>,
id: &str,
) -> WaResult<FeatureBrief> {
let row = store
.get_feature_brief_row(id)
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let abs_path = meeting_dir(&row.meeting_id).join(&row.path);
@@ -3270,29 +3287,132 @@ pub async fn set_brief_exposed(
}
#[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) ----