use std::path::PathBuf;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("config error: {0}")]
Config(String),
#[error("io error at {context}: {source}")]
Io {
context: String,
#[source]
source: std::io::Error,
},
#[error("could not spawn subprocess: {0}")]
Spawn(#[source] std::io::Error),
#[error("schema error: {0}")]
Schema(String),
#[error("editor config: read failed at {path}: {source}")]
EditorConfigRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("editor config: JSON error at {path}: {source}")]
EditorConfigJson {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("editor config: backup failed for {path}: {source}")]
EditorConfigBackup {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("editor config: write failed at {path}: {source}")]
EditorConfigWrite {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("panic: {0}")]
Panic(String),
#[error("mcp initialize error: {0}")]
McpInitialize(#[source] Box<rmcp::service::ServerInitializeError>),
#[error("mcp protocol error: {0}")]
Mcp(#[source] Box<rmcp::ServiceError>),
}
impl From<rmcp::service::ServerInitializeError> for Error {
fn from(err: rmcp::service::ServerInitializeError) -> Self {
Self::McpInitialize(Box::new(err))
}
}
impl From<Box<rmcp::service::ServerInitializeError>> for Error {
fn from(err: Box<rmcp::service::ServerInitializeError>) -> Self {
Self::McpInitialize(err)
}
}
impl From<rmcp::ServiceError> for Error {
fn from(err: rmcp::ServiceError) -> Self {
Self::Mcp(Box::new(err))
}
}
impl From<Box<rmcp::ServiceError>> for Error {
fn from(err: Box<rmcp::ServiceError>) -> Self {
Self::Mcp(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_error_display_pin() {
let e = Error::Config("bad path".into());
assert_eq!(e.to_string(), "config error: bad path");
}
#[test]
fn spawn_error_display_pin() {
let e = Error::Spawn(std::io::Error::other("nope"));
assert_eq!(e.to_string(), "could not spawn subprocess: nope");
}
#[test]
fn io_error_display_includes_context_and_source() {
let e = Error::Io {
context: "open ./mcp-tools.json".into(),
source: std::io::Error::other("disk gone"),
};
assert_eq!(
e.to_string(),
"io error at open ./mcp-tools.json: disk gone"
);
}
#[test]
fn schema_error_display_pin() {
let e = Error::Schema("missing required field".into());
assert_eq!(e.to_string(), "schema error: missing required field");
}
#[test]
fn editor_config_read_display_pin() {
let e = Error::EditorConfigRead {
path: PathBuf::from("/etc/x.json"),
source: std::io::Error::other("permission denied"),
};
assert_eq!(
e.to_string(),
"editor config: read failed at /etc/x.json: permission denied"
);
}
#[test]
fn editor_config_json_display_pin() {
let json_err = serde_json::from_str::<serde_json::Value>("{not-json")
.expect_err("malformed JSON must error");
let e = Error::EditorConfigJson {
path: PathBuf::from("/tmp/y.json"),
source: json_err,
};
let s = e.to_string();
assert!(
s.starts_with("editor config: JSON error at /tmp/y.json: "),
"got {s}"
);
}
#[test]
fn editor_config_backup_display_pin() {
let e = Error::EditorConfigBackup {
path: PathBuf::from("/tmp/z.backup.json"),
source: std::io::Error::other("readonly"),
};
assert_eq!(
e.to_string(),
"editor config: backup failed for /tmp/z.backup.json: readonly"
);
}
#[test]
fn editor_config_write_display_pin() {
let e = Error::EditorConfigWrite {
path: PathBuf::from("/tmp/w.json"),
source: std::io::Error::other("no space"),
};
assert_eq!(
e.to_string(),
"editor config: write failed at /tmp/w.json: no space"
);
}
#[test]
fn panic_error_display_pin() {
let e = Error::Panic("middleware blew up".into());
assert_eq!(e.to_string(), "panic: middleware blew up");
}
}