diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index 4c9cd618..4997428d 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use super::{ - ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject, - JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, TaskMetadata, + ClientCapabilities, ClientNotification, ClientRequest, CustomNotification, CustomRequest, + Extensions, Implementation, JsonObject, JsonRpcMessage, LoggingLevel, NumberOrString, + ProgressToken, ProtocolVersion, ServerNotification, ServerRequest, TaskMetadata, }; pub trait GetMeta { @@ -200,6 +201,16 @@ variant_extension! { #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct Meta(pub JsonObject); const PROGRESS_TOKEN_FIELD: &str = "progressToken"; + +/// `_meta` key carrying the MCP protocol version for this request. +pub const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion"; +/// `_meta` key carrying the client implementation identity for this request. +pub const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo"; +/// `_meta` key carrying the client capabilities for this request. +pub const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities"; +/// `_meta` key carrying the requested per-request log level. +pub const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel"; + impl Meta { pub fn new() -> Self { Self(JsonObject::new()) @@ -249,11 +260,72 @@ impl Meta { }; } + /// Get the MCP protocol version carried in `_meta`, if present and valid. + pub fn protocol_version(&self) -> Option { + self.decode_value(META_KEY_PROTOCOL_VERSION) + } + + /// Set the MCP protocol version carried in `_meta`. + pub fn set_protocol_version(&mut self, protocol_version: ProtocolVersion) { + self.0.insert( + META_KEY_PROTOCOL_VERSION.to_string(), + Value::String(protocol_version.to_string()), + ); + } + + /// Get the client implementation identity carried in `_meta`, if present and valid. + pub fn client_info(&self) -> Option { + self.decode_value(META_KEY_CLIENT_INFO) + } + + /// Set the client implementation identity carried in `_meta`. + pub fn set_client_info(&mut self, client_info: Implementation) { + self.insert_serialized(META_KEY_CLIENT_INFO, client_info); + } + + /// Get the client capabilities carried in `_meta`, if present and valid. + pub fn client_capabilities(&self) -> Option { + self.decode_value(META_KEY_CLIENT_CAPABILITIES) + } + + /// Set the client capabilities carried in `_meta`. + pub fn set_client_capabilities(&mut self, client_capabilities: ClientCapabilities) { + self.insert_serialized(META_KEY_CLIENT_CAPABILITIES, client_capabilities); + } + + /// Get the requested per-request log level carried in `_meta`, if present and valid. + pub fn log_level(&self) -> Option { + self.decode_value(META_KEY_LOG_LEVEL) + } + + /// Set the requested per-request log level carried in `_meta`. + pub fn set_log_level(&mut self, log_level: LoggingLevel) { + self.insert_serialized(META_KEY_LOG_LEVEL, log_level); + } + pub fn extend(&mut self, other: Meta) { for (k, v) in other.0.into_iter() { self.0.insert(k, v); } } + + fn decode_value(&self, key: &str) -> Option + where + T: for<'de> Deserialize<'de>, + { + self.0 + .get(key) + .and_then(|value| serde_json::from_value(value.clone()).ok()) + } + + fn insert_serialized(&mut self, key: &str, value: T) + where + T: Serialize, + { + let value = serde_json::to_value(value) + .expect("MCP meta helper value should serialize to valid JSON"); + self.0.insert(key.to_string(), value); + } } impl Deref for Meta { diff --git a/crates/rmcp/tests/test_meta_helpers.rs b/crates/rmcp/tests/test_meta_helpers.rs new file mode 100644 index 00000000..2d0fd87e --- /dev/null +++ b/crates/rmcp/tests/test_meta_helpers.rs @@ -0,0 +1,71 @@ +#![allow(deprecated)] + +use rmcp::model::{ + ClientCapabilities, Implementation, LoggingLevel, META_KEY_CLIENT_CAPABILITIES, + META_KEY_CLIENT_INFO, META_KEY_LOG_LEVEL, META_KEY_PROTOCOL_VERSION, Meta, ProtocolVersion, +}; +use serde_json::json; + +#[test] +fn meta_setters_store_sep_2575_values() { + let mut meta = Meta::new(); + meta.set_protocol_version(ProtocolVersion::V_2026_07_28); + meta.set_client_info(Implementation::new("test-client", "1.0.0")); + meta.set_client_capabilities(ClientCapabilities::default()); + meta.set_log_level(LoggingLevel::Warning); + + assert_eq!( + meta.get(META_KEY_PROTOCOL_VERSION), + Some(&json!("2026-07-28")) + ); + assert_eq!( + meta.get(META_KEY_CLIENT_INFO), + Some(&json!({ "name": "test-client", "version": "1.0.0" })) + ); + assert_eq!(meta.get(META_KEY_CLIENT_CAPABILITIES), Some(&json!({}))); + assert_eq!(meta.get(META_KEY_LOG_LEVEL), Some(&json!("warning"))); +} + +#[test] +fn meta_accessors_decode_wire_values() { + let meta: Meta = serde_json::from_value(json!({ + "progressToken": "progress-1", + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + "name": "wire-client", + "version": "9.0.0" + }, + "io.modelcontextprotocol/clientCapabilities": { + "sampling": {} + }, + "io.modelcontextprotocol/logLevel": "error" + })) + .unwrap(); + + assert_eq!(meta.protocol_version(), Some(ProtocolVersion::V_2026_07_28)); + assert_eq!( + meta.client_info(), + Some(Implementation::new("wire-client", "9.0.0")) + ); + assert!( + meta.client_capabilities() + .is_some_and(|capabilities| capabilities.sampling.is_some()) + ); + assert_eq!(meta.log_level(), Some(LoggingLevel::Error)); +} + +#[test] +fn meta_accessors_ignore_missing_or_malformed_values() { + let meta: Meta = serde_json::from_value(json!({ + "io.modelcontextprotocol/protocolVersion": 20260728, + "io.modelcontextprotocol/clientInfo": "not an implementation", + "io.modelcontextprotocol/clientCapabilities": "not capabilities", + "io.modelcontextprotocol/logLevel": "loud" + })) + .unwrap(); + + assert_eq!(meta.protocol_version(), None); + assert_eq!(meta.client_info(), None); + assert_eq!(meta.client_capabilities(), None); + assert_eq!(meta.log_level(), None); +}