Conversation
alexhancock
commented
Feb 25, 2026
- adds conformance server and client for testing against https://github.com/modelcontextprotocol/conformance
- adds results from initial run of https://github.com/modelcontextprotocol/conformance/tree/main/.claude/skills/mcp-sdk-tier-audit skill (via goose)
- various small changes applied during the testing loop
| let http = reqwest::Client::builder() | ||
| .redirect(reqwest::redirect::Policy::none()) | ||
| .build()?; | ||
| let resp = http.get(&auth_url).send().await?; |
Check failure
Code scanning / CodeQL
Cleartext transmission of sensitive information
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, this problem is fixed by enforcing that sensitive information is only sent over encrypted transport. For HTTP client code, that typically means validating that URLs are HTTPS before sending a request, and failing otherwise. This removes reliance on external configuration and prevents accidental use of insecure endpoints.
For this specific case, the best fix without changing the functional behavior is to validate that auth_url uses the https scheme before calling http.get. If the authorization server were misconfigured to use http, the client will now fail fast instead of leaking the authorization request over cleartext. Concretely, in conformance/src/bin/client.rs inside perform_oauth_flow_preregistered, after obtaining auth_url on line 290, we should parse it as a url::Url, check that url.scheme() == "https", and only then pass it to reqwest::Client::get. This uses the already-imported url crate (as evidenced by later use of url::Url::parse(location)?;), so no new dependencies are required. The rest of the flow (redirect handling, extracting code and state, exchanging for a token) remains unchanged.
| @@ -288,12 +288,21 @@ | ||
| let scopes = manager.select_scopes(None, &[]); | ||
| let scope_refs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect(); | ||
| let auth_url = manager.get_authorization_url(&scope_refs).await?; | ||
| let auth_url_parsed = url::Url::parse(&auth_url)?; | ||
|
|
||
| // Ensure we only send sensitive OAuth data over HTTPS | ||
| if auth_url_parsed.scheme() != "https" { | ||
| return Err(anyhow::anyhow!( | ||
| "Insecure authorization URL scheme: {}", | ||
| auth_url_parsed.scheme() | ||
| )); | ||
| } | ||
|
|
||
| // Headless redirect | ||
| let http = reqwest::Client::builder() | ||
| .redirect(reqwest::redirect::Policy::none()) | ||
| .build()?; | ||
| let resp = http.get(&auth_url).send().await?; | ||
| let resp = http.get(auth_url_parsed.as_str()).send().await?; | ||
| let location = resp | ||
| .headers() | ||
| .get("location") |
|
|
||
| let http = reqwest::Client::new(); | ||
| let resp = http | ||
| .post(&token_endpoint) |
Check failure
Code scanning / CodeQL
Cleartext transmission of sensitive information
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, the issue is best fixed by ensuring that sensitive information like client_secret is only ever sent over an encrypted channel (HTTPS). Since the token endpoint URL is discovered at runtime and could be attacker‑controlled or misconfigured, the client must validate that the URL uses a secure scheme before sending credentials, and fail if it does not.
The most direct fix, without changing overall functionality, is: after discovering metadata in run_client_credentials_basic, validate that metadata.token_endpoint uses the https scheme. If it is not HTTPS (e.g., missing scheme, HTTP, or anything else), return an error instead of sending the request. To implement this, we can parse token_endpoint with the standard url crate, or (to avoid new dependencies) with Rust’s standard reqwest::Url type, since reqwest is already in use. We only modify conformance/src/bin/client.rs: after let token_endpoint = metadata.token_endpoint.clone(); we parse it as a reqwest::Url, verify url.scheme() == "https", and abort with an error if not. Then we call .post(url) instead of .post(&token_endpoint), so we know the checked URL is what is used for the request. No changes are needed in crates/rmcp/src/transport/auth.rs for this fix.
Concretely, in run_client_credentials_basic:
- Add parsing:
let token_endpoint_url = reqwest::Url::parse(&token_endpoint)?; - Add a scheme check: if
token_endpoint_url.scheme() != "https", return ananyhow::Errorstating that a secure HTTPS token endpoint is required. - Use
token_endpoint_urlin the.post(...)call.
No new imports are needed becausereqwest::Urlis in the same crate asreqwest::Clientand can be referenced asreqwest::Url.
| @@ -570,9 +570,18 @@ | ||
| let token_endpoint = metadata.token_endpoint.clone(); | ||
| manager.set_metadata(metadata); | ||
|
|
||
| // Ensure that we only send client credentials over HTTPS. | ||
| let token_endpoint_url = reqwest::Url::parse(&token_endpoint)?; | ||
| if token_endpoint_url.scheme() != "https" { | ||
| return Err(anyhow::anyhow!( | ||
| "Insecure token endpoint URL ({}); HTTPS is required for client credentials", | ||
| token_endpoint_url | ||
| )); | ||
| } | ||
|
|
||
| let http = reqwest::Client::new(); | ||
| let resp = http | ||
| .post(&token_endpoint) | ||
| .post(token_endpoint_url) | ||
| .basic_auth(client_id, Some(client_secret)) | ||
| .header("content-type", "application/x-www-form-urlencoded") | ||
| .body("grant_type=client_credentials") |
| urlencoding::encode(&key), | ||
| ); | ||
| let resp = http | ||
| .post(&token_endpoint) |
Check failure
Code scanning / CodeQL
Cleartext transmission of sensitive information
751f597 to
345af49
Compare
b5561fe to
f7c920f
Compare
DaleSeo
left a comment
There was a problem hiding this comment.
Great job on this, @alexhancock! Setting up the conformance suite is a big step forward!
I added some suggestions in my review. They're mostly about reducing duplication and a few naming and logging issues, but nothing is blocking.
| pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05")); | ||
| // Keep LATEST at 2025-03-26 until full 2025-06-18 compliance and automated testing are in place. | ||
| pub const LATEST: Self = Self::V_2025_03_26; | ||
| pub const LATEST: Self = Self::V_2025_06_18; |
crates/rmcp/src/transport/common/reqwest/streamable_http_client.rs
Outdated
Show resolved
Hide resolved
| "test_tool_with_progress" => { | ||
| let progress_token = cx.meta.get_progress_token(); | ||
|
|
||
| if let Some(token) = &progress_token { | ||
| let _ = cx | ||
| .peer | ||
| .notify_progress(ProgressNotificationParam { | ||
| progress_token: token.clone(), | ||
| progress: 0.0, | ||
| total: Some(100.0), | ||
| message: Some("Starting".into()), | ||
| }) | ||
| .await; | ||
| } | ||
|
|
||
| tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; | ||
|
|
||
| if let Some(token) = &progress_token { | ||
| let _ = cx | ||
| .peer | ||
| .notify_progress(ProgressNotificationParam { | ||
| progress_token: token.clone(), | ||
| progress: 50.0, | ||
| total: Some(100.0), | ||
| message: Some("Halfway".into()), | ||
| }) | ||
| .await; | ||
| } | ||
|
|
||
| tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; | ||
|
|
||
| if let Some(token) = &progress_token { | ||
| let _ = cx | ||
| .peer | ||
| .notify_progress(ProgressNotificationParam { | ||
| progress_token: token.clone(), | ||
| progress: 100.0, | ||
| total: Some(100.0), | ||
| message: Some("Complete".into()), | ||
| }) | ||
| .await; | ||
| } | ||
|
|
||
| Ok(CallToolResult { | ||
| content: vec![Content::text("Progress test completed")], | ||
| structured_content: None, | ||
| is_error: None, | ||
| meta: None, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Similar to logging above: three almost identical blocks that could be a loop over [(0.0, "Starting"), (50.0, "Halfway"), (100.0, "Complete")].
* adds conformance server and client * adds results from initial run of https://github.com/modelcontextprotocol/conformance/tree/main/.claude/skills/mcp-sdk-tier-audit skill * various small changes applied during the testing loop Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
0c13e6b to
13e7b76
Compare
DaleSeo
left a comment
There was a problem hiding this comment.
Thanks for addressing my feedback!