use super::*;
pub(super) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(text) = payload.downcast_ref::<&'static str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"panicked with a non-string payload".to_string()
}
}
#[derive(Clone)]
pub(super) enum ClientPanicMessage {
Detailed,
Fixed(Arc<str>),
}
#[derive(Clone)]
pub(super) enum ToolNameDisclosure {
Omit,
Original,
Fixed(Arc<str>),
}
impl ToolNameDisclosure {
pub(super) fn value<'a>(&'a self, original: &'a str) -> Option<&'a str> {
match self {
Self::Omit => None,
Self::Original => Some(original),
Self::Fixed(name) => Some(name),
}
}
pub(super) fn mode(&self) -> &'static str {
match self {
Self::Omit => "omitted",
Self::Original => "original",
Self::Fixed(_) => "fixed",
}
}
}
#[derive(Clone)]
pub struct PanicPolicy {
pub(super) client_message: ClientPanicMessage,
pub(super) client_tool_name: ToolNameDisclosure,
pub(super) log_tool_name: ToolNameDisclosure,
pub(super) include_payload_in_logs: bool,
}
impl std::fmt::Debug for PanicPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let client_message = match self.client_message {
ClientPanicMessage::Detailed => "detailed",
ClientPanicMessage::Fixed(_) => "fixed",
};
f.debug_struct("PanicPolicy")
.field("client_message", &client_message)
.field("client_tool_name", &self.client_tool_name.mode())
.field("log_tool_name", &self.log_tool_name.mode())
.field("include_payload_in_logs", &self.include_payload_in_logs)
.finish()
}
}
impl PanicPolicy {
pub fn redacted(client_message: impl Into<String>) -> Self {
Self {
client_message: ClientPanicMessage::Fixed(Arc::from(client_message.into())),
client_tool_name: ToolNameDisclosure::Omit,
log_tool_name: ToolNameDisclosure::Omit,
include_payload_in_logs: false,
}
}
pub(super) fn detailed() -> Self {
Self {
client_message: ClientPanicMessage::Detailed,
client_tool_name: ToolNameDisclosure::Original,
log_tool_name: ToolNameDisclosure::Original,
include_payload_in_logs: true,
}
}
#[must_use]
pub fn include_tool_name_in_client_message(mut self, include: bool) -> Self {
self.client_tool_name = if include {
ToolNameDisclosure::Original
} else {
ToolNameDisclosure::Omit
};
self
}
#[must_use]
pub fn client_tool_name(mut self, name: impl Into<String>) -> Self {
self.client_tool_name = ToolNameDisclosure::Fixed(Arc::from(name.into()));
self
}
#[must_use]
pub fn include_tool_name_in_logs(mut self, include: bool) -> Self {
self.log_tool_name = if include {
ToolNameDisclosure::Original
} else {
ToolNameDisclosure::Omit
};
self
}
#[must_use]
pub fn log_tool_name(mut self, name: impl Into<String>) -> Self {
self.log_tool_name = ToolNameDisclosure::Fixed(Arc::from(name.into()));
self
}
#[must_use]
pub fn include_payload_in_logs(mut self, include: bool) -> Self {
self.include_payload_in_logs = include;
self
}
pub(super) fn client_message(&self, tool_name: &str, payload: Option<&str>) -> String {
match &self.client_message {
ClientPanicMessage::Detailed => format!(
"tool '{tool_name}' panicked: {}",
payload.unwrap_or("<redacted>")
),
ClientPanicMessage::Fixed(message) => match self.client_tool_name.value(tool_name) {
Some(name) => format!("tool '{name}': {message}"),
None => message.to_string(),
},
}
}
pub(super) fn needs_payload(&self) -> bool {
matches!(self.client_message, ClientPanicMessage::Detailed) || self.include_payload_in_logs
}
#[cfg(feature = "websocket")]
pub(super) fn internal_error_message(&self, error: &dyn std::fmt::Display) -> String {
match &self.client_message {
ClientPanicMessage::Detailed => error.to_string(),
ClientPanicMessage::Fixed(message) => message.to_string(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TaskOperation {
Create,
Get,
Update,
Cancel,
ParkInput,
Execute,
Resume,
Finalize,
}
#[non_exhaustive]
pub enum TaskFailure {
NotFound,
Expired,
Store(TaskStoreError),
Internal(&'static str),
InvalidArguments(&'static str),
Handler,
}
impl std::fmt::Debug for TaskFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound => f.write_str("NotFound"),
Self::Expired => f.write_str("Expired"),
Self::Store(error) => {
let kind = match error {
TaskStoreError::Encode(_) => "Encode",
TaskStoreError::Decode(_) => "Decode",
TaskStoreError::Backend(_) => "Backend",
TaskStoreError::InvalidTransition(_) => "InvalidTransition",
};
write!(f, "Store({kind})")
}
Self::Internal(message) => f.debug_tuple("Internal").field(message).finish(),
Self::InvalidArguments(message) => {
f.debug_tuple("InvalidArguments").field(message).finish()
}
Self::Handler => f.write_str("Handler"),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct TaskErrorContext {
operation: TaskOperation,
task_id: Option<String>,
failure: TaskFailure,
}
impl TaskErrorContext {
pub(super) fn new(
operation: TaskOperation,
task_id: Option<&str>,
failure: TaskFailure,
) -> Self {
Self {
operation,
task_id: task_id.map(str::to_owned),
failure,
}
}
pub const fn operation(&self) -> TaskOperation {
self.operation
}
pub fn task_id(&self) -> Option<&str> {
self.task_id.as_deref()
}
pub const fn failure(&self) -> &TaskFailure {
&self.failure
}
}
type TaskErrorMapper = dyn Fn(&TaskErrorContext) -> JsonRpcError + Send + Sync + 'static;
#[derive(Clone)]
pub struct TaskErrorPolicy {
mapper: Arc<TaskErrorMapper>,
}
impl std::fmt::Debug for TaskErrorPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TaskErrorPolicy").finish_non_exhaustive()
}
}
impl Default for TaskErrorPolicy {
fn default() -> Self {
Self::new(default_task_error)
}
}
impl TaskErrorPolicy {
pub fn new<F>(mapper: F) -> Self
where
F: Fn(&TaskErrorContext) -> JsonRpcError + Send + Sync + 'static,
{
Self {
mapper: Arc::new(mapper),
}
}
pub(super) fn map(&self, context: &TaskErrorContext) -> JsonRpcError {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.mapper)(context))) {
Ok(error) => error,
Err(_) => {
tracing::error!(
target: "mcp::tasks",
"task error policy panicked; using the redacted fallback"
);
JsonRpcError::internal_error("Task error policy failed")
}
}
}
pub(crate) fn map_store_error(
&self,
operation: TaskOperation,
task_id: &str,
error: TaskStoreError,
) -> JsonRpcError {
self.map(&TaskErrorContext::new(
operation,
Some(task_id),
TaskFailure::Store(error),
))
}
pub(crate) fn map_internal_error(
&self,
operation: TaskOperation,
task_id: &str,
message: &'static str,
) -> JsonRpcError {
self.map(&TaskErrorContext::new(
operation,
Some(task_id),
TaskFailure::Internal(message),
))
}
}
pub(super) fn default_task_error(context: &TaskErrorContext) -> JsonRpcError {
match context.failure() {
TaskFailure::NotFound => JsonRpcError::invalid_params(format!(
"Task not found: {}",
context.task_id().unwrap_or("<unknown>")
)),
TaskFailure::Expired => JsonRpcError::invalid_params(format!(
"Task expired: {}",
context.task_id().unwrap_or("<unknown>")
))
.with_data(serde_json::json!({ "reason": "task_expired" })),
TaskFailure::Store(_) => JsonRpcError::internal_error("Task store operation failed"),
TaskFailure::Internal(message) => JsonRpcError::internal_error(*message),
TaskFailure::InvalidArguments(message) => JsonRpcError::invalid_params(*message),
TaskFailure::Handler => JsonRpcError::internal_error("Task handler failed"),
}
}