Add oauth_resource handling for MCP login flows (#12866)
Addresses bug https://github.com/openai/codex/issues/12589 Builds on community PR #12763. This adds `oauth_resource` support for MCP `streamable_http` servers and wires it through the relevant config and login paths. It fixes the bug where the configured OAuth resource was not reliably included in the authorization request, causing MCP login to omit the expected `resource` parameter.
This commit is contained in:
parent
6fe3dc2e22
commit
cee009d117
14 changed files with 201 additions and 1 deletions
|
|
@ -4152,6 +4152,7 @@ impl CodexMessageProcessor {
|
|||
http_headers,
|
||||
env_http_headers,
|
||||
scopes.as_deref().unwrap_or_default(),
|
||||
server.oauth_resource.as_deref(),
|
||||
timeout_secs,
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
};
|
||||
|
||||
servers.insert(name.clone(), new_entry);
|
||||
|
|
@ -272,6 +273,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
|||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&Vec::new(),
|
||||
None,
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
)
|
||||
|
|
@ -356,6 +358,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
|||
http_headers,
|
||||
env_http_headers,
|
||||
&scopes,
|
||||
server.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1146,6 +1146,10 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"oauth_resource": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"default": null,
|
||||
"type": "boolean"
|
||||
|
|
|
|||
|
|
@ -195,6 +195,11 @@ mod document_helpers {
|
|||
{
|
||||
entry["scopes"] = array_from_iter(scopes.iter().cloned());
|
||||
}
|
||||
if let Some(resource) = &config.oauth_resource
|
||||
&& !resource.is_empty()
|
||||
{
|
||||
entry["oauth_resource"] = value(resource.clone());
|
||||
}
|
||||
|
||||
entry
|
||||
}
|
||||
|
|
@ -1465,6 +1470,7 @@ gpt-5 = "gpt-5.1"
|
|||
enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1489,6 +1495,7 @@ gpt-5 = "gpt-5.1"
|
|||
enabled_tools: None,
|
||||
disabled_tools: Some(vec!["forbidden".to_string()]),
|
||||
scopes: None,
|
||||
oauth_resource: Some("https://resource.example.com".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1507,6 +1514,7 @@ bearer_token_env_var = \"TOKEN\"
|
|||
enabled = false
|
||||
startup_timeout_sec = 5.0
|
||||
disabled_tools = [\"forbidden\"]
|
||||
oauth_resource = \"https://resource.example.com\"
|
||||
|
||||
[mcp_servers.http.http_headers]
|
||||
Z-Header = \"z\"
|
||||
|
|
@ -1556,6 +1564,7 @@ foo = { command = "cmd" }
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1602,6 +1611,7 @@ foo = { command = "cmd" } # keep me
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1647,6 +1657,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1693,6 +1704,7 @@ foo = { command = "cmd" }
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2433,6 +2433,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2452,6 +2453,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3489,6 +3491,7 @@ profile = "project"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -3646,6 +3649,7 @@ bearer_token = "secret"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -3717,6 +3721,7 @@ ZIG_VAR = "3"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -3768,6 +3773,7 @@ ZIG_VAR = "3"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -3817,6 +3823,7 @@ ZIG_VAR = "3"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -3882,6 +3889,7 @@ startup_timeout_sec = 2.0
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
apply_blocking(
|
||||
|
|
@ -3959,6 +3967,7 @@ X-Auth = "DOCS_AUTH"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -3989,6 +3998,7 @@ X-Auth = "DOCS_AUTH"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
apply_blocking(
|
||||
|
|
@ -4057,6 +4067,7 @@ url = "https://example.com/mcp"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
|
|
@ -4077,6 +4088,7 @@ url = "https://example.com/mcp"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
|
@ -4160,6 +4172,7 @@ url = "https://example.com/mcp"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -4205,6 +4218,7 @@ url = "https://example.com/mcp"
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -4250,6 +4264,7 @@ url = "https://example.com/mcp"
|
|||
enabled_tools: Some(vec!["allowed".to_string()]),
|
||||
disabled_tools: Some(vec!["blocked".to_string()]),
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -4278,6 +4293,51 @@ url = "https://example.com/mcp"
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: Some("https://resource.example.com".to_string()),
|
||||
},
|
||||
)]);
|
||||
|
||||
apply_blocking(
|
||||
codex_home.path(),
|
||||
None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
|
||||
)?;
|
||||
|
||||
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
|
||||
let serialized = std::fs::read_to_string(&config_path)?;
|
||||
assert!(serialized.contains(r#"oauth_resource = "https://resource.example.com""#));
|
||||
|
||||
let loaded = load_global_mcp_servers(codex_home.path()).await?;
|
||||
let docs = loaded.get("docs").expect("docs entry");
|
||||
assert_eq!(
|
||||
docs.oauth_resource.as_deref(),
|
||||
Some("https://resource.example.com")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_model_updates_defaults() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ pub struct McpServerConfig {
|
|||
/// Optional OAuth scopes to request during MCP login.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
|
||||
/// Optional OAuth resource parameter to include during MCP login (RFC 8707).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
// Raw MCP config shape used for deserialization and JSON Schema generation.
|
||||
|
|
@ -143,6 +147,8 @@ pub(crate) struct RawMcpServerConfig {
|
|||
pub disabled_tools: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub scopes: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub oauth_resource: Option<String>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
|
|
@ -166,6 +172,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
|||
let enabled_tools = raw.enabled_tools.clone();
|
||||
let disabled_tools = raw.disabled_tools.clone();
|
||||
let scopes = raw.scopes.clone();
|
||||
let oauth_resource = raw.oauth_resource.clone();
|
||||
|
||||
fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E>
|
||||
where
|
||||
|
|
@ -189,6 +196,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
|||
throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?;
|
||||
throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?;
|
||||
throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?;
|
||||
throw_if_set("stdio", "oauth_resource", raw.oauth_resource.as_ref())?;
|
||||
McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: raw.args.clone().unwrap_or_default(),
|
||||
|
|
@ -222,6 +230,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
|
|||
enabled_tools,
|
||||
disabled_tools,
|
||||
scopes,
|
||||
oauth_resource,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1093,6 +1102,22 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_streamable_http_server_config_with_oauth_resource() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
r#"
|
||||
url = "https://example.com/mcp"
|
||||
oauth_resource = "https://api.example.com"
|
||||
"#,
|
||||
)
|
||||
.expect("should deserialize http config with oauth_resource");
|
||||
|
||||
assert_eq!(
|
||||
cfg.oauth_resource,
|
||||
Some("https://api.example.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_server_config_with_tool_filters() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
|
|
@ -1147,6 +1172,20 @@ mod tests {
|
|||
"#,
|
||||
)
|
||||
.expect_err("should reject env_http_headers for stdio transport");
|
||||
|
||||
let err = toml::from_str::<McpServerConfig>(
|
||||
r#"
|
||||
command = "echo"
|
||||
oauth_resource = "https://api.example.com"
|
||||
"#,
|
||||
)
|
||||
.expect_err("should reject oauth_resource for stdio transport");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("oauth_resource is not supported for stdio"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
|||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
server_config.oauth_resource.as_deref(),
|
||||
config.mcp_oauth_callback_port,
|
||||
config.mcp_oauth_callback_url.as_deref(),
|
||||
)
|
||||
|
|
@ -387,6 +388,7 @@ fn mcp_dependency_to_server_config(
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -411,6 +413,7 @@ fn mcp_dependency_to_server_config(
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -468,6 +471,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
@ -516,6 +520,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
)]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2115,6 +2115,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
};
|
||||
|
|
@ -2162,6 +2163,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -246,6 +247,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -463,6 +465,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -581,6 +584,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -740,6 +744,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -959,6 +964,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ fn rmcp_server_config(command: String) -> McpServerConfig {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -464,6 +465,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
@ -729,6 +731,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
},
|
||||
);
|
||||
config
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub async fn perform_oauth_login(
|
|||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_resource: Option<&str>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
) -> Result<()> {
|
||||
|
|
@ -60,6 +61,7 @@ pub async fn perform_oauth_login(
|
|||
store_mode,
|
||||
headers,
|
||||
scopes,
|
||||
oauth_resource,
|
||||
true,
|
||||
callback_port,
|
||||
callback_url,
|
||||
|
|
@ -78,6 +80,7 @@ pub async fn perform_oauth_login_return_url(
|
|||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
oauth_resource: Option<&str>,
|
||||
timeout_secs: Option<i64>,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
|
|
@ -92,6 +95,7 @@ pub async fn perform_oauth_login_return_url(
|
|||
store_mode,
|
||||
headers,
|
||||
scopes,
|
||||
oauth_resource,
|
||||
false,
|
||||
callback_port,
|
||||
callback_url,
|
||||
|
|
@ -303,6 +307,7 @@ impl OauthLoginFlow {
|
|||
store_mode: OAuthCredentialsStoreMode,
|
||||
headers: OauthHeaders,
|
||||
scopes: &[String],
|
||||
oauth_resource: Option<&str>,
|
||||
launch_browser: bool,
|
||||
callback_port: Option<u16>,
|
||||
callback_url: Option<&str>,
|
||||
|
|
@ -340,7 +345,11 @@ impl OauthLoginFlow {
|
|||
oauth_state
|
||||
.start_authorization(&scope_refs, &redirect_uri, Some("Codex"))
|
||||
.await?;
|
||||
let auth_url = oauth_state.get_authorization_url().await?;
|
||||
let auth_url = append_query_param(
|
||||
&oauth_state.get_authorization_url().await?,
|
||||
"resource",
|
||||
oauth_resource,
|
||||
);
|
||||
let timeout_secs = timeout_secs.unwrap_or(DEFAULT_OAUTH_TIMEOUT_SECS).max(1);
|
||||
let timeout = Duration::from_secs(timeout_secs as u64);
|
||||
|
||||
|
|
@ -431,9 +440,29 @@ impl OauthLoginFlow {
|
|||
}
|
||||
}
|
||||
|
||||
fn append_query_param(url: &str, key: &str, value: Option<&str>) -> String {
|
||||
let Some(value) = value else {
|
||||
return url.to_string();
|
||||
};
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return url.to_string();
|
||||
}
|
||||
if let Ok(mut parsed) = Url::parse(url) {
|
||||
parsed.query_pairs_mut().append_pair(key, value);
|
||||
return parsed.to_string();
|
||||
}
|
||||
let encoded = urlencoding::encode(value);
|
||||
let separator = if url.contains('?') { "&" } else { "?" };
|
||||
format!("{url}{separator}{key}={encoded}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::CallbackOutcome;
|
||||
use super::append_query_param;
|
||||
use super::callback_path_from_redirect_uri;
|
||||
use super::parse_oauth_callback;
|
||||
|
||||
|
|
@ -461,4 +490,36 @@ mod tests {
|
|||
.expect("redirect URI should parse");
|
||||
assert_eq!(path, "/oauth/callback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_query_param_adds_resource_to_absolute_url() {
|
||||
let url = append_query_param(
|
||||
"https://example.com/authorize?scope=read",
|
||||
"resource",
|
||||
Some("https://api.example.com"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://example.com/authorize?scope=read&resource=https%3A%2F%2Fapi.example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_query_param_ignores_empty_values() {
|
||||
let url = append_query_param(
|
||||
"https://example.com/authorize?scope=read",
|
||||
"resource",
|
||||
Some(" "),
|
||||
);
|
||||
|
||||
assert_eq!(url, "https://example.com/authorize?scope=read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_query_param_handles_unparseable_url() {
|
||||
let url = append_query_param("not a url", "resource", Some("api/resource"));
|
||||
|
||||
assert_eq!(url, "not a url?resource=api%2Fresource");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2633,6 +2633,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
};
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
servers.insert("docs".to_string(), stdio_config);
|
||||
|
|
@ -2656,6 +2657,7 @@ mod tests {
|
|||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
};
|
||||
servers.insert("http".to_string(), http_config);
|
||||
config
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue