Skip to content

Commit c3dfba2

Browse files
author
Mohammod Al Amin Ashik
committed
backup
1 parent feaf27f commit c3dfba2

6 files changed

Lines changed: 337 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ impl ServerHandler for McpMuxGatewayHandler {
102102
protocol_version: Default::default(),
103103
capabilities: ServerCapabilities::builder()
104104
.enable_tools_with(ToolsCapability {
105-
list_changed: Some(false), // Stateless mode - no notifications
105+
list_changed: Some(true),
106106
})
107107
.enable_prompts_with(PromptsCapability {
108-
list_changed: Some(false), // Stateless mode - no notifications
108+
list_changed: Some(true),
109109
})
110110
.enable_resources_with(ResourcesCapability {
111111
subscribe: Some(false),
112-
list_changed: Some(false), // Stateless mode - no notifications
112+
list_changed: Some(true),
113113
})
114114
.build(),
115115
server_info: Implementation {
@@ -150,19 +150,34 @@ impl ServerHandler for McpMuxGatewayHandler {
150150
}
151151

152152
async fn on_initialized(&self, context: NotificationContext<RoleServer>) {
153-
// Silently process - entry already logged in oauth_middleware
154-
let _oauth_ctx = match self.get_oauth_context(&context.extensions) {
153+
let oauth_ctx = match self.get_oauth_context(&context.extensions) {
155154
Ok(ctx) => ctx,
156155
Err(e) => {
157-
warn!("Failed to extract OAuth context: {}", e);
156+
warn!("Failed to extract OAuth context on_initialized: {}", e);
158157
return;
159158
}
160159
};
161160

162-
// In stateless mode:
163-
// - No session tracking
164-
// - No notification registration
165-
// - Each request is independent
161+
// Register peer with MCPNotifier for list_changed notification delivery
162+
let peer = std::sync::Arc::new(context.peer);
163+
self.notification_bridge
164+
.register_peer(oauth_ctx.client_id.clone(), peer);
165+
166+
// Mark the client stream as active immediately - RMCP's session transport
167+
// handles SSE streaming and message caching internally
168+
self.notification_bridge
169+
.mark_client_stream_active(&oauth_ctx.client_id);
170+
171+
// Pre-populate feature hashes to prevent spurious first notifications
172+
self.notification_bridge
173+
.prime_hashes_for_space(oauth_ctx.space_id)
174+
.await;
175+
176+
info!(
177+
client_id = %oauth_ctx.client_id,
178+
space_id = %oauth_ctx.space_id,
179+
"Client initialized - peer registered for notifications"
180+
);
166181
}
167182

168183
async fn list_tools(

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -240,29 +240,27 @@ impl GatewayServer {
240240
let handler =
241241
McpMuxGatewayHandler::new(Arc::new(self.services.clone()), notification_bridge.clone());
242242

243-
// Create STATELESS MCP service
244-
// stateful_mode: false means:
245-
// - No Mcp-Session-Id header
246-
// - GET/DELETE return 405 automatically (no notification streams)
247-
// - Each POST is independent
248-
// - Avoids the stream management issues that caused connection loops
249-
// Trade-off: Cannot send list_changed notifications
243+
// Create STATEFUL MCP service (full Streamable HTTP per spec 2025-11-25)
244+
// stateful_mode: true means:
245+
// - Mcp-Session-Id header for session management
246+
// - GET endpoint for SSE streams (server-initiated notifications)
247+
// - DELETE endpoint for session termination
248+
// - list_changed notifications delivered via SSE
250249
let mcp_service = StreamableHttpService::new(
251250
move || {
252-
debug!("[Gateway] Creating handler instance for MCP request");
251+
debug!("[Gateway] Creating handler instance for MCP session");
253252
Ok(handler.clone())
254253
},
255254
LocalSessionManager::default().into(),
256255
StreamableHttpServerConfig {
257-
stateful_mode: false,
256+
stateful_mode: true,
258257
sse_keep_alive: Some(std::time::Duration::from_secs(30)),
259-
sse_retry: None,
258+
sse_retry: Some(std::time::Duration::from_secs(3)),
260259
cancellation_token: CancellationToken::new(),
261260
},
262261
);
263262

264263
// Wrap MCP service with OAuth middleware
265-
// In stateless mode, no session healing needed - rmcp handles 405 for GET/DELETE
266264
let mcp_routes =
267265
Router::new()
268266
.nest_service("/mcp", mcp_service)

tests/rust/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ url = "2.5"
4949
# Sync primitives for tests
5050
parking_lot = "0.12"
5151

52+
# RMCP for streamable HTTP transport tests
53+
rmcp = { version = "0.14.0", features = [
54+
"client",
55+
"server",
56+
"transport-streamable-http-server",
57+
"transport-streamable-http-client-reqwest",
58+
] }
59+
tokio-util = { version = "0.7", features = ["rt"] }
60+
axum = "0.8"
61+
5262
[lib]
5363
path = "src/lib.rs"
5464

@@ -71,3 +81,7 @@ path = "tests/oauth/mod.rs"
7181
[[test]]
7282
name = "integration"
7383
path = "tests/integration/mod.rs"
84+
85+
[[test]]
86+
name = "streamable_http"
87+
path = "tests/streamable_http/mod.rs"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! Streamable HTTP Transport Integration Tests
2+
//!
3+
//! Tests the full stateful Streamable HTTP transport with:
4+
//! - Session management (Mcp-Session-Id)
5+
//! - Server-initiated notifications (list_changed via SSE)
6+
//! - Proper protocol negotiation
7+
8+
mod notifications;

0 commit comments

Comments
 (0)