From 5b19b89db3fe738f3cf6e604f1f63e85eca77623 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Tue, 3 Mar 2026 15:42:31 +0800 Subject: [PATCH 1/3] refactor: remove client active/inactive status tracking The has_active_tokens field and clients_with_tokens in-memory set tracked whether clients had been issued tokens to show Active/Inactive status badges in the UI. This status was unreliable (in-memory only, lost on restart) and not needed. Removes the status badges from the clients page, the tracking state from the gateway, and the field from both gateway and Tauri response structs. Signed-off-by: Mohammod Al Amin Ashik --- apps/desktop/src-tauri/src/commands/oauth.rs | 43 +++++++------- .../src/features/clients/ClientsPage.tsx | 39 ++----------- apps/desktop/src/lib/api/gateway.ts | 1 - crates/mcpmux-gateway/src/server/handlers.rs | 56 +++++++------------ crates/mcpmux-gateway/src/server/state.rs | 3 - scripts/take-screenshots.cjs | 4 +- tests/e2e/specs/clients.spec.ts | 12 ++-- 7 files changed, 52 insertions(+), 106 deletions(-) diff --git a/apps/desktop/src-tauri/src/commands/oauth.rs b/apps/desktop/src-tauri/src/commands/oauth.rs index 48bcb6e5..2eba910a 100644 --- a/apps/desktop/src-tauri/src/commands/oauth.rs +++ b/apps/desktop/src-tauri/src/commands/oauth.rs @@ -668,28 +668,25 @@ pub async fn get_oauth_clients( // Map to response format let client_infos: Vec = clients .into_iter() - .map(|client| { - OAuthClientInfo { - client_id: client.client_id, - registration_type: client.registration_type.as_str().to_string(), - client_name: client.client_name, - client_alias: client.client_alias, - redirect_uris: client.redirect_uris, - scope: client.scope, - approved: client.approved, - logo_uri: client.logo_uri, - client_uri: client.client_uri, - software_id: client.software_id, - software_version: client.software_version, - metadata_url: client.metadata_url, - metadata_cached_at: client.metadata_cached_at, - metadata_cache_ttl: client.metadata_cache_ttl, - connection_mode: client.connection_mode, - locked_space_id: client.locked_space_id, - last_seen: client.last_seen, - created_at: client.created_at, - has_active_tokens: false, // TODO: Check if client has active tokens - } + .map(|client| OAuthClientInfo { + client_id: client.client_id, + registration_type: client.registration_type.as_str().to_string(), + client_name: client.client_name, + client_alias: client.client_alias, + redirect_uris: client.redirect_uris, + scope: client.scope, + approved: client.approved, + logo_uri: client.logo_uri, + client_uri: client.client_uri, + software_id: client.software_id, + software_version: client.software_version, + metadata_url: client.metadata_url, + metadata_cached_at: client.metadata_cached_at, + metadata_cache_ttl: client.metadata_cache_ttl, + connection_mode: client.connection_mode, + locked_space_id: client.locked_space_id, + last_seen: client.last_seen, + created_at: client.created_at, }) .collect(); @@ -763,7 +760,6 @@ pub struct OAuthClientInfo { pub locked_space_id: Option, pub last_seen: Option, pub created_at: String, - pub has_active_tokens: bool, } /// Request to update client settings @@ -837,7 +833,6 @@ pub async fn update_oauth_client( locked_space_id: updated_client.locked_space_id, last_seen: updated_client.last_seen, created_at: updated_client.created_at, - has_active_tokens: false, }) } diff --git a/apps/desktop/src/features/clients/ClientsPage.tsx b/apps/desktop/src/features/clients/ClientsPage.tsx index 475d5770..3d158488 100644 --- a/apps/desktop/src/features/clients/ClientsPage.tsx +++ b/apps/desktop/src/features/clients/ClientsPage.tsx @@ -11,8 +11,6 @@ import { Lock, Unlock, HelpCircle, - Wifi, - WifiOff, RefreshCw, Settings, Trash2, @@ -649,21 +647,6 @@ export default function ClientsPage() { - {/* Status Badge */} -
- - {client.has_active_tokens ? ( - <> Active - ) : ( - <> Inactive - )} - -
- {/* Connection Mode */}
@@ -722,25 +705,11 @@ export default function ClientsPage() {
- {/* Quick Status */} -
- - {selectedClient.has_active_tokens ? ( - <> Connected - ) : ( - <> Inactive - )} + {selectedClient.software_version && ( + + v{selectedClient.software_version} - {selectedClient.software_version && ( - - v{selectedClient.software_version} - - )} -
+ )} {/* Scrollable Content */} diff --git a/apps/desktop/src/lib/api/gateway.ts b/apps/desktop/src/lib/api/gateway.ts index 03023ffe..fd3f0604 100644 --- a/apps/desktop/src/lib/api/gateway.ts +++ b/apps/desktop/src/lib/api/gateway.ts @@ -131,7 +131,6 @@ export interface OAuthClient { locked_space_id: string | null; last_seen: string | null; created_at: string; - has_active_tokens: boolean; } /** diff --git a/crates/mcpmux-gateway/src/server/handlers.rs b/crates/mcpmux-gateway/src/server/handlers.rs index 3e4129a4..22376b2c 100644 --- a/crates/mcpmux-gateway/src/server/handlers.rs +++ b/crates/mcpmux-gateway/src/server/handlers.rs @@ -602,12 +602,9 @@ pub async fn oauth_token( let client_id_for_tracking = pending.client_id.clone(); drop(gateway_state); - // Track that this client has active tokens and emit event + // Update last_seen and emit event { - let mut gateway_state = state.write().await; - gateway_state - .clients_with_tokens - .insert(client_id_for_tracking.clone()); + let gateway_state = state.read().await; // Update last_seen in database if let Some(repo) = gateway_state.inbound_client_repository() { @@ -949,7 +946,6 @@ pub struct OAuthClientInfoResponse { pub locked_space_id: Option, pub last_seen: Option, pub created_at: String, - pub has_active_tokens: bool, } /// List all registered OAuth clients @@ -969,28 +965,24 @@ pub async fn oauth_list_clients( Ok(db_clients) => { let clients: Vec = db_clients .into_iter() - .map(|c| { - let has_active = gateway_state.clients_with_tokens.contains(&c.client_id); - OAuthClientInfoResponse { - client_id: c.client_id, - registration_type: c.registration_type.as_str().to_string(), - client_name: c.client_name, - client_alias: c.client_alias, - redirect_uris: c.redirect_uris, - scope: c.scope, - logo_uri: c.logo_uri, - client_uri: c.client_uri, - software_id: c.software_id, - software_version: c.software_version, - metadata_url: c.metadata_url, - metadata_cached_at: c.metadata_cached_at, - metadata_cache_ttl: c.metadata_cache_ttl, - connection_mode: c.connection_mode, - locked_space_id: c.locked_space_id, - last_seen: c.last_seen, - created_at: c.created_at, - has_active_tokens: has_active, - } + .map(|c| OAuthClientInfoResponse { + client_id: c.client_id, + registration_type: c.registration_type.as_str().to_string(), + client_name: c.client_name, + client_alias: c.client_alias, + redirect_uris: c.redirect_uris, + scope: c.scope, + logo_uri: c.logo_uri, + client_uri: c.client_uri, + software_id: c.software_id, + software_version: c.software_version, + metadata_url: c.metadata_url, + metadata_cached_at: c.metadata_cached_at, + metadata_cache_ttl: c.metadata_cache_ttl, + connection_mode: c.connection_mode, + locked_space_id: c.locked_space_id, + last_seen: c.last_seen, + created_at: c.created_at, }) .collect(); info!("[OAuth] Listed {} clients from database", clients.len()); @@ -1213,9 +1205,6 @@ pub async fn oauth_update_client( .await { Ok(Some(client)) => { - let has_active = gateway_state - .clients_with_tokens - .contains(&client.client_id); let response = OAuthClientInfoResponse { client_id: client.client_id, registration_type: client.registration_type.as_str().to_string(), @@ -1234,7 +1223,6 @@ pub async fn oauth_update_client( locked_space_id: client.locked_space_id, last_seen: client.last_seen, created_at: client.created_at, - has_active_tokens: has_active, }; info!("[OAuth] Client updated: {}", response.client_id); Json(response).into_response() @@ -1264,7 +1252,7 @@ pub async fn oauth_delete_client( ) -> Response { info!("[OAuth] Deleting client: {}", client_id); - let mut gateway_state = state.write().await; + let gateway_state = state.read().await; let Some(repo) = gateway_state.inbound_client_repository() else { warn!("[OAuth] Database not available for client deletion"); @@ -1273,8 +1261,6 @@ pub async fn oauth_delete_client( match repo.delete_client(&client_id).await { Ok(true) => { - // Remove from active tokens set - gateway_state.clients_with_tokens.remove(&client_id); info!("[OAuth] Client deleted: {}", client_id); StatusCode::NO_CONTENT.into_response() } diff --git a/crates/mcpmux-gateway/src/server/state.rs b/crates/mcpmux-gateway/src/server/state.rs index 582f21af..d4a7baf1 100644 --- a/crates/mcpmux-gateway/src/server/state.rs +++ b/crates/mcpmux-gateway/src/server/state.rs @@ -51,8 +51,6 @@ pub struct GatewayState { pub oauth_tokens: HashMap, /// Pending authorization codes (code -> PendingAuthorization) pub pending_authorizations: HashMap, - /// Set of client_ids that have been issued tokens (for "active" status) - pub clients_with_tokens: std::collections::HashSet, /// JWT signing secret (for issuing access tokens) pub jwt_signing_secret: Option>, /// Database connection (for persistent OAuth storage) @@ -74,7 +72,6 @@ impl GatewayState { access_keys: HashMap::new(), oauth_tokens: HashMap::new(), pending_authorizations: HashMap::new(), - clients_with_tokens: std::collections::HashSet::new(), jwt_signing_secret: None, db: None, inbound_client_repository: None, diff --git a/scripts/take-screenshots.cjs b/scripts/take-screenshots.cjs index 25daf3bc..7a0fdb3f 100644 --- a/scripts/take-screenshots.cjs +++ b/scripts/take-screenshots.cjs @@ -138,8 +138,8 @@ const FEATURE_SETS = [ ]; const OAUTH_CLIENTS = [ - { client_id: 'cursor-001', registration_type: 'dcr', client_name: 'Cursor', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'cursor', software_version: '0.45.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T09:30:00Z', created_at: '2026-01-20T10:00:00Z', has_active_tokens: true }, - { client_id: 'vscode-001', registration_type: 'dcr', client_name: 'VS Code', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'vscode', software_version: '1.96.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T08:45:00Z', created_at: '2026-01-22T10:00:00Z', has_active_tokens: true }, + { client_id: 'cursor-001', registration_type: 'dcr', client_name: 'Cursor', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'cursor', software_version: '0.45.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T09:30:00Z', created_at: '2026-01-20T10:00:00Z' }, + { client_id: 'vscode-001', registration_type: 'dcr', client_name: 'VS Code', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'vscode', software_version: '1.96.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T08:45:00Z', created_at: '2026-01-22T10:00:00Z' }, ]; function mkRegistry(id, name, desc, alias, icon, categories, auth, transportType, publisher, caps, hostingType, badges) { diff --git a/tests/e2e/specs/clients.spec.ts b/tests/e2e/specs/clients.spec.ts index a1c5fa4e..d489b8f3 100644 --- a/tests/e2e/specs/clients.spec.ts +++ b/tests/e2e/specs/clients.spec.ts @@ -55,18 +55,18 @@ test.describe('Clients Page', () => { }); test.describe('Client Details', () => { - test('should show client connection status', async ({ page }) => { + test('should show client details', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); await page.locator('nav button:has-text("Clients")').click(); - + const clientCards = page.locator('[class*="rounded"][class*="border"]'); const count = await clientCards.count(); - + if (count > 0) { - // Clients should have status indicators - const statusIndicator = page.locator('[class*="bg-green"], [class*="bg-red"], text=/connected|active/i'); - // May or may not be visible + // Clients should have connection mode indicators + const firstCard = clientCards.first(); + await expect(firstCard).toBeVisible(); } }); From 6f912ebb3a618b775cb3bf115609296475213ea5 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Tue, 3 Mar 2026 23:59:31 +0800 Subject: [PATCH 2/3] chore: upgrade rmcp to v0.17.0 and reqwest to v0.13 - Update rmcp from v0.15.0 to v0.17.0 - Update reqwest from v0.12 to v0.13 (required by rmcp 0.17) - Add reqwest "form" feature (now optional in reqwest 0.13) - Rename reqwest "rustls-tls" feature to "rustls" (reqwest 0.13 rename) - Remove [patch.crates-io] for SSE channel fix (resolved upstream) - Add new StoredCredentials.token_received_at field (rmcp 0.17) - Add new StreamableHttpServerConfig.json_response field (rmcp 0.17) Signed-off-by: Mohammod Al Amin Ashik --- Cargo.lock | 218 +++++++----------- Cargo.toml | 9 +- .../src/pool/credential_store.rs | 5 + crates/mcpmux-gateway/src/server/mod.rs | 1 + tests/rust/Cargo.toml | 4 +- .../streamable_http/gateway_notifications.rs | 1 + .../tests/streamable_http/notifications.rs | 1 + 7 files changed, 95 insertions(+), 144 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04a26590..987df857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,6 +277,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -527,6 +549,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -594,6 +618,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -684,7 +717,7 @@ dependencies = [ "bitflags 2.10.0", "core-foundation 0.10.1", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -1324,15 +1357,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -1340,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -1354,12 +1378,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1375,6 +1393,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2002,22 +2026,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -2351,6 +2359,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -2611,7 +2629,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcpmux" -version = "0.2.2" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", @@ -2626,7 +2644,7 @@ dependencies = [ "notify", "notify-debouncer-mini", "open", - "reqwest 0.12.28", + "reqwest 0.13.2", "serde", "serde_json", "tauri", @@ -2650,7 +2668,7 @@ dependencies = [ [[package]] name = "mcpmux-core" -version = "0.2.2" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", @@ -2662,7 +2680,7 @@ dependencies = [ "glob", "lazy_static", "regex", - "reqwest 0.12.28", + "reqwest 0.13.2", "serde", "serde_json", "tempfile", @@ -2675,7 +2693,7 @@ dependencies = [ [[package]] name = "mcpmux-gateway" -version = "0.2.2" +version = "0.3.0" dependencies = [ "anyhow", "async-stream", @@ -2695,7 +2713,7 @@ dependencies = [ "open", "parking_lot", "rand 0.8.5", - "reqwest 0.12.28", + "reqwest 0.13.2", "rmcp", "serde", "serde_json", @@ -2715,13 +2733,13 @@ dependencies = [ [[package]] name = "mcpmux-mcp" -version = "0.2.2" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", "hex", "mcpmux-core", - "reqwest 0.12.28", + "reqwest 0.13.2", "ring", "rmcp", "serde", @@ -2734,7 +2752,7 @@ dependencies = [ [[package]] name = "mcpmux-storage" -version = "0.2.2" +version = "0.3.0" dependencies = [ "anyhow", "async-trait", @@ -2837,23 +2855,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.1.6", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.9.0" @@ -3266,56 +3267,12 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -3830,6 +3787,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -4104,21 +4062,15 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -4129,30 +4081,29 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots", ] [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -4161,13 +4112,16 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -4222,8 +4176,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.15.0" -source = "git+https://github.com/mcpmux/rust-sdk.git?branch=fix%2Fsse-channel-replacement-conflict#4fd9ae79f3b56d8346097c82ba5b3827f1b9fd21" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0ce46f9101dc911f07e1468084c057839d15b08040d110820c5513312ef56a" dependencies = [ "async-trait", "base64 0.22.1", @@ -4238,7 +4193,7 @@ dependencies = [ "pin-project-lite", "process-wrap", "rand 0.10.0", - "reqwest 0.12.28", + "reqwest 0.13.2", "rmcp-macros", "schemars 1.2.1", "serde", @@ -4256,8 +4211,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.15.0" -source = "git+https://github.com/mcpmux/rust-sdk.git?branch=fix%2Fsse-channel-replacement-conflict#4fd9ae79f3b56d8346097c82ba5b3827f1b9fd21" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abad6f5f46e220e3bda2fc90fd1ad64c1c2a2bd716d52c845eb5c9c64cda7542" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -4324,6 +4280,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -4338,7 +4295,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -4387,6 +4344,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5110,7 +5068,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.1", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -5350,7 +5308,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.13.1", + "reqwest 0.13.2", "rustls", "semver", "serde", @@ -5509,7 +5467,7 @@ dependencies = [ "os_pipe", "parking_lot", "pretty_assertions", - "reqwest 0.12.28", + "reqwest 0.13.2", "rmcp", "serde", "serde_json", @@ -5665,16 +5623,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -6274,9 +6222,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 966e8982..a174a8b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ os_pipe = "1" # MCP Protocol # NOTE: Never use local path dependency - E:\one-mcp\rust-sdk is for source lookup only -rmcp = { version = "0.15.0", features = [ +rmcp = { version = "0.17.0", features = [ "client", "server", "transport-io", @@ -69,7 +69,7 @@ rmcp = { version = "0.15.0", features = [ ] } # HTTP -reqwest = { version = "0.12", features = ["json", "rustls-tls"] } +reqwest = { version = "0.13", features = ["json", "form", "rustls"] } axum = { version = "0.8", features = ["macros"] } http-body-util = "0.1" @@ -84,9 +84,4 @@ lto = true codegen-units = 1 strip = true -# Temporary patch: fixes SSE channel replacement bug (notifications lost on reconnect) -# Remove once upstream merges https://github.com/modelcontextprotocol/rust-sdk/pull/660 -[patch.crates-io] -rmcp = { git = "https://github.com/mcpmux/rust-sdk.git", branch = "fix/sse-channel-replacement-conflict" } - diff --git a/crates/mcpmux-gateway/src/pool/credential_store.rs b/crates/mcpmux-gateway/src/pool/credential_store.rs index fdf20a06..ba15c0de 100644 --- a/crates/mcpmux-gateway/src/pool/credential_store.rs +++ b/crates/mcpmux-gateway/src/pool/credential_store.rs @@ -208,6 +208,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: None, }) } (Some(reg), None) => { @@ -219,6 +220,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: None, granted_scopes: Vec::new(), + token_received_at: None, }) } (None, Some(access)) => { @@ -231,6 +233,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: String::new(), token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: None, }) } (None, None) => { @@ -588,6 +591,7 @@ mod tests { client_id: "new-client-id".to_string(), token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: None, }; store.save(credentials).await.unwrap(); @@ -646,6 +650,7 @@ mod tests { client_id: "client-id".to_string(), token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: None, }; store.save(credentials).await.unwrap(); diff --git a/crates/mcpmux-gateway/src/server/mod.rs b/crates/mcpmux-gateway/src/server/mod.rs index c2cef882..3d3c6e0d 100644 --- a/crates/mcpmux-gateway/src/server/mod.rs +++ b/crates/mcpmux-gateway/src/server/mod.rs @@ -255,6 +255,7 @@ impl GatewayServer { LocalSessionManager::default().into(), StreamableHttpServerConfig { stateful_mode: true, + json_response: false, sse_keep_alive: Some(std::time::Duration::from_secs(30)), sse_retry: Some(std::time::Duration::from_secs(3)), cancellation_token: CancellationToken::new(), diff --git a/tests/rust/Cargo.toml b/tests/rust/Cargo.toml index 74f1a9b8..c7edfc35 100644 --- a/tests/rust/Cargo.toml +++ b/tests/rust/Cargo.toml @@ -41,7 +41,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # HTTP mocking for OAuth tests wiremock = "0.6" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.13", features = ["json"] } # URL parsing for OAuth tests url = "2.5" @@ -50,7 +50,7 @@ url = "2.5" parking_lot = "0.12" # RMCP for streamable HTTP transport tests -rmcp = { version = "0.15.0", features = [ +rmcp = { version = "0.17.0", features = [ "client", "server", "transport-streamable-http-server", diff --git a/tests/rust/tests/streamable_http/gateway_notifications.rs b/tests/rust/tests/streamable_http/gateway_notifications.rs index 84ae324e..88c96eb5 100644 --- a/tests/rust/tests/streamable_http/gateway_notifications.rs +++ b/tests/rust/tests/streamable_http/gateway_notifications.rs @@ -215,6 +215,7 @@ impl TestGateway { Arc::new(LocalSessionManager::default()), StreamableHttpServerConfig { stateful_mode: true, + json_response: false, sse_keep_alive: Some(std::time::Duration::from_secs(15)), sse_retry: Some(std::time::Duration::from_secs(3)), cancellation_token: ct.child_token(), diff --git a/tests/rust/tests/streamable_http/notifications.rs b/tests/rust/tests/streamable_http/notifications.rs index 99ca2e7b..382eac36 100644 --- a/tests/rust/tests/streamable_http/notifications.rs +++ b/tests/rust/tests/streamable_http/notifications.rs @@ -127,6 +127,7 @@ async fn start_test_server(handler: TestNotificationHandler) -> (String, Cancell Arc::new(LocalSessionManager::default()), StreamableHttpServerConfig { stateful_mode: true, + json_response: false, sse_keep_alive: Some(std::time::Duration::from_secs(15)), sse_retry: Some(std::time::Duration::from_secs(3)), cancellation_token: ct.child_token(), From 400bf4102f6752f52a22e23bac0d5ab17a5cac1e Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Wed, 4 Mar 2026 07:33:26 +0800 Subject: [PATCH 3/3] fix: populate token_received_at to enable proactive token refresh Since build_token_response already recalculates expires_in as remaining time from stored expires_at, setting token_received_at=now at load time makes rmcp's expiry math correct (remaining = expires_in - 0), enabling client-side token refresh before expiry instead of waiting for a 401. Signed-off-by: Mohammod Al Amin Ashik --- .../src/pool/credential_store.rs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/mcpmux-gateway/src/pool/credential_store.rs b/crates/mcpmux-gateway/src/pool/credential_store.rs index ba15c0de..49fdf250 100644 --- a/crates/mcpmux-gateway/src/pool/credential_store.rs +++ b/crates/mcpmux-gateway/src/pool/credential_store.rs @@ -5,6 +5,7 @@ //! CredentialStore interface. use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use async_trait::async_trait; use chrono::{Duration, Utc}; @@ -159,6 +160,19 @@ impl DatabaseCredentialStore { } } +/// Current time as seconds since UNIX epoch, matching rmcp's `AuthorizationManager::now_epoch_secs()`. +/// +/// Used when loading credentials from the database: since `build_token_response` recalculates +/// `expires_in` as remaining time from the stored `expires_at`, setting `token_received_at = now` +/// makes rmcp's expiry arithmetic correct (`remaining = expires_in - (now - received_at)` = `expires_in`), +/// enabling proactive token refresh before expiry instead of waiting for a 401. +fn now_epoch_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + #[async_trait] impl CredentialStore for DatabaseCredentialStore { async fn load(&self) -> Result, AuthError> { @@ -208,7 +222,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: Some(token_response), granted_scopes: Vec::new(), - token_received_at: None, + token_received_at: Some(now_epoch_secs()), }) } (Some(reg), None) => { @@ -220,7 +234,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: None, granted_scopes: Vec::new(), - token_received_at: None, + token_received_at: Some(now_epoch_secs()), }) } (None, Some(access)) => { @@ -233,7 +247,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: String::new(), token_response: Some(token_response), granted_scopes: Vec::new(), - token_received_at: None, + token_received_at: Some(now_epoch_secs()), }) } (None, None) => {