zeph-mcp 0.18.2

MCP client with multi-server lifecycle and Qdrant tool registry for Zeph
Documentation
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

#[derive(Debug, thiserror::Error)]
pub enum McpError {
    #[error("connection failed for server '{server_id}': {message}")]
    Connection { server_id: String, message: String },

    #[error("tool call failed: {server_id}/{tool_name}: {message}")]
    ToolCall {
        server_id: String,
        tool_name: String,
        message: String,
    },

    #[error("server '{server_id}' not found")]
    ServerNotFound { server_id: String },

    #[error("server '{server_id}' is already connected")]
    ServerAlreadyConnected { server_id: String },

    #[error("tool '{tool_name}' not found on server '{server_id}'")]
    ToolNotFound {
        server_id: String,
        tool_name: String,
    },

    #[error("tool call timed out after {timeout_secs}s: {server_id}/{tool_name}")]
    Timeout {
        server_id: String,
        tool_name: String,
        timeout_secs: u64,
    },

    #[error("Qdrant error: {0}")]
    Qdrant(#[from] Box<qdrant_client::QdrantError>),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("integer conversion: {0}")]
    IntConversion(#[from] std::num::TryFromIntError),

    #[error("SSRF blocked: URL '{url}' resolves to private/reserved IP {addr}")]
    SsrfBlocked { url: String, addr: String },

    #[error("invalid URL '{url}': {message}")]
    InvalidUrl { url: String, message: String },

    #[error("embedding error: {0}")]
    Embedding(String),

    #[error("MCP command '{command}' not allowed")]
    CommandNotAllowed { command: String },

    #[error("env var '{var_name}' is blocked for MCP server processes")]
    EnvVarBlocked { var_name: String },

    #[error("policy violation: {0}")]
    PolicyViolation(String),

    #[error("OAuth error for server '{server_id}': {message}")]
    OAuthError { server_id: String, message: String },

    #[error("OAuth callback timed out for server '{server_id}' after {timeout_secs}s")]
    OAuthCallbackTimeout {
        server_id: String,
        timeout_secs: u64,
    },

    #[error("tool list refresh rejected for '{server_id}': list is locked after initial connect")]
    ToolListLocked { server_id: String },
}

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

    #[test]
    fn connection_error_display() {
        let err = McpError::Connection {
            server_id: "github".into(),
            message: "refused".into(),
        };
        assert_eq!(
            err.to_string(),
            "connection failed for server 'github': refused"
        );
    }

    #[test]
    fn tool_call_error_display() {
        let err = McpError::ToolCall {
            server_id: "fs".into(),
            tool_name: "read_file".into(),
            message: "not found".into(),
        };
        assert_eq!(err.to_string(), "tool call failed: fs/read_file: not found");
    }

    #[test]
    fn server_not_found_display() {
        let err = McpError::ServerNotFound {
            server_id: "missing".into(),
        };
        assert_eq!(err.to_string(), "server 'missing' not found");
    }

    #[test]
    fn tool_not_found_display() {
        let err = McpError::ToolNotFound {
            server_id: "fs".into(),
            tool_name: "delete".into(),
        };
        assert_eq!(err.to_string(), "tool 'delete' not found on server 'fs'");
    }

    #[test]
    fn server_already_connected_display() {
        let err = McpError::ServerAlreadyConnected {
            server_id: "github".into(),
        };
        assert_eq!(err.to_string(), "server 'github' is already connected");
    }

    #[test]
    fn timeout_error_display() {
        let err = McpError::Timeout {
            server_id: "slow".into(),
            tool_name: "query".into(),
            timeout_secs: 30,
        };
        assert_eq!(err.to_string(), "tool call timed out after 30s: slow/query");
    }
}