use serde::{Deserialize, Serialize};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
pub const MCP_PROTOCOL_VERSION_2025_03: &str = "2025-03-26";
pub const MCP_PROTOCOL_VERSION_2025_06: &str = "2025-06-18";
pub const MCP_PROTOCOL_VERSION_2026_07: &str = "2026-07-28";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "auto"))]
#[serde(rename_all = "snake_case")]
pub enum McpProtocolMode {
#[default]
Auto,
#[serde(rename = "2025-03-26", alias = "legacy")]
V2025March,
#[serde(rename = "2025-06-18", alias = "stable")]
V2025June,
#[serde(rename = "2026-07-28", alias = "rc")]
V2026July,
}
impl McpProtocolMode {
pub fn is_auto(&self) -> bool {
matches!(self, McpProtocolMode::Auto)
}
pub fn pinned_version(&self) -> Option<&'static str> {
match self {
McpProtocolMode::Auto => None,
McpProtocolMode::V2025March => Some(MCP_PROTOCOL_VERSION_2025_03),
McpProtocolMode::V2025June => Some(MCP_PROTOCOL_VERSION_2025_06),
McpProtocolMode::V2026July => Some(MCP_PROTOCOL_VERSION_2026_07),
}
}
pub fn pinned_stateful(&self) -> Option<bool> {
match self {
McpProtocolMode::Auto => None,
McpProtocolMode::V2025March | McpProtocolMode::V2025June => Some(true),
McpProtocolMode::V2026July => Some(false),
}
}
}
impl std::fmt::Display for McpProtocolMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
McpProtocolMode::Auto => write!(f, "auto"),
McpProtocolMode::V2025March => write!(f, "{MCP_PROTOCOL_VERSION_2025_03}"),
McpProtocolMode::V2025June => write!(f, "{MCP_PROTOCOL_VERSION_2025_06}"),
McpProtocolMode::V2026July => write!(f, "{MCP_PROTOCOL_VERSION_2026_07}"),
}
}
}
impl From<&str> for McpProtocolMode {
fn from(s: &str) -> Self {
match s {
MCP_PROTOCOL_VERSION_2025_03 | "legacy" => McpProtocolMode::V2025March,
MCP_PROTOCOL_VERSION_2025_06 | "stable" => McpProtocolMode::V2025June,
MCP_PROTOCOL_VERSION_2026_07 | "rc" => McpProtocolMode::V2026July,
_ => McpProtocolMode::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "url"))]
#[serde(rename_all = "snake_case")]
pub enum McpElicitationPolicy {
#[default]
Url,
UrlAndForm,
None,
}
impl McpElicitationPolicy {
pub fn is_default(&self) -> bool {
matches!(self, McpElicitationPolicy::Url)
}
pub fn allows_url(&self) -> bool {
matches!(
self,
McpElicitationPolicy::Url | McpElicitationPolicy::UrlAndForm
)
}
pub fn allows_form(&self) -> bool {
matches!(self, McpElicitationPolicy::UrlAndForm)
}
}
impl std::fmt::Display for McpElicitationPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
McpElicitationPolicy::Url => write!(f, "url"),
McpElicitationPolicy::UrlAndForm => write!(f, "url_and_form"),
McpElicitationPolicy::None => write!(f, "none"),
}
}
}
impl From<&str> for McpElicitationPolicy {
fn from(s: &str) -> Self {
match s {
"url_and_form" => McpElicitationPolicy::UrlAndForm,
"none" => McpElicitationPolicy::None,
_ => McpElicitationPolicy::Url,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mcp_server::ScopedMcpServer;
use serde_json::json;
#[test]
fn elicitation_policy_defaults_to_url_and_stays_out_of_serialized_config() {
let scoped: ScopedMcpServer =
serde_json::from_value(json!({"type":"http","url":"https://example.com/mcp"})).unwrap();
assert_eq!(scoped.elicitation_policy, McpElicitationPolicy::Url);
let wire = serde_json::to_value(&scoped).unwrap();
assert!(
wire.get("elicitation_policy").is_none(),
"default policy must keep stored config byte-identical: {wire}"
);
for (value, policy) in [
("url_and_form", McpElicitationPolicy::UrlAndForm),
("none", McpElicitationPolicy::None),
] {
let scoped: ScopedMcpServer = serde_json::from_value(json!({
"type":"http","url":"https://example.com/mcp","elicitation_policy":value
}))
.unwrap();
assert_eq!(scoped.elicitation_policy, policy);
assert_eq!(
serde_json::to_value(&scoped).unwrap()["elicitation_policy"],
json!(value)
);
assert_eq!(McpElicitationPolicy::from(value), policy);
}
assert!(McpElicitationPolicy::Url.allows_url());
assert!(!McpElicitationPolicy::Url.allows_form());
assert!(McpElicitationPolicy::UrlAndForm.allows_form());
assert!(!McpElicitationPolicy::None.allows_url());
assert!(!McpElicitationPolicy::None.allows_form());
assert_eq!(
McpElicitationPolicy::from("everything"),
McpElicitationPolicy::Url
);
}
}