use serde::{Deserialize, Serialize};
use crate::protocol::{ClientCapabilities, Implementation, ProgressToken};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatelessRequestMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_token: Option<ProgressToken>,
#[serde(
rename = "io.modelcontextprotocol/protocolVersion",
skip_serializing_if = "Option::is_none"
)]
pub protocol_version: Option<String>,
#[serde(
rename = "io.modelcontextprotocol/clientInfo",
skip_serializing_if = "Option::is_none"
)]
pub client_info: Option<Implementation>,
#[serde(
rename = "io.modelcontextprotocol/clientCapabilities",
skip_serializing_if = "Option::is_none"
)]
pub client_capabilities: Option<ClientCapabilities>,
#[serde(
rename = "io.modelcontextprotocol/logLevel",
skip_serializing_if = "Option::is_none"
)]
pub log_level: Option<LogLevel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
Info,
Notice,
Warning,
Error,
Critical,
Alert,
Emergency,
}
#[doc(inline)]
pub use crate::protocol::{DiscoverParams, DiscoverResult};
#[doc(inline)]
pub use crate::error::UnsupportedProtocolVersionData;
pub mod error_codes {
pub const UNSUPPORTED_PROTOCOL_VERSION: i32 = -32004;
#[deprecated(
since = "0.12.0",
note = "SEP-1442 draft assignment was wrong; SEP-2575 FINAL uses -32004. \
Use `UNSUPPORTED_PROTOCOL_VERSION` or \
`crate::error::McpErrorCode::UnsupportedProtocolVersion`."
)]
pub const UNSUPPORTED_VERSION: i32 = -32000;
#[deprecated(
since = "0.11.0",
note = "SEP-2243 reclaims -32001 for HeaderMismatch. Use \
McpErrorCode::SessionRequired (-32006) or SessionNotFound (-32005)."
)]
pub const INVALID_SESSION: i32 = -32001;
}
#[derive(Debug, Clone)]
pub struct StatelessConfig {
pub require_protocol_version: bool,
pub optional_sessions: bool,
pub enable_discover: bool,
}
impl Default for StatelessConfig {
fn default() -> Self {
Self::new()
}
}
impl StatelessConfig {
pub fn new() -> Self {
Self {
require_protocol_version: true,
optional_sessions: true,
enable_discover: true,
}
}
pub fn backward_compatible() -> Self {
Self {
require_protocol_version: false,
optional_sessions: true,
enable_discover: true,
}
}
}
pub fn validate_protocol_version(
version: &str,
) -> std::result::Result<(), crate::error::JsonRpcError> {
use crate::protocol::SUPPORTED_PROTOCOL_VERSIONS;
if SUPPORTED_PROTOCOL_VERSIONS.contains(&version) {
Ok(())
} else {
Err(crate::error::JsonRpcError::unsupported_protocol_version(
version,
SUPPORTED_PROTOCOL_VERSIONS.iter().copied(),
))
}
}
impl StatelessRequestMeta {
pub fn from_params(params: &serde_json::Value) -> Option<Self> {
params
.get("_meta")
.and_then(|meta| serde_json::from_value(meta.clone()).ok())
}
pub fn has_client_capabilities(&self) -> bool {
self.client_capabilities.is_some()
}
pub fn has_client_info(&self) -> bool {
self.client_info.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_serializes_with_spec_keys() {
use crate::protocol::{ClientCapabilities, Implementation};
let meta = StatelessRequestMeta {
progress_token: None,
protocol_version: Some("2026-07-28".to_string()),
client_info: Some(Implementation {
name: "test-client".into(),
version: "1.0.0".into(),
title: None,
description: None,
icons: None,
website_url: None,
meta: None,
}),
client_capabilities: Some(ClientCapabilities::default()),
log_level: Some(LogLevel::Info),
};
let json: serde_json::Value = serde_json::to_value(&meta).unwrap();
assert_eq!(
json["io.modelcontextprotocol/protocolVersion"],
"2026-07-28"
);
assert_eq!(
json["io.modelcontextprotocol/clientInfo"]["name"],
"test-client"
);
assert!(json["io.modelcontextprotocol/clientCapabilities"].is_object());
assert_eq!(json["io.modelcontextprotocol/logLevel"], "info");
assert!(
json.get("modelcontextprotocol.io/mcpProtocolVersion")
.is_none()
);
assert!(json.get("modelcontextprotocol.io/sessionId").is_none());
assert!(json.get("io.modelcontextprotocol/sessionId").is_none());
assert!(json.get("io.modelcontextprotocol/roots").is_none());
}
#[test]
fn meta_deserializes_from_spec_keys() {
let json = r#"{
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/logLevel": "debug"
}"#;
let meta: StatelessRequestMeta = serde_json::from_str(json).unwrap();
assert_eq!(meta.protocol_version.as_deref(), Some("2026-07-28"));
assert_eq!(meta.client_info.as_ref().unwrap().name, "test-client");
assert!(meta.has_client_capabilities());
assert!(meta.has_client_info());
assert_eq!(meta.log_level, Some(LogLevel::Debug));
}
#[test]
fn from_params_extracts_meta_object() {
let params = serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
},
"other": "ignored"
});
let meta = StatelessRequestMeta::from_params(¶ms).expect("meta present");
assert_eq!(meta.protocol_version.as_deref(), Some("2026-07-28"));
}
#[test]
fn test_discover_result_serialization() {
use crate::protocol::{Implementation, ServerCapabilities};
let result = DiscoverResult {
supported_versions: vec!["2025-11-25".to_string(), "2025-03-26".to_string()],
capabilities: ServerCapabilities::default(),
server_info: Implementation {
name: "test-server".to_string(),
version: "1.0.0".to_string(),
title: None,
description: None,
icons: None,
website_url: None,
meta: None,
},
instructions: Some("Test instructions".to_string()),
meta: None,
};
let json = serde_json::to_value(&result).unwrap();
assert!(json["supportedVersions"].is_array());
assert_eq!(json["serverInfo"]["name"], "test-server");
}
#[test]
fn test_unsupported_protocol_version_data_shape() {
let data = UnsupportedProtocolVersionData {
supported: vec!["2026-07-28".to_string(), "2025-11-25".to_string()],
requested: "2027-01-01".to_string(),
};
let json = serde_json::to_value(&data).unwrap();
assert_eq!(json["supported"][0], "2026-07-28");
assert_eq!(json["requested"], "2027-01-01");
assert!(
json.get("supportedVersions").is_none(),
"spec field name is 'supported', not 'supportedVersions': {json}"
);
}
#[test]
fn test_config_defaults() {
let config = StatelessConfig::new();
assert!(config.require_protocol_version);
assert!(config.optional_sessions);
assert!(config.enable_discover);
}
#[test]
fn test_config_backward_compatible() {
let config = StatelessConfig::backward_compatible();
assert!(!config.require_protocol_version);
assert!(config.optional_sessions);
}
#[test]
fn test_from_params_no_meta() {
let params = serde_json::json!({
"name": "test-tool"
});
let meta = StatelessRequestMeta::from_params(¶ms);
assert!(meta.is_none());
}
#[test]
fn test_has_client_capabilities() {
let meta = StatelessRequestMeta {
progress_token: None,
protocol_version: None,
client_info: None,
client_capabilities: Some(ClientCapabilities::default()),
log_level: None,
};
assert!(meta.has_client_capabilities());
let meta_without = StatelessRequestMeta::default();
assert!(!meta_without.has_client_capabilities());
}
#[test]
fn legacy_sep1442_draft_keys_are_silently_ignored() {
let json = r#"{
"modelcontextprotocol.io/mcpProtocolVersion": "2025-11-25",
"modelcontextprotocol.io/sessionId": "old",
"modelcontextprotocol.io/roots": []
}"#;
let meta: StatelessRequestMeta = serde_json::from_str(json).unwrap();
assert!(meta.protocol_version.is_none());
assert!(meta.client_info.is_none());
assert!(meta.client_capabilities.is_none());
}
}