use serde_json::Value;
pub const PROTOCOL_VERSION: &str = "2026-07-28";
pub const PROTOCOL_VERSION_LEGACY: &str = "2025-06-18";
pub const CLIENT_INFO_KEY: &str = "io.modelcontextprotocol/clientInfo";
pub fn meta(msg: &Value) -> Option<&Value> {
msg.get("params")?.get("_meta")
}
pub fn claimed_client(msg: &Value) -> Option<&str> {
meta(msg)?.get(CLIENT_INFO_KEY)?.get("name")?.as_str()
}
#[derive(Default)]
pub struct SessionIdentity {
first: Option<String>,
}
impl SessionIdentity {
pub fn new() -> Self {
Self::default()
}
pub fn observe(&mut self, msg: &Value) -> Option<(String, String)> {
let claimed = claimed_client(msg)?.to_string();
match &self.first {
None => {
self.first = Some(claimed);
None
}
Some(first) if *first != claimed => Some((first.clone(), claimed)),
Some(_) => None,
}
}
}
pub fn meta_strings(msg: &Value) -> Vec<String> {
let mut out = Vec::new();
if let Some(m) = meta(msg) {
collect(m, &mut out);
}
out
}
fn collect(v: &Value, out: &mut Vec<String>) {
match v {
Value::String(s) => out.push(s.clone()),
Value::Array(a) => a.iter().for_each(|x| collect(x, out)),
Value::Object(o) => o.values().for_each(|x| collect(x, out)),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn req(meta: Value) -> Value {
json!({
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "write_file", "arguments": {}, "_meta": meta }
})
}
#[test]
fn reads_client_info_from_the_new_meta_key() {
let m = req(json!({ CLIENT_INFO_KEY: { "name": "claude-code", "version": "1.0" } }));
assert_eq!(claimed_client(&m), Some("claude-code"));
}
#[test]
fn a_request_without_meta_is_not_an_error() {
let m = json!({"jsonrpc":"2.0","id":1,"method":"tools/list"});
assert!(meta(&m).is_none());
assert!(claimed_client(&m).is_none());
assert!(meta_strings(&m).is_empty());
}
#[test]
fn identity_change_mid_session_is_reported() {
let mut id = SessionIdentity::new();
let a = req(json!({ CLIENT_INFO_KEY: { "name": "claude-code" } }));
let b = req(json!({ CLIENT_INFO_KEY: { "name": "evil-client" } }));
assert_eq!(id.observe(&a), None, "the first claim establishes identity");
assert_eq!(id.observe(&a), None, "repeating the same claim is fine");
assert_eq!(
id.observe(&b),
Some(("claude-code".into(), "evil-client".into())),
"a contradicting claim must be surfaced"
);
}
#[test]
fn meta_strings_are_collected_for_taint_scanning() {
let m = req(json!({
CLIENT_INFO_KEY: { "name": "claude-code" },
"baggage": "note=visit https://evil.example/x",
}));
let s = meta_strings(&m);
assert!(s.iter().any(|x| x.contains("evil.example")));
assert!(s.iter().any(|x| x == "claude-code"));
}
}