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:
Eric Traut authored and GitHub committed 2026-02-26 20:10:12 -08:00
1 parent 6fe3dc2e22
commit cee009d117
14 files changed
+201 -1

No files matched your search

@@ -4152,6 +4152,7 @@ impl CodexMessageProcessor {
http_headers, http_headers,
env_http_headers, env_http_headers,
scopes.as_deref().unwrap_or_default(), scopes.as_deref().unwrap_or_default(),
server.oauth_resource.as_deref(),
timeout_secs, timeout_secs,
config.mcp_oauth_callback_port, config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(), config.mcp_oauth_callback_url.as_deref(),
+3
View File
@@ -250,6 +250,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}; };
servers.insert(name.clone(), new_entry); 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.http_headers,
oauth_config.env_http_headers, oauth_config.env_http_headers,
&Vec::new(), &Vec::new(),
None,
config.mcp_oauth_callback_port, config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(), config.mcp_oauth_callback_url.as_deref(),
) )
@@ -356,6 +358,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
http_headers, http_headers,
env_http_headers, env_http_headers,
&scopes, &scopes,
server.oauth_resource.as_deref(),
config.mcp_oauth_callback_port, config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(), config.mcp_oauth_callback_url.as_deref(),
) )
+4
View File
@@ -1146,6 +1146,10 @@
}, },
"type": "object" "type": "object"
}, },
"oauth_resource": {
"default": null,
"type": "string"
},
"required": { "required": {
"default": null, "default": null,
"type": "boolean" "type": "boolean"
+12
View File
@@ -195,6 +195,11 @@ mod document_helpers {
{ {
entry["scopes"] = array_from_iter(scopes.iter().cloned()); 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 entry
} }
@@ -1465,6 +1470,7 @@ gpt-5 = "gpt-5.1"
enabled_tools: Some(vec!["one".to_string(), "two".to_string()]), enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
@@ -1489,6 +1495,7 @@ gpt-5 = "gpt-5.1"
enabled_tools: None, enabled_tools: None,
disabled_tools: Some(vec!["forbidden".to_string()]), disabled_tools: Some(vec!["forbidden".to_string()]),
scopes: None, scopes: None,
oauth_resource: Some("https://resource.example.com".to_string()),
}, },
); );
@@ -1507,6 +1514,7 @@ bearer_token_env_var = \"TOKEN\"
enabled = false enabled = false
startup_timeout_sec = 5.0 startup_timeout_sec = 5.0
disabled_tools = [\"forbidden\"] disabled_tools = [\"forbidden\"]
oauth_resource = \"https://resource.example.com\"
[mcp_servers.http.http_headers] [mcp_servers.http.http_headers]
Z-Header = \"z\" Z-Header = \"z\"
@@ -1556,6 +1564,7 @@ foo = { command = "cmd" }
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
@@ -1602,6 +1611,7 @@ foo = { command = "cmd" } # keep me
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
@@ -1647,6 +1657,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
@@ -1693,6 +1704,7 @@ foo = { command = "cmd" }
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
+60
View File
@@ -2433,6 +2433,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
} }
} }
@@ -2452,6 +2453,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
} }
} }
@@ -3489,6 +3491,7 @@ profile = "project"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
@@ -3646,6 +3649,7 @@ bearer_token = "secret"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -3717,6 +3721,7 @@ ZIG_VAR = "3"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -3768,6 +3773,7 @@ ZIG_VAR = "3"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -3817,6 +3823,7 @@ ZIG_VAR = "3"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -3882,6 +3889,7 @@ startup_timeout_sec = 2.0
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
apply_blocking( apply_blocking(
@@ -3959,6 +3967,7 @@ X-Auth = "DOCS_AUTH"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -3989,6 +3998,7 @@ X-Auth = "DOCS_AUTH"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
apply_blocking( apply_blocking(
@@ -4057,6 +4067,7 @@ url = "https://example.com/mcp"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
), ),
( (
@@ -4077,6 +4088,7 @@ url = "https://example.com/mcp"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
), ),
]); ]);
@@ -4160,6 +4172,7 @@ url = "https://example.com/mcp"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -4205,6 +4218,7 @@ url = "https://example.com/mcp"
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -4250,6 +4264,7 @@ url = "https://example.com/mcp"
enabled_tools: Some(vec!["allowed".to_string()]), enabled_tools: Some(vec!["allowed".to_string()]),
disabled_tools: Some(vec!["blocked".to_string()]), disabled_tools: Some(vec!["blocked".to_string()]),
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -4278,6 +4293,51 @@ url = "https://example.com/mcp"
Ok(()) 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] #[tokio::test]
async fn set_model_updates_defaults() -> anyhow::Result<()> { async fn set_model_updates_defaults() -> anyhow::Result<()> {
let codex_home = TempDir::new()?; let codex_home = TempDir::new()?;
+39
View File
@@ -99,6 +99,10 @@ pub struct McpServerConfig {
/// Optional OAuth scopes to request during MCP login. /// Optional OAuth scopes to request during MCP login.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub scopes: Option<Vec<String>>, 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. // 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>>, pub disabled_tools: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub scopes: Option<Vec<String>>, pub scopes: Option<Vec<String>>,
#[serde(default)]
pub oauth_resource: Option<String>,
} }
impl<'de> Deserialize<'de> for McpServerConfig { impl<'de> Deserialize<'de> for McpServerConfig {
@@ -166,6 +172,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
let enabled_tools = raw.enabled_tools.clone(); let enabled_tools = raw.enabled_tools.clone();
let disabled_tools = raw.disabled_tools.clone(); let disabled_tools = raw.disabled_tools.clone();
let scopes = raw.scopes.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> fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E>
where 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", "bearer_token", raw.bearer_token.as_ref())?;
throw_if_set("stdio", "http_headers", raw.http_headers.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", "env_http_headers", raw.env_http_headers.as_ref())?;
throw_if_set("stdio", "oauth_resource", raw.oauth_resource.as_ref())?;
McpServerTransportConfig::Stdio { McpServerTransportConfig::Stdio {
command, command,
args: raw.args.clone().unwrap_or_default(), args: raw.args.clone().unwrap_or_default(),
@@ -222,6 +230,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
enabled_tools, enabled_tools,
disabled_tools, disabled_tools,
scopes, 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] #[test]
fn deserialize_server_config_with_tool_filters() { fn deserialize_server_config_with_tool_filters() {
let cfg: McpServerConfig = toml::from_str( let cfg: McpServerConfig = toml::from_str(
@@ -1147,6 +1172,20 @@ mod tests {
"#, "#,
) )
.expect_err("should reject env_http_headers for stdio transport"); .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] #[test]
+1
View File
@@ -139,6 +139,7 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
} }
} }
@@ -241,6 +241,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
oauth_config.http_headers, oauth_config.http_headers,
oauth_config.env_http_headers, oauth_config.env_http_headers,
&[], &[],
server_config.oauth_resource.as_deref(),
config.mcp_oauth_callback_port, config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(), config.mcp_oauth_callback_url.as_deref(),
) )
@@ -387,6 +388,7 @@ fn mcp_dependency_to_server_config(
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}); });
} }
@@ -411,6 +413,7 @@ fn mcp_dependency_to_server_config(
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}); });
} }
@@ -468,6 +471,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -516,6 +520,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
)]); )]);
@@ -2115,6 +2115,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
auth_status: McpAuthStatus::Unsupported, auth_status: McpAuthStatus::Unsupported,
}; };
@@ -2162,6 +2163,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
auth_status: McpAuthStatus::Unsupported, auth_status: McpAuthStatus::Unsupported,
}; };
+6
View File
@@ -104,6 +104,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -246,6 +247,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -463,6 +465,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -581,6 +584,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -740,6 +744,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -959,6 +964,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
+1
View File
@@ -133,6 +133,7 @@ fn rmcp_server_config(command: String) -> McpServerConfig {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
} }
} }
+3
View File
@@ -370,6 +370,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -464,6 +465,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -729,6 +731,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}, },
); );
config config
@@ -47,6 +47,7 @@ pub async fn perform_oauth_login(
http_headers: Option<HashMap<String, String>>, http_headers: Option<HashMap<String, String>>,
env_http_headers: Option<HashMap<String, String>>, env_http_headers: Option<HashMap<String, String>>,
scopes: &[String], scopes: &[String],
oauth_resource: Option<&str>,
callback_port: Option<u16>, callback_port: Option<u16>,
callback_url: Option<&str>, callback_url: Option<&str>,
) -> Result<()> { ) -> Result<()> {
@@ -60,6 +61,7 @@ pub async fn perform_oauth_login(
store_mode, store_mode,
headers, headers,
scopes, scopes,
oauth_resource,
true, true,
callback_port, callback_port,
callback_url, callback_url,
@@ -78,6 +80,7 @@ pub async fn perform_oauth_login_return_url(
http_headers: Option<HashMap<String, String>>, http_headers: Option<HashMap<String, String>>,
env_http_headers: Option<HashMap<String, String>>, env_http_headers: Option<HashMap<String, String>>,
scopes: &[String], scopes: &[String],
oauth_resource: Option<&str>,
timeout_secs: Option<i64>, timeout_secs: Option<i64>,
callback_port: Option<u16>, callback_port: Option<u16>,
callback_url: Option<&str>, callback_url: Option<&str>,
@@ -92,6 +95,7 @@ pub async fn perform_oauth_login_return_url(
store_mode, store_mode,
headers, headers,
scopes, scopes,
oauth_resource,
false, false,
callback_port, callback_port,
callback_url, callback_url,
@@ -303,6 +307,7 @@ impl OauthLoginFlow {
store_mode: OAuthCredentialsStoreMode, store_mode: OAuthCredentialsStoreMode,
headers: OauthHeaders, headers: OauthHeaders,
scopes: &[String], scopes: &[String],
oauth_resource: Option<&str>,
launch_browser: bool, launch_browser: bool,
callback_port: Option<u16>, callback_port: Option<u16>,
callback_url: Option<&str>, callback_url: Option<&str>,
@@ -340,7 +345,11 @@ impl OauthLoginFlow {
oauth_state oauth_state
.start_authorization(&scope_refs, &redirect_uri, Some("Codex")) .start_authorization(&scope_refs, &redirect_uri, Some("Codex"))
.await?; .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_secs = timeout_secs.unwrap_or(DEFAULT_OAUTH_TIMEOUT_SECS).max(1);
let timeout = Duration::from_secs(timeout_secs as u64); 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)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq;
use super::CallbackOutcome; use super::CallbackOutcome;
use super::append_query_param;
use super::callback_path_from_redirect_uri; use super::callback_path_from_redirect_uri;
use super::parse_oauth_callback; use super::parse_oauth_callback;
@@ -461,4 +490,36 @@ mod tests {
.expect("redirect URI should parse"); .expect("redirect URI should parse");
assert_eq!(path, "/oauth/callback"); 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");
}
} }
+2
View File
@@ -2633,6 +2633,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}; };
let mut servers = config.mcp_servers.get().clone(); let mut servers = config.mcp_servers.get().clone();
servers.insert("docs".to_string(), stdio_config); servers.insert("docs".to_string(), stdio_config);
@@ -2656,6 +2657,7 @@ mod tests {
enabled_tools: None, enabled_tools: None,
disabled_tools: None, disabled_tools: None,
scopes: None, scopes: None,
oauth_resource: None,
}; };
servers.insert("http".to_string(), http_config); servers.insert("http".to_string(), http_config);
config config