use std::collections::HashSet;
use super::jsonrpc::{self, Envelope, ParseError};
pub const HEADER_PROTOCOL_VERSION: &str = "mcp-protocol-version";
pub const HEADER_METHOD: &str = "mcp-method";
pub const HEADER_NAME: &str = "mcp-name";
pub const HEADER_PARAM_PREFIX: &str = "mcp-param-";
const BASE64_SENTINEL_PREFIX: &str = "=?base64?";
const BASE64_SENTINEL_SUFFIX: &str = "?=";
pub const FIRST_VALIDATED_VERSION: &str = "2026-07-28";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow {
method: Option<String>,
target: Option<String>,
},
Deny {
reason: DenyReason,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DenyReason {
HeaderBodyMismatch {
header: &'static str,
header_value: String,
body_value: String,
},
ParamHeaderMismatch {
header: String,
header_value: String,
body_value: String,
},
UnvalidatedProtocolVersion {
claimed: Option<String>,
},
TargetNotAllowed {
method: String,
target: String,
},
MethodNotAllowed {
method: String,
},
Unparseable {
error: String,
},
}
impl std::fmt::Display for DenyReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HeaderBodyMismatch {
header,
header_value,
body_value,
} => write!(
f,
"{header} header says {header_value:?} but the request body says \
{body_value:?}; policy is resolved against the body, and a request \
that disagrees with itself is refused"
),
Self::ParamHeaderMismatch {
header,
header_value,
body_value,
} => write!(
f,
"{header} header says {header_value:?} but the matching tool argument says \
{body_value:?}; a request whose headers and body disagree routes one way \
and executes another"
),
Self::UnvalidatedProtocolVersion { claimed } => match claimed {
Some(version) => write!(
f,
"protocol version {version:?} predates {FIRST_VALIDATED_VERSION}, which is \
the first revision requiring mirrored headers to match the body; header \
values from older revisions cannot be trusted for policy"
),
None => write!(
f,
"no {HEADER_PROTOCOL_VERSION} header, so the request claims a revision \
that predates header/body validation"
),
},
Self::TargetNotAllowed { method, target } => {
write!(f, "{target:?} is not permitted for {method} on this route")
}
Self::MethodNotAllowed { method } => {
write!(f, "method {method:?} is not permitted on this route")
}
Self::Unparseable { error } => write!(
f,
"request body could not be inspected ({error}) and this route requires \
inspection to apply its policy"
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UninspectableBody {
#[default]
Deny,
Allow,
}
#[derive(Debug, Clone, Default)]
pub struct Policy {
pub allowed_methods: HashSet<String>,
pub denied_methods: HashSet<String>,
pub allowed_targets: HashSet<String>,
pub denied_targets: HashSet<String>,
pub require_validated_version: bool,
pub validate_param_headers: bool,
pub on_uninspectable: UninspectableBody,
}
pub fn decode_header_value(raw: &str) -> Option<String> {
let Some(inner) = raw
.strip_prefix(BASE64_SENTINEL_PREFIX)
.and_then(|rest| rest.strip_suffix(BASE64_SENTINEL_SUFFIX))
else {
return Some(raw.to_string());
};
let bytes = base64_decode(inner)?;
String::from_utf8(bytes).ok()
}
fn base64_decode(input: &str) -> Option<Vec<u8>> {
fn sextet(byte: u8) -> Option<u32> {
match byte {
b'A'..=b'Z' => Some(u32::from(byte - b'A')),
b'a'..=b'z' => Some(u32::from(byte - b'a') + 26),
b'0'..=b'9' => Some(u32::from(byte - b'0') + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let raw = input.as_bytes();
let unpadded = raw
.strip_suffix(b"==")
.or_else(|| raw.strip_suffix(b"="))
.unwrap_or(raw);
let padding = raw.len() - unpadded.len();
if !raw.len().is_multiple_of(4) || padding > 2 {
return None;
}
let mut out = Vec::with_capacity(raw.len() / 4 * 3);
for chunk in unpadded.chunks(4) {
let mut accumulator = 0u32;
for (index, byte) in chunk.iter().enumerate() {
accumulator |= sextet(*byte)? << (18 - 6 * index);
}
let produced = match chunk.len() {
4 => 3,
3 => 2,
2 => 1,
_ => return None,
};
for byte_index in 0..produced {
out.push(((accumulator >> (16 - 8 * byte_index)) & 0xFF) as u8);
}
}
Some(out)
}
pub fn version_is_validated(version: &str) -> bool {
let looks_dated = version.len() == 10
&& version.as_bytes()[4] == b'-'
&& version.as_bytes()[7] == b'-'
&& version
.bytes()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit());
looks_dated && version >= FIRST_VALIDATED_VERSION
}
pub fn evaluate(policy: &Policy, headers: &[(String, String)], body: &[u8]) -> Decision {
let lookup = |name: &str| {
headers
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())
};
if policy.require_validated_version {
let claimed = lookup(HEADER_PROTOCOL_VERSION);
let validated = claimed.as_deref().is_some_and(version_is_validated);
if !validated {
return Decision::Deny {
reason: DenyReason::UnvalidatedProtocolVersion { claimed },
};
}
}
let envelope = match jsonrpc::parse(body) {
Ok(envelope) => envelope,
Err(error) => {
return match policy.on_uninspectable {
UninspectableBody::Allow => Decision::Allow {
method: None,
target: None,
},
UninspectableBody::Deny => Decision::Deny {
reason: DenyReason::Unparseable {
error: error.to_string(),
},
},
};
}
};
if let Some(decision) = check_header_agreement(policy, headers, &lookup, &envelope) {
return decision;
}
if let Some(method) = envelope.method.as_deref() {
if !permitted(method, &policy.allowed_methods, &policy.denied_methods) {
return Decision::Deny {
reason: DenyReason::MethodNotAllowed {
method: method.to_string(),
},
};
}
if let Some(target) = envelope.target.as_deref() {
if !permitted(target, &policy.allowed_targets, &policy.denied_targets) {
return Decision::Deny {
reason: DenyReason::TargetNotAllowed {
method: method.to_string(),
target: target.to_string(),
},
};
}
}
}
Decision::Allow {
method: envelope.method,
target: envelope.target,
}
}
fn check_header_agreement(
policy: &Policy,
headers: &[(String, String)],
lookup: &dyn Fn(&str) -> Option<String>,
envelope: &Envelope,
) -> Option<Decision> {
let mismatch = |header: &'static str, header_value: String, body_value: &str| {
Some(Decision::Deny {
reason: DenyReason::HeaderBodyMismatch {
header,
header_value,
body_value: body_value.to_string(),
},
})
};
if let Some(raw) = lookup(HEADER_METHOD) {
match envelope.method.as_deref() {
Some(body_method) if body_method == raw => {}
Some(body_method) => return mismatch(HEADER_METHOD, raw, body_method),
None => return mismatch(HEADER_METHOD, raw, ""),
}
}
if let Some(raw) = lookup(HEADER_NAME) {
let decoded = decode_header_value(&raw).unwrap_or_else(|| raw.clone());
match envelope.target.as_deref() {
Some(body_target) if body_target == decoded => {}
Some(body_target) => return mismatch(HEADER_NAME, decoded, body_target),
None => return mismatch(HEADER_NAME, decoded, ""),
}
}
if policy.validate_param_headers {
for (name, raw) in headers {
let Some(suffix) = name.strip_prefix(HEADER_PARAM_PREFIX) else {
continue;
};
let Some((_, body_value)) = envelope
.arguments
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(suffix))
else {
continue;
};
let decoded = decode_header_value(raw).unwrap_or_else(|| raw.clone());
if &decoded != body_value {
return Some(Decision::Deny {
reason: DenyReason::ParamHeaderMismatch {
header: name.clone(),
header_value: decoded,
body_value: body_value.clone(),
},
});
}
}
}
None
}
fn permitted(value: &str, allowed: &HashSet<String>, denied: &HashSet<String>) -> bool {
if !allowed.is_empty() && !allowed.contains(value) {
return false;
}
!denied.contains(value)
}
#[cfg(test)]
mod tests {
use super::*;
fn headers_from(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_lowercase(), v.to_string()))
.collect()
}
fn permissive_policy() -> Policy {
Policy {
allowed_methods: HashSet::new(),
denied_methods: HashSet::new(),
allowed_targets: HashSet::new(),
denied_targets: HashSet::new(),
require_validated_version: true,
validate_param_headers: true,
on_uninspectable: UninspectableBody::Deny,
}
}
fn tools_call(name: &str) -> Vec<u8> {
format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"{name}"}}}}"#)
.into_bytes()
}
fn good_headers(name: &str) -> Vec<(String, String)> {
headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", name),
])
}
#[test]
fn a_consistent_request_is_allowed() {
let decision = evaluate(
&permissive_policy(),
&good_headers("get_weather"),
&tools_call("get_weather"),
);
assert_eq!(
decision,
Decision::Allow {
method: Some("tools/call".to_string()),
target: Some("get_weather".to_string()),
}
);
}
#[test]
fn a_header_naming_a_different_tool_than_the_body_is_denied() {
let mut policy = permissive_policy();
policy.allowed_targets = ["read_file".to_string()].into_iter().collect();
let decision = evaluate(
&policy,
&good_headers("read_file"),
&tools_call("delete_everything"),
);
match decision {
Decision::Deny {
reason:
DenyReason::HeaderBodyMismatch {
header,
header_value,
body_value,
},
} => {
assert_eq!(header, HEADER_NAME);
assert_eq!(header_value, "read_file");
assert_eq!(body_value, "delete_everything");
}
other => panic!("expected a header/body mismatch denial, got {other:?}"),
}
}
#[test]
fn a_method_header_disagreeing_with_the_body_is_denied() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/list"),
("mcp-name", "get_weather"),
]);
let decision = evaluate(&permissive_policy(), &headers, &tools_call("get_weather"));
assert!(
matches!(
decision,
Decision::Deny {
reason: DenyReason::HeaderBodyMismatch {
header: HEADER_METHOD,
..
}
}
),
"got {decision:?}"
);
}
#[test]
fn an_older_protocol_version_cannot_be_used_to_escape_validation() {
let headers = headers_from(&[
("mcp-protocol-version", "2025-06-18"),
("mcp-method", "tools/call"),
("mcp-name", "read_file"),
]);
let decision = evaluate(
&permissive_policy(),
&headers,
&tools_call("delete_everything"),
);
assert!(
matches!(
decision,
Decision::Deny {
reason: DenyReason::UnvalidatedProtocolVersion { .. }
}
),
"got {decision:?}"
);
}
#[test]
fn a_missing_protocol_version_header_is_denied() {
let headers = headers_from(&[("mcp-method", "tools/call")]);
let decision = evaluate(&permissive_policy(), &headers, &tools_call("t"));
assert!(matches!(
decision,
Decision::Deny {
reason: DenyReason::UnvalidatedProtocolVersion { claimed: None }
}
));
}
#[test]
fn version_validation_can_be_turned_off_deliberately() {
let mut policy = permissive_policy();
policy.require_validated_version = false;
let headers = headers_from(&[("mcp-method", "tools/call"), ("mcp-name", "t")]);
assert!(matches!(
evaluate(&policy, &headers, &tools_call("t")),
Decision::Allow { .. }
));
}
#[test]
fn a_denied_tool_is_refused_even_when_headers_agree() {
let mut policy = permissive_policy();
policy.denied_targets = ["execute_sql".to_string()].into_iter().collect();
let decision = evaluate(
&policy,
&good_headers("execute_sql"),
&tools_call("execute_sql"),
);
assert!(matches!(
decision,
Decision::Deny {
reason: DenyReason::TargetNotAllowed { .. }
}
));
}
#[test]
fn a_tool_outside_the_allowlist_is_refused() {
let mut policy = permissive_policy();
policy.allowed_targets = ["get_weather".to_string()].into_iter().collect();
let decision = evaluate(
&policy,
&good_headers("send_email"),
&tools_call("send_email"),
);
assert!(matches!(
decision,
Decision::Deny {
reason: DenyReason::TargetNotAllowed { .. }
}
));
}
#[test]
fn an_empty_allowlist_does_not_deny_everything() {
let mut policy = permissive_policy();
policy.denied_targets = ["dangerous".to_string()].into_iter().collect();
assert!(matches!(
evaluate(&policy, &good_headers("harmless"), &tools_call("harmless")),
Decision::Allow { .. }
));
}
#[test]
fn a_denied_method_is_refused() {
let mut policy = permissive_policy();
policy.denied_methods = ["tools/call".to_string()].into_iter().collect();
let decision = evaluate(&policy, &good_headers("anything"), &tools_call("anything"));
assert!(matches!(
decision,
Decision::Deny {
reason: DenyReason::MethodNotAllowed { .. }
}
));
}
#[test]
fn an_uninspectable_body_is_denied_by_default() {
let headers = headers_from(&[("mcp-protocol-version", "2026-07-28")]);
let decision = evaluate(&permissive_policy(), &headers, b"{not json");
assert!(matches!(
decision,
Decision::Deny {
reason: DenyReason::Unparseable { .. }
}
));
}
#[test]
fn an_uninspectable_body_can_be_allowed_deliberately() {
let mut policy = permissive_policy();
policy.on_uninspectable = UninspectableBody::Allow;
let headers = headers_from(&[("mcp-protocol-version", "2026-07-28")]);
assert!(matches!(
evaluate(&policy, &headers, b"{not json"),
Decision::Allow { .. }
));
}
#[test]
fn a_batch_request_is_undecidable_and_denied_by_default() {
let headers = headers_from(&[("mcp-protocol-version", "2026-07-28")]);
let body = br#"[{"jsonrpc":"2.0","id":1,"method":"tools/call"}]"#;
assert!(matches!(
evaluate(&permissive_policy(), &headers, body),
Decision::Deny {
reason: DenyReason::Unparseable { .. }
}
));
}
#[test]
fn absent_mirrored_headers_leave_the_body_to_decide() {
let mut policy = permissive_policy();
policy.denied_targets = ["blocked".to_string()].into_iter().collect();
let headers = headers_from(&[("mcp-protocol-version", "2026-07-28")]);
assert!(matches!(
evaluate(&policy, &headers, &tools_call("fine")),
Decision::Allow { .. }
));
assert!(matches!(
evaluate(&policy, &headers, &tools_call("blocked")),
Decision::Deny {
reason: DenyReason::TargetNotAllowed { .. }
}
));
}
mod param_headers {
use super::*;
fn execute_sql(region: &str) -> Vec<u8> {
format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{
"name":"execute_sql",
"arguments":{{"region":"{region}","query":"SELECT 1"}}}}}}"#
)
.into_bytes()
}
fn headers_with_region(region: &str) -> Vec<(String, String)> {
headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "execute_sql"),
("mcp-param-region", region),
])
}
#[test]
fn an_agreeing_param_header_is_allowed() {
assert!(matches!(
evaluate(
&permissive_policy(),
&headers_with_region("us-west1"),
&execute_sql("us-west1")
),
Decision::Allow { .. }
));
}
#[test]
fn a_param_header_disagreeing_with_the_argument_is_denied() {
let decision = evaluate(
&permissive_policy(),
&headers_with_region("us-west1"),
&execute_sql("eu-central-1"),
);
match decision {
Decision::Deny {
reason:
DenyReason::ParamHeaderMismatch {
header,
header_value,
body_value,
},
} => {
assert_eq!(header, "mcp-param-region");
assert_eq!(header_value, "us-west1");
assert_eq!(body_value, "eu-central-1");
}
other => panic!("expected a param mismatch, got {other:?}"),
}
}
#[test]
fn a_header_matching_no_argument_is_left_alone() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "execute_sql"),
("mcp-param-reg", "us-west1"),
]);
assert!(
matches!(
evaluate(&permissive_policy(), &headers, &execute_sql("eu-central-1")),
Decision::Allow { .. }
),
"an unmatchable label must not deny a legitimate request"
);
}
#[test]
fn the_argument_match_is_case_insensitive() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "execute_sql"),
("Mcp-Param-Region", "eu-central-1"),
]);
assert!(matches!(
evaluate(&permissive_policy(), &headers, &execute_sql("eu-central-1")),
Decision::Allow { .. }
));
}
#[test]
fn param_validation_can_be_turned_off() {
let mut policy = permissive_policy();
policy.validate_param_headers = false;
assert!(matches!(
evaluate(
&policy,
&headers_with_region("us-west1"),
&execute_sql("eu-central-1")
),
Decision::Allow { .. }
));
}
#[test]
fn a_sentinel_encoded_param_is_decoded_before_comparison() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "execute_sql"),
("mcp-param-region", "=?base64?ZXUtY2VudHJhbC0xIA==?="),
]);
assert!(matches!(
evaluate(
&permissive_policy(),
&headers,
&execute_sql("eu-central-1 ")
),
Decision::Allow { .. }
));
}
#[test]
fn integer_and_boolean_arguments_compare_correctly() {
let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"t","arguments":{"limit":42,"dry_run":true}}}"#;
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "t"),
("mcp-param-limit", "42"),
("mcp-param-dry_run", "true"),
]);
assert!(matches!(
evaluate(&permissive_policy(), &headers, body),
Decision::Allow { .. }
));
}
#[test]
fn a_float_argument_is_not_compared() {
let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"t","arguments":{"ratio":1.5}}}"#;
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "t"),
("mcp-param-ratio", "1.5"),
]);
assert!(matches!(
evaluate(&permissive_policy(), &headers, body),
Decision::Allow { .. }
));
}
}
mod base64_sentinel {
use super::*;
#[test]
fn a_plain_value_passes_through() {
assert_eq!(decode_header_value("us-west1").as_deref(), Some("us-west1"));
}
#[test]
fn a_sentinel_encoded_value_is_decoded() {
assert_eq!(
decode_header_value("=?base64?SGVsbG8sIOS4lueVjA==?=").as_deref(),
Some("Hello, 世界")
);
}
#[test]
fn padding_variants_decode() {
assert_eq!(
decode_header_value("=?base64?IHBhZGRlZCA=?=").as_deref(),
Some(" padded ")
);
assert_eq!(
decode_header_value("=?base64?bGluZTEKbGluZTI=?=").as_deref(),
Some("line1\nline2")
);
}
#[test]
fn an_encoded_header_agrees_with_the_decoded_body_value() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "=?base64?5aSp5rCX?="),
]);
let body = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"天気"}}"#;
assert!(
matches!(
evaluate(&permissive_policy(), &headers, body.as_bytes()),
Decision::Allow { .. }
),
"an encoded header must not be mistaken for a mismatch"
);
}
#[test]
fn a_malformed_sentinel_does_not_decode() {
assert_eq!(decode_header_value("=?base64?not!valid!?="), None);
}
#[test]
fn a_near_identical_encoded_value_is_still_a_mismatch() {
let headers = headers_from(&[
("mcp-protocol-version", "2026-07-28"),
("mcp-method", "tools/call"),
("mcp-name", "=?base64?5aSp5rCU?="),
]);
let body = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"天気"}}"#;
assert!(
matches!(
evaluate(&permissive_policy(), &headers, body.as_bytes()),
Decision::Deny {
reason: DenyReason::HeaderBodyMismatch { .. }
}
),
"天气 and 天気 are different tools"
);
}
}
mod version_ordering {
use super::*;
#[test]
fn the_first_validated_revision_qualifies() {
assert!(version_is_validated(FIRST_VALIDATED_VERSION));
}
#[test]
fn later_revisions_qualify() {
assert!(version_is_validated("2026-11-01"));
assert!(version_is_validated("2027-01-01"));
}
#[test]
fn earlier_revisions_do_not() {
assert!(!version_is_validated("2025-06-18"));
assert!(!version_is_validated("2025-03-26"));
assert!(!version_is_validated("2024-11-05"));
}
#[test]
fn a_malformed_version_does_not_qualify() {
assert!(!version_is_validated("latest"));
assert!(!version_is_validated("9999"));
assert!(!version_is_validated(""));
assert!(!version_is_validated("2026-07-28-extra"));
}
}
}