Skip to content

Commit 62e179c

Browse files
committed
feat(gateway): push tools/list_changed when Tool Optimization switch toggles
The mcpmux_* (Tool Optimization) master switch is appended to every session regardless of space, but flipping it previously took effect only on a client's next list_tools. Add DomainEvent::MetaToolsEnabledChanged, emit it from set_meta_tools_enabled, and have MCPNotifier fan out tools/list_changed to every connected session (global, cross-space) so the mcpmux_* namespace appears / disappears immediately. Also forwarded to the desktop UI as `meta-tools-changed`. Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent e403132 commit 62e179c

4 files changed

Lines changed: 104 additions & 1 deletion

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,15 @@ fn map_domain_event_to_ui(event: &DomainEvent) -> (&'static str, serde_json::Val
699699
"space_id": space_id,
700700
}),
701701
),
702+
703+
// The global Tool Optimization (mcpmux_*) master switch flipped. The
704+
// gateway-side MCPNotifier handles the `tools/list_changed` push to
705+
// connected MCP clients; this forwards the new value to the desktop UI
706+
// so any open Built-in Servers / Settings view reflects it live.
707+
DomainEvent::MetaToolsEnabledChanged { enabled } => (
708+
"meta-tools-changed",
709+
serde_json::json!({ "enabled": enabled }),
710+
),
702711
}
703712
}
704713

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
//! Settings commands for auto-start and system tray behavior
22
3+
use std::sync::Arc;
4+
5+
use mcpmux_core::DomainEvent;
36
use serde::{Deserialize, Serialize};
47
use tauri::State;
58
use tauri_plugin_autostart::AutoLaunchManager;
9+
use tokio::sync::RwLock;
610
use tracing::{debug, info};
711

12+
use super::gateway::GatewayAppState;
813
use crate::state::AppState;
914

1015
/// Startup and system tray settings
@@ -152,6 +157,7 @@ pub async fn get_meta_tools_enabled(app_state: State<'_, AppState>) -> Result<bo
152157
pub async fn set_meta_tools_enabled(
153158
enabled: bool,
154159
app_state: State<'_, AppState>,
160+
gateway_state: State<'_, Arc<RwLock<GatewayAppState>>>,
155161
) -> Result<(), String> {
156162
app_state
157163
.settings_repository
@@ -162,6 +168,20 @@ pub async fn set_meta_tools_enabled(
162168
.await
163169
.map_err(|e| format!("Failed to save meta_tools_enabled: {}", e))?;
164170
info!("[Settings] meta_tools_enabled = {}", enabled);
171+
172+
// Push tools/list_changed to every connected session so the mcpmux_*
173+
// namespace appears / disappears immediately instead of on their next
174+
// list_tools. Best-effort: the gateway not running (no subscribers) is a
175+
// normal condition and must not fail the toggle.
176+
{
177+
let gw_state = gateway_state.read().await;
178+
if let Some(ref gw) = gw_state.gateway_state {
179+
gw.read()
180+
.await
181+
.emit_domain_event(DomainEvent::MetaToolsEnabledChanged { enabled });
182+
}
183+
}
184+
165185
Ok(())
166186
}
167187

crates/mcpmux-core/src/domain/event.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,16 @@ pub enum DomainEvent {
395395
/// Redacted summary of the payload the LLM supplied (no secrets).
396396
summary: String,
397397
},
398+
399+
// ════════════════════════════════════════════════════════════════════════
400+
// BUILT-IN SERVERS
401+
// ════════════════════════════════════════════════════════════════════════
402+
/// The global "Tool Optimization" (self-management `mcpmux_*`) master
403+
/// switch was toggled. The meta-tools are appended to *every* session's
404+
/// `tools/list` regardless of space, so flipping this changes what every
405+
/// connected client sees — the notifier re-pushes `tools/list_changed`
406+
/// to all sessions rather than scoping to one space.
407+
MetaToolsEnabledChanged { enabled: bool },
398408
}
399409

400410
// ============================================================================
@@ -435,6 +445,7 @@ impl DomainEvent {
435445
Self::WorkspaceNeedsBinding { .. } => "workspace_needs_binding",
436446
Self::SessionRootsChanged => "session_roots_changed",
437447
Self::MetaToolInvoked { .. } => "meta_tool_invoked",
448+
Self::MetaToolsEnabledChanged { .. } => "meta_tools_enabled_changed",
438449
}
439450
}
440451

