use everruns_core::{
MCP_PROTOCOL_VERSION_LEGACY, MCP_PROTOCOL_VERSION_RC, MCP_PROTOCOL_VERSION_STABLE,
McpProtocolMode,
};
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
pub const CLIENT_NAME: &str = "everruns";
pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
const CLIENT_INFO_META_KEY: &str = "io.modelcontextprotocol/clientInfo";
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_SESSION_ID: &str = "Mcp-Session-Id";
pub const DEFAULT_STATEFUL_VERSION: &str = MCP_PROTOCOL_VERSION_STABLE;
pub fn client_info() -> Value {
json!({ "name": CLIENT_NAME, "version": CLIENT_VERSION })
}
pub fn request_meta() -> Value {
let mut meta = Map::new();
meta.insert(CLIENT_INFO_META_KEY.to_string(), client_info());
Value::Object(meta)
}
pub fn routable_headers(version: &str, method: &str, name: Option<&str>) -> Vec<(String, String)> {
let mut headers = vec![
(HEADER_PROTOCOL_VERSION.to_string(), version.to_string()),
(HEADER_METHOD.to_string(), method.to_string()),
];
if let Some(name) = name {
headers.push((HEADER_NAME.to_string(), name.to_string()));
}
headers
}
pub fn tools_list_body(id: i64) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"method": "tools/list",
"params": { "_meta": request_meta() }
})
}
pub fn tools_call_body(id: i64, name: &str, arguments: &Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"method": "tools/call",
"params": {
"name": name,
"arguments": arguments,
"_meta": request_meta(),
}
})
}
pub fn initialize_body(id: i64, version: &str) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"method": "initialize",
"params": {
"protocolVersion": version,
"capabilities": {},
"clientInfo": client_info(),
}
})
}
pub fn initialized_notification() -> Value {
json!({ "jsonrpc": "2.0", "method": "notifications/initialized" })
}
pub fn protocol_version_from_initialize(body: &str) -> Option<String> {
let value: Value = serde_json::from_str(body).ok()?;
value
.get("result")?
.get("protocolVersion")?
.as_str()
.map(|s| s.to_string())
}
pub fn session_id_from_headers(headers: &BTreeMap<String, String>) -> Option<String> {
headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(HEADER_SESSION_ID))
.map(|(_, value)| value.clone())
.filter(|value| !value.is_empty())
}
pub fn looks_like_handshake_required(status: u16, body: &str) -> bool {
let lower = body.to_ascii_lowercase();
if matches!(status, 400 | 404 | 405 | 409 | 426)
&& (lower.contains("session")
|| lower.contains("initialize")
|| lower.contains("mcp-session-id"))
{
return true;
}
if status == 400
&& (lower.contains("protocol version")
|| lower.contains("protocol-version")
|| lower.contains("protocolversion"))
&& (lower.contains("unsupported")
|| lower.contains("not supported")
|| lower.contains("invalid"))
{
return true;
}
lower.contains("server not initialized")
|| lower.contains("session required")
|| lower.contains("missing session")
|| lower.contains("not initialized")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Negotiated {
pub version: String,
pub stateful: bool,
pub session_id: Option<String>,
}
impl Negotiated {
pub fn stateless(version: impl Into<String>) -> Self {
Self {
version: version.into(),
stateful: false,
session_id: None,
}
}
pub fn initial_for_mode(mode: McpProtocolMode) -> Self {
match mode {
McpProtocolMode::Auto | McpProtocolMode::Rc => {
Negotiated::stateless(MCP_PROTOCOL_VERSION_RC)
}
McpProtocolMode::Stable => Self {
version: MCP_PROTOCOL_VERSION_STABLE.to_string(),
stateful: true,
session_id: None,
},
McpProtocolMode::Legacy => Self {
version: MCP_PROTOCOL_VERSION_LEGACY.to_string(),
stateful: true,
session_id: None,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_meta_carries_client_info_under_canonical_key() {
let meta = request_meta();
let info = meta.get(CLIENT_INFO_META_KEY).expect("client info present");
assert_eq!(info.get("name").and_then(|v| v.as_str()), Some(CLIENT_NAME));
assert_eq!(
info.get("version").and_then(|v| v.as_str()),
Some(CLIENT_VERSION)
);
}
#[test]
fn routable_headers_include_name_only_for_calls() {
let list = routable_headers(MCP_PROTOCOL_VERSION_RC, "tools/list", None);
assert!(list.iter().all(|(k, _)| k != HEADER_NAME));
assert!(
list.iter()
.any(|(k, v)| k == HEADER_PROTOCOL_VERSION && v == MCP_PROTOCOL_VERSION_RC)
);
let call = routable_headers(MCP_PROTOCOL_VERSION_RC, "tools/call", Some("search"));
assert!(call.iter().any(|(k, v)| k == HEADER_NAME && v == "search"));
assert!(
call.iter()
.any(|(k, v)| k == HEADER_METHOD && v == "tools/call")
);
}
#[test]
fn bodies_carry_meta() {
let list = tools_list_body(1);
assert!(list["params"]["_meta"].is_object());
let call = tools_call_body(2, "search", &json!({"q": "x"}));
assert_eq!(call["params"]["name"], "search");
assert!(call["params"]["_meta"].is_object());
assert_eq!(call["params"]["arguments"]["q"], "x");
}
#[test]
fn parses_protocol_version_and_session_id() {
let body = r#"{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{}}}"#;
assert_eq!(
protocol_version_from_initialize(body).as_deref(),
Some("2025-03-26")
);
let mut headers = BTreeMap::new();
headers.insert("mcp-session-id".to_string(), "abc123".to_string());
assert_eq!(session_id_from_headers(&headers).as_deref(), Some("abc123"));
headers.clear();
assert_eq!(session_id_from_headers(&headers), None);
}
#[test]
fn handshake_detection_requires_explicit_fallback_signals() {
assert!(looks_like_handshake_required(
400,
"Bad Request: Mcp-Session-Id header is required"
));
assert!(looks_like_handshake_required(
200,
r#"{"error":{"code":-32600,"message":"Server not initialized"}}"#
));
assert!(looks_like_handshake_required(
400,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Unsupported protocol version: 2026-07-28"}}"#
));
assert!(looks_like_handshake_required(
400,
"Invalid MCP-Protocol-Version header"
));
assert!(!looks_like_handshake_required(500, "internal server error"));
assert!(!looks_like_handshake_required(400, "invalid arguments"));
assert!(!looks_like_handshake_required(
400,
"unsupported tool argument"
));
}
#[test]
fn initial_negotiation_matches_mode() {
assert!(!Negotiated::initial_for_mode(McpProtocolMode::Auto).stateful);
assert!(!Negotiated::initial_for_mode(McpProtocolMode::Rc).stateful);
assert!(Negotiated::initial_for_mode(McpProtocolMode::Stable).stateful);
assert!(Negotiated::initial_for_mode(McpProtocolMode::Legacy).stateful);
assert_eq!(
Negotiated::initial_for_mode(McpProtocolMode::Legacy).version,
MCP_PROTOCOL_VERSION_LEGACY
);
}
}