diff --git a/conformance/src/bin/server.rs b/conformance/src/bin/server.rs index 0e779f66..09bdd5e9 100644 --- a/conformance/src/bin/server.rs +++ b/conformance/src/bin/server.rs @@ -103,7 +103,7 @@ impl ConformanceServer { async fn call_mrtr_tool( &self, request: CallToolRequestParams, - meta: &Meta, + meta: &RequestMetaObject, ) -> Result { let responses = request.input_responses.as_ref(); match request.name.as_ref() { diff --git a/crates/rmcp/src/handler/server/common.rs b/crates/rmcp/src/handler/server/common.rs index aa1cc313..8d3e4a8a 100644 --- a/crates/rmcp/src/handler/server/common.rs +++ b/crates/rmcp/src/handler/server/common.rs @@ -209,13 +209,13 @@ where } } -impl FromContextPart for crate::model::Meta +impl FromContextPart for crate::model::RequestMetaObject where C: AsRequestContext, { fn from_context_part(context: &mut C) -> Result { let request_context = context.as_request_context_mut(); - let mut meta = crate::model::Meta::default(); + let mut meta = crate::model::RequestMetaObject::default(); std::mem::swap(&mut meta, &mut request_context.meta); Ok(meta) } diff --git a/crates/rmcp/src/handler/server/router/tool/tool_traits.rs b/crates/rmcp/src/handler/server/router/tool/tool_traits.rs index df6594da..436c3df3 100644 --- a/crates/rmcp/src/handler/server/router/tool/tool_traits.rs +++ b/crates/rmcp/src/handler/server/router/tool/tool_traits.rs @@ -9,7 +9,7 @@ use crate::{ tool::schema_for_output, wrapper::{Json, Parameters}, }, - model::{Icon, JsonObject, Meta, ToolAnnotations, ToolExecution}, + model::{Icon, JsonObject, MetaObject, ToolAnnotations, ToolExecution}, schemars::JsonSchema, service::{MaybeSend, MaybeSendFuture}, }; @@ -77,7 +77,7 @@ pub trait ToolBase { fn icons() -> Option> { None } - fn meta() -> Option { + fn meta() -> Option { None } } diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 70de62ea..36651119 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -339,7 +339,8 @@ pub struct ProgressToken(pub NumberOrString); pub struct Request { pub method: M, pub params: P, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -372,7 +373,8 @@ pub struct RequestOptionalParam { pub method: M, // #[serde(skip_serializing_if = "Option::is_none")] pub params: Option

, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -394,7 +396,8 @@ impl RequestOptionalParam { #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RequestNoParam { pub method: M, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -415,7 +418,8 @@ impl GetExtensions for RequestNoParam { pub struct Notification { pub method: M, pub params: P, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -437,7 +441,8 @@ impl Notification { #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct NotificationNoParam { pub method: M, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -782,7 +787,7 @@ pub struct CancelledNotificationParam { #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl CancelledNotificationParam { @@ -818,7 +823,8 @@ pub type CancelledNotification = pub struct CustomNotification { pub method: String, pub params: Option, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -853,7 +859,8 @@ impl CustomNotification { pub struct CustomRequest { pub method: String, pub params: Option, - /// extensions will carry anything possible in the context, including [`Meta`] + /// extensions will carry anything possible in the context, including the metadata + /// ([`RequestMetaObject`] for requests, [`NotificationMetaObject`] for notifications) /// /// this is similar with the Extensions in `http` crate #[cfg_attr(feature = "schemars", schemars(skip))] @@ -898,7 +905,7 @@ pub type InitializedNotification = NotificationNoParam, + pub meta: Option, /// The MCP protocol version this client supports pub protocol_version: ProtocolVersion, /// The capabilities this client supports (sampling, roots, etc.) @@ -925,10 +932,10 @@ impl InitializeRequestParams { } impl RequestParamsMeta for InitializeRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -956,7 +963,7 @@ pub struct InitializeResult { #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl InitializeResult { @@ -1167,7 +1174,7 @@ impl Implementation { pub struct PaginatedRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, } @@ -1180,10 +1187,10 @@ impl PaginatedRequestParams { } impl RequestParamsMeta for PaginatedRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1214,7 +1221,7 @@ pub struct ProgressNotificationParam { #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ProgressNotificationParam { @@ -1287,7 +1294,7 @@ macro_rules! paginated_result { #[serde(default)] pub result_type: ResultType, #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub next_cursor: Option, /// Time, in milliseconds, that this result may be treated as fresh (SEP-2549). @@ -1365,7 +1372,7 @@ const_string!(ReadResourceRequestMethod = "resources/read"); pub struct ReadResourceRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// The URI of the resource to read pub uri: String, /// Client responses to server-initiated input requests from a previous @@ -1389,7 +1396,7 @@ impl ReadResourceRequestParams { } /// Set the metadata for this request. - pub fn with_meta(mut self, meta: Meta) -> Self { + pub fn with_meta(mut self, meta: RequestMetaObject) -> Self { self.meta = Some(meta); self } @@ -1408,10 +1415,10 @@ impl ReadResourceRequestParams { } impl RequestParamsMeta for ReadResourceRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1446,7 +1453,7 @@ pub struct ReadResourceResult { /// The actual content of the resource pub contents: Vec, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ReadResourceResult { @@ -1491,7 +1498,7 @@ const_string!(SubscribeRequestMethod = "resources/subscribe"); pub struct SubscribeRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// The URI of the resource to subscribe to pub uri: String, } @@ -1507,10 +1514,10 @@ impl SubscribeRequestParams { } impl RequestParamsMeta for SubscribeRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1531,7 +1538,7 @@ const_string!(UnsubscribeRequestMethod = "resources/unsubscribe"); pub struct UnsubscribeRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// The URI of the resource to unsubscribe from pub uri: String, } @@ -1547,10 +1554,10 @@ impl UnsubscribeRequestParams { } impl RequestParamsMeta for UnsubscribeRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1572,7 +1579,7 @@ pub struct ResourceUpdatedNotificationParam { /// The URI of the resource that was updated pub uri: String, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ResourceUpdatedNotificationParam { @@ -1611,7 +1618,7 @@ const_string!(GetPromptRequestMethod = "prompts/get"); pub struct GetPromptRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub arguments: Option, @@ -1643,7 +1650,7 @@ impl GetPromptRequestParams { } /// Set the metadata for this request. - pub fn with_meta(mut self, meta: Meta) -> Self { + pub fn with_meta(mut self, meta: RequestMetaObject) -> Self { self.meta = Some(meta); self } @@ -1662,10 +1669,10 @@ impl GetPromptRequestParams { } impl RequestParamsMeta for GetPromptRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1722,7 +1729,7 @@ const_string!(SetLevelRequestMethod = "logging/setLevel"); pub struct SetLevelRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// The desired logging level pub level: LoggingLevel, } @@ -1735,10 +1742,10 @@ impl SetLevelRequestParams { } impl RequestParamsMeta for SetLevelRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -1773,7 +1780,7 @@ pub struct LoggingMessageNotificationParam { /// The actual log data pub data: Value, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl LoggingMessageNotificationParam { @@ -1994,7 +2001,7 @@ pub struct SamplingMessage { /// The actual content of the message (text, image, audio, tool use, or tool result) pub content: SamplingContent, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } /// Content types for sampling messages (SEP-1577). @@ -2161,7 +2168,7 @@ pub enum ContextInclusion { pub struct CreateMessageRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// Task metadata for async task management (SEP-1319) #[serde(skip_serializing_if = "Option::is_none")] pub task: Option, @@ -2196,10 +2203,10 @@ pub struct CreateMessageRequestParams { } impl RequestParamsMeta for CreateMessageRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -2516,7 +2523,7 @@ impl CompletionContext { pub struct CompleteRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, pub r#ref: Reference, pub argument: ArgumentInfo, /// Optional context containing previously resolved argument values @@ -2543,10 +2550,10 @@ impl CompleteRequestParams { } impl RequestParamsMeta for CompleteRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -2646,7 +2653,7 @@ pub struct CompleteResult { pub result_type: ResultType, pub completion: CompletionInfo, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl CompleteResult { @@ -2789,7 +2796,7 @@ pub struct Root { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl Root { @@ -2809,7 +2816,7 @@ impl Root { } /// Sets the protocol-level metadata for this root. - pub fn with_meta(mut self, meta: Meta) -> Self { + pub fn with_meta(mut self, meta: MetaObject) -> Self { self.meta = Some(meta); self } @@ -2833,7 +2840,7 @@ pub type ListRootsRequest = RequestNoParam; pub struct ListRootsResult { pub roots: Vec, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ListRootsResult { @@ -2843,7 +2850,7 @@ impl ListRootsResult { } /// 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 } @@ -2889,14 +2896,14 @@ enum CreateElicitationRequestParamDeserializeHelper { #[serde(rename = "form", rename_all = "camelCase")] FormElicitationParam { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, message: String, requested_schema: ElicitationSchema, }, #[serde(rename = "url", rename_all = "camelCase")] UrlElicitationParam { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, message: String, url: String, elicitation_id: String, @@ -2904,7 +2911,7 @@ enum CreateElicitationRequestParamDeserializeHelper { #[serde(untagged, rename_all = "camelCase")] FormElicitationParamBackwardsCompat { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, message: String, requested_schema: ElicitationSchema, }, @@ -2988,7 +2995,7 @@ pub enum ElicitRequestParams { FormElicitationParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, /// Human-readable message explaining what input is needed from the user. /// This should be clear and provide sufficient context for the user to understand /// what information they need to provide. @@ -3003,7 +3010,7 @@ pub enum ElicitRequestParams { UrlElicitationParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, /// Human-readable message explaining what input is needed from the user. /// This should be clear and provide sufficient context for the user to understand /// what information they need to provide. @@ -3018,13 +3025,13 @@ pub enum ElicitRequestParams { } impl RequestParamsMeta for ElicitRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { match self { ElicitRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(), ElicitRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(), } } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { match self { ElicitRequestParams::FormElicitationParams { meta, .. } => meta, ElicitRequestParams::UrlElicitationParams { meta, .. } => meta, @@ -3059,7 +3066,7 @@ pub struct ElicitResult { /// Optional protocol-level metadata for this result. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ElicitResult { @@ -3079,7 +3086,7 @@ impl ElicitResult { } /// Set the metadata on 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 } @@ -3121,7 +3128,7 @@ pub struct CallToolResult { pub is_error: Option, /// Optional protocol-level metadata for this result #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } // Custom Deserialize implementation that: @@ -3143,7 +3150,7 @@ impl<'de> Deserialize<'de> for CallToolResult { structured_content: Option, is_error: Option, #[serde(rename = "_meta")] - meta: Option, + meta: Option, } let helper = Helper::deserialize(deserializer)?; @@ -3290,7 +3297,7 @@ impl CallToolResult { } /// Set the metadata on this result - pub fn with_meta(mut self, meta: Option) -> Self { + pub fn with_meta(mut self, meta: Option) -> Self { self.meta = meta; self } @@ -3349,7 +3356,7 @@ const_string!(CallToolRequestMethod = "tools/call"); pub struct CallToolRequestParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// The name of the tool to call pub name: Cow<'static, str>, /// Arguments to pass to the tool (must match the tool's input schema) @@ -3407,10 +3414,10 @@ impl CallToolRequestParams { } impl RequestParamsMeta for CallToolRequestParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -3503,7 +3510,7 @@ pub struct GetPromptResult { pub description: Option, pub messages: Vec, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl GetPromptResult { @@ -3543,7 +3550,7 @@ pub type GetTaskInfoRequest = GetTaskRequest; pub struct GetTaskParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, pub task_id: String, } @@ -3557,10 +3564,10 @@ impl GetTaskParams { } impl RequestParamsMeta for GetTaskParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -3589,7 +3596,7 @@ pub type GetTaskResultRequest = GetTaskPayloadRequest; pub struct GetTaskPayloadParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, pub task_id: String, } @@ -3603,10 +3610,10 @@ impl GetTaskPayloadParams { } impl RequestParamsMeta for GetTaskPayloadParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -3626,7 +3633,7 @@ pub type CancelTaskRequest = Request; pub struct CancelTaskParams { /// Protocol-level metadata for this request (SEP-1319) #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, pub task_id: String, } @@ -3640,10 +3647,10 @@ impl CancelTaskParams { } impl RequestParamsMeta for CancelTaskParams { - fn meta(&self) -> Option<&Meta> { + fn meta(&self) -> Option<&RequestMetaObject> { self.meta.as_ref() } - fn meta_mut(&mut self) -> &mut Option { + fn meta_mut(&mut self) -> &mut Option { &mut self.meta } } @@ -3666,7 +3673,7 @@ const_string!(TaskStatusNotificationMethod = "notifications/tasks/status"); #[non_exhaustive] pub struct TaskStatusNotificationParam { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, #[serde(flatten)] pub task: crate::model::Task, } @@ -3676,7 +3683,7 @@ impl TaskStatusNotificationParam { Self { meta: None, task } } - pub fn with_meta(mut self, meta: Meta) -> Self { + pub fn with_meta(mut self, meta: NotificationMetaObject) -> Self { self.meta = Some(meta); self } @@ -3717,7 +3724,7 @@ pub struct ListTasksResult { #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } impl ListTasksResult { @@ -4487,7 +4494,9 @@ mod tests { { assert_eq!( meta, - Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))) + Some(RequestMetaObject(MetaObject( + object!({ "meta_form_key_1": "meta form value 1" }) + ))) ); assert_eq!(message, "Please provide more details."); assert_eq!(requested_schema.title, Some(Cow::from("User Details"))); @@ -4514,7 +4523,9 @@ mod tests { { assert_eq!( meta, - Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))) + Some(RequestMetaObject(MetaObject( + object!({ "meta_url_key_1": "meta url value 1" }) + ))) ); assert_eq!(message, "Please fill out the form at the following URL."); assert_eq!(url, "https://example.com/form"); @@ -4527,7 +4538,9 @@ mod tests { #[test] fn test_elicitation_serialization() { let form_elicitation = ElicitRequestParams::FormElicitationParams { - meta: Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))), + meta: Some(RequestMetaObject(MetaObject( + object!({ "meta_form_key_1": "meta form value 1" }), + ))), message: "Please provide more details.".to_string(), requested_schema: ElicitationSchema::builder() .title("User Details") @@ -4551,7 +4564,9 @@ mod tests { assert_eq!(json_form, expected_form_json); let url_elicitation = ElicitRequestParams::UrlElicitationParams { - meta: Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))), + meta: Some(RequestMetaObject(MetaObject( + object!({ "meta_url_key_1": "meta url value 1" }), + ))), message: "Please fill out the form at the following URL.".to_string(), url: "https://example.com/form".to_string(), elicitation_id: "elicitation-123".to_string(), diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index d454255e..11b2f0e5 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -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, + pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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, + pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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, + pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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, + pub meta: Option, /// Optional annotations describing how the client should use this content. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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, + pub meta: Option, } /// 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, + pub meta: Option, pub tool_use_id: String, pub content: Vec, #[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()); } diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index bc1b94b4..53675ecc 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -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; + fn meta_mut(&mut self) -> &mut Option; /// 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::() - .unwrap_or(Meta::static_empty()) + .get::() + .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::() - .unwrap_or(Meta::static_empty()) + .get::() + .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::().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,11 +274,164 @@ 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 + /// 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) + } + + /// Write a string-valued `_meta` field. + fn set_str(&mut self, field: &str, value: impl Into) { + self.0 + .insert(field.to_string(), Value::String(value.into())); + } + + /// Get the W3C `traceparent` value (SEP-414), if present. + pub fn get_traceparent(&self) -> Option<&str> { + self.get_str(Self::TRACEPARENT_FIELD) + } + + /// Set the W3C `traceparent` value (SEP-414). + /// + /// ``` + /// use rmcp::model::MetaObject; + /// + /// let mut meta = MetaObject::new(); + /// meta.set_traceparent("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"); + /// assert_eq!( + /// meta.get_traceparent(), + /// Some("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"), + /// ); + /// ``` + pub fn set_traceparent(&mut self, value: impl Into) { + self.set_str(Self::TRACEPARENT_FIELD, value); + } + + /// Get the W3C `tracestate` value (SEP-414), if present. + pub fn get_tracestate(&self) -> Option<&str> { + self.get_str(Self::TRACESTATE_FIELD) + } + + /// Set the W3C `tracestate` value (SEP-414). + pub fn set_tracestate(&mut self, value: impl Into) { + self.set_str(Self::TRACESTATE_FIELD, value); + } + + /// Get the W3C `baggage` value (SEP-414), if present. + pub fn get_baggage(&self) -> Option<&str> { + self.get_str(Self::BAGGAGE_FIELD) + } + + /// Set the W3C `baggage` value (SEP-414). + pub fn set_baggage(&mut self, value: impl Into) { + self.set_str(Self::BAGGAGE_FIELD, value); + } + + /// 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(&self, key: &str) -> Option + where + T: for<'de> Deserialize<'de>, + { + self.0.get(key).and_then(|value| T::deserialize(value).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 MetaObject { + type Target = JsonObject; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for MetaObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From 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); @@ -253,55 +439,28 @@ impl Meta { } pub(crate) fn static_empty() -> &'static Self { - static EMPTY: std::sync::OnceLock = std::sync::OnceLock::new(); + static EMPTY: std::sync::OnceLock = 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 { - 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, - }) + self.0.decode_value(Self::PROGRESS_TOKEN_FIELD) } + /// Set the progress token carried in `_meta`. 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()), - ), - }; + 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 { - self.decode_value(Self::META_KEY_PROTOCOL_VERSION) + 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.insert( + self.0.0.insert( Self::META_KEY_PROTOCOL_VERSION.to_string(), Value::String(protocol_version.to_string()), ); @@ -309,123 +468,246 @@ impl Meta { /// Get the client implementation identity carried in `_meta`, if present and valid. pub fn client_info(&self) -> Option { - self.decode_value(Self::META_KEY_CLIENT_INFO) + 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.insert_serialized(Self::META_KEY_CLIENT_INFO, client_info); + 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 { - self.decode_value(Self::META_KEY_CLIENT_CAPABILITIES) + 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.insert_serialized(Self::META_KEY_CLIENT_CAPABILITIES, client_capabilities); + 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 { - self.decode_value(Self::META_KEY_LOG_LEVEL) + 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.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) - } - - /// Write a string-valued `_meta` field. - fn set_str(&mut self, field: &str, value: impl Into) { self.0 - .insert(field.to_string(), Value::String(value.into())); - } - - /// Get the W3C `traceparent` value (SEP-414), if present. - pub fn get_traceparent(&self) -> Option<&str> { - self.get_str(Self::TRACEPARENT_FIELD) + .insert_serialized(Self::META_KEY_LOG_LEVEL, log_level); } - /// Set the W3C `traceparent` value (SEP-414). + /// 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::Meta; + /// use rmcp::model::{ProtocolVersion, RequestMetaObject}; /// - /// let mut meta = Meta::new(); - /// meta.set_traceparent("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"); + /// 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.get_traceparent(), - /// Some("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"), + /// meta.missing_required_keys(&ProtocolVersion::V_2026_07_28), + /// RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec(), /// ); /// ``` - pub fn set_traceparent(&mut self, value: impl Into) { - self.set_str(Self::TRACEPARENT_FIELD, value); + 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 } - /// Get the W3C `tracestate` value (SEP-414), if present. - pub fn get_tracestate(&self) -> Option<&str> { - self.get_str(Self::TRACESTATE_FIELD) + /// Insert every entry of `other`, overwriting existing keys on conflict. + pub fn extend(&mut self, other: RequestMetaObject) { + self.0.extend(other.0); } +} - /// Set the W3C `tracestate` value (SEP-414). - pub fn set_tracestate(&mut self, value: impl Into) { - self.set_str(Self::TRACESTATE_FIELD, value); +impl Deref for RequestMetaObject { + type Target = MetaObject; + + fn deref(&self) -> &Self::Target { + &self.0 } +} - /// Get the W3C `baggage` value (SEP-414), if present. - pub fn get_baggage(&self) -> Option<&str> { - self.get_str(Self::BAGGAGE_FIELD) +impl DerefMut for RequestMetaObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } +} - /// Set the W3C `baggage` value (SEP-414). - pub fn set_baggage(&mut self, value: impl Into) { - self.set_str(Self::BAGGAGE_FIELD, value); +impl From for RequestMetaObject { + fn from(meta: MetaObject) -> Self { + Self(meta) } +} - pub fn extend(&mut self, other: Meta) { - for (k, v) in other.0.into_iter() { - self.0.insert(k, v); - } +impl From for RequestMetaObject { + fn from(object: JsonObject) -> Self { + Self(MetaObject(object)) } +} - fn decode_value(&self, key: &str) -> Option - where - T: for<'de> Deserialize<'de>, - { - self.0.get(key).and_then(|value| T::deserialize(value).ok()) +#[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::(); + let client_info = generator.subschema_for::(); + let client_capabilities = generator.subschema_for::(); + let log_level = generator.subschema_for::(); + // 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, + }) } +} - 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); +/// 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 = 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 { + 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 Meta { - type Target = JsonObject; +impl Deref for NotificationMetaObject { + type Target = MetaObject; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for Meta { +impl DerefMut for NotificationMetaObject { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +impl From for NotificationMetaObject { + fn from(meta: MetaObject) -> Self { + Self(meta) + } +} + +impl From 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::(); + 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 JsonRpcMessage 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: Option, } 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 { + fn meta_mut(&mut self) -> &mut Option { &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() + ); + } + } } diff --git a/crates/rmcp/src/model/mrtr.rs b/crates/rmcp/src/model/mrtr.rs index 2b8ba71b..7e08a5cc 100644 --- a/crates/rmcp/src/model/mrtr.rs +++ b/crates/rmcp/src/model/mrtr.rs @@ -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, + pub meta: Option, } /// Custom deserializer that requires `resultType: "input_required"` to prevent @@ -238,7 +238,7 @@ impl<'de> Deserialize<'de> for InputRequiredResult { input_requests: Option, request_state: Option, #[serde(rename = "_meta")] - meta: Option, + meta: Option, } 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 } diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index e438260b..46f5168e 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -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>, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } 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: Option, annotations: Option, ) -> Self { use base64::{Engine, prelude::BASE64_STANDARD}; @@ -166,7 +166,7 @@ impl PromptMessage { role: Role, data: &[u8], mime_type: &str, - meta: Option, + meta: Option, annotations: Option, ) -> Self { use base64::{Engine, prelude::BASE64_STANDARD}; @@ -188,8 +188,8 @@ impl PromptMessage { uri: String, mime_type: Option, text: Option, - resource_meta: Option, - resource_content_meta: Option, + resource_meta: Option, + resource_content_meta: Option, annotations: Option, ) -> Self { let resource_contents = match text { @@ -216,7 +216,11 @@ impl PromptMessage { } } - pub fn new_text_with_meta>(role: Role, text: S, meta: Option) -> Self { + pub fn new_text_with_meta>( + role: Role, + text: S, + meta: Option, + ) -> Self { Self { role, content: ContentBlock::Text(TextContent { diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index 0381d4d8..9c27dce2 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -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>, /// Optional protocol-level metadata for this resource. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// Optional annotations describing how the client should use this resource. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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>, /// Optional protocol-level metadata for this resource template. #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, /// Optional annotations describing how the client should use this template. #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -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, text: String, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, }, #[serde(rename_all = "camelCase")] BlobResourceContents { @@ -185,7 +185,7 @@ pub enum ResourceContents { mime_type: Option, blob: String, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - meta: Option, + meta: Option, }, } @@ -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()); } diff --git a/crates/rmcp/src/model/serde_impl.rs b/crates/rmcp/src/model/serde_impl.rs index 7ff91099..6a7197ae 100644 --- a/crates/rmcp/src/model/serde_impl.rs +++ b/crates/rmcp/src/model/serde_impl.rs @@ -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>, + _meta: Option>, #[serde(flatten)] _rest: P, } @@ -25,7 +32,7 @@ impl 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 = rest_value + let params_meta: Option = 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 { 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> { + let legacy = extensions.get::().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> { + let typed = extensions.get::().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> { + let typed = extensions + .get::() + .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(meta: Option>) -> Extensions +where + T: From + Clone + Send + Sync + 'static, +{ + let mut extensions = Extensions::new(); + if let Some(meta) = meta { + extensions.insert(T::from(meta.into_owned())); + } + extensions +} + impl Serialize for Request where M: Serialize, @@ -87,14 +141,12 @@ where where S: serde::Serializer, { - let extensions = &self.extensions; - let _meta = extensions.get::().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::(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::().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::(_meta), method: body.method, params, }) @@ -185,14 +226,26 @@ where where S: serde::Serializer, { - let extensions = &self.extensions; - let _meta = extensions.get::().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>::deserialize(deserializer)?; + let _meta = body.params.and_then(|params| params._meta); Ok(RequestNoParam { - extensions, + extensions: extensions_with_meta::(_meta), method: body.method, }) } @@ -222,14 +275,12 @@ where where S: serde::Serializer, { - let extensions = &self.extensions; - let _meta = extensions.get::().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::(_meta), method: body.method, params, }) @@ -279,14 +323,26 @@ where where S: serde::Serializer, { - let extensions = &self.extensions; - let _meta = extensions.get::().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>::deserialize(deserializer)?; + let _meta = body.params.and_then(|params| params._meta); Ok(NotificationNoParam { - extensions, + extensions: extensions_with_meta::(_meta), method: body.method, }) } @@ -312,8 +368,7 @@ impl Serialize for CustomRequest { where S: serde::Serializer, { - let extensions = &self.extensions; - let _meta = extensions.get::().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::(_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::().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::(_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::().unwrap(); - assert_eq!(meta.0.get("traceId").unwrap(), "round-trip"); + let meta = deserialized.extensions.get::().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 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::() + .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(¬ification).unwrap(); + assert_eq!(value["params"]["_meta"]["traceId"], json!("init-trace")); + + let deserialized: InitializedNotification = serde_json::from_value(value).unwrap(); + let meta = deserialized + .extensions + .get::() + .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(¬ification).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); + } } diff --git a/crates/rmcp/src/model/task.rs b/crates/rmcp/src/model/task.rs index 8f493425..e57fdd87 100644 --- a/crates/rmcp/src/model/task.rs +++ b/crates/rmcp/src/model/task.rs @@ -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, + pub meta: Option, } 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, + pub meta: Option, #[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, + pub meta: Option, #[serde(flatten)] pub task: Task, } diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index 25d9def5..ec2ad741 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -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>, /// Optional additional metadata for this tool #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, + pub meta: Option, } /// 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 } diff --git a/crates/rmcp/src/service.rs b/crates/rmcp/src/service.rs index 2345a5e4..6a7aa53e 100644 --- a/crates/rmcp/src/service.rs +++ b/crates/rmcp/src/service.rs @@ -52,8 +52,8 @@ use crate::{ error::ErrorData as McpError, model::{ CancelledNotification, CancelledNotificationParam, Extensions, GetExtensions, GetMeta, - JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Meta, - NumberOrString, ProgressToken, RequestId, + JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, + NotificationMetaObject, NumberOrString, ProgressToken, RequestId, RequestMetaObject, }, transport::{DynamicTransportError, IntoTransport, Transport}, }; @@ -109,17 +109,17 @@ impl TransferObject for T where #[allow(private_bounds, reason = "there's no the third implementation")] pub trait ServiceRole: std::fmt::Debug + Send + Sync + 'static + Copy + Clone { - type Req: TransferObject + GetMeta + GetExtensions; + type Req: TransferObject + GetMeta + GetExtensions; type Resp: TransferObject; type Not: TryInto + From + TransferObject; - type PeerReq: TransferObject + GetMeta + GetExtensions; + type PeerReq: TransferObject + GetMeta + GetExtensions; type PeerResp: TransferObject; type PeerNot: TryInto + From + TransferObject - + GetMeta + + GetMeta + GetExtensions; type InitializeError; const IS_CLIENT: bool; @@ -544,7 +544,7 @@ type ProxyOutbound = mpsc::Receiver>; #[non_exhaustive] pub struct PeerRequestOptions { pub timeout: Option, - pub meta: Option, + pub meta: Option, /// Reset the request timeout when a matching progress notification is received. pub reset_timeout_on_progress: bool, /// Maximum total time to wait for the request, regardless of progress notifications. @@ -862,7 +862,7 @@ pub struct RequestContext { /// this token will be cancelled when the [`CancelledNotification`] is received. pub ct: CancellationToken, pub id: RequestId, - pub meta: Meta, + pub meta: RequestMetaObject, pub extensions: Extensions, /// An interface to fetch the remote client or server pub peer: Peer, @@ -874,7 +874,7 @@ impl RequestContext { Self { ct: CancellationToken::new(), id, - meta: Meta::default(), + meta: RequestMetaObject::default(), extensions: Extensions::default(), peer, } @@ -895,7 +895,7 @@ impl RequestContext { #[derive(Debug, Clone)] #[non_exhaustive] pub struct NotificationContext { - pub meta: Meta, + pub meta: NotificationMetaObject, pub extensions: Extensions, /// An interface to fetch the remote client or server pub peer: Peer, @@ -1171,7 +1171,7 @@ where let context_ct = request_ct.child_token(); local_ct_pool.insert(id.clone(), request_ct); let mut extensions = Extensions::new(); - let mut meta = Meta::new(); + let mut meta = RequestMetaObject::new(); // avoid clone // swap meta firstly, otherwise progress token will be lost std::mem::swap(&mut meta, request.get_meta_mut()); @@ -1226,7 +1226,7 @@ where { let service = shared_service.clone(); let mut extensions = Extensions::new(); - let mut meta = Meta::new(); + let mut meta = NotificationMetaObject::new(); // avoid clone std::mem::swap(&mut extensions, notification.extensions_mut()); std::mem::swap(&mut meta, notification.get_meta_mut()); diff --git a/crates/rmcp/src/service/client.rs b/crates/rmcp/src/service/client.rs index 92951261..e61dea36 100644 --- a/crates/rmcp/src/service/client.rs +++ b/crates/rmcp/src/service/client.rs @@ -115,11 +115,11 @@ where let mut context = NotificationContext { peer: peer.clone(), - meta: Meta::default(), + meta: NotificationMetaObject::default(), extensions: Extensions::default(), }; - if let Some(meta) = logging.extensions.get_mut::() { + if let Some(meta) = logging.extensions.get_mut::() { std::mem::swap(&mut context.meta, meta); } std::mem::swap(&mut context.extensions, &mut logging.extensions); @@ -890,7 +890,7 @@ where fn input_request_context(&self, key: &str, request: &mut T) -> RequestContext where - T: GetMeta + GetExtensions, + T: GetMeta + GetExtensions, { let mut meta = Default::default(); let mut extensions = Default::default(); diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 0c294aa4..b2bc39a7 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -125,7 +125,7 @@ async fn test_elicitation_result_serialization() { assert_eq!(deserialized.meta, None); // Test protocol-level metadata round-trips as _meta. - let meta_result = ElicitResult::new(ElicitationAction::Accept).with_meta(Meta(object!({ + let meta_result = ElicitResult::new(ElicitationAction::Accept).with_meta(MetaObject(object!({ "traceId": "elicitation-123" }))); @@ -139,7 +139,7 @@ async fn test_elicitation_result_serialization() { let deserialized: ElicitResult = serde_json::from_value(expected).unwrap(); assert_eq!( deserialized.meta, - Some(Meta(object!({ "traceId": "elicitation-123" }))) + Some(MetaObject(object!({ "traceId": "elicitation-123" }))) ); } diff --git a/crates/rmcp/tests/test_embedded_resource_meta.rs b/crates/rmcp/tests/test_embedded_resource_meta.rs index 167108e8..5854f3f5 100644 --- a/crates/rmcp/tests/test_embedded_resource_meta.rs +++ b/crates/rmcp/tests/test_embedded_resource_meta.rs @@ -1,12 +1,12 @@ -use rmcp::model::{ContentBlock, EmbeddedResource, Meta, ResourceContents}; +use rmcp::model::{ContentBlock, EmbeddedResource, MetaObject, ResourceContents}; use serde_json::json; #[test] fn serialize_embedded_text_resource_with_meta() { - let mut resource_content_meta = Meta::new(); + let mut resource_content_meta = MetaObject::new(); resource_content_meta.insert("inner".to_string(), json!(2)); - let mut resource_meta = Meta::new(); + let mut resource_meta = MetaObject::new(); resource_meta.insert("top".to_string(), json!(1)); let content = ContentBlock::Resource( @@ -90,10 +90,10 @@ fn deserialize_embedded_text_resource_with_meta() { #[test] fn serialize_embedded_blob_resource_with_meta() { - let mut resource_content_meta = Meta::new(); + let mut resource_content_meta = MetaObject::new(); resource_content_meta.insert("blob_inner".to_string(), json!(true)); - let mut resource_meta = Meta::new(); + let mut resource_meta = MetaObject::new(); resource_meta.insert("blob_top".to_string(), json!("t")); let content = ContentBlock::Resource( diff --git a/crates/rmcp/tests/test_message_schema.rs b/crates/rmcp/tests/test_message_schema.rs index f05c263c..2b0990d7 100644 --- a/crates/rmcp/tests/test_message_schema.rs +++ b/crates/rmcp/tests/test_message_schema.rs @@ -61,6 +61,60 @@ mod tests { ); } + /// The three metadata definitions must expose the MCP 2026-07-28 draft + /// vocabulary: `MetaObject` is an open map, `RequestMetaObject` reserves + /// `progressToken` plus the SEP-2575 keys, and `NotificationMetaObject` + /// reserves `io.modelcontextprotocol/subscriptionId`. The keys the draft + /// marks as required stay optional because rmcp generates one schema + /// shared by every supported protocol version; draft-strict validation is + /// a runtime concern (`RequestMetaObject::missing_required_keys`). + #[test] + fn test_metadata_definitions_match_draft_schema() { + let settings = SchemaSettings::draft07(); + let schema = settings + .into_generator() + .into_root_schema_for::(); + let schema = serde_json::to_value(&schema).expect("Failed to serialize schema"); + let definitions = &schema["definitions"]; + + assert_eq!( + definitions["MetaObject"], + serde_json::json!({ + "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.", + "type": "object", + "additionalProperties": true, + }) + ); + + assert_eq!( + definitions["RequestMetaObject"], + serde_json::json!({ + "description": "Metadata reserved by MCP on requests. Extension keys are also allowed.", + "type": "object", + "properties": { + "progressToken": { "$ref": "#/definitions/ProgressToken" }, + "io.modelcontextprotocol/protocolVersion": { "type": "string" }, + "io.modelcontextprotocol/clientInfo": { "$ref": "#/definitions/Implementation" }, + "io.modelcontextprotocol/clientCapabilities": { "$ref": "#/definitions/ClientCapabilities" }, + "io.modelcontextprotocol/logLevel": { "$ref": "#/definitions/LoggingLevel" }, + }, + "additionalProperties": true, + }) + ); + + assert_eq!( + definitions["NotificationMetaObject"], + serde_json::json!({ + "description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/subscriptionId": { "$ref": "#/definitions/NumberOrString" }, + }, + "additionalProperties": true, + }) + ); + } + #[test] fn test_server_json_rpc_message_schema() { let settings = SchemaSettings::draft07(); diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index 81a5ab25..9a5871be 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -86,11 +86,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -128,11 +131,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "description": "Arguments to pass to the tool (must match the tool's input schema)", @@ -187,11 +193,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -210,11 +219,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "reason": { "type": [ @@ -334,11 +346,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "argument": { "$ref": "#/definitions/ArgumentInfo" @@ -474,11 +489,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The actual content of the message (text, image, audio, tool use, or tool result)", @@ -550,11 +568,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "action": { "description": "The user's decision on how to handle the elicitation request", @@ -638,11 +659,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -727,11 +751,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "type": [ @@ -773,11 +800,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -797,11 +827,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -873,11 +906,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -952,11 +988,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "capabilities": { "description": "The capabilities this client supports (sampling, roots, etc.)", @@ -1169,11 +1208,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "roots": { "type": "array", @@ -1212,6 +1254,11 @@ "emergency" ] }, + "MetaObject": { + "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.", + "type": "object", + "additionalProperties": true + }, "Notification": { "type": "object", "properties": { @@ -1257,6 +1304,16 @@ "params" ] }, + "NotificationMetaObject": { + "description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "additionalProperties": true + }, "NotificationNoParam": { "type": "object", "properties": { @@ -1294,11 +1351,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "cursor": { "type": [ @@ -1322,11 +1382,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "description": "An optional message describing the current progress.", @@ -1397,11 +1460,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "inputResponses": { "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", @@ -1641,6 +1707,28 @@ "params" ] }, + "RequestMetaObject": { + "description": "Metadata reserved by MCP on requests. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/clientCapabilities": { + "$ref": "#/definitions/ClientCapabilities" + }, + "io.modelcontextprotocol/clientInfo": { + "$ref": "#/definitions/Implementation" + }, + "io.modelcontextprotocol/logLevel": { + "$ref": "#/definitions/LoggingLevel" + }, + "io.modelcontextprotocol/protocolVersion": { + "type": "string" + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + } + }, + "additionalProperties": true + }, "RequestNoParam": { "type": "object", "properties": { @@ -1763,11 +1851,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this resource.", @@ -1841,11 +1932,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "mimeType": { "type": [ @@ -1869,11 +1963,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "blob": { "type": "string" @@ -1925,11 +2022,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "name": { "type": [ @@ -2116,11 +2216,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "level": { "description": "The desired logging level", @@ -2147,11 +2250,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource to subscribe to", @@ -2252,11 +2358,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -2347,11 +2456,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -2378,11 +2490,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "type": "array", @@ -2412,11 +2527,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "id": { "type": "string" @@ -2459,11 +2577,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource to unsubscribe from", diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index 81a5ab25..9a5871be 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -86,11 +86,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -128,11 +131,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "description": "Arguments to pass to the tool (must match the tool's input schema)", @@ -187,11 +193,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -210,11 +219,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "reason": { "type": [ @@ -334,11 +346,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "argument": { "$ref": "#/definitions/ArgumentInfo" @@ -474,11 +489,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The actual content of the message (text, image, audio, tool use, or tool result)", @@ -550,11 +568,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "action": { "description": "The user's decision on how to handle the elicitation request", @@ -638,11 +659,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -727,11 +751,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "type": [ @@ -773,11 +800,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -797,11 +827,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "taskId": { "type": "string" @@ -873,11 +906,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -952,11 +988,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "capabilities": { "description": "The capabilities this client supports (sampling, roots, etc.)", @@ -1169,11 +1208,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "roots": { "type": "array", @@ -1212,6 +1254,11 @@ "emergency" ] }, + "MetaObject": { + "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.", + "type": "object", + "additionalProperties": true + }, "Notification": { "type": "object", "properties": { @@ -1257,6 +1304,16 @@ "params" ] }, + "NotificationMetaObject": { + "description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "additionalProperties": true + }, "NotificationNoParam": { "type": "object", "properties": { @@ -1294,11 +1351,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "cursor": { "type": [ @@ -1322,11 +1382,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "description": "An optional message describing the current progress.", @@ -1397,11 +1460,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "inputResponses": { "description": "Client responses to server-initiated input requests from a previous\n[`InputRequiredResult`].", @@ -1641,6 +1707,28 @@ "params" ] }, + "RequestMetaObject": { + "description": "Metadata reserved by MCP on requests. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/clientCapabilities": { + "$ref": "#/definitions/ClientCapabilities" + }, + "io.modelcontextprotocol/clientInfo": { + "$ref": "#/definitions/Implementation" + }, + "io.modelcontextprotocol/logLevel": { + "$ref": "#/definitions/LoggingLevel" + }, + "io.modelcontextprotocol/protocolVersion": { + "type": "string" + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + } + }, + "additionalProperties": true + }, "RequestNoParam": { "type": "object", "properties": { @@ -1763,11 +1851,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this resource.", @@ -1841,11 +1932,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "mimeType": { "type": [ @@ -1869,11 +1963,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "blob": { "type": "string" @@ -1925,11 +2022,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "name": { "type": [ @@ -2116,11 +2216,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "level": { "description": "The desired logging level", @@ -2147,11 +2250,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource to subscribe to", @@ -2252,11 +2358,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -2347,11 +2456,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -2378,11 +2490,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "type": "array", @@ -2412,11 +2527,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "id": { "type": "string" @@ -2459,11 +2577,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource to unsubscribe from", diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index ddd0eaad..ee7ecf56 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -76,11 +76,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -171,11 +174,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The content returned by the tool (text, images, etc.)", @@ -211,11 +217,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -279,11 +288,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "reason": { "type": [ @@ -303,15 +315,89 @@ } } }, - "CompleteResult": { + "ClientCapabilities": { + "title": "Builder", + "description": "```rust\n# use rmcp::model::ClientCapabilities;\nlet cap = ClientCapabilities::builder()\n .enable_experimental()\n .build();\n```", "type": "object", "properties": { - "_meta": { + "elicitation": { + "description": "Capability to handle elicitation requests from servers for interactive user input", + "anyOf": [ + { + "$ref": "#/definitions/ElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "experimental": { "type": [ "object", "null" ], - "additionalProperties": true + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "extensions": { + "description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "roots": { + "description": "Capability for filesystem roots (deprecated by SEP-2577).", + "anyOf": [ + { + "$ref": "#/definitions/RootsCapabilities" + }, + { + "type": "null" + } + ] + }, + "sampling": { + "description": "Capability for LLM sampling requests (SEP-1577, deprecated by SEP-2577).", + "anyOf": [ + { + "$ref": "#/definitions/SamplingCapability" + }, + { + "type": "null" + } + ] + }, + "tasks": { + "anyOf": [ + { + "$ref": "#/definitions/TasksCapability" + }, + { + "type": "null" + } + ] + } + } + }, + "CompleteResult": { + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "completion": { "$ref": "#/definitions/CompletionInfo" @@ -495,11 +581,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "includeContext": { "description": "How much context to include from MCP servers", @@ -608,11 +697,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "task": { "$ref": "#/definitions/Task" @@ -658,11 +750,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "type": "string" @@ -685,11 +780,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "elicitationId": { "type": "string" @@ -716,11 +814,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "type": "string" @@ -742,11 +843,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "action": { "description": "The user's decision on how to handle the elicitation request", @@ -784,6 +888,34 @@ } ] }, + "ElicitationCapability": { + "description": "Elicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", + "type": "object", + "properties": { + "form": { + "description": "Whether client supports form-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/FormElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "url": { + "description": "Whether client supports URL-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/UrlElicitationCapability" + }, + { + "type": "null" + } + ] + } + } + }, "ElicitationCreateRequestMethod": { "type": "string", "format": "const", @@ -856,11 +988,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -935,15 +1070,31 @@ "message" ] }, - "GetPromptResult": { + "FormElicitationCapability": { + "description": "Capability for handling elicitation requests from servers.\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.\n\nCapability for form mode elicitation.", "type": "object", "properties": { - "_meta": { + "schemaValidation": { + "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", "type": [ - "object", + "boolean", "null" - ], - "additionalProperties": true + ] + } + } + }, + "GetPromptResult": { + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "description": { "type": [ @@ -979,11 +1130,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -1100,11 +1254,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -1178,11 +1335,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "capabilities": { "description": "The capabilities this server provides (tools, resources, prompts, etc.)", @@ -1257,11 +1417,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "inputRequests": { "description": "Server-initiated requests that the client must fulfill before retrying.", @@ -1524,11 +1687,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1577,14 +1743,17 @@ ] }, "ListResourceTemplatesResult": { - "type": "object", - "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1636,11 +1805,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1697,11 +1869,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "nextCursor": { "type": [ @@ -1724,11 +1899,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1801,11 +1979,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "data": { "description": "The actual log data" @@ -1832,6 +2013,11 @@ "data" ] }, + "MetaObject": { + "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.", + "type": "object", + "additionalProperties": true + }, "ModelHint": { "description": "A hint suggesting a preferred model name or family.\n\nModel hints are advisory suggestions that help clients choose appropriate\nmodels. They can be specific model names or general families like \"claude\" or \"gpt\".", "type": "object", @@ -1973,6 +2159,16 @@ "params" ] }, + "NotificationMetaObject": { + "description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "additionalProperties": true + }, "NotificationNoParam": { "type": "object", "properties": { @@ -2140,11 +2336,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "description": "An optional message describing the current progress.", @@ -2188,11 +2387,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "type": [ @@ -2303,11 +2505,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -2382,6 +2587,28 @@ "params" ] }, + "RequestMetaObject": { + "description": "Metadata reserved by MCP on requests. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/clientCapabilities": { + "$ref": "#/definitions/ClientCapabilities" + }, + "io.modelcontextprotocol/clientInfo": { + "$ref": "#/definitions/Implementation" + }, + "io.modelcontextprotocol/logLevel": { + "$ref": "#/definitions/LoggingLevel" + }, + "io.modelcontextprotocol/protocolVersion": { + "type": "string" + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + } + }, + "additionalProperties": true + }, "RequestNoParam": { "type": "object", "properties": { @@ -2410,11 +2637,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this resource.", @@ -2488,11 +2718,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "mimeType": { "type": [ @@ -2516,11 +2749,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "blob": { "type": "string" @@ -2553,11 +2789,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource template.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this template.", @@ -2625,11 +2864,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource that was updated", @@ -2676,6 +2918,40 @@ } ] }, + "RootsCapabilities": { + "description": "Roots capability. Deprecated by SEP-2577; remains functional and will be\nremoved in a future release.\nSee .", + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "SamplingCapability": { + "description": "Sampling capability with optional sub-capabilities (SEP-1577).\n\nDeprecated by SEP-2577; remains functional and will be removed in a future\nrelease.\nSee .", + "type": "object", + "properties": { + "context": { + "description": "Support for `includeContext` (soft-deprecated)", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "tools": { + "description": "Support for `tools` and `toolChoice` parameters", + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + } + }, "SamplingContent": { "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ @@ -2695,11 +2971,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The actual content of the message (text, image, audio, tool use, or tool result)", @@ -3226,11 +3505,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -3341,11 +3623,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -3478,11 +3763,14 @@ "properties": { "_meta": { "description": "Optional additional metadata for this tool", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional additional tool information.", @@ -3658,11 +3946,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "type": "array", @@ -3692,11 +3983,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "id": { "type": "string" @@ -3848,6 +4142,10 @@ "type", "enum" ] + }, + "UrlElicitationCapability": { + "description": "Capability for URL mode elicitation.", + "type": "object" } } } \ No newline at end of file diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index ddd0eaad..ee7ecf56 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -76,11 +76,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -171,11 +174,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The content returned by the tool (text, images, etc.)", @@ -211,11 +217,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -279,11 +288,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "reason": { "type": [ @@ -303,15 +315,89 @@ } } }, - "CompleteResult": { + "ClientCapabilities": { + "title": "Builder", + "description": "```rust\n# use rmcp::model::ClientCapabilities;\nlet cap = ClientCapabilities::builder()\n .enable_experimental()\n .build();\n```", "type": "object", "properties": { - "_meta": { + "elicitation": { + "description": "Capability to handle elicitation requests from servers for interactive user input", + "anyOf": [ + { + "$ref": "#/definitions/ElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "experimental": { "type": [ "object", "null" ], - "additionalProperties": true + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "extensions": { + "description": "Optional MCP extensions that the client supports (SEP-1724).\nKeys are extension identifiers (e.g., `\"io.modelcontextprotocol/ui\"`),\nvalues are per-extension settings objects. An empty object indicates\nsupport with no settings.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "roots": { + "description": "Capability for filesystem roots (deprecated by SEP-2577).", + "anyOf": [ + { + "$ref": "#/definitions/RootsCapabilities" + }, + { + "type": "null" + } + ] + }, + "sampling": { + "description": "Capability for LLM sampling requests (SEP-1577, deprecated by SEP-2577).", + "anyOf": [ + { + "$ref": "#/definitions/SamplingCapability" + }, + { + "type": "null" + } + ] + }, + "tasks": { + "anyOf": [ + { + "$ref": "#/definitions/TasksCapability" + }, + { + "type": "null" + } + ] + } + } + }, + "CompleteResult": { + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "completion": { "$ref": "#/definitions/CompletionInfo" @@ -495,11 +581,14 @@ "properties": { "_meta": { "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "includeContext": { "description": "How much context to include from MCP servers", @@ -608,11 +697,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "task": { "$ref": "#/definitions/Task" @@ -658,11 +750,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "type": "string" @@ -685,11 +780,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "elicitationId": { "type": "string" @@ -716,11 +814,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/RequestMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "type": "string" @@ -742,11 +843,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this result.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "action": { "description": "The user's decision on how to handle the elicitation request", @@ -784,6 +888,34 @@ } ] }, + "ElicitationCapability": { + "description": "Elicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", + "type": "object", + "properties": { + "form": { + "description": "Whether client supports form-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/FormElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "url": { + "description": "Whether client supports URL-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/UrlElicitationCapability" + }, + { + "type": "null" + } + ] + } + } + }, "ElicitationCreateRequestMethod": { "type": "string", "format": "const", @@ -856,11 +988,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -935,15 +1070,31 @@ "message" ] }, - "GetPromptResult": { + "FormElicitationCapability": { + "description": "Capability for handling elicitation requests from servers.\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.\n\nCapability for form mode elicitation.", "type": "object", "properties": { - "_meta": { + "schemaValidation": { + "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", "type": [ - "object", + "boolean", "null" - ], - "additionalProperties": true + ] + } + } + }, + "GetPromptResult": { + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "description": { "type": [ @@ -979,11 +1130,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -1100,11 +1254,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -1178,11 +1335,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "capabilities": { "description": "The capabilities this server provides (tools, resources, prompts, etc.)", @@ -1257,11 +1417,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "inputRequests": { "description": "Server-initiated requests that the client must fulfill before retrying.", @@ -1524,11 +1687,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1577,14 +1743,17 @@ ] }, "ListResourceTemplatesResult": { - "type": "object", - "properties": { - "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "type": "object", + "properties": { + "_meta": { + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1636,11 +1805,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1697,11 +1869,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "nextCursor": { "type": [ @@ -1724,11 +1899,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -1801,11 +1979,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "data": { "description": "The actual log data" @@ -1832,6 +2013,11 @@ "data" ] }, + "MetaObject": { + "description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.", + "type": "object", + "additionalProperties": true + }, "ModelHint": { "description": "A hint suggesting a preferred model name or family.\n\nModel hints are advisory suggestions that help clients choose appropriate\nmodels. They can be specific model names or general families like \"claude\" or \"gpt\".", "type": "object", @@ -1973,6 +2159,16 @@ "params" ] }, + "NotificationMetaObject": { + "description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/subscriptionId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "additionalProperties": true + }, "NotificationNoParam": { "type": "object", "properties": { @@ -2140,11 +2336,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "message": { "description": "An optional message describing the current progress.", @@ -2188,11 +2387,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "arguments": { "type": [ @@ -2303,11 +2505,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "cacheScope": { "description": "Scope describing who may cache this result (SEP-2549).\nRequired by spec version 2026-07-28, but optional here to maintain compatibility\nwith older spec versions.", @@ -2382,6 +2587,28 @@ "params" ] }, + "RequestMetaObject": { + "description": "Metadata reserved by MCP on requests. Extension keys are also allowed.", + "type": "object", + "properties": { + "io.modelcontextprotocol/clientCapabilities": { + "$ref": "#/definitions/ClientCapabilities" + }, + "io.modelcontextprotocol/clientInfo": { + "$ref": "#/definitions/Implementation" + }, + "io.modelcontextprotocol/logLevel": { + "$ref": "#/definitions/LoggingLevel" + }, + "io.modelcontextprotocol/protocolVersion": { + "type": "string" + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + } + }, + "additionalProperties": true + }, "RequestNoParam": { "type": "object", "properties": { @@ -2410,11 +2637,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this resource.", @@ -2488,11 +2718,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "mimeType": { "type": [ @@ -2516,11 +2749,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "blob": { "type": "string" @@ -2553,11 +2789,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this resource template.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this template.", @@ -2625,11 +2864,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "uri": { "description": "The URI of the resource that was updated", @@ -2676,6 +2918,40 @@ } ] }, + "RootsCapabilities": { + "description": "Roots capability. Deprecated by SEP-2577; remains functional and will be\nremoved in a future release.\nSee .", + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "SamplingCapability": { + "description": "Sampling capability with optional sub-capabilities (SEP-1577).\n\nDeprecated by SEP-2577; remains functional and will be removed in a future\nrelease.\nSee .", + "type": "object", + "properties": { + "context": { + "description": "Support for `includeContext` (soft-deprecated)", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "tools": { + "description": "Support for `tools` and `toolChoice` parameters", + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + } + }, "SamplingContent": { "description": "Single or array content wrapper (SEP-1577).", "anyOf": [ @@ -2695,11 +2971,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "description": "The actual content of the message (text, image, audio, tool use, or tool result)", @@ -3226,11 +3505,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/NotificationMetaObject" + }, + { + "type": "null" + } + ] }, "createdAt": { "description": "ISO-8601 creation timestamp.", @@ -3341,11 +3623,14 @@ "properties": { "_meta": { "description": "Optional protocol-level metadata for this content block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional annotations describing how the client should use this content.", @@ -3478,11 +3763,14 @@ "properties": { "_meta": { "description": "Optional additional metadata for this tool", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "annotations": { "description": "Optional additional tool information.", @@ -3658,11 +3946,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "content": { "type": "array", @@ -3692,11 +3983,14 @@ "type": "object", "properties": { "_meta": { - "type": [ - "object", - "null" - ], - "additionalProperties": true + "anyOf": [ + { + "$ref": "#/definitions/MetaObject" + }, + { + "type": "null" + } + ] }, "id": { "type": "string" @@ -3848,6 +4142,10 @@ "type", "enum" ] + }, + "UrlElicitationCapability": { + "description": "Capability for URL mode elicitation.", + "type": "object" } } } \ No newline at end of file diff --git a/crates/rmcp/tests/test_meta_helpers.rs b/crates/rmcp/tests/test_meta_helpers.rs index a22a420f..a45c6a4e 100644 --- a/crates/rmcp/tests/test_meta_helpers.rs +++ b/crates/rmcp/tests/test_meta_helpers.rs @@ -1,6 +1,8 @@ #![allow(deprecated)] -use rmcp::model::{ClientCapabilities, Implementation, LoggingLevel, Meta, ProtocolVersion}; +use rmcp::model::{ + ClientCapabilities, Implementation, LoggingLevel, ProtocolVersion, RequestMetaObject, +}; use serde_json::json; const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion"; @@ -10,7 +12,7 @@ const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel"; #[test] fn meta_setters_store_sep_2575_values() { - let mut meta = Meta::new(); + let mut meta = RequestMetaObject::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()); @@ -30,7 +32,7 @@ fn meta_setters_store_sep_2575_values() { #[test] fn meta_accessors_decode_wire_values() { - let meta: Meta = serde_json::from_value(json!({ + let meta: RequestMetaObject = serde_json::from_value(json!({ "progressToken": "progress-1", "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { @@ -58,7 +60,7 @@ fn meta_accessors_decode_wire_values() { #[test] fn meta_accessors_ignore_missing_or_malformed_values() { - let meta: Meta = serde_json::from_value(json!({ + let meta: RequestMetaObject = serde_json::from_value(json!({ "io.modelcontextprotocol/protocolVersion": 20260728, "io.modelcontextprotocol/clientInfo": "not an implementation", "io.modelcontextprotocol/clientCapabilities": "not capabilities", diff --git a/crates/rmcp/tests/test_progress_subscriber.rs b/crates/rmcp/tests/test_progress_subscriber.rs index 18f91c21..8df1b7ca 100644 --- a/crates/rmcp/tests/test_progress_subscriber.rs +++ b/crates/rmcp/tests/test_progress_subscriber.rs @@ -3,7 +3,9 @@ use futures::StreamExt; use rmcp::{ ClientHandler, Peer, RoleServer, ServerHandler, ServiceExt, handler::{client::progress::ProgressDispatcher, server::tool::ToolRouter}, - model::{CallToolRequestParams, ClientRequest, Meta, ProgressNotificationParam, Request}, + model::{ + CallToolRequestParams, ClientRequest, ProgressNotificationParam, Request, RequestMetaObject, + }, service::PeerRequestOptions, tool, tool_handler, tool_router, }; @@ -61,7 +63,7 @@ impl Default for MyServer { impl MyServer { #[tool] pub async fn some_progress( - meta: Meta, + meta: RequestMetaObject, client: Peer, ) -> Result<(), rmcp::ErrorData> { let progress_token = meta diff --git a/crates/rmcp/tests/test_request_timeout_progress.rs b/crates/rmcp/tests/test_request_timeout_progress.rs index 6eeac42e..f37101f8 100644 --- a/crates/rmcp/tests/test_request_timeout_progress.rs +++ b/crates/rmcp/tests/test_request_timeout_progress.rs @@ -11,8 +11,8 @@ use std::{ use rmcp::{ ClientHandler, Peer, RoleServer, ServiceError, ServiceExt, model::{ - CallToolRequestParams, ClientRequest, Meta, NumberOrString, ProgressNotificationParam, - ProgressToken, Request, + CallToolRequestParams, ClientRequest, NumberOrString, ProgressNotificationParam, + ProgressToken, Request, RequestMetaObject, }, service::PeerRequestOptions, tool, tool_router, @@ -46,7 +46,7 @@ impl ProgressTimeoutServer { #[tool] async fn delayed_with_progress( &self, - meta: Meta, + meta: RequestMetaObject, client: Peer, ) -> Result<(), rmcp::ErrorData> { let progress_token = meta @@ -173,7 +173,7 @@ async fn generated_progress_token_overrides_option_meta_token() -> anyhow::Resul let client = start_pair().await?; let mut options = PeerRequestOptions::with_timeout(Duration::from_millis(75)).reset_timeout_on_progress(); - options.meta = Some(Meta::with_progress_token(ProgressToken( + options.meta = Some(RequestMetaObject::with_progress_token(ProgressToken( NumberOrString::Number(999_999), ))); diff --git a/crates/rmcp/tests/test_tool_result_meta.rs b/crates/rmcp/tests/test_tool_result_meta.rs index d164e843..37585ac7 100644 --- a/crates/rmcp/tests/test_tool_result_meta.rs +++ b/crates/rmcp/tests/test_tool_result_meta.rs @@ -1,10 +1,10 @@ -use rmcp::model::{CallToolResult, ContentBlock, Meta}; +use rmcp::model::{CallToolResult, ContentBlock, MetaObject}; use serde_json::{Value, json}; #[test] fn serialize_tool_result_with_meta() { let content = vec![ContentBlock::text("ok")]; - let mut meta = Meta::new(); + let mut meta = MetaObject::new(); meta.insert("foo".to_string(), json!("bar")); let result = CallToolResult::success(content).with_meta(Some(meta)); let v = serde_json::to_value(&result).unwrap(); diff --git a/crates/rmcp/tests/test_trace_context.rs b/crates/rmcp/tests/test_trace_context.rs index 50214b71..15354a49 100644 --- a/crates/rmcp/tests/test_trace_context.rs +++ b/crates/rmcp/tests/test_trace_context.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use rmcp::{ RoleServer, ServerHandler, ServiceExt, - model::{ClientRequest, CustomRequest, CustomResult, Meta}, + model::{ClientRequest, CustomRequest, CustomResult, RequestMetaObject}, service::{PeerRequestOptions, RequestContext}, }; use serde_json::json; @@ -17,7 +17,7 @@ const BAGGAGE: &str = "userId=alice,region=us-east-1"; /// Records the `_meta` it receives on the incoming request so the test can assert passthrough. struct TraceCapturingServer { receive_signal: Arc, - seen: Arc>>, + seen: Arc>>, } impl ServerHandler for TraceCapturingServer { @@ -56,7 +56,7 @@ async fn trace_context_meta_survives_round_trip() -> anyhow::Result<()> { let client = ().serve(client_transport).await?; // Client attaches trace context to the outgoing request's `_meta`. - let mut meta = Meta::new(); + let mut meta = RequestMetaObject::new(); meta.set_traceparent(TRACEPARENT); meta.set_tracestate(TRACESTATE); meta.set_baggage(BAGGAGE); diff --git a/examples/servers/src/common/counter.rs b/examples/servers/src/common/counter.rs index 29258a98..3cac2b2b 100644 --- a/examples/servers/src/common/counter.rs +++ b/examples/servers/src/common/counter.rs @@ -179,7 +179,7 @@ impl Counter { /// This is an example prompt that takes one required argument, message #[prompt( name = "example_prompt", - meta = Meta(rmcp::object!({"meta_key": "meta_value"})) + meta = MetaObject(rmcp::object!({"meta_key": "meta_value"})) )] async fn example_prompt( &self, @@ -225,8 +225,8 @@ impl Counter { } } -#[tool_handler(meta = Meta(rmcp::object!({"tool_meta_key": "tool_meta_value"})))] -#[prompt_handler(meta = Meta(rmcp::object!({"router_meta_key": "router_meta_value"})))] +#[tool_handler(meta = MetaObject(rmcp::object!({"tool_meta_key": "tool_meta_value"})))] +#[prompt_handler(meta = MetaObject(rmcp::object!({"router_meta_key": "router_meta_value"})))] #[task_handler] impl ServerHandler for Counter { fn get_info(&self) -> ServerInfo {