Skip to content

Commit 379be61

Browse files
committed
feat!: make discovery source explicit
1 parent c493017 commit 379be61

3 files changed

Lines changed: 87 additions & 78 deletions

File tree

conformance/src/bin/client.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,8 @@ async fn migration_token(
509509
return Ok(manager.get_access_token().await?);
510510
}
511511

512-
let metadata = manager.discover_metadata().await?;
513-
manager.set_metadata(metadata);
512+
let discovered = manager.discover_metadata().await?;
513+
manager.set_metadata(discovered.metadata);
514514
manager
515515
.register_client("conformance-client", REDIRECT_URI, &[])
516516
.await?;
@@ -609,9 +609,9 @@ async fn run_client_credentials_basic(
609609
.unwrap_or("conformance-test-secret");
610610

611611
let mut manager = AuthorizationManager::new(server_url).await?;
612-
let metadata = manager.discover_metadata().await?;
613-
let token_endpoint = metadata.token_endpoint.clone();
614-
manager.set_metadata(metadata);
612+
let discovered = manager.discover_metadata().await?;
613+
let token_endpoint = discovered.metadata.token_endpoint.clone();
614+
manager.set_metadata(discovered.metadata);
615615

616616
let http = reqwest::Client::new();
617617
let resp = http

crates/rmcp/src/transport/auth.rs

Lines changed: 57 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ pub enum AuthorizationMetadataSource {
591591
/// URL (`/authorize`, `/token`, `/register`) for compatibility with the
592592
/// 2025-03-26 MCP spec's default-endpoint fallback. The server gave no
593593
/// evidence that it supports OAuth.
594+
///
595+
/// [Newer MCP revisions] require metadata discovery and do not define an
596+
/// endpoint-synthesis fallback.
597+
///
598+
/// [Newer MCP revisions]: https://modelcontextprotocol.io/specification/draft/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements
594599
LegacyEndpointFallback,
595600
}
596601

@@ -1268,8 +1273,10 @@ impl AuthorizationManager {
12681273

12691274
/// Set OAuth2 authorization metadata
12701275
///
1271-
/// This should be called after discovering metadata via `discover_metadata()`
1272-
/// and before creating an `AuthorizationSession`.
1276+
/// This should be called with
1277+
/// [`DiscoveredAuthorizationMetadata::metadata`] after
1278+
/// [`Self::discover_metadata`] and before creating an
1279+
/// [`AuthorizationSession`].
12731280
pub fn set_metadata(&mut self, metadata: AuthorizationMetadata) {
12741281
self.metadata = Some(metadata);
12751282
}
@@ -1284,8 +1291,8 @@ impl AuthorizationManager {
12841291
&& stored.token_response.is_some()
12851292
{
12861293
if self.metadata.is_none() {
1287-
let metadata = self.discover_metadata().await?;
1288-
self.metadata = Some(metadata);
1294+
let discovered = self.discover_metadata().await?;
1295+
self.metadata = Some(discovered.metadata);
12891296
}
12901297

12911298
if let (Some(stored_issuer), Some(current_issuer)) =
@@ -1352,27 +1359,32 @@ impl AuthorizationManager {
13521359
Ok(())
13531360
}
13541361

1355-
/// discover oauth2 metadata (per SEP-985: Protected Resource Metadata first, then direct OAuth)
1362+
/// Discover OAuth 2.0 metadata and report how it was obtained.
13561363
///
1357-
/// When no metadata can be discovered, this falls back to legacy default
1358-
/// endpoints derived from the base URL rather than returning an error, so
1359-
/// a successful result does not prove the server supports OAuth. Callers
1360-
/// that need to tell verified discovery apart from the synthesized
1361-
/// fallback should use [`Self::discover_metadata_with_source`] instead.
1362-
pub async fn discover_metadata(&self) -> Result<AuthorizationMetadata, AuthError> {
1363-
Ok(self.discover_metadata_with_source().await?.metadata)
1364-
}
1365-
1366-
/// Discover oauth2 metadata along with how it was obtained.
1364+
/// Discovery follows SEP-985: protected resource metadata first, then
1365+
/// direct OAuth 2.0 Authorization Server Metadata or OpenID Connect
1366+
/// Discovery. When discovery finds nothing, the result contains legacy
1367+
/// default endpoints derived from the base URL and
1368+
/// [`AuthorizationMetadataSource::LegacyEndpointFallback`].
13671369
///
1368-
/// Unlike [`Self::discover_metadata`], the returned
1369-
/// [`AuthorizationMetadataSource`] lets callers distinguish metadata the
1370-
/// server actually published from the legacy default-endpoint fallback
1371-
/// that is synthesized when discovery finds nothing
1372-
/// ([`AuthorizationMetadataSource::LegacyEndpointFallback`]).
1373-
pub async fn discover_metadata_with_source(
1374-
&self,
1375-
) -> Result<DiscoveredAuthorizationMetadata, AuthError> {
1370+
/// # Examples
1371+
///
1372+
/// ```no_run
1373+
/// use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadataSource};
1374+
///
1375+
/// # async fn discover() -> Result<(), Box<dyn std::error::Error>> {
1376+
/// let mut manager = AuthorizationManager::new("https://mcp.example.com").await?;
1377+
/// let discovered = manager.discover_metadata().await?;
1378+
///
1379+
/// if discovered.source == AuthorizationMetadataSource::LegacyEndpointFallback {
1380+
/// println!("the server did not publish OAuth metadata");
1381+
/// }
1382+
///
1383+
/// manager.set_metadata(discovered.metadata);
1384+
/// # Ok(())
1385+
/// # }
1386+
/// ```
1387+
pub async fn discover_metadata(&self) -> Result<DiscoveredAuthorizationMetadata, AuthError> {
13761388
if let Some(metadata) = self.discover_oauth_server_via_resource_metadata().await? {
13771389
return Ok(DiscoveredAuthorizationMetadata {
13781390
metadata,
@@ -3473,8 +3485,8 @@ impl OAuthState {
34733485

34743486
*manager.current_scopes.write().await = granted_scopes.clone();
34753487

3476-
let metadata = manager.discover_metadata().await?;
3477-
manager.metadata = Some(metadata);
3488+
let discovered = manager.discover_metadata().await?;
3489+
manager.metadata = Some(discovered.metadata);
34783490

34793491
let stored = StoredCredentials {
34803492
client_id: client_id.to_string(),
@@ -3529,7 +3541,7 @@ impl OAuthState {
35293541
};
35303542
debug!("start discovery");
35313543
let metadata = match manager.discover_metadata().await {
3532-
Ok(metadata) => metadata,
3544+
Ok(discovered) => discovered.metadata,
35333545
Err(e) => {
35343546
*self = OAuthState::Unauthorized(manager);
35353547
return Err(e);
@@ -3721,8 +3733,8 @@ impl OAuthState {
37213733
};
37223734

37233735
// Discover metadata
3724-
let metadata = manager.discover_metadata().await?;
3725-
manager.metadata = Some(metadata);
3736+
let discovered = manager.discover_metadata().await?;
3737+
manager.metadata = Some(discovered.metadata);
37263738

37273739
// Validate server supports the requested auth method
37283740
manager.validate_client_credentials_metadata(&config)?;
@@ -3857,7 +3869,7 @@ mod tests {
38573869
.await
38583870
.unwrap();
38593871

3860-
let metadata = manager.discover_metadata().await.unwrap();
3872+
let metadata = manager.discover_metadata().await.unwrap().metadata;
38613873

38623874
assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
38633875
assert_eq!(
@@ -3920,7 +3932,7 @@ mod tests {
39203932
.await
39213933
.unwrap();
39223934

3923-
let metadata = manager.discover_metadata().await.unwrap();
3935+
let metadata = manager.discover_metadata().await.unwrap().metadata;
39243936

39253937
assert_eq!(
39263938
(
@@ -4102,7 +4114,7 @@ mod tests {
41024114
.await
41034115
.unwrap();
41044116

4105-
let metadata = manager.discover_metadata().await.unwrap();
4117+
let metadata = manager.discover_metadata().await.unwrap().metadata;
41064118

41074119
assert_eq!(
41084120
(
@@ -4137,7 +4149,7 @@ mod tests {
41374149
}
41384150

41394151
#[tokio::test]
4140-
async fn discover_metadata_falls_back_to_legacy_default_endpoints() {
4152+
async fn discover_metadata_reports_legacy_fallback_when_nothing_is_discovered() {
41414153
let client = RecordingOAuthHttpClient::with_responses(vec![
41424154
empty_response(404),
41434155
empty_response(404),
@@ -4152,20 +4164,22 @@ mod tests {
41524164
.await
41534165
.unwrap();
41544166

4155-
let metadata = manager.discover_metadata().await.unwrap();
4167+
let discovered = manager.discover_metadata().await.unwrap();
41564168

41574169
assert_eq!(
41584170
(
4159-
metadata.authorization_endpoint.as_str(),
4160-
metadata.token_endpoint.as_str(),
4161-
metadata.registration_endpoint.as_deref(),
4171+
discovered.source,
4172+
discovered.metadata.authorization_endpoint.as_str(),
4173+
discovered.metadata.token_endpoint.as_str(),
4174+
discovered.metadata.registration_endpoint.as_deref(),
41624175
client
41634176
.requests()
41644177
.iter()
41654178
.map(|request| request.uri.as_str())
41664179
.collect::<Vec<_>>(),
41674180
),
41684181
(
4182+
AuthorizationMetadataSource::LegacyEndpointFallback,
41694183
"https://legacy.example.com/authorize",
41704184
"https://legacy.example.com/token",
41714185
Some("https://legacy.example.com/register"),
@@ -4181,37 +4195,7 @@ mod tests {
41814195
}
41824196

41834197
#[tokio::test]
4184-
async fn discover_metadata_with_source_reports_legacy_fallback_when_nothing_is_discovered() {
4185-
let client = RecordingOAuthHttpClient::with_responses(vec![
4186-
empty_response(404),
4187-
empty_response(404),
4188-
empty_response(404),
4189-
empty_response(404),
4190-
empty_response(404),
4191-
]);
4192-
let manager = AuthorizationManager::new_with_oauth_http_client(
4193-
"https://legacy.example.com/",
4194-
Arc::new(client),
4195-
)
4196-
.await
4197-
.unwrap();
4198-
4199-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4200-
4201-
assert_eq!(
4202-
(
4203-
discovered.source,
4204-
discovered.metadata.authorization_endpoint.as_str(),
4205-
),
4206-
(
4207-
AuthorizationMetadataSource::LegacyEndpointFallback,
4208-
"https://legacy.example.com/authorize",
4209-
)
4210-
);
4211-
}
4212-
4213-
#[tokio::test]
4214-
async fn discover_metadata_with_source_reports_protected_resource_metadata() {
4198+
async fn discover_metadata_reports_protected_resource_metadata() {
42154199
let challenge = oauth2::http::Response::builder()
42164200
.status(401)
42174201
.header(
@@ -4245,7 +4229,7 @@ mod tests {
42454229
.await
42464230
.unwrap();
42474231

4248-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4232+
let discovered = manager.discover_metadata().await.unwrap();
42494233

42504234
assert_eq!(
42514235
(
@@ -4260,7 +4244,7 @@ mod tests {
42604244
}
42614245

42624246
#[tokio::test]
4263-
async fn discover_metadata_with_source_reports_authorization_server_metadata() {
4247+
async fn discover_metadata_reports_authorization_server_metadata() {
42644248
let client = RecordingOAuthHttpClient::with_responses(vec![
42654249
empty_response(404),
42664250
empty_response(404),
@@ -4281,7 +4265,7 @@ mod tests {
42814265
.await
42824266
.unwrap();
42834267

4284-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4268+
let discovered = manager.discover_metadata().await.unwrap();
42854269

42864270
assert_eq!(
42874271
(
@@ -4398,7 +4382,7 @@ mod tests {
43984382
)
43994383
.await
44004384
.unwrap();
4401-
manager.metadata = Some(manager.discover_metadata().await.unwrap());
4385+
manager.metadata = Some(manager.discover_metadata().await.unwrap().metadata);
44024386

44034387
let request = AuthorizationRequest::new("http://localhost:8080/callback")
44044388
.with_preregistered_client("preregistered-client");
@@ -4853,7 +4837,7 @@ mod tests {
48534837
.await
48544838
.unwrap();
48554839

4856-
let metadata = manager.discover_metadata().await.unwrap();
4840+
let metadata = manager.discover_metadata().await.unwrap().metadata;
48574841
let requests = client.requests();
48584842

48594843
assert_eq!(
@@ -4910,7 +4894,7 @@ mod tests {
49104894
.await
49114895
.unwrap();
49124896

4913-
let metadata = manager.discover_metadata().await.unwrap();
4897+
let metadata = manager.discover_metadata().await.unwrap().metadata;
49144898

49154899
assert_eq!(
49164900
(

docs/OAUTH_SUPPORT.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ Use this path when OAuth traffic must go through a browser fetch API, a remote
108108
execution environment, a company gateway, a test fake, or any other non-reqwest
109109
transport.
110110

111+
#### Inspect discovery provenance directly
112+
113+
Most applications can use `OAuthState` without calling metadata discovery
114+
directly. When using `AuthorizationManager`, `discover_metadata()` returns both
115+
the metadata and how it was obtained:
116+
117+
```rust ignore
118+
use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadataSource};
119+
120+
let mut manager = AuthorizationManager::new(&server_url).await?;
121+
let discovered = manager.discover_metadata().await?;
122+
123+
if discovered.source == AuthorizationMetadataSource::LegacyEndpointFallback {
124+
tracing::warn!(
125+
"the server did not publish OAuth metadata; using the 2025-03-26 fallback endpoints"
126+
);
127+
}
128+
129+
manager.set_metadata(discovered.metadata);
130+
```
131+
132+
`ProtectedResourceMetadata` and `AuthorizationServerMetadata` indicate
133+
server-published metadata. `LegacyEndpointFallback` indicates endpoints
134+
synthesized for compatibility with the 2025-03-26 MCP specification.
135+
111136
### 3. Start authorization with OAuthState
112137

113138
The `OAuthState` state machine manages the full authorization lifecycle.

0 commit comments

Comments
 (0)