Skip to main content

oxicode_agent/mcp/
error.rs

1//! Typed errors for MCP operations.
2//!
3//! Introduced for the consent gate (F-2, code audit 2026-07-25): new MCP
4//! code paths use a typed error enum instead of opaque `anyhow` strings, so
5//! callers can match on [`McpError::ConsentDenied`] without downcasting.
6//!
7//! Scope note: the rest of the `mcp` module still uses `anyhow::Result` for
8//! historical reasons (tracked as finding F-7). This module is the typed
9//! beachhead — new error-bearing code should extend it rather than reach for
10//! `anyhow::anyhow!`.
11
12use thiserror::Error;
13
14/// Errors raised by the MCP manager's consent-aware paths.
15#[derive(Debug, Error)]
16pub enum McpError {
17    /// Server spawn/connect denied because the server is not in the consent
18    /// allow-list (its stored [`crate::mcp::ConsentState`] is `Ask` for an
19    /// unknown server, or `Deny` after an explicit revocation).
20    ///
21    /// Actionable: the message names the `oxicode mcp trust <server>` command so
22    /// the user knows exactly how to proceed.
23    #[error(
24        "MCP server {server:?} is not trusted — run `oxicode mcp trust {server}` to allow, \
25         or remove the entry from mcp.json if you did not configure it"
26    )]
27    ConsentDenied {
28        /// The server name as it appears in `mcp.json` / `mcpServers`.
29        server: String,
30    },
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    #[test]
38    fn consent_denied_display_names_server_and_command() {
39        let e = McpError::ConsentDenied {
40            server: "github".to_string(),
41        };
42        let msg = format!("{e}");
43        assert!(msg.contains("github"), "message must name the server");
44        assert!(
45            msg.contains("oxicode mcp trust github"),
46            "message must surface the exact remediation command, got: {msg}"
47        );
48    }
49
50    #[test]
51    fn consent_denied_is_send_and_sync() {
52        // Ensures the error can travel through anyhow::Result across async
53        // boundaries (tokio requires Send).
54        fn assert_send_sync<T: Send + Sync + 'static>() {}
55        assert_send_sync::<McpError>();
56    }
57}