sqlserver-mcp 0.4.1

SQL Server 2025/2022/2019/2017 - master/msdb/sandbox combined catalog MCP server, generated by mcpify.
Documentation
// SQL Server 2025 - master/msdb/sandbox combined catalog MCP server — generated by mcpify. Do not hand-edit.

use thiserror::Error;

/// Mirrors `targets::typescript`'s `McpifyError` hierarchy (`errors.ts`):
/// one variant per error family, each carrying the literal `code()` string
/// callers match on instead of downcasting.
#[derive(Debug, Error)]
pub enum McpifyError {
    #[error("{0}")]
    Configuration(String),

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

    #[error("{message}{}", format_validation_details(details))]
    Validation {
        message: String,
        details: Option<serde_json::Value>,
    },

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

    #[error("circuit breaker is open")]
    CircuitBreakerOpen,

    #[error("rate limit exceeded")]
    RateLimitExceeded,
}

/// Renders `Validation::details` (a JSON array of jsonschema/Ajv violation
/// strings, or `None` when the schema itself is missing) as a `": a; b; c"`
/// suffix, so `Display`/`to_string()` — what `run_tool` sends back as the
/// MCP tool's error text — carries the actual violated constraints instead
/// of just the generic "invalid input for 'X'" / "unexpected response
/// shape for 'X'" message.
fn format_validation_details(details: &Option<serde_json::Value>) -> String {
    match details.as_ref().and_then(serde_json::Value::as_array) {
        Some(errors) if !errors.is_empty() => {
            let joined = errors
                .iter()
                .filter_map(serde_json::Value::as_str)
                .collect::<Vec<_>>()
                .join("; ");
            format!(": {joined}")
        }
        _ => String::new(),
    }
}

impl McpifyError {
    pub fn code(&self) -> &'static str {
        match self {
            Self::Configuration(_) => "CONFIGURATION_ERROR",
            Self::Authentication(_) => "AUTHENTICATION_ERROR",
            Self::Validation { .. } => "VALIDATION_ERROR",
            Self::NotFound(_) => "NOT_FOUND",
            Self::CircuitBreakerOpen => "CIRCUIT_BREAKER_OPEN",
            Self::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
        }
    }
}

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

    #[test]
    fn every_variant_reports_its_error_code() {
        assert_eq!(
            McpifyError::Configuration("x".into()).code(),
            "CONFIGURATION_ERROR"
        );
        assert_eq!(
            McpifyError::Authentication("x".into()).code(),
            "AUTHENTICATION_ERROR"
        );
        assert_eq!(
            McpifyError::Validation {
                message: "x".into(),
                details: None
            }
            .code(),
            "VALIDATION_ERROR"
        );
        assert_eq!(McpifyError::NotFound("x".into()).code(), "NOT_FOUND");
        assert_eq!(
            McpifyError::CircuitBreakerOpen.code(),
            "CIRCUIT_BREAKER_OPEN"
        );
        assert_eq!(McpifyError::RateLimitExceeded.code(), "RATE_LIMIT_EXCEEDED");
    }

    #[test]
    fn validation_display_appends_details_when_present() {
        let err = McpifyError::Validation {
            message: "unexpected response shape for 'getAllProjects'".into(),
            details: Some(serde_json::json!(["\"foo\" is not of type \"object\""])),
        };
        assert_eq!(
            err.to_string(),
            "unexpected response shape for 'getAllProjects': \"foo\" is not of type \"object\""
        );
    }

    #[test]
    fn validation_display_omits_suffix_when_details_absent() {
        let err = McpifyError::Validation {
            message: "invalid input for 'getIssue'".into(),
            details: None,
        };
        assert_eq!(err.to_string(), "invalid input for 'getIssue'");
    }
}