feat: add SEP-2575 meta helpers (#942)

Expose typed accessors for the per-request protocol version, client info,
client capabilities, and log level entries carried in _meta without making
those fields required for older peers.

Fixes #869
This commit is contained in:
John Howard 2026-07-01 11:40:30 -07:00 committed by GitHub
parent 80a74795e9
commit 288f99653e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 164 additions and 20 deletions

View file

@ -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 {
@ -199,8 +200,14 @@ variant_extension! {
#[serde(transparent)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct Meta(pub JsonObject);
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
impl Meta {
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
pub fn new() -> Self {
Self(JsonObject::new())
}
@ -218,8 +225,12 @@ impl Meta {
}
pub fn get_progress_token(&self) -> Option<ProgressToken> {
self.0.get(PROGRESS_TOKEN_FIELD).and_then(|v| match v {
Value::String(s) => Some(ProgressToken(NumberOrString::String(s.to_string().into()))),
self.0
.get(Self::PROGRESS_TOKEN_FIELD)
.and_then(|v| match v {
Value::String(s) => {
Some(ProgressToken(NumberOrString::String(s.to_string().into())))
}
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Some(ProgressToken(NumberOrString::Number(i)))
@ -240,20 +251,80 @@ impl Meta {
pub fn set_progress_token(&mut self, token: ProgressToken) {
match token.0 {
NumberOrString::String(ref s) => self.0.insert(
PROGRESS_TOKEN_FIELD.to_string(),
Self::PROGRESS_TOKEN_FIELD.to_string(),
Value::String(s.to_string()),
),
NumberOrString::Number(n) => self
.0
.insert(PROGRESS_TOKEN_FIELD.to_string(), Value::Number(n.into())),
NumberOrString::Number(n) => self.0.insert(
Self::PROGRESS_TOKEN_FIELD.to_string(),
Value::Number(n.into()),
),
};
}
/// Get the MCP protocol version carried in `_meta`, if present and valid.
pub fn protocol_version(&self) -> Option<ProtocolVersion> {
self.decode_value(Self::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(
Self::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<Implementation> {
self.decode_value(Self::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(Self::META_KEY_CLIENT_INFO, client_info);
}
/// Get the client capabilities carried in `_meta`, if present and valid.
pub fn client_capabilities(&self) -> Option<ClientCapabilities> {
self.decode_value(Self::META_KEY_CLIENT_CAPABILITIES)
}
/// Set the client capabilities carried in `_meta`.
pub fn set_client_capabilities(&mut self, client_capabilities: ClientCapabilities) {
self.insert_serialized(Self::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<LoggingLevel> {
self.decode_value(Self::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(Self::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<T>(&self, key: &str) -> Option<T>
where
T: for<'de> Deserialize<'de>,
{
self.0.get(key).and_then(|value| T::deserialize(value).ok())
}
fn insert_serialized<T>(&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 {

View file

@ -0,0 +1,73 @@
#![allow(deprecated)]
use rmcp::model::{ClientCapabilities, Implementation, LoggingLevel, Meta, ProtocolVersion};
use serde_json::json;
const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
#[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);
}