jumperless-mcp 0.1.0

MCP server for the Jumperless V5 — persistent USB-serial bridge exposing the firmware API to LLMs
//! Error hierarchy for MCP servers.
//!
//! Two top-level enums:
//! - [`McpError`] — general MCP server errors (transport, tool dispatch, protocol, etc.)
//! - [`DiscoveryError`] — USB device discovery failures

use thiserror::Error;

/// General MCP server error. Returned by [`crate::Subsystem`] lifecycle methods,
/// [`crate::run`], [`crate::run_federation`], and the transport layer.
#[derive(Error, Debug)]
pub enum McpError {
    #[error("subsystem discovery failed: {0}")]
    Discovery(#[from] DiscoveryError),

    #[error("transport error: {0}")]
    Transport(String),

    /// Tool not found in the subsystem or federation registry.
    #[error("tool not found: '{name}'")]
    ToolNotFound { name: String },

    /// Tool name is syntactically invalid (e.g., wrong prefix, malformed).
    #[error("tool name malformed: '{name}' — {reason}")]
    ToolNameMalformed { name: String, reason: String },

    /// Serial port open or I/O failure during connect/disconnect.
    #[error("connection error: {0}")]
    Connection(String),

    #[error("protocol error: {0}")]
    Protocol(String),

    /// Escape hatch: subsystem-specific error that doesn't fit the above.
    /// Wrap with `McpError::Subsystem(Box::new(e))`.
    #[error("subsystem-specific: {0}")]
    Subsystem(#[source] Box<dyn std::error::Error + Send + Sync>),

    /// Multiple errors accumulated (e.g., during federation disconnect/shutdown).
    /// Used when all subsystems are attempted regardless of failures.
    #[error("multiple errors: {}", .0.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("; "))]
    Multiple(Vec<McpError>),
}

/// USB device discovery failures. Raised by the helpers in [`crate::base::usb`].
#[derive(Error, Debug)]
pub enum DiscoveryError {
    /// No device matched the search criteria. `message` explains what was searched
    /// and how many results were found, to aid diagnosis.
    #[error("device not found: {message}")]
    NotFound { message: String },

    /// Device(s) found but the requested port_index is out of range.
    ///
    /// Distinct from `NotFound` (which means no matching device at all).
    /// This variant fires when the VID:PID matched `found` ports but the caller
    /// requested index `requested` which is ≥ `found`.
    #[error(
        "device(s) found but requested port_index {requested} is out of range \
         (found {found} matching VID:PID {vid:04x}:{pid:04x})"
    )]
    IndexOutOfRange {
        requested: usize,
        found: usize,
        vid: u16,
        pid: u16,
    },

    /// Multiple devices matched where exactly one was expected. `count` is the
    /// number of matches; `hint` suggests how to disambiguate.
    #[error("{count} devices matched — {hint}")]
    Ambiguous { count: usize, hint: String },

    #[error("permission denied accessing serial port")]
    PermissionDenied,

    #[error("--port value is empty (omit the flag entirely to enable auto-discovery)")]
    InvalidPort,

    #[error("serialport library error: {0}")]
    Serial(#[from] serialport::Error),
}