use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "http"))]
#[serde(rename_all = "lowercase")]
pub enum McpServerTransportType {
Http,
Stdio,
}
impl McpServerTransportType {
pub fn is_local(&self) -> bool {
matches!(self, McpServerTransportType::Stdio)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "api_key"))]
#[serde(rename_all = "snake_case")]
pub enum McpServerAuthMode {
#[default]
None,
ApiKey,
#[serde(rename = "oauth", alias = "o_auth")]
OAuth,
}
impl std::fmt::Display for McpServerAuthMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
McpServerAuthMode::None => write!(f, "none"),
McpServerAuthMode::ApiKey => write!(f, "api_key"),
McpServerAuthMode::OAuth => write!(f, "oauth"),
}
}
}
impl From<&str> for McpServerAuthMode {
fn from(s: &str) -> Self {
match s {
"api_key" => McpServerAuthMode::ApiKey,
"oauth" => McpServerAuthMode::OAuth,
_ => McpServerAuthMode::None,
}
}
}
impl McpServerAuthMode {
pub fn is_none(&self) -> bool {
matches!(self, McpServerAuthMode::None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "service"))]
#[serde(rename_all = "lowercase")]
pub enum McpServerActsAs {
#[default]
None,
Service,
User,
#[serde(rename = "user_or_service")]
UserOrService,
}
impl McpServerActsAs {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn uses_user_grant(&self) -> bool {
matches!(self, Self::User | Self::UserOrService)
}
pub fn uses_service_grant(&self) -> bool {
matches!(self, Self::Service | Self::UserOrService)
}
pub fn resolution_order(&self) -> &'static [McpServerActsAs] {
match self {
Self::None => &[],
Self::Service => &[Self::Service],
Self::User => &[Self::User],
Self::UserOrService => &[Self::User, Self::Service],
}
}
}
impl std::fmt::Display for McpServerActsAs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Service => write!(f, "service"),
Self::User => write!(f, "user"),
Self::UserOrService => write!(f, "user_or_service"),
}
}
}
impl From<&str> for McpServerActsAs {
fn from(value: &str) -> Self {
match value {
"service" => Self::Service,
"user" => Self::User,
"user_or_service" => Self::UserOrService,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "ask"))]
#[serde(rename_all = "lowercase")]
pub enum McpConnectInChat {
#[default]
Ask,
Never,
}
impl McpConnectInChat {
pub fn is_ask(&self) -> bool {
matches!(self, Self::Ask)
}
pub fn allows_card(&self) -> bool {
self.is_ask()
}
}
impl std::fmt::Display for McpConnectInChat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ask => write!(f, "ask"),
Self::Never => write!(f, "never"),
}
}
}
impl From<&str> for McpConnectInChat {
fn from(value: &str) -> Self {
match value {
"never" => Self::Never,
_ => Self::Ask,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "catalog:linear"))]
pub struct McpServerPresetRef(String);
impl McpServerPresetRef {
pub fn catalog_name(&self) -> &str {
self.0.strip_prefix("catalog:").unwrap_or(&self.0)
}
}
impl std::fmt::Display for McpServerPresetRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for McpServerPresetRef {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let Some(name) = value.strip_prefix("catalog:") else {
return Err("MCP server preset reference must start with 'catalog:'".to_string());
};
if name.trim().is_empty() {
return Err("MCP server catalog preset name cannot be empty".to_string());
}
Ok(Self(value.to_string()))
}
}
impl Serialize for McpServerPresetRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for McpServerPresetRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
mod policy;
pub use policy::{
MCP_PROTOCOL_VERSION_2025_03, MCP_PROTOCOL_VERSION_2025_06, MCP_PROTOCOL_VERSION_2026_07,
McpElicitationPolicy, McpProtocolMode,
};
pub fn normalize_mcp_error_code(code: i64) -> i64 {
match code {
-32002 => -32602,
other => other,
}
}
impl std::fmt::Display for McpServerTransportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
McpServerTransportType::Http => write!(f, "http"),
McpServerTransportType::Stdio => write!(f, "stdio"),
}
}
}
impl From<&str> for McpServerTransportType {
fn from(s: &str) -> Self {
match s {
"stdio" => McpServerTransportType::Stdio,
_ => McpServerTransportType::Http,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(try_from = "ScopedMcpServerWire", into = "ScopedMcpServerWire")]
pub struct ScopedMcpServer {
#[serde(
default = "default_scoped_transport_type",
rename = "type",
alias = "transport_type"
)]
#[cfg_attr(feature = "openapi", schema(rename = "type"))]
pub transport_type: McpServerTransportType,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub url: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
#[serde(default, skip_serializing_if = "McpServerAuthMode::is_none")]
pub auth_mode: McpServerAuthMode,
#[serde(default, skip_serializing_if = "McpProtocolMode::is_auto")]
pub protocol_mode: McpProtocolMode,
#[serde(default, skip_serializing_if = "McpElicitationPolicy::is_default")]
pub elicitation_policy: McpElicitationPolicy,
#[serde(skip_serializing_if = "Option::is_none")]
pub oauth_provider_id: Option<String>,
#[serde(
default = "default_scoped_tool_discovery",
skip_serializing_if = "is_true"
)]
pub tool_discovery: bool,
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(rename = "use"))]
pub preset: Option<McpServerPresetRef>,
#[serde(
default,
rename = "actsAs",
alias = "acts_as",
skip_serializing_if = "McpServerActsAs::is_none"
)]
#[cfg_attr(feature = "openapi", schema(rename = "actsAs"))]
pub acts_as: McpServerActsAs,
#[serde(
default,
rename = "connectInChat",
alias = "connect_in_chat",
skip_serializing_if = "McpConnectInChat::is_ask"
)]
#[cfg_attr(feature = "openapi", schema(rename = "connectInChat"))]
pub connect_in_chat: McpConnectInChat,
#[serde(default, skip_serializing_if = "is_false")]
pub deferred: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ScopedMcpServerWire {
#[serde(
rename = "type",
alias = "transport_type",
skip_serializing_if = "Option::is_none"
)]
transport_type: Option<McpServerTransportType>,
#[serde(default, skip_serializing_if = "String::is_empty")]
url: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
env: HashMap<String, String>,
#[serde(default, skip_serializing_if = "McpServerAuthMode::is_none")]
auth_mode: McpServerAuthMode,
#[serde(default, skip_serializing_if = "McpProtocolMode::is_auto")]
protocol_mode: McpProtocolMode,
#[serde(default, skip_serializing_if = "McpElicitationPolicy::is_default")]
elicitation_policy: McpElicitationPolicy,
#[serde(skip_serializing_if = "Option::is_none")]
oauth_provider_id: Option<String>,
#[serde(
default = "default_scoped_tool_discovery",
skip_serializing_if = "is_true"
)]
tool_discovery: bool,
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
preset: Option<McpServerPresetRef>,
#[serde(
default,
rename = "actsAs",
alias = "acts_as",
skip_serializing_if = "McpServerActsAs::is_none"
)]
acts_as: McpServerActsAs,
#[serde(
default,
rename = "connectInChat",
alias = "connect_in_chat",
skip_serializing_if = "McpConnectInChat::is_ask"
)]
connect_in_chat: McpConnectInChat,
#[serde(default, skip_serializing_if = "is_false")]
deferred: bool,
}
impl TryFrom<ScopedMcpServerWire> for ScopedMcpServer {
type Error = String;
fn try_from(wire: ScopedMcpServerWire) -> Result<Self, Self::Error> {
if wire.preset.is_some() && wire.transport_type.is_some() {
return Err(
"MCP server preset reference cannot be combined with inline field 'type'"
.to_string(),
);
}
Ok(Self {
transport_type: wire
.transport_type
.unwrap_or_else(default_scoped_transport_type),
url: wire.url,
headers: wire.headers,
command: wire.command,
args: wire.args,
env: wire.env,
auth_mode: wire.auth_mode,
protocol_mode: wire.protocol_mode,
elicitation_policy: wire.elicitation_policy,
oauth_provider_id: wire.oauth_provider_id,
tool_discovery: wire.tool_discovery,
preset: wire.preset,
acts_as: wire.acts_as,
connect_in_chat: wire.connect_in_chat,
deferred: wire.deferred,
})
}
}
impl From<ScopedMcpServer> for ScopedMcpServerWire {
fn from(server: ScopedMcpServer) -> Self {
Self {
transport_type: server.preset.is_none().then_some(server.transport_type),
url: server.url,
headers: server.headers,
command: server.command,
args: server.args,
env: server.env,
auth_mode: server.auth_mode,
protocol_mode: server.protocol_mode,
elicitation_policy: server.elicitation_policy,
oauth_provider_id: server.oauth_provider_id,
tool_discovery: server.tool_discovery,
preset: server.preset,
acts_as: server.acts_as,
connect_in_chat: server.connect_in_chat,
deferred: server.deferred,
}
}
}
impl Default for ScopedMcpServer {
fn default() -> Self {
Self {
transport_type: McpServerTransportType::Http,
url: String::new(),
headers: HashMap::new(),
auth_mode: McpServerAuthMode::None,
protocol_mode: McpProtocolMode::Auto,
elicitation_policy: McpElicitationPolicy::Url,
oauth_provider_id: None,
tool_discovery: true,
command: None,
args: Vec::new(),
env: HashMap::new(),
preset: None,
acts_as: McpServerActsAs::None,
connect_in_chat: McpConnectInChat::Ask,
deferred: false,
}
}
}
pub const USER_MCP_CAPABILITY_ID: &str = "user_mcp";
pub const USER_MCP_CONNECT_SETTING: &str = "connect";
pub type ScopedMcpServers = BTreeMap<String, ScopedMcpServer>;
#[derive(Debug, Clone)]
pub struct McpSecretBindingMetadata {
pub server_name: String,
pub tool_name: String,
pub parameter_name: String,
pub configured: bool,
pub setup_url: String,
}
pub fn apply_mcp_secret_binding_schemas(
definitions: &mut [crate::runtime::ToolDefinition],
bindings: &[McpSecretBindingMetadata],
) {
for binding in bindings {
if !is_valid_mcp_server_name(&binding.server_name) {
continue;
}
let tool_name = crate::runtime::mcp_tool_name(&binding.server_name, &binding.tool_name);
let Some(crate::runtime::ToolDefinition::Builtin(definition)) = definitions
.iter_mut()
.find(|definition| definition.name() == tool_name)
else {
continue;
};
remove_bound_parameter(&mut definition.parameters, &binding.parameter_name);
if let Some(full) = definition.full_parameters.as_mut() {
remove_bound_parameter(full, &binding.parameter_name);
}
let status = if binding.configured {
"configured"
} else {
"setup required"
};
definition.description.push_str(&format!(
"\n\nCredential '{}' is securely bound ({status}); do not request or supply it. Setup: {}",
binding.parameter_name, binding.setup_url
));
}
}
fn remove_bound_parameter(schema: &mut Value, parameter_name: &str) {
let Some(object) = schema.as_object_mut() else {
return;
};
if let Some(properties) = object.get_mut("properties").and_then(Value::as_object_mut) {
properties.remove(parameter_name);
}
if let Some(required) = object.get_mut("required").and_then(Value::as_array_mut) {
required.retain(|value| value.as_str() != Some(parameter_name));
}
}
fn default_scoped_transport_type() -> McpServerTransportType {
McpServerTransportType::Http
}
fn default_scoped_tool_discovery() -> bool {
true
}
fn is_true(value: &bool) -> bool {
*value
}
fn is_false(value: &bool) -> bool {
!*value
}
pub fn scoped_mcp_servers_is_empty(servers: &ScopedMcpServers) -> bool {
servers.is_empty()
}
pub fn merge_scoped_mcp_servers(
base: &ScopedMcpServers,
overlay: &ScopedMcpServers,
) -> ScopedMcpServers {
let mut merged = base.clone();
merged.extend(overlay.clone());
merged
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct McpToolDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<McpToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct McpToolAnnotations {
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "readOnlyHint"
)]
pub read_only_hint: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "destructiveHint"
)]
pub destructive_hint: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "idempotentHint"
)]
pub idempotent_hint: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "openWorldHint"
)]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolsListRequest {
pub jsonrpc: String,
pub id: i64,
pub method: String,
}
impl Default for McpToolsListRequest {
fn default() -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: 1,
method: "tools/list".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolsListResponse {
pub jsonrpc: String,
pub id: i64,
#[serde(default)]
pub result: Option<McpToolsListResult>,
#[serde(default)]
pub error: Option<McpError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolsListResult {
pub tools: Vec<McpToolDefinition>,
#[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallRequest {
pub jsonrpc: String,
pub id: i64,
pub method: String,
pub params: McpToolCallParams,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallParams {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
impl McpToolCallRequest {
pub fn new(id: i64, name: String, arguments: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
method: "tools/call".to_string(),
params: McpToolCallParams { name, arguments },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallResponse {
pub jsonrpc: String,
pub id: i64,
#[serde(default)]
pub result: Option<McpToolCallResult>,
#[serde(default)]
pub error: Option<McpError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallResult {
pub content: Vec<McpContent>,
#[serde(rename = "isError", default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum McpContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource {
uri: String,
mime_type: Option<String>,
text: Option<String>,
},
}
pub fn mcp_tool_name(server_name: &str, tool_name: &str) -> String {
format!(
"mcp_{}__{}",
sanitize_mcp_server_name(server_name),
tool_name
)
}
pub fn sanitize_mcp_server_name(server_name: &str) -> String {
server_name
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect::<String>()
}
pub fn is_valid_mcp_server_name(server_name: &str) -> bool {
let prefix = sanitize_mcp_server_name(server_name);
!prefix.is_empty() && !prefix.contains("__") && !prefix.ends_with('_')
}
pub fn is_mcp_tool(tool_name: &str) -> bool {
tool_name.starts_with("mcp_")
}
pub fn parse_mcp_tool_name(tool_name: &str) -> Option<(String, String)> {
if !tool_name.starts_with("mcp_") {
return None;
}
let rest = &tool_name[4..]; if let Some(pos) = rest.find("__") {
let server_prefix = rest[..pos].to_string();
let original_name = rest[pos + 2..].to_string(); if !server_prefix.is_empty() && !original_name.is_empty() {
return Some((server_prefix, original_name));
}
}
None
}
pub fn mcp_oauth_provider_id_for_uuid(server_id: uuid::Uuid) -> String {
format!("mcp_oauth_{}", server_id)
}
pub fn mcp_oauth_session_secret_name(server_id: uuid::Uuid, field: &str) -> String {
format!("mcp_oauth:{}:{}", server_id, field)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum McpErrorCode {
ToolNotFound,
ToolTimeout,
ToolPanicked,
InvalidArguments,
PermissionDenied,
QuotaExceeded,
NetworkBlocked,
McpServerUnreachable,
Internal,
#[serde(other)]
Unknown,
}
impl McpErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
McpErrorCode::ToolNotFound => "tool_not_found",
McpErrorCode::ToolTimeout => "tool_timeout",
McpErrorCode::ToolPanicked => "tool_panicked",
McpErrorCode::InvalidArguments => "invalid_arguments",
McpErrorCode::PermissionDenied => "permission_denied",
McpErrorCode::QuotaExceeded => "quota_exceeded",
McpErrorCode::NetworkBlocked => "network_blocked",
McpErrorCode::McpServerUnreachable => "mcp_server_unreachable",
McpErrorCode::Internal => "internal",
McpErrorCode::Unknown => "unknown",
}
}
pub fn default_category(&self) -> McpErrorCategory {
match self {
McpErrorCode::ToolTimeout
| McpErrorCode::McpServerUnreachable
| McpErrorCode::QuotaExceeded => McpErrorCategory::Transient,
McpErrorCode::InvalidArguments => McpErrorCategory::Validation,
McpErrorCode::PermissionDenied => McpErrorCategory::Auth,
McpErrorCode::ToolNotFound
| McpErrorCode::ToolPanicked
| McpErrorCode::NetworkBlocked => McpErrorCategory::Permanent,
McpErrorCode::Internal | McpErrorCode::Unknown => McpErrorCategory::Permanent,
}
}
pub fn default_retryable(&self) -> bool {
matches!(
self,
McpErrorCode::ToolTimeout
| McpErrorCode::McpServerUnreachable
| McpErrorCode::QuotaExceeded
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum McpErrorCategory {
Transient,
Permanent,
Validation,
Auth,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct McpExecuteError {
pub code: McpErrorCode,
pub message: String,
pub category: McpErrorCategory,
pub retryable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_seconds: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cause_chain: Vec<String>,
}
impl McpExecuteError {
pub fn new(code: McpErrorCode, message: impl Into<String>) -> Self {
Self {
category: code.default_category(),
retryable: code.default_retryable(),
code,
message: message.into(),
retry_after_seconds: None,
hint: None,
cause_chain: Vec::new(),
}
}
pub fn with_category(mut self, category: McpErrorCategory) -> Self {
self.category = category;
self
}
pub fn with_retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
pub fn with_retry_after_seconds(mut self, seconds: u32) -> Self {
self.retry_after_seconds = Some(seconds);
self
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
self.cause_chain.push(cause.into());
self
}
}
pub fn classify_mcp_execute_error(message: &str) -> McpExecuteError {
let lower = message.to_ascii_lowercase();
let code = if lower.starts_with("bad_request:") || lower.starts_with("unprocessable:") {
McpErrorCode::InvalidArguments
} else if lower.starts_with("not_found:") {
McpErrorCode::ToolNotFound
} else if lower.starts_with("conflict:") {
McpErrorCode::InvalidArguments
} else if lower.starts_with("forbidden:") {
McpErrorCode::PermissionDenied
} else if lower.starts_with("internal:") {
McpErrorCode::Internal
} else if lower.contains("timed out") || lower.contains("timeout") {
McpErrorCode::ToolTimeout
} else if lower.starts_with("unknown tool") {
McpErrorCode::ToolNotFound
} else if lower.starts_with("missing required parameter") || lower.contains("invalid argument")
{
McpErrorCode::InvalidArguments
} else if lower.contains("permission denied")
|| lower.contains("forbidden")
|| lower.contains("not authorized")
|| lower.contains("unauthorized")
{
McpErrorCode::PermissionDenied
} else if lower.contains("quota") || lower.contains("rate limit") {
McpErrorCode::QuotaExceeded
} else if lower.contains("network blocked") || lower.contains("egress") {
McpErrorCode::NetworkBlocked
} else if lower.contains("mcp server") && lower.contains("unreachable") {
McpErrorCode::McpServerUnreachable
} else if lower.contains("panicked") {
McpErrorCode::ToolPanicked
} else {
McpErrorCode::Internal
};
McpExecuteError::new(code, message)
}
#[cfg(test)]
#[path = "mcp_server_tests.rs"]
mod tests;