use crate::domain::ir::{JsonObject, LogLevel};
use crate::domain::session::ProgressToken;
use crate::domain::{CapabilitySet, ProtocolVersion};
pub const PROTOCOL_VERSION_KEY: &str = "io.modelcontextprotocol/protocolVersion";
pub const CLIENT_CAPABILITIES_KEY: &str = "io.modelcontextprotocol/clientCapabilities";
pub const CLIENT_INFO_KEY: &str = "io.modelcontextprotocol/clientInfo";
pub const LOG_LEVEL_KEY: &str = "io.modelcontextprotocol/logLevel";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestMeta {
pub version: ProtocolVersion,
pub capabilities: CapabilitySet,
pub client_name: Option<String>,
pub log_level: Option<LogLevel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RequestMetaError {
#[error("the request carries no `_meta` block")]
Absent,
#[error("`_meta` is not a JSON object")]
Malformed,
#[error("`_meta` is missing `{key}`", key = PROTOCOL_VERSION_KEY)]
MissingVersion,
#[error("`{PROTOCOL_VERSION_KEY}` is not a protocol version this build implements")]
MalformedVersion,
#[error("`_meta` is missing `{key}`", key = CLIENT_CAPABILITIES_KEY)]
MissingCapabilities,
#[error("`{LOG_LEVEL_KEY}` is not a logging level")]
MalformedLogLevel,
#[error("`{CLIENT_CAPABILITIES_KEY}` is not a JSON object")]
MalformedCapabilities,
}
impl RequestMeta {
pub fn read(params: &JsonObject) -> Result<Self, RequestMetaError> {
let parsed: serde_json::Value =
serde_json::from_str(params.as_str()).map_err(|_| RequestMetaError::Malformed)?;
let object = parsed.as_object().ok_or(RequestMetaError::Malformed)?;
let meta = object
.get("_meta")
.ok_or(RequestMetaError::Absent)?
.as_object()
.ok_or(RequestMetaError::Malformed)?;
let version = meta
.get(PROTOCOL_VERSION_KEY)
.ok_or(RequestMetaError::MissingVersion)?
.as_str()
.ok_or(RequestMetaError::MalformedVersion)?
.parse::<ProtocolVersion>()
.map_err(|_| RequestMetaError::MalformedVersion)?;
let capabilities = meta
.get(CLIENT_CAPABILITIES_KEY)
.ok_or(RequestMetaError::MissingCapabilities)?
.as_object()
.ok_or(RequestMetaError::MalformedCapabilities)?;
let log_level = match meta.get(LOG_LEVEL_KEY) {
None => None,
Some(level) => Some(
level
.as_str()
.and_then(|text| text.parse::<LogLevel>().ok())
.ok_or(RequestMetaError::MalformedLogLevel)?,
),
};
Ok(Self {
version,
log_level,
capabilities: client_capabilities(capabilities),
client_name: meta
.get(CLIENT_INFO_KEY)
.and_then(serde_json::Value::as_object)
.and_then(|info| info.get("name"))
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
})
}
}
#[must_use]
pub fn version_text(params: &JsonObject) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(params.as_str()).ok()?;
parsed
.get("_meta")?
.get(PROTOCOL_VERSION_KEY)?
.as_str()
.map(ToOwned::to_owned)
}
#[must_use]
pub fn progress_token(params: &JsonObject) -> Option<ProgressToken> {
let parsed: serde_json::Value = serde_json::from_str(params.as_str()).ok()?;
ProgressToken::from_json(parsed.get("_meta")?.get("progressToken")?)
}
fn client_capabilities(object: &serde_json::Map<String, serde_json::Value>) -> CapabilitySet {
crate::application::mcp::initialize::client_capabilities(object)
}
#[must_use]
pub fn declares_version(params: &serde_json::Value) -> bool {
params
.get("_meta")
.and_then(serde_json::Value::as_object)
.is_some_and(|meta| meta.contains_key(PROTOCOL_VERSION_KEY))
}
#[cfg(test)]
mod tests {
use crate::application::mcp::request_meta::{RequestMeta, RequestMetaError};
use crate::domain::ir::JsonObject;
use crate::domain::{Capability, CapabilitySet, ProtocolVersion};
#[derive(Debug)]
enum Failure {
Message(String),
Fixture(crate::domain::ir::JsonObjectError),
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Message(text) => f.write_str(text),
Self::Fixture(error) => write!(f, "bad fixture: {error}"),
}
}
}
impl std::error::Error for Failure {}
impl From<String> for Failure {
fn from(text: String) -> Self {
Self::Message(text)
}
}
impl From<crate::domain::ir::JsonObjectError> for Failure {
fn from(error: crate::domain::ir::JsonObjectError) -> Self {
Self::Fixture(error)
}
}
impl From<RequestMetaError> for Failure {
fn from(error: RequestMetaError) -> Self {
Self::Message(error.to_string())
}
}
type TestError = Failure;
#[test]
fn both_required_keys_are_read() -> Result<(), TestError> {
let params = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"sampling":{},"roots":{}}}}"#,
)?;
let meta = RequestMeta::read(¶ms)?;
assert_eq!(meta.version, ProtocolVersion::V2026_07_28);
assert!(meta.capabilities.contains(Capability::Sampling));
assert!(meta.capabilities.contains(Capability::Roots));
assert_eq!(meta.client_name, None);
Ok(())
}
#[test]
fn an_unknown_capability_is_ignored() -> Result<(), TestError> {
let params = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"somethingNew":{}}}}"#,
)?;
let meta = RequestMeta::read(¶ms)?;
assert_eq!(meta.capabilities, CapabilitySet::empty());
Ok(())
}
#[test]
fn each_missing_required_key_is_distinguished() -> Result<(), TestError> {
let no_meta = JsonObject::new("{}")?;
assert_eq!(
RequestMeta::read(&no_meta).err(),
Some(RequestMetaError::Absent),
"a request with no _meta is an older revision, not a fault",
);
let no_version =
JsonObject::new(r#"{"_meta":{"io.modelcontextprotocol/clientCapabilities":{}}}"#)?;
assert_eq!(
RequestMeta::read(&no_version).err(),
Some(RequestMetaError::MissingVersion),
);
let no_capabilities = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}"#,
)?;
assert_eq!(
RequestMeta::read(&no_capabilities).err(),
Some(RequestMetaError::MissingCapabilities),
);
let bad_version = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"not-a-version","io.modelcontextprotocol/clientCapabilities":{}}}"#,
)?;
assert_eq!(
RequestMeta::read(&bad_version).err(),
Some(RequestMetaError::MalformedVersion),
"a named-but-unknown version is an unsupported version, not a missing key",
);
let bad_capabilities = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":"yes"}}"#,
)?;
assert_eq!(
RequestMeta::read(&bad_capabilities).err(),
Some(RequestMetaError::MalformedCapabilities),
);
Ok(())
}
#[test]
fn client_info_is_read_when_present_and_ignored_when_not() -> Result<(), TestError> {
let named = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"probe","version":"1"}}}"#,
)?;
assert_eq!(
RequestMeta::read(&named)?.client_name.as_deref(),
Some("probe"),
);
let malformed = JsonObject::new(
r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":"probe"}}"#,
)?;
assert_eq!(RequestMeta::read(&malformed)?.client_name, None);
Ok(())
}
}