@@ -462,6 +473,9 @@ impl DomainEvent {
462473
| Self::ResourcesChanged { .. } => true,
463474
// Binding changes reshuffle every peer's resolution in the space
464475
Self::WorkspaceBindingChanged { .. } => true,
476+
// The meta-tools master switch adds/removes the mcpmux_* namespace
477+
// from every session's tool list.
478+
Self::MetaToolsEnabledChanged { .. } => true,
465479
// WorkspaceNeedsBinding is a UI prompt — doesn't itself change what
466480
// tools a client sees, just invites the user to configure.
467481
// All other events don't affect MCP capabilities
@@ -502,7 +516,8 @@ impl DomainEvent {
502516
| Self::GatewayStarted { .. }
503517
| Self::GatewayStopped
504518
| Self::SessionRootsChanged
505-
| Self::MetaToolInvoked { .. } => None,
519+
| Self::MetaToolInvoked { .. }
520+
| Self::MetaToolsEnabledChanged { .. } => None,
506521
}
507522
}
508523

crates/mcpmux-gateway/src/consumers/mcp_notifier.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,19 @@ impl MCPNotifier {
458458
self.notify_all_list_changed(space_id, true).await;
459459
}
460460

461+
// The global meta-tools master switch flipped. The mcpmux_* tools
462+
// are appended to every session regardless of space, so a per-space
463+
// fanout would miss sessions in other spaces. Push tools/list_changed
464+
// to every session so each refetches and the namespace appears /
465+
// disappears immediately instead of on its next list_tools.
466+
DomainEvent::MetaToolsEnabledChanged { enabled } => {
467+
info!(
468+
enabled,
469+
"[MCPNotifier] 📨 MetaToolsEnabledChanged - notifying ALL sessions (tools/list_changed)"
470+
);
471+
self.notify_all_sessions_tools_changed().await;
472+
}
473+
461474
// ============ Backend Server Notifications (Pass-through with Throttling) ============
462475
// IMPORTANT: These events come from backend MCP servers. Some servers are "chatty" and
463476
// emit list_changed when queried (not just when features actually change). Our throttling
@@ -725,6 +738,52 @@ impl MCPNotifier {
725738
}
726739
}
727740

741+
/// Push `tools/list_changed` to EVERY session with an active stream,
742+
/// regardless of space. Used for global, cross-space changes (the
743+
/// meta-tools master switch) that alter every session's tool list.
744+
///
745+
/// Deliberately bypasses the per-space content hash + throttle: those are
746+
/// keyed by space-feature content and have no visibility into the
747+
/// `mcpmux_*` namespace, so they'd wrongly dedupe this away. The trigger
748+
/// is a rare, explicit user action, so unconditional fanout is safe.
749+
async fn notify_all_sessions_tools_changed(&self) {
750+
if DISABLE_ALL_NOTIFICATIONS {
751+
trace!("[MCPNotifier] 🚫 NOTIFICATIONS DISABLED - skipping global tools/list_changed");
752+
return;
753+
}
754+
755+
let targets: Vec<(String, String, Arc<Peer<RoleServer>>)> = {
756+
let sessions = self.sessions.read();
757+
sessions
758+
.iter()
759+
.filter(|(_, e)| e.has_active_stream)
760+
.map(|(sid, e)| (sid.clone(), e.client_id.clone(), e.peer.clone()))
761+
.collect()
762+
};
763+
764+
if targets.is_empty() {
765+
debug!("[MCPNotifier] No sessions with active streams for global tools/list_changed");
766+
return;
767+
}
768+
769+
info!(
770+
session_count = targets.len(),
771+
"[MCPNotifier] 📤 Sending tools/list_changed to all {} session(s)",
772+
targets.len()
773+
);
774+
775+
for (session_id, client_id, peer) in targets {
776+
if let Err(e) = peer.notify_tool_list_changed().await {
777+
warn!(
778+
%session_id,
779+
%client_id,
780+
error = ?e,
781+
"[MCPNotifier] Failed to send global tools/list_changed to session"
782+
);
783+
}
784+
}
785+
}
786+
728787
/// Internal method to actually send tools/list_changed notification (no throttling)
729788
async fn send_tools_list_changed(&self, space_id: Uuid, _timestamp: Instant) {
730789
// DEBUG: Kill switch to disable all notifications

0 commit comments

Comments
 (0)