rmcp 3.1.0

Rust SDK for Model Context Protocol
Documentation
use std::{any::TypeId, error::Error};

use rmcp::{
    service::ClientInitializeError,
    transport::{
        AuthError, DynamicTransportError,
        streamable_http_client::{AuthRequiredError, InsufficientScopeError, StreamableHttpError},
    },
};
use thiserror::Error;

type TestHttpError = StreamableHttpError<std::io::Error>;

#[derive(Debug, Error)]
#[error("outer transport wrapper")]
struct OuterError(#[source] TestHttpError);

fn initialization_error(error: impl Error + Send + Sync + 'static) -> ClientInitializeError {
    ClientInitializeError::TransportError {
        error: DynamicTransportError::from_parts(
            "test transport",
            TypeId::of::<()>(),
            Box::new(error),
        ),
        context: "initialize".into(),
    }
}

#[test]
fn classifies_local_authorization_required() {
    let error = TestHttpError::Auth(AuthError::AuthorizationRequired);

    assert!(initialization_error(error).is_authorization_required());
}

#[test]
fn classifies_http_authorization_challenge() {
    let error =
        TestHttpError::AuthRequired(AuthRequiredError::new("Bearer realm=\"mcp\"".to_owned()));

    assert!(initialization_error(error).is_authorization_required());
}

#[test]
fn classifies_authorization_required_through_multiple_sources() {
    let error = OuterError(TestHttpError::Auth(AuthError::AuthorizationRequired));

    assert!(initialization_error(error).is_authorization_required());
}

#[test]
fn does_not_classify_unrelated_transport_errors() {
    let closed = TestHttpError::TransportChannelClosed;
    let refresh = TestHttpError::Auth(AuthError::TokenRefreshFailed("timeout".to_owned()));
    let scope = TestHttpError::InsufficientScope(InsufficientScopeError::new(
        "Bearer error=\"insufficient_scope\"".to_owned(),
        Some("admin".to_owned()),
    ));

    assert!(!initialization_error(closed).is_authorization_required());
    assert!(!initialization_error(refresh).is_authorization_required());
    assert!(!initialization_error(scope).is_authorization_required());
}

#[test]
fn does_not_classify_non_transport_initialization_errors() {
    assert!(!ClientInitializeError::Cancelled.is_authorization_required());
    assert!(
        !ClientInitializeError::ConnectionClosed("server closed the connection".to_owned())
            .is_authorization_required()
    );
}

#[test]
fn http_challenge_remains_available_as_an_error_source() {
    let error =
        TestHttpError::AuthRequired(AuthRequiredError::new("Bearer realm=\"mcp\"".to_owned()));

    let source = error.source().expect("auth challenge should be a source");
    let challenge = source
        .downcast_ref::<AuthRequiredError>()
        .expect("source should retain the challenge type");

    assert_eq!(challenge.www_authenticate_header, "Bearer realm=\"mcp\"");
}