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/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/pool/credential_store.rs b/crates/mcpmux-gateway/src/pool/credential_store.rs index fdf20a06..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,6 +222,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: Some(now_epoch_secs()), }) } (Some(reg), None) => { @@ -219,6 +234,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: reg.client_id, token_response: None, granted_scopes: Vec::new(), + token_received_at: Some(now_epoch_secs()), }) } (None, Some(access)) => { @@ -231,6 +247,7 @@ impl CredentialStore for DatabaseCredentialStore { client_id: String::new(), token_response: Some(token_response), granted_scopes: Vec::new(), + token_received_at: Some(now_epoch_secs()), }) } (None, None) => { @@ -588,6 +605,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 +664,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/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/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/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(); } }); 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(),