feat: expose oauth discovery metadata source (#1041)
* feat: expose oauth discovery metadata source * feat!: return source from metadata resolution
This commit is contained in:
parent
6839cfd613
commit
60a7a75703
4 changed files with 277 additions and 39 deletions
|
|
@ -509,8 +509,8 @@ async fn migration_token(
|
|||
return Ok(manager.get_access_token().await?);
|
||||
}
|
||||
|
||||
let metadata = manager.discover_metadata().await?;
|
||||
manager.set_metadata(metadata);
|
||||
let resolution = manager.resolve_metadata().await?;
|
||||
manager.set_metadata(resolution.metadata);
|
||||
manager
|
||||
.register_client("conformance-client", REDIRECT_URI, &[])
|
||||
.await?;
|
||||
|
|
@ -609,9 +609,9 @@ async fn run_client_credentials_basic(
|
|||
.unwrap_or("conformance-test-secret");
|
||||
|
||||
let mut manager = AuthorizationManager::new(server_url).await?;
|
||||
let metadata = manager.discover_metadata().await?;
|
||||
let token_endpoint = metadata.token_endpoint.clone();
|
||||
manager.set_metadata(metadata);
|
||||
let resolution = manager.resolve_metadata().await?;
|
||||
let token_endpoint = resolution.metadata.token_endpoint.clone();
|
||||
manager.set_metadata(resolution.metadata);
|
||||
|
||||
let http = reqwest::Client::new();
|
||||
let resp = http
|
||||
|
|
|
|||
|
|
@ -578,6 +578,43 @@ pub struct AuthorizationMetadata {
|
|||
pub additional_fields: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// How [`AuthorizationMetadata`] was obtained during discovery.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum AuthorizationMetadataSource {
|
||||
/// Discovered through RFC 9728 protected resource metadata.
|
||||
ProtectedResourceMetadata,
|
||||
/// Discovered through RFC 8414 / OpenID Connect metadata at the server's
|
||||
/// base URL.
|
||||
AuthorizationServerMetadata,
|
||||
/// Nothing was discovered; the endpoints were synthesized from the base
|
||||
/// URL (`/authorize`, `/token`, `/register`) for compatibility with the
|
||||
/// 2025-03-26 MCP spec's default-endpoint fallback. The server gave no
|
||||
/// evidence that it supports OAuth.
|
||||
///
|
||||
/// [Newer MCP revisions] require metadata discovery and do not define an
|
||||
/// endpoint-synthesis fallback.
|
||||
///
|
||||
/// [Newer MCP revisions]: https://modelcontextprotocol.io/specification/draft/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements
|
||||
LegacyEndpointFallback,
|
||||
}
|
||||
|
||||
impl AuthorizationMetadataSource {
|
||||
/// Whether the metadata was actually published by the server, as opposed
|
||||
/// to synthesized by the client as a legacy compatibility fallback.
|
||||
pub fn is_discovered(self) -> bool {
|
||||
!matches!(self, Self::LegacyEndpointFallback)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`AuthorizationMetadata`] together with how it was resolved.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct AuthorizationMetadataResolution {
|
||||
pub metadata: AuthorizationMetadata,
|
||||
pub source: AuthorizationMetadataSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct ResourceServerMetadata {
|
||||
resource: Option<String>,
|
||||
|
|
@ -1236,8 +1273,10 @@ impl AuthorizationManager {
|
|||
|
||||
/// Set OAuth2 authorization metadata
|
||||
///
|
||||
/// This should be called after discovering metadata via `discover_metadata()`
|
||||
/// and before creating an `AuthorizationSession`.
|
||||
/// This should be called with
|
||||
/// [`AuthorizationMetadataResolution::metadata`] after
|
||||
/// [`Self::resolve_metadata`] and before creating an
|
||||
/// [`AuthorizationSession`].
|
||||
pub fn set_metadata(&mut self, metadata: AuthorizationMetadata) {
|
||||
self.metadata = Some(metadata);
|
||||
}
|
||||
|
|
@ -1252,8 +1291,8 @@ impl AuthorizationManager {
|
|||
&& stored.token_response.is_some()
|
||||
{
|
||||
if self.metadata.is_none() {
|
||||
let metadata = self.discover_metadata().await?;
|
||||
self.metadata = Some(metadata);
|
||||
let resolution = self.resolve_metadata().await?;
|
||||
self.metadata = Some(resolution.metadata);
|
||||
}
|
||||
|
||||
if let (Some(stored_issuer), Some(current_issuer)) =
|
||||
|
|
@ -1320,18 +1359,51 @@ impl AuthorizationManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// discover oauth2 metadata (per SEP-985: Protected Resource Metadata first, then direct OAuth)
|
||||
pub async fn discover_metadata(&self) -> Result<AuthorizationMetadata, AuthError> {
|
||||
/// Resolve OAuth 2.0 metadata and report how it was obtained.
|
||||
///
|
||||
/// Discovery follows SEP-985: protected resource metadata first, then
|
||||
/// direct OAuth 2.0 Authorization Server Metadata or OpenID Connect
|
||||
/// Discovery. When discovery finds nothing, the result contains legacy
|
||||
/// default endpoints derived from the base URL and
|
||||
/// [`AuthorizationMetadataSource::LegacyEndpointFallback`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadataSource};
|
||||
///
|
||||
/// # async fn resolve() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut manager = AuthorizationManager::new("https://mcp.example.com").await?;
|
||||
/// let resolution = manager.resolve_metadata().await?;
|
||||
///
|
||||
/// if resolution.source == AuthorizationMetadataSource::LegacyEndpointFallback {
|
||||
/// println!("the server did not publish OAuth metadata");
|
||||
/// }
|
||||
///
|
||||
/// manager.set_metadata(resolution.metadata);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn resolve_metadata(&self) -> Result<AuthorizationMetadataResolution, AuthError> {
|
||||
if let Some(metadata) = self.discover_oauth_server_via_resource_metadata().await? {
|
||||
return Ok(metadata);
|
||||
return Ok(AuthorizationMetadataResolution {
|
||||
metadata,
|
||||
source: AuthorizationMetadataSource::ProtectedResourceMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(metadata) = self.try_discover_oauth_server(&self.base_url).await? {
|
||||
return Ok(metadata);
|
||||
return Ok(AuthorizationMetadataResolution {
|
||||
metadata,
|
||||
source: AuthorizationMetadataSource::AuthorizationServerMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
debug!("falling back to legacy OAuth endpoints derived from the base URL");
|
||||
Ok(Self::legacy_authorization_metadata(&self.base_url))
|
||||
Ok(AuthorizationMetadataResolution {
|
||||
metadata: Self::legacy_authorization_metadata(&self.base_url),
|
||||
source: AuthorizationMetadataSource::LegacyEndpointFallback,
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_authorization_metadata(base_url: &Url) -> AuthorizationMetadata {
|
||||
|
|
@ -3413,8 +3485,8 @@ impl OAuthState {
|
|||
|
||||
*manager.current_scopes.write().await = granted_scopes.clone();
|
||||
|
||||
let metadata = manager.discover_metadata().await?;
|
||||
manager.metadata = Some(metadata);
|
||||
let resolution = manager.resolve_metadata().await?;
|
||||
manager.metadata = Some(resolution.metadata);
|
||||
|
||||
let stored = StoredCredentials {
|
||||
client_id: client_id.to_string(),
|
||||
|
|
@ -3468,8 +3540,8 @@ impl OAuthState {
|
|||
));
|
||||
};
|
||||
debug!("start discovery");
|
||||
let metadata = match manager.discover_metadata().await {
|
||||
Ok(metadata) => metadata,
|
||||
let metadata = match manager.resolve_metadata().await {
|
||||
Ok(resolution) => resolution.metadata,
|
||||
Err(e) => {
|
||||
*self = OAuthState::Unauthorized(manager);
|
||||
return Err(e);
|
||||
|
|
@ -3661,8 +3733,8 @@ impl OAuthState {
|
|||
};
|
||||
|
||||
// Discover metadata
|
||||
let metadata = manager.discover_metadata().await?;
|
||||
manager.metadata = Some(metadata);
|
||||
let resolution = manager.resolve_metadata().await?;
|
||||
manager.metadata = Some(resolution.metadata);
|
||||
|
||||
// Validate server supports the requested auth method
|
||||
manager.validate_client_credentials_metadata(&config)?;
|
||||
|
|
@ -3691,10 +3763,10 @@ mod tests {
|
|||
|
||||
use super::{
|
||||
AuthError, AuthorizationCallback, AuthorizationManager, AuthorizationMetadata,
|
||||
AuthorizationRequest, AuthorizationSession, CredentialStore, InMemoryCredentialStore,
|
||||
InMemoryStateStore, OAuthClientConfig, OAuthHttpClient, OAuthHttpClientError,
|
||||
OAuthHttpClientFuture, OAuthHttpRedirectPolicy, OAuthHttpRequest, ScopeUpgradeConfig,
|
||||
StateStore, StoredAuthorizationState, is_https_url,
|
||||
AuthorizationMetadataSource, AuthorizationRequest, AuthorizationSession, CredentialStore,
|
||||
InMemoryCredentialStore, InMemoryStateStore, OAuthClientConfig, OAuthHttpClient,
|
||||
OAuthHttpClientError, OAuthHttpClientFuture, OAuthHttpRedirectPolicy, OAuthHttpRequest,
|
||||
ScopeUpgradeConfig, StateStore, StoredAuthorizationState, is_https_url,
|
||||
};
|
||||
use crate::transport::auth::VendorExtraTokenFields;
|
||||
|
||||
|
|
@ -3797,7 +3869,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let metadata = manager.resolve_metadata().await.unwrap().metadata;
|
||||
|
||||
assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
|
||||
assert_eq!(
|
||||
|
|
@ -3860,7 +3932,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let metadata = manager.resolve_metadata().await.unwrap().metadata;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
|
|
@ -3919,7 +3991,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = manager.discover_metadata().await.unwrap_err();
|
||||
let error = manager.resolve_metadata().await.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
|
|
@ -4042,7 +4114,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let metadata = manager.resolve_metadata().await.unwrap().metadata;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
|
|
@ -4077,7 +4149,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_metadata_falls_back_to_legacy_default_endpoints() {
|
||||
async fn resolve_metadata_reports_legacy_fallback_when_nothing_is_discovered() {
|
||||
let client = RecordingOAuthHttpClient::with_responses(vec![
|
||||
empty_response(404),
|
||||
empty_response(404),
|
||||
|
|
@ -4092,13 +4164,14 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let resolution = manager.resolve_metadata().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
metadata.authorization_endpoint.as_str(),
|
||||
metadata.token_endpoint.as_str(),
|
||||
metadata.registration_endpoint.as_deref(),
|
||||
resolution.source,
|
||||
resolution.metadata.authorization_endpoint.as_str(),
|
||||
resolution.metadata.token_endpoint.as_str(),
|
||||
resolution.metadata.registration_endpoint.as_deref(),
|
||||
client
|
||||
.requests()
|
||||
.iter()
|
||||
|
|
@ -4106,6 +4179,7 @@ mod tests {
|
|||
.collect::<Vec<_>>(),
|
||||
),
|
||||
(
|
||||
AuthorizationMetadataSource::LegacyEndpointFallback,
|
||||
"https://legacy.example.com/authorize",
|
||||
"https://legacy.example.com/token",
|
||||
Some("https://legacy.example.com/register"),
|
||||
|
|
@ -4120,6 +4194,108 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_metadata_reports_protected_resource_metadata() {
|
||||
let challenge = oauth2::http::Response::builder()
|
||||
.status(401)
|
||||
.header(
|
||||
"www-authenticate",
|
||||
r#"Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource""#,
|
||||
)
|
||||
.body(Vec::new())
|
||||
.unwrap();
|
||||
let client = RecordingOAuthHttpClient::with_responses(vec![
|
||||
challenge,
|
||||
http_response(
|
||||
200,
|
||||
serde_json::json!({
|
||||
"resource": "https://mcp.example.com/mcp",
|
||||
"authorization_servers": ["https://auth.example.com"]
|
||||
}),
|
||||
),
|
||||
http_response(
|
||||
200,
|
||||
serde_json::json!({
|
||||
"issuer": "https://auth.example.com",
|
||||
"authorization_endpoint": "https://auth.example.com/authorize",
|
||||
"token_endpoint": "https://auth.example.com/token"
|
||||
}),
|
||||
),
|
||||
]);
|
||||
let manager = AuthorizationManager::new_with_oauth_http_client(
|
||||
"https://mcp.example.com/mcp",
|
||||
Arc::new(client),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resolution = manager.resolve_metadata().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
resolution.source,
|
||||
resolution.metadata.token_endpoint.as_str(),
|
||||
),
|
||||
(
|
||||
AuthorizationMetadataSource::ProtectedResourceMetadata,
|
||||
"https://auth.example.com/token",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_metadata_reports_authorization_server_metadata() {
|
||||
let client = RecordingOAuthHttpClient::with_responses(vec![
|
||||
empty_response(404),
|
||||
empty_response(404),
|
||||
empty_response(404),
|
||||
http_response(
|
||||
200,
|
||||
serde_json::json!({
|
||||
"issuer": "https://mcp.example.com",
|
||||
"authorization_endpoint": "https://mcp.example.com/oauth/authorize",
|
||||
"token_endpoint": "https://mcp.example.com/oauth/token"
|
||||
}),
|
||||
),
|
||||
]);
|
||||
let manager = AuthorizationManager::new_with_oauth_http_client(
|
||||
"https://mcp.example.com/",
|
||||
Arc::new(client),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resolution = manager.resolve_metadata().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
resolution.source,
|
||||
resolution.metadata.token_endpoint.as_str(),
|
||||
),
|
||||
(
|
||||
AuthorizationMetadataSource::AuthorizationServerMetadata,
|
||||
"https://mcp.example.com/oauth/token",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::protected_resource_metadata(
|
||||
AuthorizationMetadataSource::ProtectedResourceMetadata,
|
||||
true
|
||||
)]
|
||||
#[case::authorization_server_metadata(
|
||||
AuthorizationMetadataSource::AuthorizationServerMetadata,
|
||||
true
|
||||
)]
|
||||
#[case::legacy_endpoint_fallback(AuthorizationMetadataSource::LegacyEndpointFallback, false)]
|
||||
fn is_discovered_is_false_only_for_the_legacy_fallback(
|
||||
#[case] source: AuthorizationMetadataSource,
|
||||
#[case] expected: bool,
|
||||
) {
|
||||
assert_eq!(source.is_discovered(), expected);
|
||||
}
|
||||
|
||||
fn preregistered_as_metadata_response() -> HttpResponse {
|
||||
http_response(
|
||||
200,
|
||||
|
|
@ -4206,7 +4382,7 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
manager.metadata = Some(manager.discover_metadata().await.unwrap());
|
||||
manager.metadata = Some(manager.resolve_metadata().await.unwrap().metadata);
|
||||
|
||||
let request = AuthorizationRequest::new("http://localhost:8080/callback")
|
||||
.with_preregistered_client("preregistered-client");
|
||||
|
|
@ -4661,7 +4837,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let metadata = manager.resolve_metadata().await.unwrap().metadata;
|
||||
let requests = client.requests();
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -4718,7 +4894,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata = manager.discover_metadata().await.unwrap();
|
||||
let metadata = manager.resolve_metadata().await.unwrap().metadata;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
|
|
@ -4767,7 +4943,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = manager.discover_metadata().await.unwrap_err();
|
||||
let error = manager.resolve_metadata().await.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(error, AuthError::MetadataError(ref message) if message.contains("resource mismatch")),
|
||||
|
|
@ -4802,7 +4978,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = manager.discover_metadata().await.unwrap_err();
|
||||
let error = manager.resolve_metadata().await.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(error, AuthError::MetadataError(ref message) if message.contains("missing required resource")),
|
||||
|
|
|
|||
|
|
@ -108,6 +108,51 @@ Use this path when OAuth traffic must go through a browser fetch API, a remote
|
|||
execution environment, a company gateway, a test fake, or any other non-reqwest
|
||||
transport.
|
||||
|
||||
#### Inspect discovery provenance directly
|
||||
|
||||
Most applications can use `OAuthState` without calling metadata discovery
|
||||
directly. When using `AuthorizationManager`, `resolve_metadata()` returns both
|
||||
the metadata and how it was obtained. A client that supports the 2025-03-26
|
||||
default-endpoint fallback can continue with synthesized metadata, while a
|
||||
client that requires server-published metadata should reject that result:
|
||||
|
||||
```rust ignore
|
||||
use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadataSource};
|
||||
|
||||
async fn configure_metadata(
|
||||
manager: &mut AuthorizationManager,
|
||||
allow_legacy_endpoint_fallback: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let resolution = manager.resolve_metadata().await?;
|
||||
|
||||
if resolution.source == AuthorizationMetadataSource::LegacyEndpointFallback {
|
||||
if !allow_legacy_endpoint_fallback {
|
||||
anyhow::bail!("the server did not publish OAuth metadata");
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"the server did not publish OAuth metadata; using the 2025-03-26 fallback endpoints"
|
||||
);
|
||||
}
|
||||
|
||||
manager.set_metadata(resolution.metadata);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`ProtectedResourceMetadata` and `AuthorizationServerMetadata` indicate
|
||||
server-published metadata, so clients can proceed with the returned metadata.
|
||||
`LegacyEndpointFallback` indicates endpoints synthesized for compatibility
|
||||
with the 2025-03-26 MCP specification. Clients should proceed only when they
|
||||
intentionally support that legacy behavior; clients using discovery as an
|
||||
OAuth capability check should treat it as unsupported.
|
||||
|
||||
Applications using `OAuthState` do not need to handle these sources directly:
|
||||
the state machine resolves metadata internally and retains the legacy fallback.
|
||||
Low-level `AuthorizationManager` users can use
|
||||
`AuthorizationMetadataSource::is_discovered()` when they only need to
|
||||
distinguish server-published metadata from synthesized metadata.
|
||||
|
||||
### 3. Start authorization with OAuthState
|
||||
|
||||
The `OAuthState` state machine manages the full authorization lifecycle.
|
||||
|
|
@ -229,7 +274,8 @@ match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
|
|||
|
||||
## Complete Examples
|
||||
|
||||
- **Client**: [`examples/clients/src/auth/oauth_client.rs`](../examples/clients/src/auth/oauth_client.rs)
|
||||
- **Authorization Code client**: [`examples/clients/src/auth/oauth_client.rs`](../examples/clients/src/auth/oauth_client.rs)
|
||||
- **Client Credentials client**: [`examples/clients/src/auth/client_credentials.rs`](../examples/clients/src/auth/client_credentials.rs)
|
||||
- **Server**: [`examples/servers/src/complex_auth_streamhttp.rs`](../examples/servers/src/complex_auth_streamhttp.rs)
|
||||
|
||||
### Running the Examples
|
||||
|
|
@ -240,6 +286,10 @@ cargo run -p mcp-server-examples --example servers_complex_auth_streamhttp
|
|||
|
||||
# Run the OAuth client (in another terminal)
|
||||
cargo run -p mcp-client-examples --example clients_oauth_client
|
||||
|
||||
# Run the Client Credentials client
|
||||
cargo run -p mcp-client-examples --example clients_client_credentials -- \
|
||||
<server_url> <client_id> <client_secret>
|
||||
```
|
||||
|
||||
## Authorization Flow Description
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ A client demonstrating how to authenticate with an MCP server using OAuth.
|
|||
- Establishes an authorized connection to the MCP server using the acquired access token
|
||||
- Demonstrates how to use the authorized connection to retrieve available tools and prompts
|
||||
|
||||
### OAuth Client Credentials (`auth/client_credentials.rs`)
|
||||
|
||||
A client demonstrating the OAuth 2.0 Client Credentials flow from SEP-1046.
|
||||
|
||||
- Accepts the server URL, client ID, and client secret as command-line arguments
|
||||
- Authenticates without an interactive browser or callback server
|
||||
- Establishes an authorized connection and retrieves the available tools
|
||||
|
||||
|
||||
### Sampling Standard I/O Client (`sampling_stdio.rs`)
|
||||
|
||||
|
|
@ -107,6 +115,10 @@ cargo run -p mcp-client-examples --example clients_collection
|
|||
# Run the OAuth client example
|
||||
cargo run -p mcp-client-examples --example clients_oauth_client
|
||||
|
||||
# Run the OAuth Client Credentials example
|
||||
cargo run -p mcp-client-examples --example clients_client_credentials -- \
|
||||
<server_url> <client_id> <client_secret>
|
||||
|
||||
# Run the sampling standard I/O client example
|
||||
cargo run -p mcp-client-examples --example clients_sampling_stdio
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue