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,
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,
}
}
+1 -1
View File
@@ -3698,7 +3698,7 @@ mod tests {
cwd: None,
url: 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");
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(());
+22 -14
View File
@@ -57,6 +57,14 @@ pub(crate) struct McpServersConfig {
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)]
#[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<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oauth_client_id: Option<String>,
pub oauth: Option<McpOAuthConfig>,
}
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();
+3 -1
View File
@@ -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<u16>,
) -> 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");
+13 -3
View File
@@ -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}'."