bevy_brp_mcp 0.22.2

MCP server for Bevy Remote Protocol (BRP) integration
use std::fmt::Formatter;
use std::path::Path;

use error_stack::Report;
use serde_json::Value;
use thiserror::Error;

use super::tool::ResultStruct;
use crate::constants::MSG_FAILED_TO_PREFIX;
use crate::constants::MSG_INVALID_PREFIX;
use crate::constants::MSG_MISSING_PREFIX;

/// Result type for the `bevy_brp_mcp` library
pub(crate) type Result<T> = core::result::Result<T, Report<Error>>;

// Internal error types for detailed error categorization
#[derive(Error)]
pub(crate) enum Error {
    #[error("BRP communication failed: {0}")]
    BrpCommunication(String),

    #[error("File operation failed: {0}")]
    FileOperation(String),

    #[error("File or path not found error: {0}")]
    FileOrPathNotFound(String),

    #[error("{0}")]
    General(String),

    #[error("Invalid argument: {0}")]
    InvalidArgument(String),

    #[error("Invalid state: {0}")]
    InvalidState(String),

    #[error("JSON-RPC error: {0}")]
    JsonRpc(String),

    #[error("Log operation failed: {0}")]
    LogOperation(String),

    #[error("Configuration error: {0}")]
    MissingMessageTemplate(String),

    #[error("Unable to extract parameters: {0}")]
    ParameterExtraction(String),

    #[error("Process management error: {0}")]
    ProcessManagement(String),

    #[error("Schema processing error: {message}")]
    SchemaProcessing {
        message:   String,
        type_name: Option<String>,
        operation: Option<String>,
        details:   Option<String>,
    },

    #[error("Structured error")] // Generic message, the real message comes from the ResultStruct
    Structured { result: Box<dyn ResultStruct> },

    #[error("Tool call error: {message}")]
    ToolCall {
        message: String,
        details: Option<Value>,
    },

    #[error("Watch operation failed: {0}")]
    WatchOperation(String),
}

impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::BrpCommunication(s) => f.debug_tuple("BrpCommunication").field(s).finish(),
            Self::FileOperation(s) => f.debug_tuple("FileOperation").field(s).finish(),
            Self::FileOrPathNotFound(s) => f.debug_tuple("FileOrPathNotFound").field(s).finish(),
            Self::General(s) => f.debug_tuple("General").field(s).finish(),
            Self::InvalidArgument(s) => f.debug_tuple("InvalidArgument").field(s).finish(),
            Self::InvalidState(s) => f.debug_tuple("InvalidState").field(s).finish(),
            Self::JsonRpc(s) => f.debug_tuple("JsonRpc").field(s).finish(),
            Self::LogOperation(s) => f.debug_tuple("LogOperation").field(s).finish(),
            Self::MissingMessageTemplate(s) => f.debug_tuple("Configuration").field(s).finish(),
            Self::ParameterExtraction(s) => f.debug_tuple("ParameterExtraction").field(s).finish(),
            Self::ProcessManagement(s) => f.debug_tuple("ProcessManagement").field(s).finish(),
            Self::SchemaProcessing {
                message,
                type_name,
                operation,
                details,
            } => f
                .debug_struct("SchemaProcessing")
                .field("message", message)
                .field("type_name", type_name)
                .field("operation", operation)
                .field("details", details)
                .finish(),
            Self::Structured { .. } => f
                .debug_struct("Structured")
                .field("result", &"<dyn ResultStruct>")
                .finish(),
            Self::ToolCall { message, details } => f
                .debug_struct("ToolCall")
                .field("message", message)
                .field("details", details)
                .finish(),
            Self::WatchOperation(s) => f.debug_tuple("WatchOperation").field(s).finish(),
        }
    }
}

impl Error {
    /// Create a "Failed to X" error
    pub(crate) fn failed_to(action: &str, details: impl std::fmt::Display) -> Self {
        Self::General(format!("{MSG_FAILED_TO_PREFIX} {action}: {details}"))
    }

    /// Create an "Invalid X" error
    pub(crate) fn invalid(what: &str, details: impl std::fmt::Display) -> Self {
        Self::InvalidArgument(format!("{MSG_INVALID_PREFIX} {what}: {details}"))
    }

    /// Create a "Missing X" error
    pub(crate) fn missing(what: &str) -> Self {
        Self::InvalidArgument(format!("{MSG_MISSING_PREFIX} {what}"))
    }

    /// Create error for IO operations
    pub(crate) fn io_failed(operation: &str, path: &Path, error: impl std::fmt::Display) -> Self {
        Self::LogOperation(format!(
            "{MSG_FAILED_TO_PREFIX} {operation} {}: {error}",
            path.display()
        ))
    }

    /// Create a tool error with just a message
    pub(crate) fn tool_call_failed(message: impl Into<String>) -> Self {
        Self::ToolCall {
            message: message.into(),
            details: None,
        }
    }

    /// Create a tool error with message and details
    pub(crate) fn tool_call_failed_with_details(
        message: impl Into<String>,
        details: Value,
    ) -> Self {
        Self::ToolCall {
            message: message.into(),
            details: Some(details),
        }
    }

    /// Create a schema processing error for a specific type
    pub(crate) fn schema_processing_for_type(
        type_name: impl Into<String>,
        operation: impl Into<String>,
        details: impl Into<String>,
    ) -> Self {
        Self::SchemaProcessing {
            message:   "Failed to process schema for type".to_string(),
            type_name: Some(type_name.into()),
            operation: Some(operation.into()),
            details:   Some(details.into()),
        }
    }
}

// Note: We don't implement From<Error> for McpError because our errors
// are handled internally and converted to structured responses.
// Errors should never escape our tool handlers.