xcstrings-mcp 0.4.0

MCP server for iOS/macOS .xcstrings localization file management
Documentation
use std::path::PathBuf;

#[derive(Debug, thiserror::Error)]
pub enum XcStringsError {
    #[error("file not found: {path}")]
    FileNotFound { path: PathBuf },

    #[error("invalid path {path}: {reason}")]
    InvalidPath { path: PathBuf, reason: String },

    #[error("not an .xcstrings file: {path}")]
    NotXcStrings { path: PathBuf },

    #[error("invalid format: {0}")]
    InvalidFormat(String),

    #[error("JSON parse error: {0}")]
    JsonParse(String),

    #[error("locale not found: {0}")]
    LocaleNotFound(String),

    #[error("locale already exists: {0}")]
    LocaleAlreadyExists(String),

    #[error("no active file — call parse_xcstrings first")]
    NoActiveFile,

    #[error("validation failed: {0}")]
    ValidationFailed(String),

    #[error("format specifier mismatch: {0}")]
    FormatSpecifierMismatch(String),

    #[error("missing plural form: {0}")]
    MissingPluralForm(String),

    #[error("invalid batch size: {0}")]
    InvalidBatchSize(String),

    #[error("file too large: {size_mb}MB (max {max_mb}MB)")]
    FileTooLarge { size_mb: u64, max_mb: u64 },

    #[error("key marked as shouldTranslate=false: {0}")]
    ShouldNotTranslate(String),

    #[error("file is locked by another process (likely Xcode): {path}")]
    FileLocked { path: PathBuf },

    #[error("cannot remove source locale: {0}")]
    CannotRemoveSourceLocale(String),

    #[error("glossary error: {0}")]
    GlossaryError(String),

    #[error("XLIFF parse error: {0}")]
    XliffParse(String),

    #[error("XLIFF format error: {0}")]
    XliffFormat(String),

    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error(transparent)]
    Serde(#[from] serde_json::Error),

    #[error("unexpected error: {0}")]
    Unexpected(String),
}

impl From<XcStringsError> for rmcp::model::ErrorData {
    fn from(e: XcStringsError) -> Self {
        rmcp::model::ErrorData::new(rmcp::model::ErrorCode::INTERNAL_ERROR, e.to_string(), None)
    }
}

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

    #[test]
    fn error_converts_to_mcp_error() {
        let err = XcStringsError::NoActiveFile;
        let mcp_err: rmcp::model::ErrorData = err.into();
        assert!(mcp_err.message.contains("no active file"));
    }

    #[test]
    fn error_display() {
        let err = XcStringsError::FileNotFound {
            path: PathBuf::from("/test.xcstrings"),
        };
        assert!(err.to_string().contains("/test.xcstrings"));

        let err = XcStringsError::FileTooLarge {
            size_mb: 100,
            max_mb: 50,
        };
        assert!(err.to_string().contains("100"));
        assert!(err.to_string().contains("50"));
    }
}