use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const VALID_MCP_ANNOTATION_HINTS: &[&str] = &[
"readOnlyHint",
"destructiveHint",
"idempotentHint",
"openWorldHint",
"title",
];
pub const VALID_MCP_CAPABILITY_KEYS: &[&str] = &[
"tools",
"resources",
"prompts",
"logging",
"roots",
"sampling",
"elicitation",
"completions",
"experimental",
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolSchema {
pub name: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Option<serde_json::Value>,
#[serde(rename = "outputSchema")]
pub output_schema: Option<serde_json::Value>,
pub icons: Option<serde_json::Value>,
#[serde(default)]
pub annotations: Option<HashMap<String, serde_json::Value>>,
#[serde(rename = "requiresApproval")]
pub requires_approval: Option<bool>,
pub confirmation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpResourceSchema {
pub uri: Option<String>,
pub name: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
#[serde(rename = "mimeType")]
pub mime_type: Option<String>,
pub size: Option<serde_json::Value>,
#[serde(default)]
pub annotations: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpResourceContentSchema {
pub uri: Option<String>,
#[serde(rename = "mimeType")]
pub mime_type: Option<String>,
pub text: Option<String>,
pub blob: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpPromptArgumentSchema {
pub name: Option<String>,
pub description: Option<String>,
pub required: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpPromptSchema {
pub name: Option<String>,
pub description: Option<String>,
pub title: Option<String>,
pub arguments: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpJsonRpcMessage {
pub jsonrpc: Option<String>,
pub id: Option<serde_json::Value>,
pub method: Option<String>,
pub params: Option<serde_json::Value>,
pub result: Option<serde_json::Value>,
pub error: Option<serde_json::Value>,
}
pub const VALID_MCP_SERVER_TYPES: &[&str] = &["stdio", "http", "sse"];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
#[serde(rename = "type")]
pub server_type: Option<String>,
pub command: Option<serde_json::Value>,
#[serde(default)]
pub args: Option<serde_json::Value>,
#[serde(default)]
pub env: Option<HashMap<String, String>>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpConfigSchema {
#[serde(rename = "mcpServers")]
pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
pub tools: Option<Vec<McpToolSchema>>,
pub resources: Option<Vec<McpResourceSchema>>,
pub prompts: Option<Vec<McpPromptSchema>>,
pub capabilities: Option<HashMap<String, serde_json::Value>>,
pub jsonrpc: Option<String>,
}
pub const VALID_JSON_SCHEMA_TYPES: &[&str] = &[
"string", "number", "integer", "boolean", "object", "array", "null",
];
pub const DEFAULT_MCP_PROTOCOL_VERSION: &str = "2025-11-25";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpInitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: Option<String>,
#[serde(rename = "clientInfo")]
pub client_info: Option<serde_json::Value>,
pub capabilities: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)] pub struct McpInitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: Option<String>,
#[serde(rename = "serverInfo")]
pub server_info: Option<serde_json::Value>,
pub capabilities: Option<serde_json::Value>,
}
pub fn is_initialize_message(value: &serde_json::Value) -> bool {
value
.get("method")
.and_then(|m| m.as_str())
.is_some_and(|m| m == "initialize")
}
pub fn is_initialize_response(value: &serde_json::Value) -> bool {
value
.get("result")
.and_then(|r| r.get("protocolVersion"))
.is_some()
}
pub fn extract_request_protocol_version(value: &serde_json::Value) -> Option<String> {
value
.get("params")
.and_then(|p| p.get("protocolVersion"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub fn extract_response_protocol_version(value: &serde_json::Value) -> Option<String> {
value
.get("result")
.and_then(|r| r.get("protocolVersion"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
impl McpToolSchema {
pub fn has_required_fields(&self) -> (bool, bool, bool) {
(
!self.name.as_deref().unwrap_or("").trim().is_empty(),
!self.description.as_deref().unwrap_or("").trim().is_empty(),
self.input_schema.is_some(),
)
}
pub fn has_meaningful_description(&self) -> bool {
self.description
.as_deref()
.is_some_and(|desc| !desc.trim().is_empty() && desc.len() >= 10)
}
pub fn has_consent_fields(&self) -> bool {
self.requires_approval == Some(true)
|| self
.confirmation
.as_deref()
.is_some_and(|c| !c.trim().is_empty())
}
pub fn has_annotations(&self) -> bool {
self.annotations.as_ref().is_some_and(|a| !a.is_empty())
}
}
impl McpJsonRpcMessage {
#[allow(dead_code)] pub fn has_valid_jsonrpc_version(&self) -> bool {
match &self.jsonrpc {
Some(version) => version == "2.0",
None => false,
}
}
}
pub fn validate_json_schema_structure(schema: &serde_json::Value) -> Vec<String> {
let mut errors = Vec::new();
if !schema.is_object() {
errors.push("inputSchema must be an object".to_string());
return errors;
}
let obj = schema.as_object().unwrap();
if let Some(type_val) = obj.get("type") {
if let Some(type_str) = type_val.as_str() {
if !VALID_JSON_SCHEMA_TYPES.contains(&type_str) {
errors.push(format!(
"Invalid JSON Schema type '{}', expected one of: {}",
type_str,
VALID_JSON_SCHEMA_TYPES.join(", ")
));
}
} else if let Some(type_arr) = type_val.as_array() {
for t in type_arr {
if let Some(t_str) = t.as_str() {
if !VALID_JSON_SCHEMA_TYPES.contains(&t_str) {
errors.push(format!(
"Invalid JSON Schema type '{}' in type array",
t_str
));
}
} else {
errors.push("'type' array elements must be strings".to_string());
}
}
} else {
errors.push("'type' field must be a string or array of strings".to_string());
}
}
if let Some(props) = obj.get("properties") {
if !props.is_object() {
errors.push("'properties' field must be an object".to_string());
}
}
if let Some(required) = obj.get("required") {
if let Some(arr) = required.as_array() {
for item in arr {
if !item.is_string() {
errors.push("'required' array must contain only strings".to_string());
break;
}
}
} else {
errors.push("'required' field must be an array".to_string());
}
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_mcp_tool_has_required_fields() {
let tool = McpToolSchema {
name: Some("test-tool".to_string()),
description: Some("A test tool".to_string()),
input_schema: Some(json!({"type": "object"})),
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: None,
};
assert_eq!(tool.has_required_fields(), (true, true, true));
}
#[test]
fn test_mcp_tool_missing_name() {
let tool = McpToolSchema {
name: None,
description: Some("A test tool".to_string()),
input_schema: Some(json!({"type": "object"})),
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: None,
};
assert_eq!(tool.has_required_fields(), (false, true, true));
}
#[test]
fn test_mcp_tool_empty_name() {
let tool = McpToolSchema {
name: Some("".to_string()),
description: Some("A test tool".to_string()),
input_schema: Some(json!({"type": "object"})),
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: None,
};
assert_eq!(tool.has_required_fields(), (false, true, true));
}
#[test]
fn test_meaningful_description() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: Some("This is a meaningful description".to_string()),
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: None,
};
assert!(tool.has_meaningful_description());
}
#[test]
fn test_short_description() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: Some("Short".to_string()),
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: None,
};
assert!(!tool.has_meaningful_description());
}
#[test]
fn test_consent_fields_requires_approval_true() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: None,
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: Some(true),
confirmation: None,
};
assert!(tool.has_consent_fields());
}
#[test]
fn test_consent_fields_requires_approval_false() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: None,
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: Some(false),
confirmation: None,
};
assert!(!tool.has_consent_fields());
}
#[test]
fn test_consent_fields_confirmation_non_empty() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: None,
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: Some("Are you sure?".to_string()),
};
assert!(tool.has_consent_fields());
}
#[test]
fn test_consent_fields_confirmation_empty() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: None,
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: Some("".to_string()),
};
assert!(!tool.has_consent_fields());
}
#[test]
fn test_consent_fields_confirmation_whitespace() {
let tool = McpToolSchema {
name: Some("test".to_string()),
description: None,
input_schema: None,
title: None,
output_schema: None,
icons: None,
annotations: None,
requires_approval: None,
confirmation: Some(" ".to_string()),
};
assert!(!tool.has_consent_fields());
}
#[test]
fn test_jsonrpc_version_valid() {
let msg = McpJsonRpcMessage {
jsonrpc: Some("2.0".to_string()),
id: None,
method: None,
params: None,
result: None,
error: None,
};
assert!(msg.has_valid_jsonrpc_version());
}
#[test]
fn test_jsonrpc_version_invalid() {
let msg = McpJsonRpcMessage {
jsonrpc: Some("1.0".to_string()),
id: None,
method: None,
params: None,
result: None,
error: None,
};
assert!(!msg.has_valid_jsonrpc_version());
}
#[test]
fn test_validate_schema_structure_valid() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
});
let errors = validate_json_schema_structure(&schema);
assert!(errors.is_empty());
}
#[test]
fn test_validate_schema_structure_invalid_type() {
let schema = json!({
"type": "invalid_type"
});
let errors = validate_json_schema_structure(&schema);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("Invalid JSON Schema type"));
}
#[test]
fn test_validate_schema_not_object() {
let schema = json!("not an object");
let errors = validate_json_schema_structure(&schema);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("must be an object"));
}
#[test]
fn test_validate_schema_type_not_string_or_array() {
let schema = json!({"type": 123});
let errors = validate_json_schema_structure(&schema);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("must be a string or array"));
}
#[test]
fn test_validate_schema_type_array_with_non_string() {
let schema = json!({"type": ["string", 123]});
let errors = validate_json_schema_structure(&schema);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("must be strings"));
}
#[test]
fn test_validate_schema_type_object_value() {
let schema = json!({"type": {"nested": "object"}});
let errors = validate_json_schema_structure(&schema);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("must be a string or array"));
}
#[test]
fn test_is_initialize_message() {
let msg = json!({"jsonrpc": "2.0", "method": "initialize", "id": 1});
assert!(super::is_initialize_message(&msg));
let other = json!({"jsonrpc": "2.0", "method": "tools/list", "id": 1});
assert!(!super::is_initialize_message(&other));
let no_method = json!({"jsonrpc": "2.0", "id": 1});
assert!(!super::is_initialize_message(&no_method));
}
#[test]
fn test_is_initialize_response() {
let response = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {"protocolVersion": "2025-11-25"}
});
assert!(super::is_initialize_response(&response));
let other_response = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {"tools": []}
});
assert!(!super::is_initialize_response(&other_response));
}
#[test]
fn test_extract_request_protocol_version() {
let msg = json!({
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {"protocolVersion": "2024-11-05"}
});
assert_eq!(
super::extract_request_protocol_version(&msg),
Some("2024-11-05".to_string())
);
let no_version = json!({
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {}
});
assert_eq!(super::extract_request_protocol_version(&no_version), None);
}
#[test]
fn test_extract_response_protocol_version() {
let response = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {"protocolVersion": "2025-11-25"}
});
assert_eq!(
super::extract_response_protocol_version(&response),
Some("2025-11-25".to_string())
);
let no_version = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {}
});
assert_eq!(super::extract_response_protocol_version(&no_version), None);
}
#[test]
fn test_default_mcp_protocol_version_constant() {
assert_eq!(super::DEFAULT_MCP_PROTOCOL_VERSION, "2025-11-25");
}
}