context_server: Handle bad WWW-Authenticate resource_metadata URLs (#53502)

In MCP OAuth, when the resource_metadata URL from the WWW-Authenticate
header from the MCP server is on the same origin, but points to a broken
endpoint (for example Pydantic Logfire doubles the path component,
producing /mcp/mcp), fall back to the RFC 9728 well-known URIs instead
of failing outright. The header URL is still tried first, as per the MCP
spec.

Release Notes:

- MCP OAuth: Handle bad URLs in WWW-Authenticate by falling back to the
well known authorization server metadata URLs.
This commit is contained in:
Tom Houlé authored and GitHub committed 2026-05-18 12:48:55 +00:00
1 parent 30efea88c3
commit db2dd21cd3
1 file changed
+78 -1
+78 -1
View File
@@ -694,7 +694,19 @@ pub async fn fetch_protected_resource_metadata(
www_authenticate: &WwwAuthenticate,
) -> Result<ProtectedResourceMetadata> {
let candidate_urls = match &www_authenticate.resource_metadata {
Some(url) if url.origin() == server_url.origin() => vec![url.clone()],
Some(url) if url.origin() == server_url.origin() => {
// Try the header-provided URL first (per MCP spec: "use the resource
// metadata URL from the parsed WWW-Authenticate headers when present"),
// then fall back to RFC 9728 well-known URIs in case the header URL is
// wrong (e.g. a buggy server that doubles the path component).
let mut urls = vec![url.clone()];
for fallback in protected_resource_metadata_urls(server_url) {
if !urls.contains(&fallback) {
urls.push(fallback);
}
}
urls
}
Some(url) => {
log::warn!(
"Ignoring cross-origin resource_metadata URL {} \
@@ -1920,6 +1932,71 @@ mod tests {
});
}
#[test]
fn test_fetch_protected_resource_metadata_falls_back_when_header_url_fails() {
// Reproduces the Pydantic Logfire case: the server's WWW-Authenticate
// header contains a resource_metadata URL with a doubled path (e.g.
// /mcp/mcp), which returns HTML instead of JSON. The client should
// fall back to the RFC 9728 well-known URL, which works correctly.
gpui::block_on(async {
let client = make_fake_http_client(|req| {
Box::pin(async move {
let uri = req.uri().to_string();
if uri
== "https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp/mcp"
{
// Buggy header URL returns HTML (like a SPA catch-all).
Ok(Response::builder()
.status(200)
.header("Content-Type", "text/html")
.body(AsyncBody::from(b"<!doctype html><html></html>".to_vec()))
.unwrap())
} else if uri
== "https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp"
{
// Correct well-known URL returns valid metadata.
json_response(
200,
r#"{
"resource": "https://mcp.example.com/api/mcp",
"authorization_servers": ["https://auth.example.com"]
}"#,
)
} else {
json_response(404, "{}")
}
})
});
let server_url = Url::parse("https://mcp.example.com/api/mcp").unwrap();
let www_auth = WwwAuthenticate {
resource_metadata: Some(
// Buggy URL with doubled path component.
Url::parse(
"https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp/mcp",
)
.unwrap(),
),
scope: None,
error: None,
error_description: None,
};
let metadata = fetch_protected_resource_metadata(&client, &server_url, &www_auth)
.await
.unwrap();
assert_eq!(
metadata.resource.as_str(),
"https://mcp.example.com/api/mcp"
);
assert_eq!(
metadata.authorization_servers[0].as_str(),
"https://auth.example.com/"
);
});
}
#[test]
fn test_fetch_protected_resource_metadata_rejects_cross_origin_url() {
gpui::block_on(async {