use serde_json::json;
use turbomcp_protocol::{
jsonrpc::*,
types::{self, *},
*,
};
#[cfg(test)]
mod jsonrpc_structural_compliance {
use super::*;
#[test]
fn test_request_id_requirements() {
let request_str = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
method: "initialize".to_string(),
params: None,
id: RequestId::from("test-123"),
};
let serialized = serde_json::to_value(&request_str).unwrap();
assert!(serialized["id"].is_string());
let request_num = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
method: "initialize".to_string(),
params: None,
id: RequestId::from(123),
};
let serialized = serde_json::to_value(&request_num).unwrap();
assert!(serialized["id"].is_number());
assert!(!serialized["id"].is_null());
}
#[test]
fn test_request_id_uniqueness_requirement() {
let id1 = RequestId::from("unique-1");
let id2 = RequestId::from("unique-2");
let id3 = RequestId::from("unique-1");
assert_ne!(id1, id2);
assert_eq!(id1, id3); }
#[test]
fn test_response_id_requirements() {
let request_id = RequestId::from("test-request");
let response = JsonRpcResponse::success(json!({"status": "ok"}), request_id.clone());
let serialized = serde_json::to_value(&response).unwrap();
assert_eq!(serialized["id"], json!(request_id.to_string()));
assert_eq!(response.request_id(), Some(&request_id));
}
#[test]
fn test_response_result_error_mutual_exclusion() {
let request_id = RequestId::from("test");
let valid_result = JsonRpcResponse::success(json!({"data": "test"}), request_id.clone());
let valid_error = JsonRpcResponse::error_response(
JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
},
request_id.clone(),
);
assert!(valid_result.result().is_some());
assert!(valid_result.error().is_none());
assert!(valid_result.is_success());
assert!(!valid_result.is_error());
assert!(valid_error.result().is_none());
assert!(valid_error.error().is_some());
assert!(!valid_error.is_success());
assert!(valid_error.is_error());
assert!(serde_json::to_value(&valid_result).is_ok());
assert!(serde_json::to_value(&valid_error).is_ok());
}
#[test]
fn test_notification_no_id_requirement() {
let notification = JsonRpcNotification {
jsonrpc: JsonRpcVersion,
method: "notifications/initialized".to_string(),
params: Some(json!({})),
};
let serialized = serde_json::to_value(¬ification).unwrap();
assert!(!serialized.as_object().unwrap().contains_key("id"));
assert_eq!(serialized["jsonrpc"], "2.0");
assert!(serialized["method"].is_string());
}
#[test]
fn test_error_code_integer_requirement() {
let error = JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
};
let serialized = serde_json::to_value(&error).unwrap();
assert!(serialized["code"].is_i64());
assert_eq!(serialized["code"], -32601);
}
}
#[cfg(test)]
mod meta_field_compliance {
#[test]
fn test_meta_key_naming_conventions() {
let valid_keys = [
"simple_name",
"name-with-hyphens",
"name.with.dots",
"name_123",
"mycompany.com/feature",
"api.mycompany.org/setting",
"123-invalid", ];
let reserved_keys = vec![
"modelcontextprotocol.io/test",
"mcp.dev/feature",
"api.modelcontextprotocol.org/setting",
"tools.mcp.com/config",
];
for key in valid_keys.iter().take(6) {
assert!(is_valid_meta_key(key), "Key should be valid: {}", key);
}
assert!(
!is_valid_meta_key("123-invalid"),
"Key should be invalid: starts with number"
);
for key in &reserved_keys {
assert!(is_reserved_meta_key(key), "Key should be reserved: {}", key);
}
}
#[test]
fn test_meta_prefix_label_validation() {
let valid_prefixes = vec![
"company.com/",
"my-org.dev/",
"api.service.net/",
"a1.b2.c3/",
];
let invalid_prefixes = vec![
"1company.com/", "company-.com/", "company..com/", "-company.com/", ];
for prefix in &valid_prefixes {
assert!(
is_valid_meta_prefix(prefix),
"Prefix should be valid: {}",
prefix
);
}
for prefix in &invalid_prefixes {
assert!(
!is_valid_meta_prefix(prefix),
"Prefix should be invalid: {}",
prefix
);
}
}
fn is_valid_meta_key(key: &str) -> bool {
if key.is_empty() {
return false;
}
let first_char = key.chars().next().unwrap();
let last_char = key.chars().last().unwrap();
first_char.is_alphabetic() && last_char.is_alphanumeric()
}
fn is_reserved_meta_key(key: &str) -> bool {
key.contains("modelcontextprotocol") || key.contains("mcp.")
}
fn is_valid_meta_prefix(prefix: &str) -> bool {
if !prefix.ends_with('/') {
return false;
}
let labels = prefix.trim_end_matches('/').split('.');
for label in labels {
if label.is_empty() {
return false;
}
let first_char = label.chars().next().unwrap();
let last_char = label.chars().last().unwrap();
if !first_char.is_alphabetic() || (!last_char.is_alphanumeric()) {
return false;
}
for ch in label.chars().skip(1).take(label.len().saturating_sub(2)) {
if !ch.is_alphanumeric() && ch != '-' {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod icon_compliance {
use turbomcp_protocol::types::core::Icon;
use url::Url;
#[test]
fn test_icon_mime_type_requirements() {
let required_types = vec!["image/png", "image/jpeg", "image/jpg"];
let recommended_types = vec!["image/svg+xml", "image/webp"];
for mime_type in &required_types {
let icon = Icon {
src: Url::parse("https://example.com/icon.png").unwrap(),
mime_type: Some(mime_type.to_string()),
sizes: None,
theme: None,
};
assert!(
validate_icon(&icon).is_ok(),
"Required MIME type should be valid: {}",
mime_type
);
}
for mime_type in &recommended_types {
let icon = Icon {
src: Url::parse("https://example.com/icon.svg").unwrap(),
mime_type: Some(mime_type.to_string()),
sizes: None,
theme: None,
};
assert!(
validate_icon(&icon).is_ok(),
"Recommended MIME type should be valid: {}",
mime_type
);
}
}
#[test]
fn test_icon_uri_security_requirements() {
let safe_uris = vec![
"https://example.com/icon.png",
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
];
let unsafe_uris = vec![
"http://example.com/icon.png", "javascript:alert('xss')", "file:///etc/passwd", "ftp://ftp.example.com/icon.png", "ws://websocket.example.com", ];
for uri in &safe_uris {
let icon = Icon {
src: Url::parse(uri).expect("Should be parseable URI"),
mime_type: Some("image/png".to_string()),
sizes: None,
theme: None,
};
assert!(
validate_icon(&icon).is_ok(),
"Safe URI should be valid: {}",
uri
);
}
for uri in &unsafe_uris {
if let Ok(parsed) = Url::parse(uri) {
let icon = Icon {
src: parsed,
mime_type: Some("image/png".to_string()),
sizes: None,
theme: None,
};
assert!(
validate_icon(&icon).is_err(),
"Unsafe URI should be rejected: {}",
uri
);
}
}
}
#[test]
fn test_icon_same_origin_requirement() {
let server_origin = Url::parse("https://myserver.com").unwrap();
let same_origin_uris = vec![
"https://myserver.com/icon.png",
"https://myserver.com/assets/icons/tool.svg",
];
let different_origin_uris = vec![
"https://evil.com/steal-data.png",
"https://cdn.example.com/icon.png",
];
for uri in &same_origin_uris {
let parsed = Url::parse(uri).unwrap();
assert!(
is_same_origin(&parsed, &server_origin),
"Same origin URI should be valid: {}",
uri
);
}
for uri in &different_origin_uris {
let parsed = Url::parse(uri).unwrap();
assert!(
!is_same_origin(&parsed, &server_origin),
"Different origin URI should be flagged: {}",
uri
);
}
}
fn validate_icon(icon: &Icon) -> std::result::Result<(), String> {
let uri = &icon.src;
if uri.scheme() == "https" || uri.scheme() == "data" {
Ok(())
} else if uri.scheme() == "http" {
Err("HTTP URIs not allowed, use HTTPS".to_string())
} else if uri.scheme() == "javascript"
|| uri.scheme() == "file"
|| uri.scheme() == "ftp"
|| uri.scheme() == "ws"
{
Err("Unsafe URI scheme detected".to_string())
} else {
Err("Invalid URI scheme".to_string())
}
}
fn is_same_origin(uri: &Url, server_origin: &Url) -> bool {
if uri.scheme() == "data" {
return true; }
uri.origin() == server_origin.origin()
}
}
#[cfg(test)]
mod lifecycle_compliance {
use super::*;
#[test]
fn test_initialization_phase_requirements() {
let init_request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
method: "initialize".to_string(),
id: RequestId::from("init-1"),
params: Some(json!({
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "TestClient",
"version": "1.0.0",
"title": "Test Client",
"websiteUrl": "https://example.com"
}
})),
};
assert_eq!(init_request.method, "initialize");
assert!(init_request.params.is_some());
let params = init_request.params.as_ref().unwrap();
assert!(params["protocolVersion"].is_string());
assert!(params["capabilities"].is_object());
assert!(params["clientInfo"].is_object());
assert!(params["clientInfo"]["name"].is_string());
}
#[test]
fn test_initialized_notification_requirement() {
let initialized_notification = JsonRpcNotification {
jsonrpc: JsonRpcVersion,
method: "notifications/initialized".to_string(),
params: Some(json!({})),
};
assert_eq!(initialized_notification.method, "notifications/initialized");
let serialized = serde_json::to_value(&initialized_notification).unwrap();
assert!(!serialized.as_object().unwrap().contains_key("id"));
}
#[test]
fn test_initialization_ordering_requirements() {
let allowed_before_init_response = vec!["ping"];
let allowed_before_initialized = vec!["ping", "logging/setLevel", "notifications/message"];
for method in &allowed_before_init_response {
assert!(
is_allowed_before_init_response(method),
"Method should be allowed before init response: {}",
method
);
}
for method in &allowed_before_initialized {
assert!(
is_allowed_before_initialized(method),
"Method should be allowed before initialized: {}",
method
);
}
let forbidden_methods = vec!["tools/list", "resources/list", "prompts/list"];
for method in &forbidden_methods {
assert!(
!is_allowed_before_init_response(method),
"Method should NOT be allowed before init response: {}",
method
);
}
}
#[test]
fn test_version_negotiation_requirements() {
let supported_versions = vec!["2025-11-25"];
let client_version = "2025-11-25";
let server_response_version = negotiate_version(client_version, &supported_versions);
assert_eq!(
server_response_version, client_version,
"Server should respond with same version if supported"
);
let unsupported_version = "1.0.0";
let server_response_version = negotiate_version(unsupported_version, &supported_versions);
assert_eq!(
server_response_version, "2025-11-25",
"Server should respond with the only supported version"
);
let client_supported = vec!["1.0.0", "1.1.0"];
let server_version = "2025-11-25";
assert!(
!client_should_accept_version(server_version, &client_supported),
"Client should reject unsupported version"
);
}
fn is_allowed_before_init_response(method: &str) -> bool {
method == "ping"
}
fn is_allowed_before_initialized(method: &str) -> bool {
matches!(
method,
"ping" | "logging/setLevel" | "notifications/message"
)
}
fn negotiate_version<'a>(
client_version: &'a str,
supported_versions: &'a [&'a str],
) -> &'a str {
if supported_versions.contains(&client_version) {
client_version
} else {
supported_versions.first().unwrap_or(&"2025-11-25")
}
}
fn client_should_accept_version(server_version: &str, client_supported: &[&str]) -> bool {
client_supported.contains(&server_version)
}
}
#[cfg(test)]
mod capability_negotiation_compliance {
use super::*;
#[test]
fn test_standard_capability_structure() {
let client_caps = ClientCapabilities {
extensions: None,
roots: Some(RootsCapabilities {
list_changed: Some(true),
}),
sampling: Some(SamplingCapabilities {}),
elicitation: Some(ElicitationCapabilities::default()),
experimental: Some({
let mut exp = std::collections::HashMap::new();
exp.insert("custom_feature".to_string(), json!({"enabled": true}));
exp
}),
#[cfg(feature = "experimental-tasks")]
tasks: None,
};
let server_caps = ServerCapabilities {
extensions: None,
prompts: Some(PromptsCapabilities {
list_changed: Some(true),
}),
resources: Some(ResourcesCapabilities {
subscribe: Some(true),
list_changed: Some(true),
}),
tools: Some(ToolsCapabilities {
list_changed: Some(true),
}),
logging: Some(LoggingCapabilities {}),
completions: Some(types::CompletionCapabilities {}),
experimental: Some({
let mut exp = std::collections::HashMap::new();
exp.insert("advanced_tools".to_string(), json!({"version": "2.0"}));
exp
}),
#[cfg(feature = "experimental-tasks")]
tasks: None,
};
let client_json = serde_json::to_value(&client_caps).unwrap();
assert!(client_json["roots"]["listChanged"].is_boolean());
assert!(client_json["sampling"].is_object());
assert!(client_json["elicitation"].is_object());
let server_json = serde_json::to_value(&server_caps).unwrap();
assert!(server_json["prompts"]["listChanged"].is_boolean());
assert!(server_json["resources"]["subscribe"].is_boolean());
assert!(server_json["tools"]["listChanged"].is_boolean());
}
#[test]
fn test_capability_enforcement() {
let client_caps = ClientCapabilities {
extensions: None,
roots: Some(RootsCapabilities {
list_changed: Some(true),
}),
sampling: None, elicitation: None,
experimental: None,
#[cfg(feature = "experimental-tasks")]
tasks: None,
};
let server_caps = ServerCapabilities {
extensions: None,
tools: Some(ToolsCapabilities {
list_changed: Some(true),
}),
prompts: None, resources: None,
logging: None,
completions: None,
experimental: None,
#[cfg(feature = "experimental-tasks")]
tasks: None,
};
assert!(can_use_capability("roots", &client_caps, &server_caps));
assert!(can_use_capability("tools", &client_caps, &server_caps));
assert!(!can_use_capability("sampling", &client_caps, &server_caps));
assert!(!can_use_capability("prompts", &client_caps, &server_caps));
}
fn can_use_capability(
capability: &str,
client_caps: &ClientCapabilities,
server_caps: &ServerCapabilities,
) -> bool {
match capability {
"roots" => client_caps.roots.is_some(),
"sampling" => client_caps.sampling.is_some(),
"elicitation" => client_caps.elicitation.is_some(),
"tools" => server_caps.tools.is_some(),
"prompts" => server_caps.prompts.is_some(),
"resources" => server_caps.resources.is_some(),
"logging" => server_caps.logging.is_some(),
_ => false,
}
}
}
#[cfg(test)]
mod protocol_version_compliance {
use super::*;
#[test]
fn test_protocol_version_constants() {
assert_eq!(PROTOCOL_VERSION, "2025-11-25");
assert!(SUPPORTED_VERSIONS.contains(&PROTOCOL_VERSION));
assert!(SUPPORTED_VERSIONS.contains(&"2025-06-18"));
assert_eq!(
SUPPORTED_VERSIONS[SUPPORTED_VERSIONS.len() - 1],
PROTOCOL_VERSION
);
let versions = SUPPORTED_VERSIONS;
for i in 0..versions.len() - 1 {
assert!(
versions[i] <= versions[i + 1],
"Versions should be in ascending order"
);
}
}
}
#[cfg(test)]
mod full_protocol_integration {
use super::*;
#[test]
fn test_complete_initialization_handshake() {
let init_request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
method: "initialize".to_string(),
id: RequestId::from("init-1"),
params: Some(json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": {
"name": "TurboMCP-Test-Client",
"version": "1.0.0"
}
})),
};
assert_eq!(init_request.method, "initialize");
let init_response = JsonRpcResponse::success(
json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true }
},
"serverInfo": {
"name": "TurboMCP-Test-Server",
"version": "1.0.0"
}
}),
init_request.id.clone(),
);
assert_eq!(init_response.request_id(), Some(&init_request.id));
assert!(init_response.result().is_some());
assert!(init_response.error().is_none());
let initialized = JsonRpcNotification {
jsonrpc: JsonRpcVersion,
method: "notifications/initialized".to_string(),
params: Some(json!({})),
};
let serialized = serde_json::to_value(&initialized).unwrap();
assert!(!serialized.as_object().unwrap().contains_key("id"));
assert_eq!(initialized.method, "notifications/initialized");
assert!(serialized["params"].is_object());
}
}