diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index b8d4f3f4..25b2824c 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -400,12 +400,14 @@ pub struct AuthorizationManager { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClientRegistrationRequest { +pub(crate) struct ClientRegistrationRequest { pub client_name: String, pub redirect_uris: Vec, pub grant_types: Vec, pub token_endpoint_auth_method: String, pub response_types: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -645,6 +647,7 @@ impl AuthorizationManager { &mut self, name: &str, redirect_uri: &str, + scopes: &[&str], ) -> Result { if self.metadata.is_none() { return Err(AuthError::NoAuthorizationSupport); @@ -667,6 +670,11 @@ impl AuthorizationManager { ], token_endpoint_auth_method: "none".to_string(), // public client response_types: vec!["code".to_string()], + scope: if scopes.is_empty() { + None + } else { + Some(scopes.join(" ")) + }, }; let response = match self @@ -720,7 +728,7 @@ impl AuthorizationManager { // as a password, which is not a goal of the client secret. client_secret: reg_response.client_secret.filter(|s| !s.is_empty()), redirect_uri: redirect_uri.to_string(), - scopes: vec![], + scopes: scopes.iter().map(|s| s.to_string()).collect(), }; self.configure_client(config.clone())?; @@ -1490,7 +1498,7 @@ impl AuthorizationSession { } else { // Fallback to dynamic registration auth_manager - .register_client(client_name.unwrap_or("MCP Client"), redirect_uri) + .register_client(client_name.unwrap_or("MCP Client"), redirect_uri, scopes) .await .map_err(|e| { AuthError::RegistrationFailed(format!("Dynamic registration failed: {}", e)) @@ -1499,7 +1507,7 @@ impl AuthorizationSession { } else { // Fallback to dynamic registration match auth_manager - .register_client(client_name.unwrap_or("MCP Client"), redirect_uri) + .register_client(client_name.unwrap_or("MCP Client"), redirect_uri, scopes) .await { Ok(config) => config, @@ -2792,4 +2800,34 @@ mod tests { "expected InternalError when OAuth client is not configured, got: {err:?}" ); } + + // -- ClientRegistrationRequest serialization -- + + #[test] + fn client_registration_request_includes_scope_when_present() { + let req = super::ClientRegistrationRequest { + client_name: "test".to_string(), + redirect_uris: vec!["http://localhost/callback".to_string()], + grant_types: vec!["authorization_code".to_string()], + token_endpoint_auth_method: "none".to_string(), + response_types: vec!["code".to_string()], + scope: Some("read write".to_string()), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["scope"], "read write"); + } + + #[test] + fn client_registration_request_omits_scope_when_none() { + let req = super::ClientRegistrationRequest { + client_name: "test".to_string(), + redirect_uris: vec!["http://localhost/callback".to_string()], + grant_types: vec!["authorization_code".to_string()], + token_endpoint_auth_method: "none".to_string(), + response_types: vec!["code".to_string()], + scope: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert!(!json.as_object().unwrap().contains_key("scope")); + } }