Skip to content

Commit b590b0b

Browse files
committed
feat(meta-tools): session override UI and settings toggle (Phase 5)
Expose session overrides in the Workspaces inspector with clear controls, wire list/clear Tauri commands, and add a settings gate for session-scope enable/disable approval. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent b29d5e8 commit b590b0b

11 files changed

Lines changed: 506 additions & 17 deletions

File tree

apps/desktop/src-tauri/src/commands/gateway.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ pub struct GatewayAppState {
7979
/// Surfaced to the desktop Workspaces tab so users can see + act on
8080
/// every folder connected clients are currently operating in.
8181
pub session_roots: Option<Arc<mcpmux_gateway::services::SessionRootsRegistry>>,
82+
/// Session-scoped server enable/disable overrides (meta-tool mutations).
83+
pub session_overrides: Option<Arc<mcpmux_gateway::services::SessionOverrideRegistry>>,
84+
/// Per-session list_changed bridge — used when the UI clears overrides.
85+
pub mcp_notifier: Option<Arc<mcpmux_gateway::consumers::MCPNotifier>>,
8286
}
8387

8488
/// Gracefully shuts down a running gateway and waits for the axum task
@@ -888,6 +892,8 @@ pub async fn start_gateway(
888892
let server_manager = server.server_manager();
889893
let grant_service = server.grant_service();
890894
let session_roots = server.session_roots();
895+
let session_overrides = server.session_overrides();
896+
let mcp_notifier = server.notification_bridge();
891897

892898
// Subscribe to OAuth completions BEFORE spawn so we don't miss early
893899
// events emitted during initial auto-connect.
@@ -935,6 +941,8 @@ pub async fn start_gateway(
935941
state.grant_service = Some(grant_service);
936942
state.approval_broker = Some(approval_broker);
937943
state.session_roots = Some(session_roots);
944+
state.session_overrides = Some(session_overrides);
945+
state.mcp_notifier = Some(mcp_notifier);
938946
info!(
939947
"[Gateway] Started — url={}, event_emitter={}, grant_service={}",
940948
url,

apps/desktop/src-tauri/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod server;
1717
pub mod server_discovery;
1818
pub mod server_feature;
1919
pub mod server_manager;
20+
pub mod session_overrides;
2021
pub mod settings;
2122
pub mod space;
2223
pub mod workspace_binding;
@@ -35,6 +36,7 @@ pub use server::*;
3536
pub use server_discovery::*;
3637
pub use server_feature::*;
3738
pub use server_manager::*;
39+
pub use session_overrides::*;
3840
pub use settings::*;
3941
pub use space::*;
4042
pub use workspace_binding::*;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//! Tauri commands for inspecting and clearing session-scoped server overrides.
2+
3+
use std::sync::Arc;
4+
5+
use mcpmux_gateway::services::SessionOverrideEntry;
6+
use serde::Serialize;
7+
use tauri::{AppHandle, Emitter, State};
8+
use tokio::sync::RwLock;
9+
use tracing::info;
10+
11+
use super::gateway::GatewayAppState;
12+
13+
/// Per-session override state surfaced to the Workspaces inspector.
14+
#[derive(Debug, Clone, Serialize)]
15+
pub struct SessionOverrideDto {
16+
pub session_id: String,
17+
pub enabled: Vec<String>,
18+
pub disabled: Vec<String>,
19+
pub roots: Vec<String>,
20+
}
21+
22+
impl SessionOverrideDto {
23+
fn from_entry(entry: SessionOverrideEntry, roots: Vec<String>) -> Self {
24+
Self {
25+
session_id: entry.session_id,
26+
enabled: entry.enabled,
27+
disabled: entry.disabled,
28+
roots,
29+
}
30+
}
31+
}
32+
33+
fn build_dtos(gateway: &GatewayAppState) -> Vec<SessionOverrideDto> {
34+
let Some(ref overrides) = gateway.session_overrides else {
35+
return vec![];
36+
};
37+
let roots_by_session: std::collections::HashMap<String, Vec<String>> = gateway
38+
.session_roots
39+
.as_ref()
40+
.map(|reg| {
41+
reg.list_all_sessions()
42+
.into_iter()
43+
.collect()
44+
})
45+
.unwrap_or_default();
46+
47+
overrides
48+
.list_all()
49+
.into_iter()
50+
.map(|entry| {
51+
let roots = roots_by_session
52+
.get(&entry.session_id)
53+
.cloned()
54+
.unwrap_or_default();
55+
SessionOverrideDto::from_entry(entry, roots)
56+
})
57+
.collect()
58+
}
59+
60+
/// List override state for one session, or every session when `session_id`
61+
/// is omitted. Returns an empty list when the gateway is not running.
62+
#[tauri::command]
63+
pub async fn list_session_overrides(
64+
session_id: Option<String>,
65+
gateway_state: State<'_, Arc<RwLock<GatewayAppState>>>,
66+
) -> Result<Vec<SessionOverrideDto>, String> {
67+
let guard = gateway_state.read().await;
68+
let mut dtos = build_dtos(&guard);
69+
if let Some(sid) = session_id {
70+
dtos.retain(|d| d.session_id == sid);
71+
}
72+
Ok(dtos)
73+
}
74+
75+
/// Drop all enable/disable overrides for a session and push list_changed so
76+
/// the client's tool list reverts to binding-only routing.
77+
#[tauri::command]
78+
pub async fn clear_session_overrides(
79+
session_id: String,
80+
gateway_state: State<'_, Arc<RwLock<GatewayAppState>>>,
81+
app_handle: AppHandle,
82+
) -> Result<(), String> {
83+
let notifier = {
84+
let guard = gateway_state.read().await;
85+
let overrides = guard
86+
.session_overrides
87+
.as_ref()
88+
.ok_or("Gateway is not running")?;
89+
overrides.clear(&session_id);
90+
guard.mcp_notifier.clone()
91+
};
92+
93+
if let Some(notifier) = notifier {
94+
notifier.notify_session_lists_changed(&session_id).await;
95+
}
96+
97+
info!("[session_overrides] cleared overrides for session {}", session_id);
98+
99+
if let Err(e) = app_handle.emit(
100+
"session-overrides-changed",
101+
serde_json::json!({ "session_id": session_id }),
102+
) {
103+
tracing::warn!("[session_overrides] failed to emit session-overrides-changed: {e}");
104+
}
105+
106+
Ok(())
107+
}

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,44 @@ pub async fn set_meta_tools_enabled(
165165
Ok(())
166166
}
167167

168+
/// Whether session-scope `mcpmux_enable_server` / `mcpmux_disable_server`
169+
/// calls require approval. Default OFF (auto-allow).
170+
#[tauri::command]
171+
pub async fn get_session_overrides_require_approval(
172+
app_state: State<'_, AppState>,
173+
) -> Result<bool, String> {
174+
match app_state
175+
.settings_repository
176+
.get("gateway.session_overrides_require_approval")
177+
.await
178+
{
179+
Ok(Some(v)) => Ok(matches!(v.as_str(), "true" | "1")),
180+
_ => Ok(false),
181+
}
182+
}
183+
184+
/// Flip the session-override approval gate. Takes effect on the next
185+
/// session-scope enable/disable meta-tool call.
186+
#[tauri::command]
187+
pub async fn set_session_overrides_require_approval(
188+
require_approval: bool,
189+
app_state: State<'_, AppState>,
190+
) -> Result<(), String> {
191+
app_state
192+
.settings_repository
193+
.set(
194+
"gateway.session_overrides_require_approval",
195+
if require_approval { "true" } else { "false" },
196+
)
197+
.await
198+
.map_err(|e| format!("Failed to save session_overrides_require_approval: {}", e))?;
199+
info!(
200+
"[Settings] session_overrides_require_approval = {}",
201+
require_approval
202+
);
203+
Ok(())
204+
}
205+
168206
#[cfg(test)]
169207
mod tests {
170208
use super::*;

apps/desktop/src-tauri/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,10 @@ pub fn run() {
912912
commands::revoke_meta_tool_grant,
913913
commands::get_meta_tools_enabled,
914914
commands::set_meta_tools_enabled,
915+
commands::get_session_overrides_require_approval,
916+
commands::set_session_overrides_require_approval,
917+
commands::list_session_overrides,
918+
commands::clear_session_overrides,
915919
// Config export commands
916920
commands::preview_config_export,
917921
commands::export_config_to_file,

apps/desktop/src/features/settings/SettingsPage.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ import {
3636
import { useAppStore, useTheme, useAnalyticsEnabled } from '@/stores';
3737
import { UpdateChecker } from './UpdateChecker';
3838
import { getMetaToolsEnabled, setMetaToolsEnabled } from '@/lib/api/metaTools';
39+
import {
40+
getSessionOverridesRequireApproval,
41+
setSessionOverridesRequireApproval,
42+
} from '@/lib/api/sessionOverrides';
3943
import { MetaToolAuditLog, MetaToolGrantsPanel } from '@/features/metaTools';
4044
import { useGatewayControl } from '@/features/gateway/useGatewayControl';
4145
import { CONTRIBUTE, openExternal } from '@/lib/contribute';
@@ -78,6 +82,10 @@ export function SettingsPage() {
7882
// Meta-tools master switch — gates the entire `mcpmux_*` namespace.
7983
const [metaToolsEnabled, setMetaToolsEnabledState] = useState<boolean>(true);
8084
const [loadingMetaTools, setLoadingMetaTools] = useState(true);
85+
const [sessionOverridesRequireApproval, setSessionOverridesRequireApprovalState] =
86+
useState<boolean>(false);
87+
const [loadingSessionOverrideApproval, setLoadingSessionOverrideApproval] =
88+
useState(true);
8189

8290
// Gateway port — persisted user override, the default the app ships
8391
// with, and the port the currently-running gateway is bound to. When
@@ -181,6 +189,12 @@ export function SettingsPage() {
181189
.then((v) => setMetaToolsEnabledState(v))
182190
.catch((e) => console.error('Failed to load meta_tools_enabled', e))
183191
.finally(() => setLoadingMetaTools(false));
192+
getSessionOverridesRequireApproval()
193+
.then((v) => setSessionOverridesRequireApprovalState(v))
194+
.catch((e) =>
195+
console.error('Failed to load session_overrides_require_approval', e)
196+
)
197+
.finally(() => setLoadingSessionOverrideApproval(false));
184198
}, []);
185199

186200
const handleToggleMetaTools = async (next: boolean) => {
@@ -200,6 +214,23 @@ export function SettingsPage() {
200214
}
201215
};
202216

217+
const handleToggleSessionOverrideApproval = async (next: boolean) => {
218+
const previous = sessionOverridesRequireApproval;
219+
setSessionOverridesRequireApprovalState(next);
220+
try {
221+
await setSessionOverridesRequireApproval(next);
222+
success(
223+
next ? 'Session overrides require approval' : 'Session overrides auto-allowed',
224+
next
225+
? 'mcpmux_enable_server / mcpmux_disable_server (session scope) will prompt before applying.'
226+
: 'Session-scope enable/disable applies immediately without a dialog.'
227+
);
228+
} catch (e) {
229+
setSessionOverridesRequireApprovalState(previous);
230+
error('Failed to save setting', e instanceof Error ? e.message : String(e));
231+
}
232+
};
233+
203234
// Load logs path on mount
204235
useEffect(() => {
205236
const loadLogsPath = async () => {
@@ -632,6 +663,29 @@ export function SettingsPage() {
632663
data-testid="meta-tools-enabled-switch"
633664
/>
634665
</div>
666+
<div className="flex items-center justify-between gap-4">
667+
<div className="flex items-start gap-3 flex-1 min-w-0">
668+
<AlertCircle className="h-5 w-5 mt-0.5 text-[rgb(var(--muted))] flex-shrink-0" />
669+
<div>
670+
<label className="text-sm font-medium">
671+
Require approval for session-scope overrides
672+
</label>
673+
<p className="text-xs text-[rgb(var(--muted))] mt-1">
674+
When on,{' '}
675+
<code className="font-mono">mcpmux_enable_server</code> /{' '}
676+
<code className="font-mono">mcpmux_disable_server</code> with{' '}
677+
<code className="font-mono">scope: &quot;session&quot;</code> show the native
678+
approval dialog. Workspace-scope writes always require approval.
679+
</p>
680+
</div>
681+
</div>
682+
<Switch
683+
checked={sessionOverridesRequireApproval}
684+
onCheckedChange={handleToggleSessionOverrideApproval}
685+
disabled={loadingSessionOverrideApproval || !metaToolsEnabled}
686+
data-testid="session-overrides-require-approval-switch"
687+
/>
688+
</div>
635689
<MetaToolGrantsPanel />
636690
<MetaToolAuditLog />
637691
</CardContent>

0 commit comments

Comments
 (0)