diff --git a/src/config/mcp_factory.rs b/src/config/mcp_factory.rs index 5d8cca7..3bc3508 100644 --- a/src/config/mcp_factory.rs +++ b/src/config/mcp_factory.rs @@ -132,7 +132,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, } } @@ -149,7 +149,7 @@ mod tests { cwd: None, url: Some(url.to_string()), headers, - oauth_client_id: None, + oauth: None, } } diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 187cef7..3efad59 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -3698,7 +3698,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, }, ); } diff --git a/src/main.rs b/src/main.rs index 8dd54d8..7d0f36f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -158,7 +158,13 @@ async fn main() -> Result<()> { } let url = spec.url.as_deref().expect("validated: remote spec has url"); - mcp::oauth::run_mcp_oauth_flow(server_name, url, spec.oauth_client_id.as_deref()).await?; + mcp::oauth::run_mcp_oauth_flow( + server_name, + url, + spec.oauth.as_ref().and_then(|o| o.client_id.as_deref()), + spec.oauth.as_ref().and_then(|o| o.callback_port), + ) + .await?; println!("Authentication saved. '{server_name}' is now available for use."); return Ok(()); diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 57a78de..2c89fc8 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -57,6 +57,14 @@ pub(crate) struct McpServersConfig { pub mcp_servers: IndexMap, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub(crate) struct McpOAuthConfig { + #[serde(rename = "clientId", skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(rename = "callbackPort", skip_serializing_if = "Option::is_none")] + pub callback_port: Option, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub(crate) struct McpServer { @@ -75,7 +83,7 @@ pub(crate) struct McpServer { #[serde(skip_serializing_if = "Option::is_none")] pub headers: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub oauth_client_id: Option, + pub oauth: Option, } impl McpServer { @@ -110,10 +118,10 @@ impl McpServer { "MCP server '{name}' is missing a \"command\" field (required for stdio transport)" )); } - if self.url.is_some() || self.headers.is_some() || self.oauth_client_id.is_some() { + if self.url.is_some() || self.headers.is_some() || self.oauth.is_some() { return Err(anyhow!( "MCP server '{name}' has type \"stdio\" but also specifies remote fields \ - (url/headers/oauth_client_id). Remove the remote fields or change the type to \"http\" or \"sse\"." + (url/headers/oauth). Remove the remote fields or change the type to \"http\" or \"sse\"." )); } } @@ -511,7 +519,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, } } @@ -524,7 +532,7 @@ mod tests { cwd: None, url: Some(url.to_string()), headers: None, - oauth_client_id: None, + oauth: None, } } @@ -537,7 +545,7 @@ mod tests { cwd: None, url: Some(url.to_string()), headers: None, - oauth_client_id: None, + oauth: None, } } @@ -569,7 +577,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -587,7 +595,7 @@ mod tests { cwd: None, url: Some("http://localhost".into()), headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -607,7 +615,7 @@ mod tests { cwd: None, url: None, headers: Some(headers), - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -632,7 +640,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -650,7 +658,7 @@ mod tests { cwd: None, url: Some("http://localhost".into()), headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -668,7 +676,7 @@ mod tests { cwd: None, url: Some("http://localhost".into()), headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -686,7 +694,7 @@ mod tests { cwd: Some("/tmp".into()), url: Some("http://localhost".into()), headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); @@ -711,7 +719,7 @@ mod tests { cwd: None, url: None, headers: None, - oauth_client_id: None, + oauth: None, }; let err = spec.validate("test").unwrap_err(); diff --git a/src/mcp/oauth.rs b/src/mcp/oauth.rs index 2dd8b52..c58fc93 100644 --- a/src/mcp/oauth.rs +++ b/src/mcp/oauth.rs @@ -80,10 +80,12 @@ pub async fn run_mcp_oauth_flow( server_name: &str, server_url: &str, configured_client_id: Option<&str>, + callback_port: Option, ) -> Result<()> { let metadata = discover_oauth_metadata(server_url).await?; - let listener = TcpListener::bind("127.0.0.1:0")?; + let bind_addr = format!("127.0.0.1:{}", callback_port.unwrap_or(0)); + let listener = TcpListener::bind(&bind_addr)?; let port = listener.local_addr()?.port(); drop(listener); let redirect_uri = format!("http://127.0.0.1:{port}/callback"); diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 9001990..28d762d 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -640,9 +640,19 @@ pub async fn run_repl_command( .url .as_deref() .expect("validated: remote spec has url"); - let client_id = spec.oauth_client_id.as_deref(); - mcp::oauth::run_mcp_oauth_flow(server_name, url, client_id) - .await?; + let client_id = spec + .oauth + .as_ref() + .and_then(|o| o.client_id.as_deref()); + let callback_port = + spec.oauth.as_ref().and_then(|o| o.callback_port); + mcp::oauth::run_mcp_oauth_flow( + server_name, + url, + client_id, + callback_port, + ) + .await?; println!( "Authentication saved. \ Restart Coyote to connect to '{server_name}'."