gallo-mcp 0.1.0

Model Context Protocol server exposing a Pico de Gallo device to AI agents
Documentation
//! Mapping from `pico-de-gallo-lib` errors to `rmcp::ErrorData`.

use pico_de_gallo_lib::host_client::HostErr;
use pico_de_gallo_lib::{PicoDeGalloError, ValidateError, WireError};
use rmcp::ErrorData;

/// True when a comms error means "the device is not currently reachable".
fn is_no_device(err: &HostErr<WireError>) -> bool {
    matches!(err, HostErr::Closed)
}

/// Map a [`PicoDeGalloError`] to an [`ErrorData`] for a tool result.
pub fn map_pdg_err<E: core::fmt::Display>(err: PicoDeGalloError<E>) -> ErrorData {
    match err {
        PicoDeGalloError::Comms(e) if is_no_device(&e) => ErrorData::internal_error(
            "no device attached: connect a Pico de Gallo and retry".to_string(),
            None,
        ),
        PicoDeGalloError::Comms(e) => {
            ErrorData::internal_error(format!("communication error: {e:?}"), None)
        }
        PicoDeGalloError::Endpoint(e) => {
            ErrorData::invalid_params(format!("device error: {e}"), None)
        }
    }
}

/// Map a [`ValidateError`] to an [`ErrorData`].
pub fn map_validate_err(err: ValidateError) -> ErrorData {
    match err {
        ValidateError::Comms(e) if is_no_device(&e) => ErrorData::internal_error(
            "no device attached: connect a Pico de Gallo and retry".to_string(),
            None,
        ),
        other => ErrorData::internal_error(format!("{other}"), None),
    }
}

/// Convert a validation-string error (from `encoding` validators) into `ErrorData`.
pub fn invalid_arg(msg: impl Into<String>) -> ErrorData {
    ErrorData::invalid_params(msg.into(), None)
}

#[cfg(test)]
mod tests {
    use super::*;
    use pico_de_gallo_lib::host_client::HostErr;
    use pico_de_gallo_lib::{PicoDeGalloError, ValidateError};

    #[test]
    fn comms_closed_becomes_no_device_message() {
        let e: PicoDeGalloError<std::convert::Infallible> =
            PicoDeGalloError::Comms(HostErr::Closed);
        let data = map_pdg_err(e);
        assert!(
            data.message.to_lowercase().contains("no device")
                || data.message.to_lowercase().contains("not attached"),
            "message was: {}",
            data.message
        );
    }

    #[test]
    fn endpoint_error_is_surfaced_via_display() {
        let e: PicoDeGalloError<String> = PicoDeGalloError::Endpoint("NoAcknowledge".into());
        let data = map_pdg_err(e);
        assert!(
            data.message.contains("NoAcknowledge"),
            "message was: {}",
            data.message
        );
    }

    #[test]
    fn validate_schema_mismatch_includes_versions() {
        let e = ValidateError::SchemaMismatch {
            expected_major: 0,
            actual_major: 0,
            expected_minor: 7,
            actual_minor: 6,
        };
        let data = map_validate_err(e);
        assert!(data.message.contains('7') && data.message.contains('6'));
    }

    #[test]
    fn validate_legacy_firmware_message() {
        let data = map_validate_err(ValidateError::LegacyFirmware);
        assert!(data.message.to_lowercase().contains("firmware"));
    }
}