use crate::version::META_NS;
use crate::wire::Implementation;
use serde_json::{Value, json};
pub fn inject_client_meta(
params: &mut Value,
protocol_version: &str,
client: &Implementation,
capabilities: &Value,
) {
let Some(obj) = params.as_object_mut() else {
return;
};
let meta = obj
.entry("_meta")
.or_insert_with(|| Value::Object(Default::default()));
let Some(m) = meta.as_object_mut() else {
return;
};
m.insert(
format!("{META_NS}protocolVersion"),
Value::String(protocol_version.to_string()),
);
m.insert(
format!("{META_NS}clientInfo"),
json!({"name": client.name, "version": client.version}),
);
m.insert(format!("{META_NS}clientCapabilities"), capabilities.clone());
}
pub fn mcp_name(method: &str, params: &Value) -> Option<String> {
match method {
"tools/call" | "prompts/get" => {
params.get("name").and_then(Value::as_str).map(String::from)
}
"resources/read" => params.get("uri").and_then(Value::as_str).map(String::from),
_ => None,
}
}
pub fn routing_headers(method: &str, params: &Value) -> Vec<(&'static str, String)> {
let mut headers = vec![("Mcp-Method", method.to_string())];
if let Some(name) = mcp_name(method, params) {
headers.push(("Mcp-Name", header_value(&name)));
}
headers
}
pub fn header_value(raw: &str) -> String {
if is_header_safe(raw) {
raw.to_string()
} else {
format!("=?base64?{}?=", base64_encode(raw.as_bytes()))
}
}
fn is_header_safe(s: &str) -> bool {
!s.is_empty()
&& s.bytes().all(|b| (0x21..=0x7e).contains(&b))
&& !(s.starts_with("=?base64?") && s.ends_with("?="))
}
pub fn param_headers(input_schema: &Value, arguments: &Value) -> Vec<(String, String)> {
let mut out = Vec::new();
collect_param_headers(input_schema, arguments, &mut out);
out
}
fn collect_param_headers(schema: &Value, instance: &Value, out: &mut Vec<(String, String)>) {
let Some(props) = schema.get("properties").and_then(Value::as_object) else {
return;
};
for (key, sub) in props {
let value = instance.get(key);
if let Some(header_name) = sub.get("x-mcp-header").and_then(Value::as_str)
&& let Some(v) = value
&& let Some(s) = primitive_to_string(v)
{
out.push((format!("Mcp-Param-{header_name}"), header_value(&s)));
}
if let Some(v) = value
&& sub.get("properties").is_some()
{
collect_param_headers(sub, v, out);
}
}
}
fn primitive_to_string(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Bool(b) => Some(if *b { "true".into() } else { "false".into() }),
Value::Number(n) if n.is_i64() || n.is_u64() => Some(n.to_string()),
_ => None,
}
}
pub fn validate_x_mcp_headers(input_schema: &Value) -> Result<(), String> {
let mut seen = std::collections::HashSet::new();
validate_schema_node(input_schema, true, &mut seen)
}
fn validate_schema_node(
node: &Value,
reachable: bool,
seen: &mut std::collections::HashSet<String>,
) -> Result<(), String> {
let Some(obj) = node.as_object() else {
return Ok(());
};
if let Some(h) = obj.get("x-mcp-header") {
let name = h.as_str().ok_or("x-mcp-header must be a string")?;
if !reachable {
return Err(format!("x-mcp-header '{name}' is not statically reachable"));
}
validate_header_name(name)?;
if !seen.insert(name.to_ascii_lowercase()) {
return Err(format!("duplicate x-mcp-header '{name}'"));
}
match obj.get("type").and_then(Value::as_str) {
Some("string") | Some("integer") | Some("boolean") => {}
Some("number") => return Err(format!("x-mcp-header '{name}' on a number type")),
_ => return Err(format!("x-mcp-header '{name}' on a non-primitive type")),
}
}
if let Some(props) = obj.get("properties").and_then(Value::as_object) {
for sub in props.values() {
validate_schema_node(sub, reachable, seen)?;
}
}
for key in ["items", "additionalProperties", "not", "if", "then", "else"] {
if let Some(sub) = obj.get(key) {
validate_schema_node(sub, false, seen)?;
}
}
for key in ["oneOf", "anyOf", "allOf", "prefixItems"] {
if let Some(arr) = obj.get(key).and_then(Value::as_array) {
for sub in arr {
validate_schema_node(sub, false, seen)?;
}
}
}
Ok(())
}
fn validate_header_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("empty x-mcp-header".into());
}
let is_tchar = |c: u8| c.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&c);
if !name.bytes().all(is_tchar) {
return Err(format!("x-mcp-header '{name}' is not a valid HTTP token"));
}
Ok(())
}
fn base64_encode(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[((n >> 18) & 63) as usize] as char);
out.push(ALPHABET[((n >> 12) & 63) as usize] as char);
out.push(if chunk.len() > 1 {
ALPHABET[((n >> 6) & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
ALPHABET[(n & 63) as usize] as char
} else {
'='
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn client() -> Implementation {
Implementation {
name: "agentd".into(),
version: "1.0.0".into(),
title: None,
}
}
#[test]
fn injects_the_three_meta_fields() {
let mut params = json!({"name": "echo", "arguments": {"x": 1}});
let caps = json!({"extensions": {"io.modelcontextprotocol/tasks": {}}});
inject_client_meta(&mut params, "2026-07-28", &client(), &caps);
let meta = ¶ms["_meta"];
assert_eq!(
meta["io.modelcontextprotocol/protocolVersion"],
"2026-07-28"
);
assert_eq!(meta["io.modelcontextprotocol/clientInfo"]["name"], "agentd");
assert_eq!(
meta["io.modelcontextprotocol/clientInfo"]["version"],
"1.0.0"
);
assert_eq!(
meta["io.modelcontextprotocol/clientCapabilities"]["extensions"]["io.modelcontextprotocol/tasks"],
json!({})
);
assert_eq!(params["name"], "echo");
assert_eq!(params["arguments"]["x"], 1);
}
#[test]
fn routing_headers_carry_method_and_name() {
let p = json!({"name": "get_weather", "arguments": {}});
let h = routing_headers("tools/call", &p);
assert_eq!(h[0], ("Mcp-Method", "tools/call".to_string()));
assert_eq!(h[1], ("Mcp-Name", "get_weather".to_string()));
let p = json!({"uri": "file:///a.json"});
let h = routing_headers("resources/read", &p);
assert_eq!(h[1], ("Mcp-Name", "file:///a.json".to_string()));
let h = routing_headers("tools/list", &json!({}));
assert_eq!(h.len(), 1);
assert_eq!(h[0].0, "Mcp-Method");
}
#[test]
fn header_value_encodes_only_when_unsafe() {
assert_eq!(header_value("get_weather"), "get_weather");
assert_eq!(header_value("file:///a.json"), "file:///a.json");
assert_eq!(
header_value("Hello, 世界"),
"=?base64?SGVsbG8sIOS4lueVjA==?="
);
assert_eq!(
header_value("a b"),
format!("=?base64?{}?=", base64_encode(b"a b"))
);
assert!(header_value("=?base64?x?=").starts_with("=?base64?"));
}
#[test]
fn param_headers_extracts_annotated_values() {
let schema = json!({
"type": "object",
"properties": {
"region": {"type": "string", "x-mcp-header": "Region"},
"limit": {"type": "integer", "x-mcp-header": "Limit"},
"query": {"type": "string"}
}
});
let args = json!({"region": "us-west1", "limit": 42, "query": "SELECT 1"});
let mut h = param_headers(&schema, &args);
h.sort();
assert_eq!(
h,
vec![
("Mcp-Param-Limit".to_string(), "42".to_string()),
("Mcp-Param-Region".to_string(), "us-west1".to_string()),
]
);
let h = param_headers(&schema, &json!({"query": "x"}));
assert!(h.is_empty());
}
#[test]
fn validate_accepts_valid_and_rejects_invalid() {
assert!(
validate_x_mcp_headers(&json!({
"type": "object",
"properties": {"r": {"type": "string", "x-mcp-header": "Region"}}
}))
.is_ok()
);
assert!(
validate_x_mcp_headers(&json!({
"properties": {"n": {"type": "number", "x-mcp-header": "N"}}
}))
.is_err()
);
assert!(
validate_x_mcp_headers(&json!({
"properties": {
"a": {"type": "string", "x-mcp-header": "Dup"},
"b": {"type": "string", "x-mcp-header": "dup"}
}
}))
.is_err()
);
assert!(
validate_x_mcp_headers(&json!({
"properties": {"list": {"type": "array",
"items": {"type": "object", "properties": {
"x": {"type": "string", "x-mcp-header": "X"}}}}}
}))
.is_err()
);
assert!(
validate_x_mcp_headers(&json!({
"properties": {"a": {"type": "string", "x-mcp-header": "bad name"}}
}))
.is_err()
);
}
#[test]
fn base64_matches_known_vectors() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_encode(b"f"), "Zg==");
assert_eq!(base64_encode(b"fo"), "Zm8=");
assert_eq!(base64_encode(b"foo"), "Zm9v");
assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
}
}