use thiserror::Error;
use tonic::Status;
use turbomcp_core::McpError;
pub type GrpcResult<T> = Result<T, GrpcError>;
#[derive(Debug, Error)]
pub enum GrpcError {
#[error("gRPC transport error: {0}")]
Transport(#[from] tonic::transport::Error),
#[error("gRPC status error: {0}")]
Status(#[from] Status),
#[error("MCP error: {0}")]
Mcp(#[from] McpError),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Timeout: {0}")]
Timeout(String),
#[error("Configuration error: {0}")]
Config(String),
}
impl GrpcError {
#[must_use]
pub fn serialization(msg: impl Into<String>) -> Self {
Self::Serialization(msg.into())
}
#[must_use]
pub fn invalid_request(msg: impl Into<String>) -> Self {
Self::InvalidRequest(msg.into())
}
#[must_use]
pub fn connection(msg: impl Into<String>) -> Self {
Self::Connection(msg.into())
}
#[must_use]
pub fn timeout(msg: impl Into<String>) -> Self {
Self::Timeout(msg.into())
}
#[must_use]
pub fn config(msg: impl Into<String>) -> Self {
Self::Config(msg.into())
}
}
impl From<GrpcError> for Status {
fn from(err: GrpcError) -> Self {
match err {
GrpcError::Transport(e) => Status::unavailable(e.to_string()),
GrpcError::Status(s) => s,
GrpcError::Mcp(e) => mcp_error_to_status(&e),
GrpcError::Serialization(msg) | GrpcError::InvalidRequest(msg) => {
Status::invalid_argument(msg)
}
GrpcError::Connection(msg) => Status::unavailable(msg),
GrpcError::Timeout(msg) => Status::deadline_exceeded(msg),
GrpcError::Config(msg) => Status::failed_precondition(msg),
}
}
}
impl From<serde_json::Error> for GrpcError {
fn from(err: serde_json::Error) -> Self {
Self::Serialization(err.to_string())
}
}
fn mcp_error_to_status(err: &McpError) -> Status {
let code = err.jsonrpc_code();
match code {
-32700 => Status::invalid_argument(format!("Parse error: {err}")),
-32600 => Status::invalid_argument(format!("Invalid request: {err}")),
-32601 => Status::unimplemented(format!("Method not found: {err}")),
-32602 => Status::invalid_argument(format!("Invalid params: {err}")),
-32603 => Status::internal(format!("Internal error: {err}")),
-32001 => Status::resource_exhausted(format!("Resource exceeded: {err}")),
-32002 => Status::cancelled(format!("Request cancelled: {err}")),
-32042 => Status::unavailable(format!("URL elicitation required: {err}")),
_ if (-32099..=-32000).contains(&code) => {
Status::internal(format!("Application error: {err}"))
}
_ => Status::unknown(format!("Unknown error: {err}")),
}
}
#[must_use]
pub fn status_to_mcp_error(status: &Status) -> McpError {
use tonic::Code;
match status.code() {
Code::InvalidArgument => McpError::invalid_params(status.message()),
Code::NotFound | Code::Unimplemented => McpError::method_not_found(status.message()),
Code::Internal => McpError::internal(status.message()),
Code::Unavailable => McpError::transport(status.message()),
Code::DeadlineExceeded => McpError::timeout(status.message()),
Code::Cancelled => McpError::cancelled(status.message()),
Code::ResourceExhausted => McpError::rate_limited(status.message()),
Code::PermissionDenied => McpError::permission_denied(status.message()),
Code::Unauthenticated => McpError::authentication(status.message()),
_ => McpError::internal(format!("gRPC error: {}", status.message())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_error_to_status() {
let err = McpError::method_not_found("unknown_method");
let status: Status = GrpcError::Mcp(err).into();
assert_eq!(status.code(), tonic::Code::Unimplemented);
}
#[test]
fn test_status_to_mcp_error() {
let status = Status::invalid_argument("bad params");
let err = status_to_mcp_error(&status);
assert_eq!(err.jsonrpc_code(), -32602);
}
#[test]
fn test_serialization_error() {
let err = GrpcError::serialization("invalid JSON");
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
}