feat!: align metadata models with draft schema (#993)

This commit is contained in:
Dale Seo 2026-07-15 08:58:46 -04:00 • committed by GitHub
commit 839922d8fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2539 additions and 1059 deletions

View file

@ -12,7 +12,7 @@
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use super::{Annotations, Meta, resource::ResourceContents};
use super::{Annotations, MetaObject, resource::ResourceContents};
// ---------------------------------------------------------------------------
// Flat content structs
@ -28,7 +28,7 @@ pub struct TextContent {
pub text: String,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -43,7 +43,7 @@ impl TextContent {
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -66,7 +66,7 @@ pub struct ImageContent {
pub mime_type: String,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -82,7 +82,7 @@ impl ImageContent {
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -105,7 +105,7 @@ pub struct AudioContent {
pub mime_type: String,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -121,7 +121,7 @@ impl AudioContent {
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -142,7 +142,7 @@ pub struct EmbeddedResource {
pub resource: ResourceContents,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -164,7 +164,7 @@ impl EmbeddedResource {
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -189,7 +189,7 @@ pub struct ToolUseContent {
pub name: String,
pub input: super::JsonObject,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
}
/// Tool execution result in user message (SEP-1577).
@ -203,7 +203,7 @@ pub struct ToolUseContent {
)]
pub struct ToolResultContent {
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
pub tool_use_id: String,
pub content: Vec<ContentBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
@ -402,7 +402,7 @@ mod tests {
#[test]
fn test_audio_content_has_meta() {
let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default());
let audio = AudioContent::new("data", "audio/wav").with_meta(MetaObject::default());
let json = serde_json::to_value(&audio).unwrap();
assert!(json.get("_meta").is_some());
}

View file

@ -5,13 +5,29 @@ use serde_json::Value;
use super::{
ClientCapabilities, ClientNotification, ClientRequest, CustomNotification, CustomRequest,
Extensions, Implementation, JsonObject, JsonRpcMessage, LoggingLevel, NumberOrString,
ProgressToken, ProtocolVersion, ServerNotification, ServerRequest, TaskMetadata,
Extensions, Implementation, JsonObject, JsonRpcMessage, LoggingLevel, ProgressToken,
ProtocolVersion, RequestId, ServerNotification, ServerRequest, TaskMetadata,
};
/// Access to the metadata carried by a message envelope's [`Extensions`].
///
/// The metadata type differs by message kind: requests carry a
/// [`RequestMetaObject`] and notifications carry a [`NotificationMetaObject`].
///
/// The envelope extensions are the canonical runtime location for `_meta`:
/// deserialization strips the wire `params._meta` into the extensions (typed
/// params `meta` fields stay empty), and the service loop moves it into
/// [`RequestContext::meta`] / [`NotificationContext::meta`] before dispatch.
/// Typed params `meta` fields are honored when serializing outgoing messages;
/// on key conflicts the extensions-level metadata wins.
///
/// [`RequestContext::meta`]: crate::service::RequestContext
/// [`NotificationContext::meta`]: crate::service::NotificationContext
pub trait GetMeta {
fn get_meta_mut(&mut self) -> &mut Meta;
fn get_meta(&self) -> &Meta;
/// The metadata type for this message kind.
type Metadata: Default;
fn get_meta_mut(&mut self) -> &mut Self::Metadata;
fn get_meta(&self) -> &Self::Metadata;
}
pub trait GetExtensions {
@ -21,15 +37,16 @@ pub trait GetExtensions {
/// Trait for request params that contain the `_meta` field.
///
/// Per the MCP 2025-11-25 spec, all request params should have an optional `_meta`
/// field that can contain a `progressToken` for tracking long-running operations.
/// Per the MCP spec, all request params may have an optional `_meta`
/// field ([`RequestMetaObject`]) that can contain a `progressToken` for
/// tracking long-running operations.
pub trait RequestParamsMeta {
/// Get a reference to the meta field
fn meta(&self) -> Option<&Meta>;
fn meta(&self) -> Option<&RequestMetaObject>;
/// Get a mutable reference to the meta field
fn meta_mut(&mut self) -> &mut Option<Meta>;
fn meta_mut(&mut self) -> &mut Option<RequestMetaObject>;
/// Set the meta field
fn set_meta(&mut self, meta: Meta) {
fn set_meta(&mut self, meta: RequestMetaObject) {
*self.meta_mut() = Some(meta);
}
/// Get the progress token from meta, if present
@ -41,7 +58,7 @@ pub trait RequestParamsMeta {
match self.meta_mut() {
Some(meta) => meta.set_progress_token(token),
none => {
let mut meta = Meta::new();
let mut meta = RequestMetaObject::new();
meta.set_progress_token(token);
*none = Some(meta);
}
@ -72,8 +89,8 @@ pub trait RequestParamsMeta {
self.meta_or_default().set_baggage(value);
}
/// Get a mutable reference to meta, inserting an empty one if absent.
fn meta_or_default(&mut self) -> &mut Meta {
self.meta_mut().get_or_insert_with(Meta::new)
fn meta_or_default(&mut self) -> &mut RequestMetaObject {
self.meta_mut().get_or_insert_with(RequestMetaObject::new)
}
}
@ -102,13 +119,14 @@ impl GetExtensions for CustomNotification {
}
impl GetMeta for CustomNotification {
fn get_meta_mut(&mut self) -> &mut Meta {
type Metadata = NotificationMetaObject;
fn get_meta_mut(&mut self) -> &mut NotificationMetaObject {
self.extensions_mut().get_or_insert_default()
}
fn get_meta(&self) -> &Meta {
fn get_meta(&self) -> &NotificationMetaObject {
self.extensions()
.get::<Meta>()
.unwrap_or(Meta::static_empty())
.get::<NotificationMetaObject>()
.unwrap_or(NotificationMetaObject::static_empty())
}
}
@ -122,19 +140,20 @@ impl GetExtensions for CustomRequest {
}
impl GetMeta for CustomRequest {
fn get_meta_mut(&mut self) -> &mut Meta {
type Metadata = RequestMetaObject;
fn get_meta_mut(&mut self) -> &mut RequestMetaObject {
self.extensions_mut().get_or_insert_default()
}
fn get_meta(&self) -> &Meta {
fn get_meta(&self) -> &RequestMetaObject {
self.extensions()
.get::<Meta>()
.unwrap_or(Meta::static_empty())
.get::<RequestMetaObject>()
.unwrap_or(RequestMetaObject::static_empty())
}
}
macro_rules! variant_extension {
(
$Enum: ident {
$Enum: ident: $Metadata: ident {
$($variant: ident)*
}
) => {
@ -155,18 +174,19 @@ macro_rules! variant_extension {
}
}
impl GetMeta for $Enum {
fn get_meta_mut(&mut self) -> &mut Meta {
type Metadata = $Metadata;
fn get_meta_mut(&mut self) -> &mut $Metadata {
self.extensions_mut().get_or_insert_default()
}
fn get_meta(&self) -> &Meta {
self.extensions().get::<Meta>().unwrap_or(Meta::static_empty())
fn get_meta(&self) -> &$Metadata {
self.extensions().get::<$Metadata>().unwrap_or($Metadata::static_empty())
}
}
};
}
variant_extension! {
ClientRequest {
ClientRequest: RequestMetaObject {
PingRequest
InitializeRequest
CompleteRequest
@ -189,7 +209,7 @@ variant_extension! {
}
variant_extension! {
ServerRequest {
ServerRequest: RequestMetaObject {
PingRequest
CreateMessageRequest
ListRootsRequest
@ -199,7 +219,7 @@ variant_extension! {
}
variant_extension! {
ClientNotification {
ClientNotification: NotificationMetaObject {
CancelledNotification
ProgressNotification
InitializedNotification
@ -210,7 +230,7 @@ variant_extension! {
}
variant_extension! {
ServerNotification {
ServerNotification: NotificationMetaObject {
CancelledNotification
ProgressNotification
LoggingMessageNotification
@ -222,18 +242,31 @@ variant_extension! {
CustomNotification
}
}
/// General-purpose `_meta` map (spec `MetaObject`).
///
/// This is the metadata shape used by results, content blocks, and catalog
/// descriptors (tools, prompts, resources, roots, ...). It preserves arbitrary
/// extension keys and offers helpers for the reserved W3C Trace Context keys
/// (SEP-414).
///
/// Request and notification `_meta` maps have additional reserved keys; see
/// [`RequestMetaObject`] and [`NotificationMetaObject`].
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct Meta(pub JsonObject);
pub struct MetaObject(pub JsonObject);
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";
/// Deprecated alias for [`MetaObject`].
///
/// This is a re-export rather than a type alias so the `Meta(...)` tuple
/// constructor keeps working. Request and notification metadata now have
/// dedicated types; use [`RequestMetaObject`] or [`NotificationMetaObject`]
/// where those are expected.
#[deprecated(note = "Use MetaObject (or RequestMetaObject / NotificationMetaObject)")]
pub use self::MetaObject as Meta;
impl MetaObject {
/// Reserved `_meta` key for the W3C Trace Context `traceparent` value (SEP-414).
const TRACEPARENT_FIELD: &str = "traceparent";
/// Reserved `_meta` key for the W3C Trace Context `tracestate` value (SEP-414).
@ -241,102 +274,11 @@ impl Meta {
/// Reserved `_meta` key for the W3C Baggage value (SEP-414).
const BAGGAGE_FIELD: &str = "baggage";
/// Create an empty metadata map.
pub fn new() -> Self {
Self(JsonObject::new())
}
/// Create a new Meta with a progress token set
pub fn with_progress_token(token: ProgressToken) -> Self {
let mut meta = Self::new();
meta.set_progress_token(token);
meta
}
pub(crate) fn static_empty() -> &'static Self {
static EMPTY: std::sync::OnceLock<Meta> = std::sync::OnceLock::new();
EMPTY.get_or_init(Default::default)
}
pub fn get_progress_token(&self) -> Option<ProgressToken> {
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)))
} else if let Some(u) = n.as_u64() {
if u <= i64::MAX as u64 {
Some(ProgressToken(NumberOrString::Number(u as i64)))
} else {
None
}
} else {
None
}
}
_ => None,
})
}
pub fn set_progress_token(&mut self, token: ProgressToken) {
match token.0 {
NumberOrString::String(ref s) => self.0.insert(
Self::PROGRESS_TOKEN_FIELD.to_string(),
Value::String(s.to_string()),
),
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);
}
/// Read a string-valued `_meta` field, or `None` if absent or not a string.
fn get_str(&self, field: &str) -> Option<&str> {
self.0.get(field).and_then(Value::as_str)
@ -356,9 +298,9 @@ impl Meta {
/// Set the W3C `traceparent` value (SEP-414).
///
/// ```
/// use rmcp::model::Meta;
/// use rmcp::model::MetaObject;
///
/// let mut meta = Meta::new();
/// let mut meta = MetaObject::new();
/// meta.set_traceparent("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01");
/// assert_eq!(
/// meta.get_traceparent(),
@ -389,10 +331,9 @@ impl Meta {
self.set_str(Self::BAGGAGE_FIELD, value);
}
pub fn extend(&mut self, other: Meta) {
for (k, v) in other.0.into_iter() {
self.0.insert(k, v);
}
/// Insert every entry of `other`, overwriting existing keys on conflict.
pub fn extend(&mut self, other: MetaObject) {
self.0.extend(other.0);
}
fn decode_value<T>(&self, key: &str) -> Option<T>
@ -412,7 +353,7 @@ impl Meta {
}
}
impl Deref for Meta {
impl Deref for MetaObject {
type Target = JsonObject;
fn deref(&self) -> &Self::Target {
@ -420,12 +361,353 @@ impl Deref for Meta {
}
}
impl DerefMut for Meta {
impl DerefMut for MetaObject {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<JsonObject> for MetaObject {
fn from(object: JsonObject) -> Self {
Self(object)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for MetaObject {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("MetaObject")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.",
"type": "object",
"additionalProperties": true,
})
}
}
/// The `_meta` map carried by requests (spec `RequestMetaObject`).
///
/// In addition to arbitrary extension keys, requests reserve:
/// - `progressToken` for progress tracking
/// - `io.modelcontextprotocol/protocolVersion` (SEP-2575)
/// - `io.modelcontextprotocol/clientInfo` (SEP-2575)
/// - `io.modelcontextprotocol/clientCapabilities` (SEP-2575)
/// - `io.modelcontextprotocol/logLevel` (SEP-2575)
///
/// The 2026-07-28 draft schema marks the protocol-version, client-info, and
/// client-capabilities keys as required; earlier protocol versions do not know
/// them. All keys therefore stay optional at runtime and in the generated
/// (version-shared) JSON schema — use
/// [`RequestMetaObject::missing_required_keys`] to validate a request against
/// the negotiated protocol version.
///
/// This type dereferences to [`MetaObject`] (and transitively to the underlying
/// map), so general helpers such as the SEP-414 trace-context accessors remain
/// available.
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(transparent)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RequestMetaObject(pub MetaObject);
impl RequestMetaObject {
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";
/// Request `_meta` keys the 2026-07-28 draft schema marks as required.
pub const DRAFT_REQUIRED_KEYS: [&str; 3] = [
Self::META_KEY_PROTOCOL_VERSION,
Self::META_KEY_CLIENT_INFO,
Self::META_KEY_CLIENT_CAPABILITIES,
];
/// Create an empty request metadata map.
pub fn new() -> Self {
Self::default()
}
/// Create a new request meta with a progress token set
pub fn with_progress_token(token: ProgressToken) -> Self {
let mut meta = Self::new();
meta.set_progress_token(token);
meta
}
pub(crate) fn static_empty() -> &'static Self {
static EMPTY: std::sync::OnceLock<RequestMetaObject> = std::sync::OnceLock::new();
EMPTY.get_or_init(Default::default)
}
/// Get the progress token carried in `_meta`, if present and valid.
pub fn get_progress_token(&self) -> Option<ProgressToken> {
self.0.decode_value(Self::PROGRESS_TOKEN_FIELD)
}
/// Set the progress token carried in `_meta`.
pub fn set_progress_token(&mut self, token: ProgressToken) {
self.0.insert_serialized(Self::PROGRESS_TOKEN_FIELD, token);
}
/// Get the MCP protocol version carried in `_meta`, if present and valid.
pub fn protocol_version(&self) -> Option<ProtocolVersion> {
self.0.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.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.0.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.0
.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.0.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.0
.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.0.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.0
.insert_serialized(Self::META_KEY_LOG_LEVEL, log_level);
}
/// Return the [`Self::DRAFT_REQUIRED_KEYS`] whose values are absent or
/// invalid in this map, if `protocol_version` requires them.
///
/// A key counts as missing when it is not present *or* when its value does
/// not decode into the expected type (e.g. a numeric `protocolVersion` or
/// a string `clientInfo`), matching what the typed accessors return.
///
/// Protocol versions before 2026-07-28 have no required request metadata,
/// so this always returns an empty list for them.
///
/// # Examples
///
/// ```
/// use rmcp::model::{ProtocolVersion, RequestMetaObject};
///
/// let meta = RequestMetaObject::new();
/// // Older protocols have no required request metadata.
/// assert!(
/// meta.missing_required_keys(&ProtocolVersion::V_2025_11_25)
/// .is_empty()
/// );
/// // The 2026-07-28 draft requires the SEP-2575 keys.
/// assert_eq!(
/// meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
/// RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec(),
/// );
/// ```
pub fn missing_required_keys(&self, protocol_version: &ProtocolVersion) -> Vec<&'static str> {
if protocol_version.as_str() < ProtocolVersion::V_2026_07_28.as_str() {
return Vec::new();
}
let mut missing = Vec::new();
if self.protocol_version().is_none() {
missing.push(Self::META_KEY_PROTOCOL_VERSION);
}
if self.client_info().is_none() {
missing.push(Self::META_KEY_CLIENT_INFO);
}
if self.client_capabilities().is_none() {
missing.push(Self::META_KEY_CLIENT_CAPABILITIES);
}
missing
}
/// Insert every entry of `other`, overwriting existing keys on conflict.
pub fn extend(&mut self, other: RequestMetaObject) {
self.0.extend(other.0);
}
}
impl Deref for RequestMetaObject {
type Target = MetaObject;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for RequestMetaObject {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<MetaObject> for RequestMetaObject {
fn from(meta: MetaObject) -> Self {
Self(meta)
}
}
impl From<JsonObject> for RequestMetaObject {
fn from(object: JsonObject) -> Self {
Self(MetaObject(object))
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for RequestMetaObject {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("RequestMetaObject")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let progress_token = generator.subschema_for::<ProgressToken>();
let client_info = generator.subschema_for::<Implementation>();
let client_capabilities = generator.subschema_for::<ClientCapabilities>();
let log_level = generator.subschema_for::<LoggingLevel>();
// rmcp generates one schema shared by every supported protocol
// version, so the keys the 2026-07-28 draft marks as required are left
// optional here: a 2025-11-25 request whose `_meta` only carries
// `progressToken` is valid. Draft-strict validation is available at
// runtime via [`RequestMetaObject::missing_required_keys`].
schemars::json_schema!({
"description": "Metadata reserved by MCP on requests. Extension keys are also allowed.",
"type": "object",
"properties": {
"progressToken": progress_token,
"io.modelcontextprotocol/protocolVersion": {
"type": "string",
},
"io.modelcontextprotocol/clientInfo": client_info,
"io.modelcontextprotocol/clientCapabilities": client_capabilities,
"io.modelcontextprotocol/logLevel": log_level,
},
"additionalProperties": true,
})
}
}
/// The `_meta` map carried by notifications (spec `NotificationMetaObject`).
///
/// In addition to arbitrary extension keys, notifications reserve
/// `io.modelcontextprotocol/subscriptionId` to correlate a notification with a
/// prior subscription request.
///
/// This type dereferences to [`MetaObject`] (and transitively to the underlying
/// map), so general helpers such as the SEP-414 trace-context accessors remain
/// available.
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(transparent)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct NotificationMetaObject(pub MetaObject);
impl NotificationMetaObject {
const META_KEY_SUBSCRIPTION_ID: &str = "io.modelcontextprotocol/subscriptionId";
/// Create an empty notification metadata map.
pub fn new() -> Self {
Self::default()
}
pub(crate) fn static_empty() -> &'static Self {
static EMPTY: std::sync::OnceLock<NotificationMetaObject> = std::sync::OnceLock::new();
EMPTY.get_or_init(Default::default)
}
/// Get the subscription id carried in `_meta`, if present and valid.
///
/// # Examples
///
/// ```
/// use rmcp::model::{NotificationMetaObject, RequestId};
///
/// let mut meta = NotificationMetaObject::new();
/// assert_eq!(meta.subscription_id(), None);
/// meta.set_subscription_id(RequestId::Number(7));
/// assert_eq!(meta.subscription_id(), Some(RequestId::Number(7)));
/// ```
pub fn subscription_id(&self) -> Option<RequestId> {
self.0.decode_value(Self::META_KEY_SUBSCRIPTION_ID)
}
/// Set the subscription id carried in `_meta`.
pub fn set_subscription_id(&mut self, subscription_id: RequestId) {
self.0
.insert_serialized(Self::META_KEY_SUBSCRIPTION_ID, subscription_id);
}
/// Insert every entry of `other`, overwriting existing keys on conflict.
pub fn extend(&mut self, other: NotificationMetaObject) {
self.0.extend(other.0);
}
}
impl Deref for NotificationMetaObject {
type Target = MetaObject;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for NotificationMetaObject {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<MetaObject> for NotificationMetaObject {
fn from(meta: MetaObject) -> Self {
Self(meta)
}
}
impl From<JsonObject> for NotificationMetaObject {
fn from(object: JsonObject) -> Self {
Self(MetaObject(object))
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for NotificationMetaObject {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("NotificationMetaObject")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let subscription_id = generator.subschema_for::<RequestId>();
schemars::json_schema!({
"description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.",
"type": "object",
"properties": {
"io.modelcontextprotocol/subscriptionId": subscription_id,
},
"additionalProperties": true,
})
}
}
impl<Req, Resp, Noti> JsonRpcMessage<Req, Resp, Noti>
where
Req: GetExtensions,
@ -450,17 +732,18 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::model::NumberOrString;
#[derive(Default)]
struct Params {
meta: Option<Meta>,
meta: Option<RequestMetaObject>,
}
impl RequestParamsMeta for Params {
fn meta(&self) -> Option<&Meta> {
fn meta(&self) -> Option<&RequestMetaObject> {
self.meta.as_ref()
}
fn meta_mut(&mut self) -> &mut Option<Meta> {
fn meta_mut(&mut self) -> &mut Option<RequestMetaObject> {
&mut self.meta
}
}
@ -469,7 +752,7 @@ mod tests {
#[test]
fn trace_context_round_trip() {
let mut meta = Meta::new();
let mut meta = MetaObject::new();
meta.set_traceparent(TRACEPARENT);
meta.set_tracestate("vendor1=value1,vendor2=value2");
meta.set_baggage("userId=alice,region=us-east-1");
@ -480,7 +763,7 @@ mod tests {
#[test]
fn absent_field_is_none() {
let meta = Meta::new();
let meta = MetaObject::new();
assert_eq!(meta.get_traceparent(), None);
assert_eq!(meta.get_tracestate(), None);
assert_eq!(meta.get_baggage(), None);
@ -488,9 +771,9 @@ mod tests {
#[test]
fn non_string_value_is_none() {
let mut meta = Meta::new();
let mut meta = MetaObject::new();
meta.0
.insert(Meta::TRACEPARENT_FIELD.to_string(), Value::from(42));
.insert(MetaObject::TRACEPARENT_FIELD.to_string(), Value::from(42));
assert_eq!(meta.get_traceparent(), None);
}
@ -501,4 +784,91 @@ mod tests {
params.set_traceparent(TRACEPARENT);
assert_eq!(params.traceparent(), Some(TRACEPARENT));
}
#[test]
fn request_meta_derefs_to_general_helpers() {
let mut meta = RequestMetaObject::new();
meta.set_traceparent(TRACEPARENT);
meta.set_progress_token(ProgressToken(NumberOrString::Number(7)));
assert_eq!(meta.get_traceparent(), Some(TRACEPARENT));
assert_eq!(
meta.get_progress_token(),
Some(ProgressToken(NumberOrString::Number(7)))
);
}
mod subscription_id {
use super::*;
#[test]
fn returns_none_when_absent() {
let meta = NotificationMetaObject::new();
assert_eq!(meta.subscription_id(), None);
}
#[test]
fn round_trips_number_id() {
let mut meta = NotificationMetaObject::new();
meta.set_subscription_id(RequestId::Number(42));
assert_eq!(meta.subscription_id(), Some(RequestId::Number(42)));
}
#[test]
fn round_trips_string_id() {
let mut meta = NotificationMetaObject::new();
meta.set_subscription_id(RequestId::String("sub-1".into()));
assert_eq!(
meta.subscription_id(),
Some(RequestId::String("sub-1".into()))
);
}
}
mod missing_required_keys {
use super::*;
#[test]
fn is_empty_for_pre_draft_protocols() {
let meta = RequestMetaObject::new();
assert!(
meta.missing_required_keys(&ProtocolVersion::V_2025_11_25)
.is_empty()
);
}
#[test]
fn lists_all_draft_keys_for_empty_meta() {
let meta = RequestMetaObject::new();
assert_eq!(
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec()
);
}
#[test]
fn treats_malformed_values_as_missing() {
let meta: RequestMetaObject = serde_json::from_value(serde_json::json!({
"io.modelcontextprotocol/protocolVersion": 123,
"io.modelcontextprotocol/clientInfo": "not an implementation",
"io.modelcontextprotocol/clientCapabilities": null,
}))
.unwrap();
assert_eq!(
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec()
);
}
#[test]
fn is_empty_when_draft_keys_are_present() {
let mut meta = RequestMetaObject::new();
meta.set_protocol_version(ProtocolVersion::V_2026_07_28);
meta.set_client_info(Implementation::from_build_env());
meta.set_client_capabilities(ClientCapabilities::default());
assert!(
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28)
.is_empty()
);
}
}
}

View file

@ -42,8 +42,8 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{
CallToolResult, CreateMessageRequest, ElicitRequest, GetPromptResult, ListRootsRequest, Meta,
ReadResourceResult, ResultType, ServerResult,
CallToolResult, CreateMessageRequest, ElicitRequest, GetPromptResult, ListRootsRequest,
MetaObject, ReadResourceResult, ResultType, ServerResult,
};
/// Default maximum number of MRTR rounds a high-level client call will drive.
@ -220,7 +220,7 @@ pub struct InputRequiredResult {
/// Optional protocol-level metadata.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
}
/// Custom deserializer that requires `resultType: "input_required"` to prevent
@ -238,7 +238,7 @@ impl<'de> Deserialize<'de> for InputRequiredResult {
input_requests: Option<InputRequests>,
request_state: Option<String>,
#[serde(rename = "_meta")]
meta: Option<Meta>,
meta: Option<MetaObject>,
}
let helper = Helper::deserialize(deserializer)?;
@ -283,7 +283,7 @@ impl InputRequiredResult {
}
/// Sets optional metadata.
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use super::{
Annotations, ContentBlock, Icon, Meta, Role,
Annotations, ContentBlock, Icon, MetaObject, Role,
content::{AudioContent, EmbeddedResource, ImageContent, TextContent},
resource::ResourceContents,
};
@ -22,7 +22,7 @@ pub struct Prompt {
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
}
impl Prompt {
@ -70,7 +70,7 @@ impl Prompt {
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -144,7 +144,7 @@ impl PromptMessage {
role: Role,
data: &[u8],
mime_type: &str,
meta: Option<Meta>,
meta: Option<MetaObject>,
annotations: Option<Annotations>,
) -> Self {
use base64::{Engine, prelude::BASE64_STANDARD};
@ -166,7 +166,7 @@ impl PromptMessage {
role: Role,
data: &[u8],
mime_type: &str,
meta: Option<Meta>,
meta: Option<MetaObject>,
annotations: Option<Annotations>,
) -> Self {
use base64::{Engine, prelude::BASE64_STANDARD};
@ -188,8 +188,8 @@ impl PromptMessage {
uri: String,
mime_type: Option<String>,
text: Option<String>,
resource_meta: Option<Meta>,
resource_content_meta: Option<Meta>,
resource_meta: Option<MetaObject>,
resource_content_meta: Option<MetaObject>,
annotations: Option<Annotations>,
) -> Self {
let resource_contents = match text {
@ -216,7 +216,11 @@ impl PromptMessage {
}
}
pub fn new_text_with_meta<S: Into<String>>(role: Role, text: S, meta: Option<Meta>) -> Self {
pub fn new_text_with_meta<S: Into<String>>(
role: Role,
text: S,
meta: Option<MetaObject>,
) -> Self {
Self {
role,
content: ContentBlock::Text(TextContent {

View file

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use super::{Annotations, Icon, Meta};
use super::{Annotations, Icon, MetaObject};
/// A known resource that the server is capable of reading (spec `Resource`).
///
@ -31,7 +31,7 @@ pub struct Resource {
pub icons: Option<Vec<Icon>>,
/// Optional protocol-level metadata for this resource.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this resource.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -77,7 +77,7 @@ impl Resource {
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -112,7 +112,7 @@ pub struct ResourceTemplate {
pub icons: Option<Vec<Icon>>,
/// Optional protocol-level metadata for this resource template.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
/// Optional annotations describing how the client should use this template.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
@ -152,7 +152,7 @@ impl ResourceTemplate {
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -176,7 +176,7 @@ pub enum ResourceContents {
mime_type: Option<String>,
text: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
meta: Option<MetaObject>,
},
#[serde(rename_all = "camelCase")]
BlobResourceContents {
@ -185,7 +185,7 @@ pub enum ResourceContents {
mime_type: Option<String>,
blob: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
meta: Option<MetaObject>,
},
}
@ -216,7 +216,7 @@ impl ResourceContents {
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
match &mut self {
Self::TextResourceContents { meta: m, .. } => *m = Some(meta),
Self::BlobResourceContents { meta: m, .. } => *m = Some(meta),
@ -303,7 +303,7 @@ mod tests {
#[test]
fn test_resource_template_with_meta() {
let resource_template =
ResourceTemplate::new("file:///{path}", "template").with_meta(Meta::default());
ResourceTemplate::new("file:///{path}", "template").with_meta(MetaObject::default());
let json = serde_json::to_value(&resource_template).unwrap();
assert!(json.get("_meta").is_some());
}

View file

@ -3,12 +3,19 @@ use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use super::{
CustomNotification, CustomRequest, Extensions, Meta, Notification, NotificationNoParam,
Request, RequestNoParam, RequestOptionalParam,
CustomNotification, CustomRequest, Extensions, JsonObject, MetaObject, Notification,
NotificationMetaObject, NotificationNoParam, Request, RequestMetaObject, RequestNoParam,
RequestOptionalParam,
};
/// Wire-side view of `params`: the `_meta` map plus the remaining fields.
///
/// All metadata types are transparent wrappers over [`JsonObject`], so the
/// serde plumbing works on the raw map; call sites wrap/unwrap the typed
/// metadata ([`RequestMetaObject`] / [`NotificationMetaObject`]).
#[derive(Deserialize)]
struct WithMeta<'a, P> {
_meta: Option<Cow<'a, Meta>>,
_meta: Option<Cow<'a, JsonObject>>,
#[serde(flatten)]
_rest: P,
}
@ -25,7 +32,7 @@ impl<P: Serialize> Serialize for WithMeta<'_, P> {
serde_json::to_value(&self._rest).map_err(serde::ser::Error::custom)?;
// Extract _meta from the serialized params (if it's an object containing one)
let params_meta: Option<Meta> = rest_value
let params_meta: Option<JsonObject> = rest_value
.as_object_mut()
.and_then(|obj| obj.remove("_meta"))
.and_then(|v| serde_json::from_value(v).ok());
@ -78,6 +85,53 @@ struct ProxyNoParam<M> {
method: M,
}
/// Combine the message-specific `_meta` map with a legacy [`MetaObject`]
/// extension (inserted through the deprecated `Meta` name), so pre-3.x code
/// does not silently lose metadata on the wire. On key conflicts the
/// message-specific map wins.
fn merge_legacy_meta<'a>(
typed: Option<&'a JsonObject>,
extensions: &'a Extensions,
) -> Option<Cow<'a, JsonObject>> {
let legacy = extensions.get::<MetaObject>().map(|meta| &meta.0);
match (typed, legacy) {
(Some(typed), None) => Some(Cow::Borrowed(typed)),
(None, Some(legacy)) => Some(Cow::Borrowed(legacy)),
(Some(typed), Some(legacy)) => {
let mut merged = legacy.clone();
merged.extend(typed.clone());
Some(Cow::Owned(merged))
}
(None, None) => None,
}
}
/// Borrow the request `_meta` map from extensions, if any.
fn request_meta(extensions: &Extensions) -> Option<Cow<'_, JsonObject>> {
let typed = extensions.get::<RequestMetaObject>().map(|meta| &meta.0.0);
merge_legacy_meta(typed, extensions)
}
/// Borrow the notification `_meta` map from extensions, if any.
fn notification_meta(extensions: &Extensions) -> Option<Cow<'_, JsonObject>> {
let typed = extensions
.get::<NotificationMetaObject>()
.map(|meta| &meta.0.0);
merge_legacy_meta(typed, extensions)
}
/// Build extensions holding a typed metadata map deserialized from `params._meta`.
fn extensions_with_meta<T>(meta: Option<Cow<'_, JsonObject>>) -> Extensions
where
T: From<JsonObject> + Clone + Send + Sync + 'static,
{
let mut extensions = Extensions::new();
if let Some(meta) = meta {
extensions.insert(T::from(meta.into_owned()));
}
extensions
}
impl<M, R> Serialize for Request<M, R>
where
M: Serialize,
@ -87,14 +141,12 @@ where
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
Proxy::serialize(
&Proxy {
method: &self.method,
params: WithMeta {
_rest: &self.params,
_meta,
_meta: request_meta(&self.extensions),
},
},
serializer,
@ -112,13 +164,8 @@ where
D: serde::Deserializer<'de>,
{
let body = Proxy::deserialize(deserializer)?;
let _meta = body.params._meta.map(|m| m.into_owned());
let mut extensions = Extensions::new();
if let Some(meta) = _meta {
extensions.insert(meta);
}
Ok(Request {
extensions,
extensions: extensions_with_meta::<RequestMetaObject>(body.params._meta),
method: body.method,
params: body.params._rest,
})
@ -134,14 +181,12 @@ where
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
Proxy::serialize(
&Proxy {
method: &self.method,
params: WithMeta {
_rest: &self.params,
_meta,
_meta: request_meta(&self.extensions),
},
},
serializer,
@ -163,14 +208,10 @@ where
let mut _meta = None;
if let Some(body_params) = body.params {
params = body_params._rest;
_meta = body_params._meta.map(|m| m.into_owned());
}
let mut extensions = Extensions::new();
if let Some(meta) = _meta {
extensions.insert(meta);
_meta = body_params._meta;
}
Ok(RequestOptionalParam {
extensions,
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
method: body.method,
params,
})
@ -185,14 +226,26 @@ where
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
ProxyNoParam::serialize(
&ProxyNoParam {
method: &self.method,
},
serializer,
)
// Emit `params` only when metadata is present, so the wire shape of
// meta-less requests stays `{"method": ...}`.
match request_meta(&self.extensions) {
Some(_meta) => Proxy::serialize(
&Proxy {
method: &self.method,
params: WithMeta {
_meta: Some(_meta),
_rest: (),
},
},
serializer,
),
None => ProxyNoParam::serialize(
&ProxyNoParam {
method: &self.method,
},
serializer,
),
}
}
}
@ -204,10 +257,10 @@ where
where
D: serde::Deserializer<'de>,
{
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
let extensions = Extensions::new();
let body = ProxyOptionalParam::<'_, _, Option<JsonObject>>::deserialize(deserializer)?;
let _meta = body.params.and_then(|params| params._meta);
Ok(RequestNoParam {
extensions,
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
method: body.method,
})
}
@ -222,14 +275,12 @@ where
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
Proxy::serialize(
&Proxy {
method: &self.method,
params: WithMeta {
_rest: &self.params,
_meta,
_meta: notification_meta(&self.extensions),
},
},
serializer,
@ -248,10 +299,7 @@ where
{
let body = ProxyOptionalParam::<'_, _, R>::deserialize(deserializer)?;
let (_meta, params) = match body.params {
Some(with_meta) => {
let meta = with_meta._meta.map(|m| m.into_owned());
(meta, with_meta._rest)
}
Some(with_meta) => (with_meta._meta, with_meta._rest),
None => {
// JSON-RPC 2.0: params is optional. Treat absent params as {}.
let empty = serde_json::Value::Object(serde_json::Map::new());
@ -259,12 +307,8 @@ where
(None, r)
}
};
let mut extensions = Extensions::new();
if let Some(meta) = _meta {
extensions.insert(meta);
}
Ok(Notification {
extensions,
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
method: body.method,
params,
})
@ -279,14 +323,26 @@ where
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
ProxyNoParam::serialize(
&ProxyNoParam {
method: &self.method,
},
serializer,
)
// Emit `params` only when metadata is present, so the wire shape of
// meta-less notifications stays `{"method": ...}`.
match notification_meta(&self.extensions) {
Some(_meta) => Proxy::serialize(
&Proxy {
method: &self.method,
params: WithMeta {
_meta: Some(_meta),
_rest: (),
},
},
serializer,
),
None => ProxyNoParam::serialize(
&ProxyNoParam {
method: &self.method,
},
serializer,
),
}
}
}
@ -298,10 +354,10 @@ where
where
D: serde::Deserializer<'de>,
{
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
let extensions = Extensions::new();
let body = ProxyOptionalParam::<'_, _, Option<JsonObject>>::deserialize(deserializer)?;
let _meta = body.params.and_then(|params| params._meta);
Ok(NotificationNoParam {
extensions,
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
method: body.method,
})
}
@ -312,8 +368,7 @@ impl Serialize for CustomRequest {
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
let _meta = request_meta(&self.extensions);
let params = self.params.as_ref();
let params = if _meta.is_some() || params.is_some() {
@ -346,14 +401,10 @@ impl<'de> Deserialize<'de> for CustomRequest {
let mut _meta = None;
if let Some(body_params) = body.params {
params = body_params._rest;
_meta = body_params._meta.map(|m| m.into_owned());
}
let mut extensions = Extensions::new();
if let Some(meta) = _meta {
extensions.insert(meta);
_meta = body_params._meta;
}
Ok(CustomRequest {
extensions,
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
method: body.method,
params,
})
@ -365,8 +416,7 @@ impl Serialize for CustomNotification {
where
S: serde::Serializer,
{
let extensions = &self.extensions;
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
let _meta = notification_meta(&self.extensions);
let params = self.params.as_ref();
let params = if _meta.is_some() || params.is_some() {
@ -399,14 +449,10 @@ impl<'de> Deserialize<'de> for CustomNotification {
let mut _meta = None;
if let Some(body_params) = body.params {
params = body_params._rest;
_meta = body_params._meta.map(|m| m.into_owned());
}
let mut extensions = Extensions::new();
if let Some(meta) = _meta {
extensions.insert(meta);
_meta = body_params._meta;
}
Ok(CustomNotification {
extensions,
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
method: body.method,
params,
})
@ -418,7 +464,8 @@ mod test {
use serde_json::json;
use crate::model::{
CallToolRequest, CallToolRequestParams, CustomRequest, Extensions, ListToolsRequest, Meta,
CallToolRequest, CallToolRequestParams, CustomRequest, Extensions, InitializedNotification,
ListToolsRequest, NotificationMetaObject, PingRequest, RequestMetaObject,
};
#[test]
@ -436,12 +483,12 @@ mod test {
// When both extensions and params contain _meta, the output should have
// a single merged _meta key (not two separate ones).
let mut extensions = Extensions::new();
let mut ext_meta = Meta::new();
ext_meta.0.insert("traceId".to_string(), json!("abc"));
let mut ext_meta = RequestMetaObject::new();
ext_meta.insert("traceId".to_string(), json!("abc"));
extensions.insert(ext_meta);
let mut params_meta = Meta::new();
params_meta.0.insert("progressToken".to_string(), json!(1));
let mut params_meta = RequestMetaObject::new();
params_meta.insert("progressToken".to_string(), json!(1));
let req = CallToolRequest {
extensions,
@ -479,8 +526,8 @@ mod test {
#[test]
fn test_meta_only_from_extensions() {
let mut extensions = Extensions::new();
let mut ext_meta = Meta::new();
ext_meta.0.insert("traceId".to_string(), json!("ext-only"));
let mut ext_meta = RequestMetaObject::new();
ext_meta.insert("traceId".to_string(), json!("ext-only"));
extensions.insert(ext_meta);
let req = CallToolRequest {
@ -503,8 +550,8 @@ mod test {
#[test]
fn test_meta_only_from_params() {
let mut params_meta = Meta::new();
params_meta.0.insert("progressToken".to_string(), json!(42));
let mut params_meta = RequestMetaObject::new();
params_meta.insert("progressToken".to_string(), json!(42));
let req = CallToolRequest {
extensions: Extensions::new(),
@ -550,19 +597,13 @@ mod test {
fn test_extensions_meta_takes_priority_on_conflict() {
// When both sources have the same key, extensions should win.
let mut extensions = Extensions::new();
let mut ext_meta = Meta::new();
ext_meta
.0
.insert("shared_key".to_string(), json!("from_extensions"));
let mut ext_meta = RequestMetaObject::new();
ext_meta.insert("shared_key".to_string(), json!("from_extensions"));
extensions.insert(ext_meta);
let mut params_meta = Meta::new();
params_meta
.0
.insert("shared_key".to_string(), json!("from_params"));
params_meta
.0
.insert("params_only".to_string(), json!("kept"));
let mut params_meta = RequestMetaObject::new();
params_meta.insert("shared_key".to_string(), json!("from_params"));
params_meta.insert("params_only".to_string(), json!("kept"));
let req = CallToolRequest {
extensions,
@ -586,10 +627,8 @@ mod test {
#[test]
fn test_round_trip_preserves_meta() {
let mut extensions = Extensions::new();
let mut ext_meta = Meta::new();
ext_meta
.0
.insert("traceId".to_string(), json!("round-trip"));
let mut ext_meta = RequestMetaObject::new();
ext_meta.insert("traceId".to_string(), json!("round-trip"));
extensions.insert(ext_meta);
let req = CallToolRequest {
@ -609,8 +648,8 @@ mod test {
let deserialized: CallToolRequest = serde_json::from_str(&serialized).unwrap();
// Extensions should have the meta after round-trip
let meta = deserialized.extensions.get::<Meta>().unwrap();
assert_eq!(meta.0.get("traceId").unwrap(), "round-trip");
let meta = deserialized.extensions.get::<RequestMetaObject>().unwrap();
assert_eq!(meta.get("traceId").unwrap(), "round-trip");
// Params should be preserved
assert_eq!(deserialized.params.name, "my_tool");
@ -630,10 +669,8 @@ mod test {
fn test_custom_request_no_duplicate_meta() {
// CustomRequest uses Option<Value> as params — verify no duplicate _meta.
let mut extensions = Extensions::new();
let mut ext_meta = Meta::new();
ext_meta
.0
.insert("traceId".to_string(), json!("custom-ext"));
let mut ext_meta = RequestMetaObject::new();
ext_meta.insert("traceId".to_string(), json!("custom-ext"));
extensions.insert(ext_meta);
let params = Some(json!({
@ -660,4 +697,162 @@ mod test {
assert_eq!(meta.get("traceId").unwrap(), "custom-ext");
assert_eq!(meta.get("progressToken").unwrap(), 99);
}
#[test]
fn test_request_no_param_meta_round_trip() {
// Ping-shaped requests must carry `params._meta` on the wire.
let mut extensions = Extensions::new();
let mut meta = RequestMetaObject::new();
meta.insert("traceId".to_string(), json!("ping-trace"));
extensions.insert(meta);
let req = PingRequest {
method: Default::default(),
extensions,
};
let value = serde_json::to_value(&req).unwrap();
assert_eq!(value["params"]["_meta"]["traceId"], json!("ping-trace"));
let deserialized: PingRequest = serde_json::from_value(value).unwrap();
let meta = deserialized
.extensions
.get::<RequestMetaObject>()
.expect("meta should survive the round-trip");
assert_eq!(meta.get("traceId").unwrap(), &json!("ping-trace"));
}
#[test]
fn test_request_no_param_without_meta_has_no_params_key() {
let req = PingRequest {
method: Default::default(),
extensions: Extensions::new(),
};
let value = serde_json::to_value(&req).unwrap();
assert!(
value.get("params").is_none(),
"meta-less no-param requests must keep the historical wire shape: {value}"
);
}
#[test]
fn test_notification_no_param_meta_round_trip() {
// Initialized-shaped notifications must carry `params._meta` on the wire.
let mut extensions = Extensions::new();
let mut meta = NotificationMetaObject::new();
meta.insert("traceId".to_string(), json!("init-trace"));
extensions.insert(meta);
let notification = InitializedNotification {
method: Default::default(),
extensions,
};
let value = serde_json::to_value(&notification).unwrap();
assert_eq!(value["params"]["_meta"]["traceId"], json!("init-trace"));
let deserialized: InitializedNotification = serde_json::from_value(value).unwrap();
let meta = deserialized
.extensions
.get::<NotificationMetaObject>()
.expect("meta should survive the round-trip");
assert_eq!(meta.get("traceId").unwrap(), &json!("init-trace"));
}
#[test]
fn test_notification_no_param_without_meta_has_no_params_key() {
let notification = InitializedNotification {
method: Default::default(),
extensions: Extensions::new(),
};
let value = serde_json::to_value(&notification).unwrap();
assert!(
value.get("params").is_none(),
"meta-less no-param notifications must keep the historical wire shape: {value}"
);
}
#[test]
fn test_no_param_ignores_unknown_params_fields() {
// Old/foreign peers may send params without _meta; both shapes must parse.
let _req: PingRequest =
serde_json::from_value(json!({"method": "ping", "params": {}})).unwrap();
let _req: PingRequest =
serde_json::from_value(json!({"method": "ping", "params": {"unknown": 1}})).unwrap();
let _req: PingRequest = serde_json::from_value(json!({"method": "ping"})).unwrap();
}
#[test]
fn test_legacy_meta_extension_still_serializes() {
// Pre-3.x code inserts `MetaObject` into extensions through the
// deprecated `Meta` name; its metadata must not be silently dropped.
let mut extensions = Extensions::new();
let mut legacy = crate::model::MetaObject::new();
legacy.insert("traceId".to_string(), json!("legacy"));
extensions.insert(legacy);
let req = CallToolRequest {
extensions,
method: Default::default(),
params: CallToolRequestParams {
meta: None,
name: "my_tool".into(),
arguments: None,
task: None,
input_responses: None,
request_state: None,
},
};
let value = serde_json::to_value(&req).unwrap();
assert_eq!(value["params"]["_meta"]["traceId"], json!("legacy"));
}
#[test]
fn test_typed_meta_wins_over_legacy_extension_on_conflict() {
let mut extensions = Extensions::new();
let mut legacy = crate::model::MetaObject::new();
legacy.insert("shared".to_string(), json!("legacy"));
legacy.insert("legacy_only".to_string(), json!("kept"));
extensions.insert(legacy);
let mut typed = RequestMetaObject::new();
typed.insert("shared".to_string(), json!("typed"));
extensions.insert(typed);
let req = CallToolRequest {
extensions,
method: Default::default(),
params: CallToolRequestParams {
meta: None,
name: "my_tool".into(),
arguments: None,
task: None,
input_responses: None,
request_state: None,
},
};
let value = serde_json::to_value(&req).unwrap();
let meta = value["params"]["_meta"].as_object().unwrap();
assert_eq!(meta.get("shared").unwrap(), "typed");
assert_eq!(meta.get("legacy_only").unwrap(), "kept");
}
#[test]
fn test_arbitrary_meta_keys_round_trip_unchanged() {
let input = json!({
"method": "tools/call",
"params": {
"_meta": {
"progressToken": 5,
"vendor.example/custom": {"nested": ["a", 1, null]},
"another-key": true
},
"name": "my_tool"
}
});
let req: CallToolRequest = serde_json::from_value(input.clone()).unwrap();
let output = serde_json::to_value(&req).unwrap();
assert_eq!(input, output);
}
}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::Meta;
use super::MetaObject;
/// Metadata for augmenting a request with task execution (spec `TaskMetadata`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
@ -139,7 +139,7 @@ impl Task {
pub struct CreateTaskResult {
pub task: Task,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
}
impl CreateTaskResult {
@ -149,7 +149,7 @@ impl CreateTaskResult {
}
/// Sets the protocol-level metadata for this result.
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}
@ -165,7 +165,7 @@ impl CreateTaskResult {
#[non_exhaustive]
pub struct GetTaskResult {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
#[serde(flatten)]
pub task: Task,
}
@ -222,7 +222,7 @@ impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult {
#[non_exhaustive]
pub struct CancelTaskResult {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
#[serde(flatten)]
pub task: Task,
}

View file

@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{Icon, JsonObject, Meta};
use super::{Icon, JsonObject, MetaObject};
/// A tool that can be used by a model.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -39,7 +39,7 @@ pub struct Tool {
pub icons: Option<Vec<Icon>>,
/// Optional additional metadata for this tool
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub meta: Option<MetaObject>,
}
/// Per-tool task support mode as defined in the MCP specification.
@ -286,7 +286,7 @@ impl Tool {
}
/// Set the metadata
pub fn with_meta(mut self, meta: Meta) -> Self {
pub fn with_meta(mut self, meta: MetaObject) -> Self {
self.meta = Some(meta);
self
}