Skip to content

Commit e72b64e

Browse files
committed
feat(gateway): add SessionOverrideRegistry and list-path composition
Phase 1 of dynamic MCP toggling: per-session enable/disable sets wired through FeatureService list paths, session GC, and composition tests. Also fixes a DashMap deadlock in SessionRootsRegistry::record_resolution. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 5e04746 commit e72b64e

19 files changed

Lines changed: 478 additions & 76 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use tracing::{debug, info, trace, warn};
2626
use uuid::Uuid;
2727

2828
use crate::pool::FeatureService;
29-
use crate::services::FeatureSetResolverService;
29+
use crate::services::{FeatureSetResolverService, SessionOverrideRegistry};
3030

3131
/// MCP Notifier — sends `list_changed` notifications to connected sessions.
3232
///
@@ -58,6 +58,8 @@ pub struct MCPNotifier {
5858
feature_set_resolver: Arc<FeatureSetResolverService>,
5959
/// Feature service for calculating content hashes
6060
feature_service: Arc<FeatureService>,
61+
/// Session override registry — reaped alongside session roots.
62+
session_overrides: Arc<SessionOverrideRegistry>,
6163
/// Throttle tracker: (space_id, notification_type) -> last_sent_timestamp
6264
/// Prevents sending duplicate notifications within THROTTLE_WINDOW
6365
throttle_tracker: Arc<RwLock<HashMap<(Uuid, NotificationType), Instant>>>,
@@ -110,11 +112,13 @@ impl MCPNotifier {
110112
pub fn new(
111113
feature_set_resolver: Arc<FeatureSetResolverService>,
112114
feature_service: Arc<FeatureService>,
115+
session_overrides: Arc<SessionOverrideRegistry>,
113116
) -> Self {
114117
Self {
115118
sessions: Arc::new(RwLock::new(HashMap::new())),
116119
feature_set_resolver,
117120
feature_service,
121+
session_overrides,
118122
throttle_tracker: Arc::new(RwLock::new(HashMap::new())),
119123
state_hashes: Arc::new(RwLock::new(HashMap::new())),
120124
}
@@ -354,6 +358,7 @@ impl MCPNotifier {
354358
// sessions that no longer exist.
355359
for sid in &dead {
356360
self.feature_set_resolver.session_roots().remove(sid);
361+
self.session_overrides.remove(sid);
357362
}
358363
info!(
359364
reaped = dead.len(),

crates/mcpmux-gateway/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub use pool::{
7676
};
7777

7878
// Services module
79-
pub use services::{EventEmitter, GrantService, PrefixCacheService};
79+
pub use services::{EventEmitter, GrantService, PrefixCacheService, SessionOverrideRegistry};
8080

8181
// MCP module (rmcp-based implementation)
8282
pub use mcp::McpMuxGatewayHandler;

crates/mcpmux-gateway/src/mcp/handler.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ impl ServerHandler for McpMuxGatewayHandler {
676676
.services
677677
.pool_services
678678
.feature_service
679-
.get_tools_for_grants(&space_id.to_string(), &feature_set_ids)
679+
.get_tools_for_grants(&space_id.to_string(), &feature_set_ids, session_id_owned.as_deref())
680680
.await
681681
.map_err(|e| McpError::internal_error(format!("Failed to get tools: {}", e), None))?;
682682

@@ -861,7 +861,11 @@ impl ServerHandler for McpMuxGatewayHandler {
861861
.services
862862
.pool_services
863863
.feature_service
864-
.get_prompts_for_grants(&space_id.to_string(), &feature_set_ids)
864+
.get_prompts_for_grants(
865+
&space_id.to_string(),
866+
&feature_set_ids,
867+
session_id_owned.as_deref(),
868+
)
865869
.await
866870
.map_err(|e| McpError::internal_error(format!("Failed to get prompts: {}", e), None))?;
867871

@@ -897,11 +901,9 @@ impl ServerHandler for McpMuxGatewayHandler {
897901
let oauth_ctx = self
898902
.get_oauth_context(&context.extensions)
899903
.map_err(|e| McpError::invalid_params(e.to_string(), None))?;
904+
let session_id_owned = extract_session_id(&context.extensions);
900905
let (space_id, feature_set_ids) = self
901-
.resolve_routing(
902-
extract_session_id(&context.extensions).as_deref(),
903-
&oauth_ctx.client_id,
904-
)
906+
.resolve_routing(session_id_owned.as_deref(), &oauth_ctx.client_id)
905907
.await?;
906908

907909
let (server_id, prompt_name) = self
@@ -916,7 +918,11 @@ impl ServerHandler for McpMuxGatewayHandler {
916918
.services
917919
.pool_services
918920
.feature_service
919-
.get_prompts_for_grants(&space_id.to_string(), &feature_set_ids)
921+
.get_prompts_for_grants(
922+
&space_id.to_string(),
923+
&feature_set_ids,
924+
session_id_owned.as_deref(),
925+
)
920926
.await
921927
.map_err(|e| {
922928
McpError::internal_error(format!("Failed to verify authorization: {}", e), None)
@@ -972,7 +978,11 @@ impl ServerHandler for McpMuxGatewayHandler {
972978
.services
973979
.pool_services
974980
.feature_service
975-
.get_resources_for_grants(&space_id.to_string(), &feature_set_ids)
981+
.get_resources_for_grants(
982+
&space_id.to_string(),
983+
&feature_set_ids,
984+
session_id_owned.as_deref(),
985+
)
976986
.await
977987
.map_err(|e| {
978988
McpError::internal_error(format!("Failed to get resources: {}", e), None)
@@ -1006,11 +1016,9 @@ impl ServerHandler for McpMuxGatewayHandler {
10061016
let oauth_ctx = self
10071017
.get_oauth_context(&context.extensions)
10081018
.map_err(|e| McpError::invalid_params(e.to_string(), None))?;
1019+
let session_id_owned = extract_session_id(&context.extensions);
10091020
let (space_id, feature_set_ids) = self
1010-
.resolve_routing(
1011-
extract_session_id(&context.extensions).as_deref(),
1012-
&oauth_ctx.client_id,
1013-
)
1021+
.resolve_routing(session_id_owned.as_deref(), &oauth_ctx.client_id)
10141022
.await?;
10151023

10161024
let server_id = self
@@ -1030,7 +1038,11 @@ impl ServerHandler for McpMuxGatewayHandler {
10301038
.services
10311039
.pool_services
10321040
.feature_service
1033-
.get_resources_for_grants(&space_id.to_string(), &feature_set_ids)
1041+
.get_resources_for_grants(
1042+
&space_id.to_string(),
1043+
&feature_set_ids,
1044+
session_id_owned.as_deref(),
1045+
)
10341046
.await
10351047
.map_err(|e| {
10361048
McpError::internal_error(format!("Failed to verify authorization: {}", e), None)

crates/mcpmux-gateway/src/pool/features/facade.rs

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! Feature Service Facade - Unified API delegating to specialized services
22
33
use anyhow::Result;
4+
use std::collections::HashSet;
45
use std::sync::Arc;
56

67
use crate::pool::instance::McpClient;
7-
use crate::services::PrefixCacheService;
8+
use crate::services::{PrefixCacheService, SessionOverrideRegistry};
89
use mcpmux_core::{FeatureSetRepository, FeatureType, ServerFeature, ServerFeatureRepository};
910

1011
use super::{
@@ -16,13 +17,15 @@ pub struct FeatureService {
1617
discovery: Arc<FeatureDiscoveryService>,
1718
resolution: Arc<FeatureResolutionService>,
1819
routing: Arc<FeatureRoutingService>,
20+
session_overrides: Arc<SessionOverrideRegistry>,
1921
}
2022

2123
impl FeatureService {
2224
pub fn new(
2325
feature_repo: Arc<dyn ServerFeatureRepository>,
2426
feature_set_repo: Arc<dyn FeatureSetRepository>,
2527
prefix_cache: Arc<PrefixCacheService>,
28+
session_overrides: Arc<SessionOverrideRegistry>,
2629
) -> Self {
2730
let discovery = Arc::new(FeatureDiscoveryService::new(feature_repo.clone()));
2831

@@ -41,6 +44,7 @@ impl FeatureService {
4144
discovery,
4245
resolution,
4346
routing,
47+
session_overrides,
4448
}
4549
}
4650

@@ -86,35 +90,98 @@ impl FeatureService {
8690
.await
8791
}
8892

89-
// Type-specific helpers
93+
/// Resolve granted feature sets to tools, applying session server overrides.
9094
pub async fn get_tools_for_grants(
9195
&self,
9296
space_id: &str,
9397
feature_set_ids: &[String],
98+
session_id: Option<&str>,
9499
) -> Result<Vec<ServerFeature>> {
95-
self.resolution
96-
.resolve_feature_sets(space_id, feature_set_ids, Some(FeatureType::Tool))
97-
.await
100+
self.get_features_for_grants(
101+
space_id,
102+
feature_set_ids,
103+
session_id,
104+
Some(FeatureType::Tool),
105+
)
106+
.await
98107
}
99108

109+
/// Resolve granted feature sets to prompts, applying session server overrides.
100110
pub async fn get_prompts_for_grants(
101111
&self,
102112
space_id: &str,
103113
feature_set_ids: &[String],
114+
session_id: Option<&str>,
104115
) -> Result<Vec<ServerFeature>> {
105-
self.resolution
106-
.resolve_feature_sets(space_id, feature_set_ids, Some(FeatureType::Prompt))
107-
.await
116+
self.get_features_for_grants(
117+
space_id,
118+
feature_set_ids,
119+
session_id,
120+
Some(FeatureType::Prompt),
121+
)
122+
.await
108123
}
109124

125+
/// Resolve granted feature sets to resources, applying session server overrides.
110126
pub async fn get_resources_for_grants(
111127
&self,
112128
space_id: &str,
113129
feature_set_ids: &[String],
130+
session_id: Option<&str>,
114131
) -> Result<Vec<ServerFeature>> {
115-
self.resolution
116-
.resolve_feature_sets(space_id, feature_set_ids, Some(FeatureType::Resource))
117-
.await
132+
self.get_features_for_grants(
133+
space_id,
134+
feature_set_ids,
135+
session_id,
136+
Some(FeatureType::Resource),
137+
)
138+
.await
139+
}
140+
141+
/// Shared list materialization: binding FS resolution + session overrides.
142+
async fn get_features_for_grants(
143+
&self,
144+
space_id: &str,
145+
feature_set_ids: &[String],
146+
session_id: Option<&str>,
147+
filter_type: Option<FeatureType>,
148+
) -> Result<Vec<ServerFeature>> {
149+
let binding_features = self
150+
.resolution
151+
.resolve_feature_sets(space_id, feature_set_ids, filter_type.clone())
152+
.await?;
153+
154+
let Some(session_id) = session_id else {
155+
return Ok(binding_features);
156+
};
157+
158+
let enabled = self.session_overrides.enabled_set(session_id);
159+
let disabled = self.session_overrides.disabled_set(session_id);
160+
161+
if enabled.is_empty() && disabled.is_empty() {
162+
return Ok(binding_features);
163+
}
164+
165+
let mut binding_servers: HashSet<String> = binding_features
166+
.iter()
167+
.map(|f| f.server_id.clone())
168+
.collect();
169+
binding_servers.extend(enabled.iter().cloned());
170+
binding_servers.retain(|server_id| !disabled.contains(server_id));
171+
172+
if binding_servers.is_empty() {
173+
return Ok(Vec::new());
174+
}
175+
176+
let all_features = self
177+
.resolution
178+
.get_all_features_for_space(space_id, filter_type)
179+
.await?;
180+
181+
Ok(all_features
182+
.into_iter()
183+
.filter(|f| f.is_available && binding_servers.contains(&f.server_id))
184+
.collect())
118185
}
119186

120187
// Delegate to FeatureRoutingService (with type-specific helpers)

crates/mcpmux-gateway/src/pool/routing.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ impl RoutingService {
8484
&self,
8585
space_id: Uuid,
8686
feature_set_ids: &[String],
87+
session_id: Option<&str>,
8788
) -> Result<Vec<RoutedTool>> {
8889
let space_id_str = space_id.to_string();
8990

9091
// Resolve feature sets to allowed features
9192
let allowed_features = self
9293
.feature_service
93-
.get_tools_for_grants(&space_id_str, feature_set_ids)
94+
.get_tools_for_grants(&space_id_str, feature_set_ids, session_id)
9495
.await?;
9596

9697
// Filter to just tools
@@ -119,12 +120,13 @@ impl RoutingService {
119120
&self,
120121
space_id: Uuid,
121122
feature_set_ids: &[String],
123+
session_id: Option<&str>,
122124
) -> Result<Vec<RoutedPrompt>> {
123125
let space_id_str = space_id.to_string();
124126

125127
let allowed_features = self
126128
.feature_service
127-
.get_prompts_for_grants(&space_id_str, feature_set_ids)
129+
.get_prompts_for_grants(&space_id_str, feature_set_ids, session_id)
128130
.await?;
129131

130132
let prompts: Vec<RoutedPrompt> = allowed_features
@@ -151,12 +153,13 @@ impl RoutingService {
151153
&self,
152154
space_id: Uuid,
153155
feature_set_ids: &[String],
156+
session_id: Option<&str>,
154157
) -> Result<Vec<RoutedResource>> {
155158
let space_id_str = space_id.to_string();
156159

157160
let allowed_features = self
158161
.feature_service
159-
.get_resources_for_grants(&space_id_str, feature_set_ids)
162+
.get_resources_for_grants(&space_id_str, feature_set_ids, session_id)
160163
.await?;
161164

162165
let resources: Vec<RoutedResource> = allowed_features

crates/mcpmux-gateway/src/pool/service_factory.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ impl ServiceFactory {
4444
deps: &GatewayDependencies,
4545
event_tx: tokio::sync::broadcast::Sender<DomainEvent>,
4646
prefix_cache: Arc<crate::services::PrefixCacheService>,
47+
session_overrides: Arc<crate::services::SessionOverrideRegistry>,
4748
) -> PoolServices {
4849
// TokenService - single source of truth for token management
4950
let token_service = Arc::new(TokenService::new(
@@ -80,7 +81,8 @@ impl ServiceFactory {
8081
let feature_service = Arc::new(FeatureService::new(
8182
deps.feature_repo.clone(),
8283
deps.feature_set_repo.clone(),
83-
prefix_cache.clone(), // Clone here since we use it again below
84+
prefix_cache.clone(),
85+
session_overrides,
8486
));
8587

8688
// ServerManager - event-driven orchestrator for server state

crates/mcpmux-gateway/src/server/handlers.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,23 +1087,23 @@ pub async fn oauth_get_client_features(
10871087
.services
10881088
.pool_services
10891089
.feature_service
1090-
.get_tools_for_grants(&space_id_str, &feature_set_ids)
1090+
.get_tools_for_grants(&space_id_str, &feature_set_ids, None)
10911091
.await
10921092
.unwrap_or_default();
10931093

10941094
let prompts = state
10951095
.services
10961096
.pool_services
10971097
.feature_service
1098-
.get_prompts_for_grants(&space_id_str, &feature_set_ids)
1098+
.get_prompts_for_grants(&space_id_str, &feature_set_ids, None)
10991099
.await
11001100
.unwrap_or_default();
11011101

11021102
let resources = state
11031103
.services
11041104
.pool_services
11051105
.feature_service
1106-
.get_resources_for_grants(&space_id_str, &feature_set_ids)
1106+
.get_resources_for_grants(&space_id_str, &feature_set_ids, None)
11071107
.await
11081108
.unwrap_or_default();
11091109

crates/mcpmux-gateway/src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ impl GatewayServer {
231231
let notification_bridge = Arc::new(MCPNotifier::new(
232232
self.services.feature_set_resolver.clone(),
233233
self.services.pool_services.feature_service.clone(),
234+
self.services.session_overrides.clone(),
234235
));
235236

236237
// Start listening to DomainEvents

0 commit comments

Comments
 (0)