use crate::types::{JsonRpcError, JsonRpcMessage, error_codes};
use serde_json::Value;
use std::borrow::Cow;
pub const MCP_METHOD: &str = "mcp-method";
pub const MCP_NAME: &str = "mcp-name";
pub const MCP_PROTOCOL_VERSION: &str = "mcp-protocol-version";
const STANDARD_HEADERS_SINCE: &str = "2026-07-28";
const SENTINEL_PREFIX: &str = "=?base64?";
const SENTINEL_SUFFIX: &str = "?=";
pub fn standard_headers(message: &JsonRpcMessage) -> Vec<(&'static str, String)> {
let Some((method, params)) = method_and_params(message) else {
return Vec::new();
};
let mut headers = vec![(MCP_METHOD, encode_value(method))];
if let Some(name) = name_for(method, params) {
headers.push((MCP_NAME, encode_value(&name)));
}
headers
}
pub fn name_for(method: &str, params: Option<&Value>) -> Option<String> {
let field = match method {
"tools/call" | "prompts/get" => "name",
"resources/read" => "uri",
_ => return None,
};
Some(params?.get(field)?.as_str()?.to_string())
}
pub fn validate<'a>(
message: &JsonRpcMessage,
header: impl Fn(&str) -> Option<&'a str>,
) -> Result<(), JsonRpcError> {
let Some((method, params)) = method_and_params(message) else {
return Ok(());
};
let required =
header(MCP_PROTOCOL_VERSION).is_some_and(|version| version >= STANDARD_HEADERS_SINCE);
check(MCP_METHOD, method, header(MCP_METHOD), required)?;
if let Some(name) = name_for(method, params) {
check(MCP_NAME, &name, header(MCP_NAME), required)?;
}
Ok(())
}
fn check(
name: &'static str,
expected: &str,
actual: Option<&str>,
required: bool,
) -> Result<(), JsonRpcError> {
let Some(actual) = actual else {
if !required {
return Ok(());
}
return Err(mismatch(
format!("Header mismatch: required header {name} is missing"),
name,
None,
expected,
));
};
let decoded = decode_value(actual);
if decoded == expected {
return Ok(());
}
Err(mismatch(
format!(
"Header mismatch: {name} header value {decoded:?} does not match body value \
{expected:?}"
),
name,
Some(&decoded),
expected,
))
}
fn mismatch(message: String, name: &'static str, header: Option<&str>, body: &str) -> JsonRpcError {
JsonRpcError {
code: error_codes::HEADER_MISMATCH,
message,
data: Some(serde_json::json!({
"mismatch": { "name": name, "header": header, "body": body }
})),
}
}
pub fn encode_value(value: &str) -> String {
if is_header_safe(value) {
return value.to_string();
}
format!(
"{SENTINEL_PREFIX}{}{SENTINEL_SUFFIX}",
base64_encode(value.as_bytes())
)
}
pub fn decode_value(value: &str) -> Cow<'_, str> {
let Some(inner) = value
.strip_prefix(SENTINEL_PREFIX)
.and_then(|rest| rest.strip_suffix(SENTINEL_SUFFIX))
else {
return Cow::Borrowed(value);
};
match base64_decode(inner).and_then(|bytes| String::from_utf8(bytes).ok()) {
Some(decoded) => Cow::Owned(decoded),
None => Cow::Borrowed(value),
}
}
fn is_header_safe(value: &str) -> bool {
let looks_encoded = value.starts_with(SENTINEL_PREFIX) && value.ends_with(SENTINEL_SUFFIX);
!looks_encoded
&& value
.bytes()
.all(|b| (0x20..=0x7e).contains(&b) || b == 0x09)
&& !value.starts_with([' ', '\t'])
&& !value.ends_with([' ', '\t'])
}
fn method_and_params(message: &JsonRpcMessage) -> Option<(&str, Option<&Value>)> {
match message {
JsonRpcMessage::Request(request) => Some((&request.method, request.params.as_ref())),
JsonRpcMessage::Notification(notification) => {
Some((¬ification.method, notification.params.as_ref()))
}
JsonRpcMessage::Response(_) => None,
}
}
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = u32::from(b[0]) << 16 | u32::from(b[1]) << 8 | u32::from(b[2]);
for i in 0..4 {
if i <= chunk.len() {
out.push(ALPHABET[(n >> (18 - i * 6)) as usize & 0x3f] as char);
} else {
out.push('=');
}
}
}
out
}
fn base64_decode(text: &str) -> Option<Vec<u8>> {
let text = text.trim_end_matches('=');
let mut out = Vec::with_capacity(text.len() * 3 / 4);
let mut buffer = 0u32;
let mut bits = 0u32;
for byte in text.bytes() {
let value = ALPHABET.iter().position(|&c| c == byte)? as u32;
buffer = buffer << 6 | value;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buffer >> bits) as u8);
}
}
Some(out)
}
#[cfg(test)]
mod test;