From ad663534bdd8e5f9d74f7e043af78977328eaea8 Mon Sep 17 00:00:00 2001 From: Josh Rutkowski Date: Tue, 7 Oct 2025 17:14:26 -0700 Subject: [PATCH 1/2] feat: add configurable OAuth redirect port (default: 7777) Add chat.oauthRedirectPort setting to configure the OAuth callback server port. Replaces random port with configurable default of 7777. --- crates/chat-cli/src/database/settings.rs | 4 ++++ crates/chat-cli/src/mcp_client/oauth_util.rs | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index e83c5d4b9b..278d672192 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -88,6 +88,8 @@ pub enum Setting { EnabledCheckpoint, #[strum(message = "Enable the delegate tool for subagent management (boolean)")] EnabledDelegate, + #[strum(message = "Port for OAuth redirect/callback server (number, default: 7777)")] + ChatOAuthRedirectPort, } impl AsRef for Setting { @@ -129,6 +131,7 @@ impl AsRef for Setting { Self::EnabledCheckpoint => "chat.enableCheckpoint", Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator", Self::EnabledDelegate => "chat.enableDelegate", + Self::ChatOAuthRedirectPort => "chat.oauthRedirectPort", } } } @@ -178,6 +181,7 @@ impl TryFrom<&str> for Setting { "chat.enableTodoList" => Ok(Self::EnabledTodoList), "chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint), "chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator), + "chat.oauthRedirectPort" => Ok(Self::ChatOAuthRedirectPort), _ => Err(DatabaseError::InvalidSetting(value.to_string())), } } diff --git a/crates/chat-cli/src/mcp_client/oauth_util.rs b/crates/chat-cli/src/mcp_client/oauth_util.rs index dfd1e9e0e7..02733e25e5 100644 --- a/crates/chat-cli/src/mcp_client/oauth_util.rs +++ b/crates/chat-cli/src/mcp_client/oauth_util.rs @@ -56,6 +56,7 @@ use tracing::{ use url::Url; use super::messenger::Messenger; +use crate::database::settings::Setting; use crate::os::Os; use crate::util::directories::{ DirectoryError, @@ -293,6 +294,7 @@ impl<'a> HttpServiceBuilder<'a> { reg_full_path.clone(), scopes, messenger, + os, ) .await?; @@ -453,6 +455,7 @@ async fn get_auth_manager( reg_full_path: PathBuf, scopes: &[String], messenger: &dyn Messenger, + os: &Os, ) -> Result { let cred_as_bytes = tokio::fs::read(&cred_full_path).await; let reg_as_bytes = tokio::fs::read(®_full_path).await; @@ -474,7 +477,7 @@ async fn get_auth_manager( _ => { info!("Error reading cached credentials"); debug!("## mcp: cache read failed. constructing auth manager from scratch"); - let (am, redirect_uri) = get_auth_manager_impl(oauth_state, scopes, messenger).await?; + let (am, redirect_uri) = get_auth_manager_impl(oauth_state, scopes, messenger, os).await?; // Client registration is done in [start_authorization] // If we have gotten past that point that means we have the info to persist the @@ -510,8 +513,16 @@ async fn get_auth_manager_impl( mut oauth_state: OAuthState, scopes: &[String], messenger: &dyn Messenger, + os: &Os, ) -> Result<(AuthorizationManager, String), OauthUtilError> { - let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0)); + // Get port from settings, default to 7777 if not configured + let port = os + .database + .settings + .get_int(Setting::ChatOAuthRedirectPort) + .map_or(7777, |p| p as u16); + + let socket_addr = SocketAddr::from(([127, 0, 0, 1], port)); let cancellation_token = tokio_util::sync::CancellationToken::new(); let (tx, rx) = tokio::sync::oneshot::channel::<(String, String)>(); From ac83ede1d48da9075113dec163dc530d2b322947 Mon Sep 17 00:00:00 2001 From: Josh Rutkowski Date: Wed, 8 Oct 2025 10:22:54 -0700 Subject: [PATCH 2/2] Revert back to randomly assigned port and support custom oauth config in agent config --- .../src/cli/chat/tools/custom_tool.rs | 12 +++++++++ crates/chat-cli/src/database/settings.rs | 4 --- crates/chat-cli/src/mcp_client/client.rs | 3 ++- crates/chat-cli/src/mcp_client/oauth_util.rs | 27 ++++++++++++------- schemas/agent-v1.json | 16 +++++++++++ 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/tools/custom_tool.rs b/crates/chat-cli/src/cli/chat/tools/custom_tool.rs index 0e45205678..2a26c1c12b 100644 --- a/crates/chat-cli/src/cli/chat/tools/custom_tool.rs +++ b/crates/chat-cli/src/cli/chat/tools/custom_tool.rs @@ -44,6 +44,15 @@ impl Default for TransportType { } } +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct OAuthConfig { + /// Custom redirect URI for OAuth flow (e.g., "127.0.0.1:7778") + /// If not specified, a random available port will be assigned by the OS + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uri: Option, +} + #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct CustomToolConfig { @@ -59,6 +68,9 @@ pub struct CustomToolConfig { /// Scopes with which oauth is done #[serde(default = "get_default_scopes")] pub oauth_scopes: Vec, + /// OAuth configuration for this server + #[serde(skip_serializing_if = "Option::is_none")] + pub oauth: Option, /// The command string used to initialize the mcp server #[serde(default)] pub command: String, diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 278d672192..e83c5d4b9b 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -88,8 +88,6 @@ pub enum Setting { EnabledCheckpoint, #[strum(message = "Enable the delegate tool for subagent management (boolean)")] EnabledDelegate, - #[strum(message = "Port for OAuth redirect/callback server (number, default: 7777)")] - ChatOAuthRedirectPort, } impl AsRef for Setting { @@ -131,7 +129,6 @@ impl AsRef for Setting { Self::EnabledCheckpoint => "chat.enableCheckpoint", Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator", Self::EnabledDelegate => "chat.enableDelegate", - Self::ChatOAuthRedirectPort => "chat.oauthRedirectPort", } } } @@ -181,7 +178,6 @@ impl TryFrom<&str> for Setting { "chat.enableTodoList" => Ok(Self::EnabledTodoList), "chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint), "chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator), - "chat.oauthRedirectPort" => Ok(Self::ChatOAuthRedirectPort), _ => Err(DatabaseError::InvalidSetting(value.to_string())), } } diff --git a/crates/chat-cli/src/mcp_client/client.rs b/crates/chat-cli/src/mcp_client/client.rs index fefaa1a9cb..6598fc71db 100644 --- a/crates/chat-cli/src/mcp_client/client.rs +++ b/crates/chat-cli/src/mcp_client/client.rs @@ -453,11 +453,12 @@ impl McpClientService { url, headers, oauth_scopes: scopes, + oauth, timeout, .. } = &self.config; - let http_service_builder = HttpServiceBuilder::new(url, os, url, *timeout, scopes, headers, messenger); + let http_service_builder = HttpServiceBuilder::new(url, os, url, *timeout, scopes, headers, oauth, messenger); let (service, auth_client_wrapper) = http_service_builder.try_build(&self).await?; diff --git a/crates/chat-cli/src/mcp_client/oauth_util.rs b/crates/chat-cli/src/mcp_client/oauth_util.rs index 02733e25e5..5a2199defe 100644 --- a/crates/chat-cli/src/mcp_client/oauth_util.rs +++ b/crates/chat-cli/src/mcp_client/oauth_util.rs @@ -56,7 +56,6 @@ use tracing::{ use url::Url; use super::messenger::Messenger; -use crate::database::settings::Setting; use crate::os::Os; use crate::util::directories::{ DirectoryError, @@ -197,6 +196,7 @@ pub struct HttpServiceBuilder<'a> { pub timeout: u64, pub scopes: &'a [String], pub headers: &'a HashMap, + pub oauth_config: &'a Option, pub messenger: &'a dyn Messenger, } @@ -208,6 +208,7 @@ impl<'a> HttpServiceBuilder<'a> { timeout: u64, scopes: &'a [String], headers: &'a HashMap, + oauth_config: &'a Option, messenger: &'a dyn Messenger, ) -> Self { Self { @@ -217,6 +218,7 @@ impl<'a> HttpServiceBuilder<'a> { timeout, scopes, headers, + oauth_config, messenger, } } @@ -232,6 +234,7 @@ impl<'a> HttpServiceBuilder<'a> { timeout, scopes, headers, + oauth_config, messenger, } = self; @@ -293,6 +296,7 @@ impl<'a> HttpServiceBuilder<'a> { cred_full_path.clone(), reg_full_path.clone(), scopes, + oauth_config, messenger, os, ) @@ -454,6 +458,7 @@ async fn get_auth_manager( cred_full_path: PathBuf, reg_full_path: PathBuf, scopes: &[String], + oauth_config: &Option, messenger: &dyn Messenger, os: &Os, ) -> Result { @@ -477,7 +482,7 @@ async fn get_auth_manager( _ => { info!("Error reading cached credentials"); debug!("## mcp: cache read failed. constructing auth manager from scratch"); - let (am, redirect_uri) = get_auth_manager_impl(oauth_state, scopes, messenger, os).await?; + let (am, redirect_uri) = get_auth_manager_impl(oauth_state, scopes, oauth_config, messenger, os).await?; // Client registration is done in [start_authorization] // If we have gotten past that point that means we have the info to persist the @@ -512,15 +517,19 @@ async fn get_auth_manager( async fn get_auth_manager_impl( mut oauth_state: OAuthState, scopes: &[String], + oauth_config: &Option, messenger: &dyn Messenger, - os: &Os, + _os: &Os, ) -> Result<(AuthorizationManager, String), OauthUtilError> { - // Get port from settings, default to 7777 if not configured - let port = os - .database - .settings - .get_int(Setting::ChatOAuthRedirectPort) - .map_or(7777, |p| p as u16); + // Get port from per-server oauth config, or use 0 for random port assignment + let port = oauth_config + .as_ref() + .and_then(|cfg| cfg.redirect_uri.as_ref()) + .and_then(|uri| { + // Parse port from redirect_uri like "127.0.0.1:7778" or ":7778" + uri.split(':').last().and_then(|p| p.parse::().ok()) + }) + .unwrap_or(0); // Port 0 = OS assigns random available port let socket_addr = SocketAddr::from(([127, 0, 0, 1], port)); let cancellation_token = tokio_util::sync::CancellationToken::new(); diff --git a/schemas/agent-v1.json b/schemas/agent-v1.json index 5e72b08476..fb4c5e8964 100644 --- a/schemas/agent-v1.json +++ b/schemas/agent-v1.json @@ -94,6 +94,22 @@ "offline_access" ] }, + "oauth": { + "description": "OAuth configuration for this server", + "type": [ + "object", + "null" + ], + "properties": { + "redirectUri": { + "description": "Custom redirect URI for OAuth flow (e.g., \"127.0.0.1:7778\"). If not specified, a random available port will be assigned by the OS", + "type": [ + "string", + "null" + ] + } + } + }, "command": { "description": "The command string used to initialize the mcp server", "type": "string",