rust-mcp-sdk 0.10.0

An asynchronous SDK and framework for building MCP-Servers and MCP-Clients, leveraging the rust-mcp-schema for type safe MCP Schema Objects.
Documentation
use serde::{Deserialize, Serialize};

/// Response from a successful token exchange (RFC 6749).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenResponse {
    pub access_token: String,
    pub token_type: String,
    #[serde(default)]
    pub expires_in: Option<u64>,
    #[serde(default)]
    pub refresh_token: Option<String>,
    #[serde(default)]
    pub scope: Option<String>,
}

impl TokenResponse {
    fn now_secs() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }

    pub fn issued_at_secs(&self) -> u64 {
        Self::now_secs()
    }

    pub fn expires_at_secs(&self) -> Option<u64> {
        self.expires_in
            .map(|secs| self.issued_at_secs().saturating_add(secs))
    }

    pub fn is_expired(&self) -> bool {
        self.expires_at_secs()
            .map(|exp| exp <= Self::now_secs().saturating_add(30))
            .unwrap_or(false)
    }
}

/// OAuth 2.0 grant types supported by the client.
///
/// Variants carry their own parameters so `McpAuthClient::exchange_token`
/// takes only a `&GrantType`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GrantType {
    /// Machine-to-machine authentication (RFC 6749 §4.4)
    ClientCredentials,
    /// User authorization code exchange (RFC 6749 §4.1)
    AuthorizationCode { code: String, redirect_uri: String },
    /// Authorization code with PKCE (RFC 7636)
    AuthorizationCodePkce {
        code: String,
        redirect_uri: String,
        /// The PKCE code verifier generated by `generate_pkce_params`
        code_verifier: String,
    },
    /// Refresh an expired access token (RFC 6749 §6)
    RefreshToken { refresh_token: String },
}

impl GrantType {
    pub fn as_str(&self) -> &'static str {
        match self {
            GrantType::ClientCredentials => "client_credentials",
            GrantType::AuthorizationCode { .. } => "authorization_code",
            GrantType::AuthorizationCodePkce { .. } => "authorization_code",
            GrantType::RefreshToken { .. } => "refresh_token",
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn token_response_deserialize_full() {
        let json = r#"{
            "access_token": "abc123",
            "token_type": "bearer",
            "expires_in": 3600,
            "refresh_token": "ref456",
            "scope": "mcp read"
        }"#;
        let token: TokenResponse = serde_json::from_str(json).unwrap();
        assert_eq!(token.access_token, "abc123");
        assert_eq!(token.token_type, "bearer");
        assert_eq!(token.expires_in, Some(3600));
        assert_eq!(token.refresh_token.as_deref(), Some("ref456"));
        assert_eq!(token.scope.as_deref(), Some("mcp read"));
        assert!(!token.is_expired());
    }

    #[test]
    fn token_response_deserialize_minimal() {
        let json = r#"{"access_token": "abc", "token_type": "bearer"}"#;
        let token: TokenResponse = serde_json::from_str(json).unwrap();
        assert_eq!(token.access_token, "abc");
        assert_eq!(token.expires_in, None);
        assert_eq!(token.refresh_token, None);
    }

    #[test]
    fn token_with_very_short_expiry_is_expired() {
        let json = r#"{"access_token": "x", "token_type": "bearer", "expires_in": 0}"#;
        let token: TokenResponse = serde_json::from_str(json).unwrap();
        assert!(token.is_expired());
    }

    #[test]
    fn grant_type_as_str() {
        assert_eq!(GrantType::ClientCredentials.as_str(), "client_credentials");
        assert_eq!(
            GrantType::AuthorizationCode {
                code: "x".into(),
                redirect_uri: "https://cb".into()
            }
            .as_str(),
            "authorization_code"
        );
        assert_eq!(
            GrantType::AuthorizationCodePkce {
                code: "x".into(),
                redirect_uri: "https://cb".into(),
                code_verifier: "v".into(),
            }
            .as_str(),
            "authorization_code"
        );
        assert_eq!(
            GrantType::RefreshToken {
                refresh_token: "rt".into()
            }
            .as_str(),
            "refresh_token"
        );
    }
}