use http::HeaderMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpHeaderInfo {
pub method: String,
pub tool_name: Option<String>,
}
impl McpHeaderInfo {
pub fn for_list() -> Self {
Self {
method: "tools/list".to_string(),
tool_name: None,
}
}
pub fn for_call(tool_name: impl Into<String>) -> Self {
Self {
method: "tools/call".to_string(),
tool_name: Some(tool_name.into()),
}
}
pub fn is_call(&self) -> bool {
self.method == "tools/call"
}
pub fn is_list(&self) -> bool {
self.method == "tools/list"
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpHeaderError {
MissingMethod,
MissingToolName,
InvalidMethod(String),
InvalidToolName(String),
}
impl std::fmt::Display for McpHeaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingMethod => write!(f, "missing required header: Mcp-Method"),
Self::MissingToolName => {
write!(f, "missing required header: Mcp-Name for tools/call method")
}
Self::InvalidMethod(m) => write!(f, "invalid MCP method: {}", m),
Self::InvalidToolName(n) => write!(f, "invalid MCP tool name: {}", n),
}
}
}
impl std::error::Error for McpHeaderError {}
pub const MCP_METHOD_TOOLS_LIST: &str = "tools/list";
pub const MCP_METHOD_TOOLS_CALL: &str = "tools/call";
pub const MCP_METHOD_PING: &str = "ping";
pub fn parse_mcp_headers(headers: &HeaderMap) -> Result<McpHeaderInfo, McpHeaderError> {
let method = headers
.get("mcp-method")
.and_then(|v: &http::HeaderValue| v.to_str().ok())
.ok_or(McpHeaderError::MissingMethod)?
.to_string();
match method.as_str() {
MCP_METHOD_TOOLS_LIST | MCP_METHOD_PING => Ok(McpHeaderInfo {
method,
tool_name: None,
}),
MCP_METHOD_TOOLS_CALL => {
let tool_name = headers
.get("mcp-name")
.and_then(|v: &http::HeaderValue| v.to_str().ok())
.ok_or(McpHeaderError::MissingToolName)?
.to_string();
if !is_valid_tool_name(&tool_name) {
return Err(McpHeaderError::InvalidToolName(tool_name));
}
Ok(McpHeaderInfo {
method,
tool_name: Some(tool_name),
})
}
other => Err(McpHeaderError::InvalidMethod(other.to_string())),
}
}
pub fn is_valid_method(method: &str) -> bool {
matches!(
method,
MCP_METHOD_TOOLS_LIST | MCP_METHOD_TOOLS_CALL | MCP_METHOD_PING
)
}
pub const MAX_TOOL_NAME_LEN: usize = 128;
pub fn is_valid_tool_name(name: &str) -> bool {
if name.is_empty() || name.len() > MAX_TOOL_NAME_LEN {
return false;
}
name.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
fn make_headers(method: Option<&str>, name: Option<&str>) -> HeaderMap {
let mut headers = HeaderMap::new();
if let Some(m) = method {
headers.insert("mcp-method", HeaderValue::from_str(m).unwrap());
}
if let Some(n) = name {
headers.insert("mcp-name", HeaderValue::from_str(n).unwrap());
}
headers
}
#[test]
fn test_parse_tools_list_headers() {
let headers = make_headers(Some("tools/list"), None);
let info = parse_mcp_headers(&headers).unwrap();
assert_eq!(info.method, "tools/list");
assert_eq!(info.tool_name, None);
assert!(info.is_list());
assert!(!info.is_call());
}
#[test]
fn test_parse_tools_call_headers() {
let headers = make_headers(Some("tools/call"), Some("my_tool"));
let info = parse_mcp_headers(&headers).unwrap();
assert_eq!(info.method, "tools/call");
assert_eq!(info.tool_name, Some("my_tool".to_string()));
assert!(info.is_call());
assert!(!info.is_list());
}
#[test]
fn test_parse_ping_headers() {
let headers = make_headers(Some("ping"), None);
let info = parse_mcp_headers(&headers).unwrap();
assert_eq!(info.method, "ping");
assert_eq!(info.tool_name, None);
}
#[test]
fn test_parse_missing_method_header() {
let headers = make_headers(None, None);
let err = parse_mcp_headers(&headers).unwrap_err();
assert_eq!(err, McpHeaderError::MissingMethod);
}
#[test]
fn test_parse_missing_tool_name_for_call() {
let headers = make_headers(Some("tools/call"), None);
let err = parse_mcp_headers(&headers).unwrap_err();
assert_eq!(err, McpHeaderError::MissingToolName);
}
#[test]
fn test_parse_invalid_method() {
let headers = make_headers(Some("invalid/method"), None);
let err = parse_mcp_headers(&headers).unwrap_err();
assert!(matches!(err, McpHeaderError::InvalidMethod(_)));
}
#[test]
fn test_is_valid_method() {
assert!(is_valid_method("tools/list"));
assert!(is_valid_method("tools/call"));
assert!(is_valid_method("ping"));
assert!(!is_valid_method("invalid"));
assert!(!is_valid_method(""));
}
#[test]
fn test_header_info_for_list() {
let info = McpHeaderInfo::for_list();
assert_eq!(info.method, "tools/list");
assert!(info.is_list());
assert!(info.tool_name.is_none());
}
#[test]
fn test_header_info_for_call() {
let info = McpHeaderInfo::for_call("test_tool");
assert_eq!(info.method, "tools/call");
assert!(info.is_call());
assert_eq!(info.tool_name, Some("test_tool".to_string()));
}
#[test]
fn test_header_info_equality() {
let a = McpHeaderInfo::for_list();
let b = McpHeaderInfo::for_list();
assert_eq!(a, b);
}
#[test]
fn test_header_info_clone() {
let info = McpHeaderInfo::for_call("tool");
let cloned = info.clone();
assert_eq!(info, cloned);
}
#[test]
fn test_header_info_debug() {
let info = McpHeaderInfo::for_list();
let debug_str = format!("{:?}", info);
assert!(debug_str.contains("tools/list"));
}
#[test]
fn test_header_error_display() {
assert_eq!(
format!("{}", McpHeaderError::MissingMethod),
"missing required header: Mcp-Method"
);
assert_eq!(
format!("{}", McpHeaderError::MissingToolName),
"missing required header: Mcp-Name for tools/call method"
);
assert_eq!(
format!("{}", McpHeaderError::InvalidMethod("bad".to_string())),
"invalid MCP method: bad"
);
assert_eq!(
format!(
"{}",
McpHeaderError::InvalidToolName("bad/name".to_string())
),
"invalid MCP tool name: bad/name"
);
}
#[test]
fn test_parse_empty_method_value() {
let headers = make_headers(Some(""), None);
let err = parse_mcp_headers(&headers).unwrap_err();
assert!(matches!(err, McpHeaderError::InvalidMethod(_)));
}
#[test]
fn test_parse_case_sensitive_method() {
let headers = make_headers(Some("TOOLS/LIST"), None);
let result = parse_mcp_headers(&headers);
assert!(result.is_err());
}
#[test]
fn test_vuln0004_is_valid_tool_name_accepts_simple_names() {
assert!(is_valid_tool_name("my_tool"));
assert!(is_valid_tool_name("get-weather"));
assert!(is_valid_tool_name("tool123"));
assert!(is_valid_tool_name("my.tool"));
assert!(is_valid_tool_name("a"));
}
#[test]
fn test_vuln0004_is_valid_tool_name_rejects_empty() {
assert!(!is_valid_tool_name(""));
}
#[test]
fn test_vuln0004_is_valid_tool_name_rejects_oversized() {
let long_name = "a".repeat(MAX_TOOL_NAME_LEN + 1);
assert!(!is_valid_tool_name(&long_name));
}
#[test]
fn test_vuln0004_is_valid_tool_name_accepts_at_max_length() {
let max_name = "a".repeat(MAX_TOOL_NAME_LEN);
assert!(is_valid_tool_name(&max_name));
}
#[test]
fn test_vuln0004_is_valid_tool_name_rejects_path_traversal() {
assert!(!is_valid_tool_name("../etc/passwd"));
assert!(!is_valid_tool_name("my/tool"));
assert!(!is_valid_tool_name("my\\tool"));
}
#[test]
fn test_vuln0004_is_valid_tool_name_rejects_special_chars() {
assert!(!is_valid_tool_name("my tool")); assert!(!is_valid_tool_name("my;tool")); assert!(!is_valid_tool_name("my'tool")); assert!(!is_valid_tool_name("my\"tool")); }
#[test]
fn test_vuln0004_parse_rejects_invalid_tool_name_path_traversal() {
let headers = make_headers(Some("tools/call"), Some("../etc/passwd"));
let err = parse_mcp_headers(&headers).unwrap_err();
assert!(matches!(err, McpHeaderError::InvalidToolName(_)));
}
#[test]
fn test_vuln0004_parse_rejects_empty_tool_name() {
let headers = make_headers(Some("tools/call"), Some(""));
let err = parse_mcp_headers(&headers).unwrap_err();
assert!(matches!(err, McpHeaderError::InvalidToolName(_)));
}
#[test]
fn test_vuln0004_parse_rejects_oversized_tool_name() {
let long_name = "a".repeat(MAX_TOOL_NAME_LEN + 1);
let headers = make_headers(Some("tools/call"), Some(&long_name));
let err = parse_mcp_headers(&headers).unwrap_err();
assert!(matches!(err, McpHeaderError::InvalidToolName(_)));
}
#[test]
fn test_vuln0004_parse_accepts_valid_tool_name() {
let headers = make_headers(Some("tools/call"), Some("my_valid_tool"));
let info = parse_mcp_headers(&headers).unwrap();
assert_eq!(info.tool_name, Some("my_valid_tool".to_string()));
}
}