use systemprompt_models::domain_error;
domain_error! {
pub enum McpDomainError {
common: [repository, io, json, validation],
#[error("MCP server not found: {0}")]
ServerNotFound(String),
#[error("Connection failed to {server}: {message}")]
ConnectionFailed { server: String, message: String },
#[error("Tool execution failed: {0}")]
ToolExecutionFailed(String),
#[error("Schema validation failed: {0}")]
SchemaValidation(String),
#[error("Registry validation failed: {0}")]
RegistryValidation(String),
#[error("Process spawn failed for {server}: {message}")]
ProcessSpawn { server: String, message: String },
#[error("Port unavailable: {port} - {message}")]
PortUnavailable { port: u16, message: String },
#[error(
"port {port} for MCP server {service} is held by process {pid}, which this \
installation did not spawn; another systemprompt installation or an unrelated \
service owns it. Free the port or move this installation's MCP ports with \
`admin config services set --port-offset`"
)]
PortOwnedByForeignProcess { port: u16, pid: u32, service: String },
#[error(
"port {port} for MCP server {service} is held by process {pid}, whose identity \
this platform cannot verify, so it will not be signalled. Stop it by hand, or \
move this installation's MCP ports with `admin config services set --port-offset`"
)]
PortHolderUnverifiable { port: u16, pid: u32, service: String },
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Authentication required for {0}")]
AuthRequired(String),
#[error(
"token issuer does not match `{expected}`; the token predates a change to \
`security.issuer`. Re-authenticate with \
`systemprompt admin session login --force-new`"
)]
TokenIssuerMismatch { expected: String },
#[error("token rejected: {0}")]
TokenRejected(#[from] systemprompt_security::AuthError),
#[error("External MCP auth unavailable for {server}: {message}")]
ExternalAuthUnavailable { server: String, message: String },
#[error("Manifest error: {0}")]
Manifest(String),
#[error("Transport error: {0}")]
Transport(String),
#[error("MCP server {server} timed out after {after_ms}ms")]
Timeout { server: String, after_ms: u64 },
#[error("Circuit breaker open for MCP server {server}; failing fast")]
CircuitOpen { server: String },
#[error("MCP server {server} unavailable: concurrency limit reached")]
DependencyUnavailable { server: String },
#[error("{0}")]
Internal(String),
#[error("Configuration: {0}")]
Config(#[from] systemprompt_models::errors::ConfigError),
#[error("services config: {0}")]
ServicesConfig(#[from] systemprompt_loader::ConfigLoadError),
#[error("extension load: {0}")]
ExtensionLoad(#[from] systemprompt_loader::ExtensionLoadError),
#[error("MCP client initialize: {0}")]
ClientInitialize(String),
#[error("MCP service error: {message}")]
ServiceError { message: String },
#[error("Task join error: {0}")]
TaskJoin(#[from] tokio::task::JoinError),
#[error("Path error: {0}")]
Path(String),
#[error("Config validation: {0}")]
ConfigValidation(String),
}
}
impl From<sqlx::Error> for McpDomainError {
fn from(err: sqlx::Error) -> Self {
Self::Repository(systemprompt_database::RepositoryError::from(err))
}
}
impl From<rmcp::service::ClientInitializeError> for McpDomainError {
fn from(e: rmcp::service::ClientInitializeError) -> Self {
Self::ClientInitialize(e.to_string())
}
}
impl From<rmcp::ServiceError> for McpDomainError {
fn from(e: rmcp::ServiceError) -> Self {
Self::ServiceError {
message: e.to_string(),
}
}
}
impl From<systemprompt_models::errors::ConfigValidationError> for McpDomainError {
fn from(e: systemprompt_models::errors::ConfigValidationError) -> Self {
Self::ConfigValidation(e.to_string())
}
}
impl From<systemprompt_models::paths::PathError> for McpDomainError {
fn from(e: systemprompt_models::paths::PathError) -> Self {
Self::Path(e.to_string())
}
}
impl McpDomainError {
#[must_use]
pub const fn classify(&self) -> systemprompt_database::resilience::Outcome {
use systemprompt_database::resilience::Outcome;
match self {
Self::ConnectionFailed { .. }
| Self::Transport(_)
| Self::Timeout { .. }
| Self::ServiceError { .. } => Outcome::Transient { retry_after: None },
_ => Outcome::Permanent,
}
}
}
pub type McpDomainResult<T> = Result<T, McpDomainError>;