feat: Added support for specifying the oauth port and client ID in MCP server configs
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 25s

This commit is contained in:
2026-07-07 14:31:51 -06:00
parent 84ce094677
commit 5be4f45671
6 changed files with 48 additions and 22 deletions
+2 -2
View File
@@ -132,7 +132,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
} }
} }
@@ -149,7 +149,7 @@ mod tests {
cwd: None, cwd: None,
url: Some(url.to_string()), url: Some(url.to_string()),
headers, headers,
oauth_client_id: None, oauth: None,
} }
} }
+1 -1
View File
@@ -3698,7 +3698,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}, },
); );
} }
+7 -1
View File
@@ -158,7 +158,13 @@ async fn main() -> Result<()> {
} }
let url = spec.url.as_deref().expect("validated: remote spec has url"); 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."); println!("Authentication saved. '{server_name}' is now available for use.");
return Ok(()); return Ok(());
+22 -14
View File
@@ -57,6 +57,14 @@ pub(crate) struct McpServersConfig {
pub mcp_servers: IndexMap<String, McpServer>, pub mcp_servers: IndexMap<String, McpServer>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub(crate) struct McpOAuthConfig {
#[serde(rename = "clientId", skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(rename = "callbackPort", skip_serializing_if = "Option::is_none")]
pub callback_port: Option<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub(crate) struct McpServer { pub(crate) struct McpServer {
@@ -75,7 +83,7 @@ pub(crate) struct McpServer {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<IndexMap<String, String>>, pub headers: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub oauth_client_id: Option<String>, pub oauth: Option<McpOAuthConfig>,
} }
impl McpServer { impl McpServer {
@@ -110,10 +118,10 @@ impl McpServer {
"MCP server '{name}' is missing a \"command\" field (required for stdio transport)" "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!( return Err(anyhow!(
"MCP server '{name}' has type \"stdio\" but also specifies remote fields \ "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, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
} }
} }
@@ -524,7 +532,7 @@ mod tests {
cwd: None, cwd: None,
url: Some(url.to_string()), url: Some(url.to_string()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
} }
} }
@@ -537,7 +545,7 @@ mod tests {
cwd: None, cwd: None,
url: Some(url.to_string()), url: Some(url.to_string()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
} }
} }
@@ -569,7 +577,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -587,7 +595,7 @@ mod tests {
cwd: None, cwd: None,
url: Some("http://localhost".into()), url: Some("http://localhost".into()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -607,7 +615,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: Some(headers), headers: Some(headers),
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -632,7 +640,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -650,7 +658,7 @@ mod tests {
cwd: None, cwd: None,
url: Some("http://localhost".into()), url: Some("http://localhost".into()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -668,7 +676,7 @@ mod tests {
cwd: None, cwd: None,
url: Some("http://localhost".into()), url: Some("http://localhost".into()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -686,7 +694,7 @@ mod tests {
cwd: Some("/tmp".into()), cwd: Some("/tmp".into()),
url: Some("http://localhost".into()), url: Some("http://localhost".into()),
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
@@ -711,7 +719,7 @@ mod tests {
cwd: None, cwd: None,
url: None, url: None,
headers: None, headers: None,
oauth_client_id: None, oauth: None,
}; };
let err = spec.validate("test").unwrap_err(); let err = spec.validate("test").unwrap_err();
+3 -1
View File
@@ -80,10 +80,12 @@ pub async fn run_mcp_oauth_flow(
server_name: &str, server_name: &str,
server_url: &str, server_url: &str,
configured_client_id: Option<&str>, configured_client_id: Option<&str>,
callback_port: Option<u16>,
) -> Result<()> { ) -> Result<()> {
let metadata = discover_oauth_metadata(server_url).await?; 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(); let port = listener.local_addr()?.port();
drop(listener); drop(listener);
let redirect_uri = format!("http://127.0.0.1:{port}/callback"); let redirect_uri = format!("http://127.0.0.1:{port}/callback");
+12 -2
View File
@@ -640,8 +640,18 @@ pub async fn run_repl_command(
.url .url
.as_deref() .as_deref()
.expect("validated: remote spec has url"); .expect("validated: remote spec has url");
let client_id = spec.oauth_client_id.as_deref(); let client_id = spec
mcp::oauth::run_mcp_oauth_flow(server_name, url, client_id) .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?; .await?;
println!( println!(
"Authentication saved. \ "Authentication saved. \