feat!: align metadata models with draft schema (#993)
This commit is contained in:
parent
92581bee53
commit
839922d8fd
8 changed files with 2539 additions and 1059 deletions
|
|
@ -12,7 +12,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{Annotations, Meta, resource::ResourceContents};
|
||||
use super::{Annotations, MetaObject, resource::ResourceContents};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flat content structs
|
||||
|
|
@ -28,7 +28,7 @@ pub struct TextContent {
|
|||
pub text: String,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -43,7 +43,7 @@ impl TextContent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ pub struct ImageContent {
|
|||
pub mime_type: String,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -82,7 +82,7 @@ impl ImageContent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ pub struct AudioContent {
|
|||
pub mime_type: String,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -121,7 +121,7 @@ impl AudioContent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ pub struct EmbeddedResource {
|
|||
pub resource: ResourceContents,
|
||||
/// Optional protocol-level metadata for this content block.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this content.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -164,7 +164,7 @@ impl EmbeddedResource {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ pub struct ToolUseContent {
|
|||
pub name: String,
|
||||
pub input: super::JsonObject,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
/// Tool execution result in user message (SEP-1577).
|
||||
|
|
@ -203,7 +203,7 @@ pub struct ToolUseContent {
|
|||
)]
|
||||
pub struct ToolResultContent {
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
pub tool_use_id: String,
|
||||
pub content: Vec<ContentBlock>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -402,7 +402,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_audio_content_has_meta() {
|
||||
let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default());
|
||||
let audio = AudioContent::new("data", "audio/wav").with_meta(MetaObject::default());
|
||||
let json = serde_json::to_value(&audio).unwrap();
|
||||
assert!(json.get("_meta").is_some());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,29 @@ use serde_json::Value;
|
|||
|
||||
use super::{
|
||||
ClientCapabilities, ClientNotification, ClientRequest, CustomNotification, CustomRequest,
|
||||
Extensions, Implementation, JsonObject, JsonRpcMessage, LoggingLevel, NumberOrString,
|
||||
ProgressToken, ProtocolVersion, ServerNotification, ServerRequest, TaskMetadata,
|
||||
Extensions, Implementation, JsonObject, JsonRpcMessage, LoggingLevel, ProgressToken,
|
||||
ProtocolVersion, RequestId, ServerNotification, ServerRequest, TaskMetadata,
|
||||
};
|
||||
|
||||
/// Access to the metadata carried by a message envelope's [`Extensions`].
|
||||
///
|
||||
/// The metadata type differs by message kind: requests carry a
|
||||
/// [`RequestMetaObject`] and notifications carry a [`NotificationMetaObject`].
|
||||
///
|
||||
/// The envelope extensions are the canonical runtime location for `_meta`:
|
||||
/// deserialization strips the wire `params._meta` into the extensions (typed
|
||||
/// params `meta` fields stay empty), and the service loop moves it into
|
||||
/// [`RequestContext::meta`] / [`NotificationContext::meta`] before dispatch.
|
||||
/// Typed params `meta` fields are honored when serializing outgoing messages;
|
||||
/// on key conflicts the extensions-level metadata wins.
|
||||
///
|
||||
/// [`RequestContext::meta`]: crate::service::RequestContext
|
||||
/// [`NotificationContext::meta`]: crate::service::NotificationContext
|
||||
pub trait GetMeta {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta;
|
||||
fn get_meta(&self) -> &Meta;
|
||||
/// The metadata type for this message kind.
|
||||
type Metadata: Default;
|
||||
fn get_meta_mut(&mut self) -> &mut Self::Metadata;
|
||||
fn get_meta(&self) -> &Self::Metadata;
|
||||
}
|
||||
|
||||
pub trait GetExtensions {
|
||||
|
|
@ -21,15 +37,16 @@ pub trait GetExtensions {
|
|||
|
||||
/// Trait for request params that contain the `_meta` field.
|
||||
///
|
||||
/// Per the MCP 2025-11-25 spec, all request params should have an optional `_meta`
|
||||
/// field that can contain a `progressToken` for tracking long-running operations.
|
||||
/// Per the MCP spec, all request params may have an optional `_meta`
|
||||
/// field ([`RequestMetaObject`]) that can contain a `progressToken` for
|
||||
/// tracking long-running operations.
|
||||
pub trait RequestParamsMeta {
|
||||
/// Get a reference to the meta field
|
||||
fn meta(&self) -> Option<&Meta>;
|
||||
fn meta(&self) -> Option<&RequestMetaObject>;
|
||||
/// Get a mutable reference to the meta field
|
||||
fn meta_mut(&mut self) -> &mut Option<Meta>;
|
||||
fn meta_mut(&mut self) -> &mut Option<RequestMetaObject>;
|
||||
/// Set the meta field
|
||||
fn set_meta(&mut self, meta: Meta) {
|
||||
fn set_meta(&mut self, meta: RequestMetaObject) {
|
||||
*self.meta_mut() = Some(meta);
|
||||
}
|
||||
/// Get the progress token from meta, if present
|
||||
|
|
@ -41,7 +58,7 @@ pub trait RequestParamsMeta {
|
|||
match self.meta_mut() {
|
||||
Some(meta) => meta.set_progress_token(token),
|
||||
none => {
|
||||
let mut meta = Meta::new();
|
||||
let mut meta = RequestMetaObject::new();
|
||||
meta.set_progress_token(token);
|
||||
*none = Some(meta);
|
||||
}
|
||||
|
|
@ -72,8 +89,8 @@ pub trait RequestParamsMeta {
|
|||
self.meta_or_default().set_baggage(value);
|
||||
}
|
||||
/// Get a mutable reference to meta, inserting an empty one if absent.
|
||||
fn meta_or_default(&mut self) -> &mut Meta {
|
||||
self.meta_mut().get_or_insert_with(Meta::new)
|
||||
fn meta_or_default(&mut self) -> &mut RequestMetaObject {
|
||||
self.meta_mut().get_or_insert_with(RequestMetaObject::new)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,13 +119,14 @@ impl GetExtensions for CustomNotification {
|
|||
}
|
||||
|
||||
impl GetMeta for CustomNotification {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta {
|
||||
type Metadata = NotificationMetaObject;
|
||||
fn get_meta_mut(&mut self) -> &mut NotificationMetaObject {
|
||||
self.extensions_mut().get_or_insert_default()
|
||||
}
|
||||
fn get_meta(&self) -> &Meta {
|
||||
fn get_meta(&self) -> &NotificationMetaObject {
|
||||
self.extensions()
|
||||
.get::<Meta>()
|
||||
.unwrap_or(Meta::static_empty())
|
||||
.get::<NotificationMetaObject>()
|
||||
.unwrap_or(NotificationMetaObject::static_empty())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,19 +140,20 @@ impl GetExtensions for CustomRequest {
|
|||
}
|
||||
|
||||
impl GetMeta for CustomRequest {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta {
|
||||
type Metadata = RequestMetaObject;
|
||||
fn get_meta_mut(&mut self) -> &mut RequestMetaObject {
|
||||
self.extensions_mut().get_or_insert_default()
|
||||
}
|
||||
fn get_meta(&self) -> &Meta {
|
||||
fn get_meta(&self) -> &RequestMetaObject {
|
||||
self.extensions()
|
||||
.get::<Meta>()
|
||||
.unwrap_or(Meta::static_empty())
|
||||
.get::<RequestMetaObject>()
|
||||
.unwrap_or(RequestMetaObject::static_empty())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! variant_extension {
|
||||
(
|
||||
$Enum: ident {
|
||||
$Enum: ident: $Metadata: ident {
|
||||
$($variant: ident)*
|
||||
}
|
||||
) => {
|
||||
|
|
@ -155,18 +174,19 @@ macro_rules! variant_extension {
|
|||
}
|
||||
}
|
||||
impl GetMeta for $Enum {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta {
|
||||
type Metadata = $Metadata;
|
||||
fn get_meta_mut(&mut self) -> &mut $Metadata {
|
||||
self.extensions_mut().get_or_insert_default()
|
||||
}
|
||||
fn get_meta(&self) -> &Meta {
|
||||
self.extensions().get::<Meta>().unwrap_or(Meta::static_empty())
|
||||
fn get_meta(&self) -> &$Metadata {
|
||||
self.extensions().get::<$Metadata>().unwrap_or($Metadata::static_empty())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
variant_extension! {
|
||||
ClientRequest {
|
||||
ClientRequest: RequestMetaObject {
|
||||
PingRequest
|
||||
InitializeRequest
|
||||
CompleteRequest
|
||||
|
|
@ -189,7 +209,7 @@ variant_extension! {
|
|||
}
|
||||
|
||||
variant_extension! {
|
||||
ServerRequest {
|
||||
ServerRequest: RequestMetaObject {
|
||||
PingRequest
|
||||
CreateMessageRequest
|
||||
ListRootsRequest
|
||||
|
|
@ -199,7 +219,7 @@ variant_extension! {
|
|||
}
|
||||
|
||||
variant_extension! {
|
||||
ClientNotification {
|
||||
ClientNotification: NotificationMetaObject {
|
||||
CancelledNotification
|
||||
ProgressNotification
|
||||
InitializedNotification
|
||||
|
|
@ -210,7 +230,7 @@ variant_extension! {
|
|||
}
|
||||
|
||||
variant_extension! {
|
||||
ServerNotification {
|
||||
ServerNotification: NotificationMetaObject {
|
||||
CancelledNotification
|
||||
ProgressNotification
|
||||
LoggingMessageNotification
|
||||
|
|
@ -222,18 +242,31 @@ variant_extension! {
|
|||
CustomNotification
|
||||
}
|
||||
}
|
||||
|
||||
/// General-purpose `_meta` map (spec `MetaObject`).
|
||||
///
|
||||
/// This is the metadata shape used by results, content blocks, and catalog
|
||||
/// descriptors (tools, prompts, resources, roots, ...). It preserves arbitrary
|
||||
/// extension keys and offers helpers for the reserved W3C Trace Context keys
|
||||
/// (SEP-414).
|
||||
///
|
||||
/// Request and notification `_meta` maps have additional reserved keys; see
|
||||
/// [`RequestMetaObject`] and [`NotificationMetaObject`].
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(transparent)]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct Meta(pub JsonObject);
|
||||
pub struct MetaObject(pub JsonObject);
|
||||
|
||||
impl Meta {
|
||||
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
|
||||
const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
|
||||
const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
|
||||
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
|
||||
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
|
||||
/// Deprecated alias for [`MetaObject`].
|
||||
///
|
||||
/// This is a re-export rather than a type alias so the `Meta(...)` tuple
|
||||
/// constructor keeps working. Request and notification metadata now have
|
||||
/// dedicated types; use [`RequestMetaObject`] or [`NotificationMetaObject`]
|
||||
/// where those are expected.
|
||||
#[deprecated(note = "Use MetaObject (or RequestMetaObject / NotificationMetaObject)")]
|
||||
pub use self::MetaObject as Meta;
|
||||
|
||||
impl MetaObject {
|
||||
/// Reserved `_meta` key for the W3C Trace Context `traceparent` value (SEP-414).
|
||||
const TRACEPARENT_FIELD: &str = "traceparent";
|
||||
/// Reserved `_meta` key for the W3C Trace Context `tracestate` value (SEP-414).
|
||||
|
|
@ -241,102 +274,11 @@ impl Meta {
|
|||
/// Reserved `_meta` key for the W3C Baggage value (SEP-414).
|
||||
const BAGGAGE_FIELD: &str = "baggage";
|
||||
|
||||
/// Create an empty metadata map.
|
||||
pub fn new() -> Self {
|
||||
Self(JsonObject::new())
|
||||
}
|
||||
|
||||
/// Create a new Meta with a progress token set
|
||||
pub fn with_progress_token(token: ProgressToken) -> Self {
|
||||
let mut meta = Self::new();
|
||||
meta.set_progress_token(token);
|
||||
meta
|
||||
}
|
||||
|
||||
pub(crate) fn static_empty() -> &'static Self {
|
||||
static EMPTY: std::sync::OnceLock<Meta> = std::sync::OnceLock::new();
|
||||
EMPTY.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub fn get_progress_token(&self) -> Option<ProgressToken> {
|
||||
self.0
|
||||
.get(Self::PROGRESS_TOKEN_FIELD)
|
||||
.and_then(|v| match v {
|
||||
Value::String(s) => {
|
||||
Some(ProgressToken(NumberOrString::String(s.to_string().into())))
|
||||
}
|
||||
Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
Some(ProgressToken(NumberOrString::Number(i)))
|
||||
} else if let Some(u) = n.as_u64() {
|
||||
if u <= i64::MAX as u64 {
|
||||
Some(ProgressToken(NumberOrString::Number(u as i64)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_progress_token(&mut self, token: ProgressToken) {
|
||||
match token.0 {
|
||||
NumberOrString::String(ref s) => self.0.insert(
|
||||
Self::PROGRESS_TOKEN_FIELD.to_string(),
|
||||
Value::String(s.to_string()),
|
||||
),
|
||||
NumberOrString::Number(n) => self.0.insert(
|
||||
Self::PROGRESS_TOKEN_FIELD.to_string(),
|
||||
Value::Number(n.into()),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the MCP protocol version carried in `_meta`, if present and valid.
|
||||
pub fn protocol_version(&self) -> Option<ProtocolVersion> {
|
||||
self.decode_value(Self::META_KEY_PROTOCOL_VERSION)
|
||||
}
|
||||
|
||||
/// Set the MCP protocol version carried in `_meta`.
|
||||
pub fn set_protocol_version(&mut self, protocol_version: ProtocolVersion) {
|
||||
self.0.insert(
|
||||
Self::META_KEY_PROTOCOL_VERSION.to_string(),
|
||||
Value::String(protocol_version.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the client implementation identity carried in `_meta`, if present and valid.
|
||||
pub fn client_info(&self) -> Option<Implementation> {
|
||||
self.decode_value(Self::META_KEY_CLIENT_INFO)
|
||||
}
|
||||
|
||||
/// Set the client implementation identity carried in `_meta`.
|
||||
pub fn set_client_info(&mut self, client_info: Implementation) {
|
||||
self.insert_serialized(Self::META_KEY_CLIENT_INFO, client_info);
|
||||
}
|
||||
|
||||
/// Get the client capabilities carried in `_meta`, if present and valid.
|
||||
pub fn client_capabilities(&self) -> Option<ClientCapabilities> {
|
||||
self.decode_value(Self::META_KEY_CLIENT_CAPABILITIES)
|
||||
}
|
||||
|
||||
/// Set the client capabilities carried in `_meta`.
|
||||
pub fn set_client_capabilities(&mut self, client_capabilities: ClientCapabilities) {
|
||||
self.insert_serialized(Self::META_KEY_CLIENT_CAPABILITIES, client_capabilities);
|
||||
}
|
||||
|
||||
/// Get the requested per-request log level carried in `_meta`, if present and valid.
|
||||
pub fn log_level(&self) -> Option<LoggingLevel> {
|
||||
self.decode_value(Self::META_KEY_LOG_LEVEL)
|
||||
}
|
||||
|
||||
/// Set the requested per-request log level carried in `_meta`.
|
||||
pub fn set_log_level(&mut self, log_level: LoggingLevel) {
|
||||
self.insert_serialized(Self::META_KEY_LOG_LEVEL, log_level);
|
||||
}
|
||||
|
||||
/// Read a string-valued `_meta` field, or `None` if absent or not a string.
|
||||
fn get_str(&self, field: &str) -> Option<&str> {
|
||||
self.0.get(field).and_then(Value::as_str)
|
||||
|
|
@ -356,9 +298,9 @@ impl Meta {
|
|||
/// Set the W3C `traceparent` value (SEP-414).
|
||||
///
|
||||
/// ```
|
||||
/// use rmcp::model::Meta;
|
||||
/// use rmcp::model::MetaObject;
|
||||
///
|
||||
/// let mut meta = Meta::new();
|
||||
/// let mut meta = MetaObject::new();
|
||||
/// meta.set_traceparent("00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01");
|
||||
/// assert_eq!(
|
||||
/// meta.get_traceparent(),
|
||||
|
|
@ -389,10 +331,9 @@ impl Meta {
|
|||
self.set_str(Self::BAGGAGE_FIELD, value);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: Meta) {
|
||||
for (k, v) in other.0.into_iter() {
|
||||
self.0.insert(k, v);
|
||||
}
|
||||
/// Insert every entry of `other`, overwriting existing keys on conflict.
|
||||
pub fn extend(&mut self, other: MetaObject) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
|
||||
fn decode_value<T>(&self, key: &str) -> Option<T>
|
||||
|
|
@ -412,7 +353,7 @@ impl Meta {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deref for Meta {
|
||||
impl Deref for MetaObject {
|
||||
type Target = JsonObject;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
|
@ -420,12 +361,353 @@ impl Deref for Meta {
|
|||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Meta {
|
||||
impl DerefMut for MetaObject {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonObject> for MetaObject {
|
||||
fn from(object: JsonObject) -> Self {
|
||||
Self(object)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for MetaObject {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("MetaObject")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"description": "See [specification/draft/basic/index#general-fields] for notes on _meta usage.",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The `_meta` map carried by requests (spec `RequestMetaObject`).
|
||||
///
|
||||
/// In addition to arbitrary extension keys, requests reserve:
|
||||
/// - `progressToken` for progress tracking
|
||||
/// - `io.modelcontextprotocol/protocolVersion` (SEP-2575)
|
||||
/// - `io.modelcontextprotocol/clientInfo` (SEP-2575)
|
||||
/// - `io.modelcontextprotocol/clientCapabilities` (SEP-2575)
|
||||
/// - `io.modelcontextprotocol/logLevel` (SEP-2575)
|
||||
///
|
||||
/// The 2026-07-28 draft schema marks the protocol-version, client-info, and
|
||||
/// client-capabilities keys as required; earlier protocol versions do not know
|
||||
/// them. All keys therefore stay optional at runtime and in the generated
|
||||
/// (version-shared) JSON schema — use
|
||||
/// [`RequestMetaObject::missing_required_keys`] to validate a request against
|
||||
/// the negotiated protocol version.
|
||||
///
|
||||
/// This type dereferences to [`MetaObject`] (and transitively to the underlying
|
||||
/// map), so general helpers such as the SEP-414 trace-context accessors remain
|
||||
/// available.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct RequestMetaObject(pub MetaObject);
|
||||
|
||||
impl RequestMetaObject {
|
||||
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
|
||||
const META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
|
||||
const META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
|
||||
const META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
|
||||
const META_KEY_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
|
||||
|
||||
/// Request `_meta` keys the 2026-07-28 draft schema marks as required.
|
||||
pub const DRAFT_REQUIRED_KEYS: [&str; 3] = [
|
||||
Self::META_KEY_PROTOCOL_VERSION,
|
||||
Self::META_KEY_CLIENT_INFO,
|
||||
Self::META_KEY_CLIENT_CAPABILITIES,
|
||||
];
|
||||
|
||||
/// Create an empty request metadata map.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create a new request meta with a progress token set
|
||||
pub fn with_progress_token(token: ProgressToken) -> Self {
|
||||
let mut meta = Self::new();
|
||||
meta.set_progress_token(token);
|
||||
meta
|
||||
}
|
||||
|
||||
pub(crate) fn static_empty() -> &'static Self {
|
||||
static EMPTY: std::sync::OnceLock<RequestMetaObject> = std::sync::OnceLock::new();
|
||||
EMPTY.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Get the progress token carried in `_meta`, if present and valid.
|
||||
pub fn get_progress_token(&self) -> Option<ProgressToken> {
|
||||
self.0.decode_value(Self::PROGRESS_TOKEN_FIELD)
|
||||
}
|
||||
|
||||
/// Set the progress token carried in `_meta`.
|
||||
pub fn set_progress_token(&mut self, token: ProgressToken) {
|
||||
self.0.insert_serialized(Self::PROGRESS_TOKEN_FIELD, token);
|
||||
}
|
||||
|
||||
/// Get the MCP protocol version carried in `_meta`, if present and valid.
|
||||
pub fn protocol_version(&self) -> Option<ProtocolVersion> {
|
||||
self.0.decode_value(Self::META_KEY_PROTOCOL_VERSION)
|
||||
}
|
||||
|
||||
/// Set the MCP protocol version carried in `_meta`.
|
||||
pub fn set_protocol_version(&mut self, protocol_version: ProtocolVersion) {
|
||||
self.0.0.insert(
|
||||
Self::META_KEY_PROTOCOL_VERSION.to_string(),
|
||||
Value::String(protocol_version.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the client implementation identity carried in `_meta`, if present and valid.
|
||||
pub fn client_info(&self) -> Option<Implementation> {
|
||||
self.0.decode_value(Self::META_KEY_CLIENT_INFO)
|
||||
}
|
||||
|
||||
/// Set the client implementation identity carried in `_meta`.
|
||||
pub fn set_client_info(&mut self, client_info: Implementation) {
|
||||
self.0
|
||||
.insert_serialized(Self::META_KEY_CLIENT_INFO, client_info);
|
||||
}
|
||||
|
||||
/// Get the client capabilities carried in `_meta`, if present and valid.
|
||||
pub fn client_capabilities(&self) -> Option<ClientCapabilities> {
|
||||
self.0.decode_value(Self::META_KEY_CLIENT_CAPABILITIES)
|
||||
}
|
||||
|
||||
/// Set the client capabilities carried in `_meta`.
|
||||
pub fn set_client_capabilities(&mut self, client_capabilities: ClientCapabilities) {
|
||||
self.0
|
||||
.insert_serialized(Self::META_KEY_CLIENT_CAPABILITIES, client_capabilities);
|
||||
}
|
||||
|
||||
/// Get the requested per-request log level carried in `_meta`, if present and valid.
|
||||
pub fn log_level(&self) -> Option<LoggingLevel> {
|
||||
self.0.decode_value(Self::META_KEY_LOG_LEVEL)
|
||||
}
|
||||
|
||||
/// Set the requested per-request log level carried in `_meta`.
|
||||
pub fn set_log_level(&mut self, log_level: LoggingLevel) {
|
||||
self.0
|
||||
.insert_serialized(Self::META_KEY_LOG_LEVEL, log_level);
|
||||
}
|
||||
|
||||
/// Return the [`Self::DRAFT_REQUIRED_KEYS`] whose values are absent or
|
||||
/// invalid in this map, if `protocol_version` requires them.
|
||||
///
|
||||
/// A key counts as missing when it is not present *or* when its value does
|
||||
/// not decode into the expected type (e.g. a numeric `protocolVersion` or
|
||||
/// a string `clientInfo`), matching what the typed accessors return.
|
||||
///
|
||||
/// Protocol versions before 2026-07-28 have no required request metadata,
|
||||
/// so this always returns an empty list for them.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rmcp::model::{ProtocolVersion, RequestMetaObject};
|
||||
///
|
||||
/// let meta = RequestMetaObject::new();
|
||||
/// // Older protocols have no required request metadata.
|
||||
/// assert!(
|
||||
/// meta.missing_required_keys(&ProtocolVersion::V_2025_11_25)
|
||||
/// .is_empty()
|
||||
/// );
|
||||
/// // The 2026-07-28 draft requires the SEP-2575 keys.
|
||||
/// assert_eq!(
|
||||
/// meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
|
||||
/// RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec(),
|
||||
/// );
|
||||
/// ```
|
||||
pub fn missing_required_keys(&self, protocol_version: &ProtocolVersion) -> Vec<&'static str> {
|
||||
if protocol_version.as_str() < ProtocolVersion::V_2026_07_28.as_str() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut missing = Vec::new();
|
||||
if self.protocol_version().is_none() {
|
||||
missing.push(Self::META_KEY_PROTOCOL_VERSION);
|
||||
}
|
||||
if self.client_info().is_none() {
|
||||
missing.push(Self::META_KEY_CLIENT_INFO);
|
||||
}
|
||||
if self.client_capabilities().is_none() {
|
||||
missing.push(Self::META_KEY_CLIENT_CAPABILITIES);
|
||||
}
|
||||
missing
|
||||
}
|
||||
|
||||
/// Insert every entry of `other`, overwriting existing keys on conflict.
|
||||
pub fn extend(&mut self, other: RequestMetaObject) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RequestMetaObject {
|
||||
type Target = MetaObject;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RequestMetaObject {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MetaObject> for RequestMetaObject {
|
||||
fn from(meta: MetaObject) -> Self {
|
||||
Self(meta)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonObject> for RequestMetaObject {
|
||||
fn from(object: JsonObject) -> Self {
|
||||
Self(MetaObject(object))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for RequestMetaObject {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("RequestMetaObject")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let progress_token = generator.subschema_for::<ProgressToken>();
|
||||
let client_info = generator.subschema_for::<Implementation>();
|
||||
let client_capabilities = generator.subschema_for::<ClientCapabilities>();
|
||||
let log_level = generator.subschema_for::<LoggingLevel>();
|
||||
// rmcp generates one schema shared by every supported protocol
|
||||
// version, so the keys the 2026-07-28 draft marks as required are left
|
||||
// optional here: a 2025-11-25 request whose `_meta` only carries
|
||||
// `progressToken` is valid. Draft-strict validation is available at
|
||||
// runtime via [`RequestMetaObject::missing_required_keys`].
|
||||
schemars::json_schema!({
|
||||
"description": "Metadata reserved by MCP on requests. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"progressToken": progress_token,
|
||||
"io.modelcontextprotocol/protocolVersion": {
|
||||
"type": "string",
|
||||
},
|
||||
"io.modelcontextprotocol/clientInfo": client_info,
|
||||
"io.modelcontextprotocol/clientCapabilities": client_capabilities,
|
||||
"io.modelcontextprotocol/logLevel": log_level,
|
||||
},
|
||||
"additionalProperties": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The `_meta` map carried by notifications (spec `NotificationMetaObject`).
|
||||
///
|
||||
/// In addition to arbitrary extension keys, notifications reserve
|
||||
/// `io.modelcontextprotocol/subscriptionId` to correlate a notification with a
|
||||
/// prior subscription request.
|
||||
///
|
||||
/// This type dereferences to [`MetaObject`] (and transitively to the underlying
|
||||
/// map), so general helpers such as the SEP-414 trace-context accessors remain
|
||||
/// available.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
|
||||
pub struct NotificationMetaObject(pub MetaObject);
|
||||
|
||||
impl NotificationMetaObject {
|
||||
const META_KEY_SUBSCRIPTION_ID: &str = "io.modelcontextprotocol/subscriptionId";
|
||||
|
||||
/// Create an empty notification metadata map.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn static_empty() -> &'static Self {
|
||||
static EMPTY: std::sync::OnceLock<NotificationMetaObject> = std::sync::OnceLock::new();
|
||||
EMPTY.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Get the subscription id carried in `_meta`, if present and valid.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use rmcp::model::{NotificationMetaObject, RequestId};
|
||||
///
|
||||
/// let mut meta = NotificationMetaObject::new();
|
||||
/// assert_eq!(meta.subscription_id(), None);
|
||||
/// meta.set_subscription_id(RequestId::Number(7));
|
||||
/// assert_eq!(meta.subscription_id(), Some(RequestId::Number(7)));
|
||||
/// ```
|
||||
pub fn subscription_id(&self) -> Option<RequestId> {
|
||||
self.0.decode_value(Self::META_KEY_SUBSCRIPTION_ID)
|
||||
}
|
||||
|
||||
/// Set the subscription id carried in `_meta`.
|
||||
pub fn set_subscription_id(&mut self, subscription_id: RequestId) {
|
||||
self.0
|
||||
.insert_serialized(Self::META_KEY_SUBSCRIPTION_ID, subscription_id);
|
||||
}
|
||||
|
||||
/// Insert every entry of `other`, overwriting existing keys on conflict.
|
||||
pub fn extend(&mut self, other: NotificationMetaObject) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NotificationMetaObject {
|
||||
type Target = MetaObject;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotificationMetaObject {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MetaObject> for NotificationMetaObject {
|
||||
fn from(meta: MetaObject) -> Self {
|
||||
Self(meta)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonObject> for NotificationMetaObject {
|
||||
fn from(object: JsonObject) -> Self {
|
||||
Self(MetaObject(object))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for NotificationMetaObject {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("NotificationMetaObject")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let subscription_id = generator.subschema_for::<RequestId>();
|
||||
schemars::json_schema!({
|
||||
"description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"io.modelcontextprotocol/subscriptionId": subscription_id,
|
||||
},
|
||||
"additionalProperties": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Resp, Noti> JsonRpcMessage<Req, Resp, Noti>
|
||||
where
|
||||
Req: GetExtensions,
|
||||
|
|
@ -450,17 +732,18 @@ where
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::NumberOrString;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Params {
|
||||
meta: Option<Meta>,
|
||||
meta: Option<RequestMetaObject>,
|
||||
}
|
||||
|
||||
impl RequestParamsMeta for Params {
|
||||
fn meta(&self) -> Option<&Meta> {
|
||||
fn meta(&self) -> Option<&RequestMetaObject> {
|
||||
self.meta.as_ref()
|
||||
}
|
||||
fn meta_mut(&mut self) -> &mut Option<Meta> {
|
||||
fn meta_mut(&mut self) -> &mut Option<RequestMetaObject> {
|
||||
&mut self.meta
|
||||
}
|
||||
}
|
||||
|
|
@ -469,7 +752,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn trace_context_round_trip() {
|
||||
let mut meta = Meta::new();
|
||||
let mut meta = MetaObject::new();
|
||||
meta.set_traceparent(TRACEPARENT);
|
||||
meta.set_tracestate("vendor1=value1,vendor2=value2");
|
||||
meta.set_baggage("userId=alice,region=us-east-1");
|
||||
|
|
@ -480,7 +763,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn absent_field_is_none() {
|
||||
let meta = Meta::new();
|
||||
let meta = MetaObject::new();
|
||||
assert_eq!(meta.get_traceparent(), None);
|
||||
assert_eq!(meta.get_tracestate(), None);
|
||||
assert_eq!(meta.get_baggage(), None);
|
||||
|
|
@ -488,9 +771,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn non_string_value_is_none() {
|
||||
let mut meta = Meta::new();
|
||||
let mut meta = MetaObject::new();
|
||||
meta.0
|
||||
.insert(Meta::TRACEPARENT_FIELD.to_string(), Value::from(42));
|
||||
.insert(MetaObject::TRACEPARENT_FIELD.to_string(), Value::from(42));
|
||||
assert_eq!(meta.get_traceparent(), None);
|
||||
}
|
||||
|
||||
|
|
@ -501,4 +784,91 @@ mod tests {
|
|||
params.set_traceparent(TRACEPARENT);
|
||||
assert_eq!(params.traceparent(), Some(TRACEPARENT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_meta_derefs_to_general_helpers() {
|
||||
let mut meta = RequestMetaObject::new();
|
||||
meta.set_traceparent(TRACEPARENT);
|
||||
meta.set_progress_token(ProgressToken(NumberOrString::Number(7)));
|
||||
assert_eq!(meta.get_traceparent(), Some(TRACEPARENT));
|
||||
assert_eq!(
|
||||
meta.get_progress_token(),
|
||||
Some(ProgressToken(NumberOrString::Number(7)))
|
||||
);
|
||||
}
|
||||
|
||||
mod subscription_id {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_absent() {
|
||||
let meta = NotificationMetaObject::new();
|
||||
assert_eq!(meta.subscription_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_number_id() {
|
||||
let mut meta = NotificationMetaObject::new();
|
||||
meta.set_subscription_id(RequestId::Number(42));
|
||||
assert_eq!(meta.subscription_id(), Some(RequestId::Number(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_string_id() {
|
||||
let mut meta = NotificationMetaObject::new();
|
||||
meta.set_subscription_id(RequestId::String("sub-1".into()));
|
||||
assert_eq!(
|
||||
meta.subscription_id(),
|
||||
Some(RequestId::String("sub-1".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod missing_required_keys {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_empty_for_pre_draft_protocols() {
|
||||
let meta = RequestMetaObject::new();
|
||||
assert!(
|
||||
meta.missing_required_keys(&ProtocolVersion::V_2025_11_25)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_all_draft_keys_for_empty_meta() {
|
||||
let meta = RequestMetaObject::new();
|
||||
assert_eq!(
|
||||
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
|
||||
RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treats_malformed_values_as_missing() {
|
||||
let meta: RequestMetaObject = serde_json::from_value(serde_json::json!({
|
||||
"io.modelcontextprotocol/protocolVersion": 123,
|
||||
"io.modelcontextprotocol/clientInfo": "not an implementation",
|
||||
"io.modelcontextprotocol/clientCapabilities": null,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28),
|
||||
RequestMetaObject::DRAFT_REQUIRED_KEYS.to_vec()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty_when_draft_keys_are_present() {
|
||||
let mut meta = RequestMetaObject::new();
|
||||
meta.set_protocol_version(ProtocolVersion::V_2026_07_28);
|
||||
meta.set_client_info(Implementation::from_build_env());
|
||||
meta.set_client_capabilities(ClientCapabilities::default());
|
||||
assert!(
|
||||
meta.missing_required_keys(&ProtocolVersion::V_2026_07_28)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
CallToolResult, CreateMessageRequest, ElicitRequest, GetPromptResult, ListRootsRequest, Meta,
|
||||
ReadResourceResult, ResultType, ServerResult,
|
||||
CallToolResult, CreateMessageRequest, ElicitRequest, GetPromptResult, ListRootsRequest,
|
||||
MetaObject, ReadResourceResult, ResultType, ServerResult,
|
||||
};
|
||||
|
||||
/// Default maximum number of MRTR rounds a high-level client call will drive.
|
||||
|
|
@ -220,7 +220,7 @@ pub struct InputRequiredResult {
|
|||
|
||||
/// Optional protocol-level metadata.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
/// Custom deserializer that requires `resultType: "input_required"` to prevent
|
||||
|
|
@ -238,7 +238,7 @@ impl<'de> Deserialize<'de> for InputRequiredResult {
|
|||
input_requests: Option<InputRequests>,
|
||||
request_state: Option<String>,
|
||||
#[serde(rename = "_meta")]
|
||||
meta: Option<Meta>,
|
||||
meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
let helper = Helper::deserialize(deserializer)?;
|
||||
|
|
@ -283,7 +283,7 @@ impl InputRequiredResult {
|
|||
}
|
||||
|
||||
/// Sets optional metadata.
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
Annotations, ContentBlock, Icon, Meta, Role,
|
||||
Annotations, ContentBlock, Icon, MetaObject, Role,
|
||||
content::{AudioContent, EmbeddedResource, ImageContent, TextContent},
|
||||
resource::ResourceContents,
|
||||
};
|
||||
|
|
@ -22,7 +22,7 @@ pub struct Prompt {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<Vec<Icon>>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
|
|
@ -70,7 +70,7 @@ impl Prompt {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ impl PromptMessage {
|
|||
role: Role,
|
||||
data: &[u8],
|
||||
mime_type: &str,
|
||||
meta: Option<Meta>,
|
||||
meta: Option<MetaObject>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
|
|
@ -166,7 +166,7 @@ impl PromptMessage {
|
|||
role: Role,
|
||||
data: &[u8],
|
||||
mime_type: &str,
|
||||
meta: Option<Meta>,
|
||||
meta: Option<MetaObject>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
|
|
@ -188,8 +188,8 @@ impl PromptMessage {
|
|||
uri: String,
|
||||
mime_type: Option<String>,
|
||||
text: Option<String>,
|
||||
resource_meta: Option<Meta>,
|
||||
resource_content_meta: Option<Meta>,
|
||||
resource_meta: Option<MetaObject>,
|
||||
resource_content_meta: Option<MetaObject>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
let resource_contents = match text {
|
||||
|
|
@ -216,7 +216,11 @@ impl PromptMessage {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_text_with_meta<S: Into<String>>(role: Role, text: S, meta: Option<Meta>) -> Self {
|
||||
pub fn new_text_with_meta<S: Into<String>>(
|
||||
role: Role,
|
||||
text: S,
|
||||
meta: Option<MetaObject>,
|
||||
) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: ContentBlock::Text(TextContent {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Annotations, Icon, Meta};
|
||||
use super::{Annotations, Icon, MetaObject};
|
||||
|
||||
/// A known resource that the server is capable of reading (spec `Resource`).
|
||||
///
|
||||
|
|
@ -31,7 +31,7 @@ pub struct Resource {
|
|||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional protocol-level metadata for this resource.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this resource.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -77,7 +77,7 @@ impl Resource {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ pub struct ResourceTemplate {
|
|||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional protocol-level metadata for this resource template.
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
/// Optional annotations describing how the client should use this template.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
|
|
@ -152,7 +152,7 @@ impl ResourceTemplate {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ pub enum ResourceContents {
|
|||
mime_type: Option<String>,
|
||||
text: String,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
meta: Option<Meta>,
|
||||
meta: Option<MetaObject>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BlobResourceContents {
|
||||
|
|
@ -185,7 +185,7 @@ pub enum ResourceContents {
|
|||
mime_type: Option<String>,
|
||||
blob: String,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
meta: Option<Meta>,
|
||||
meta: Option<MetaObject>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ impl ResourceContents {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
match &mut self {
|
||||
Self::TextResourceContents { meta: m, .. } => *m = Some(meta),
|
||||
Self::BlobResourceContents { meta: m, .. } => *m = Some(meta),
|
||||
|
|
@ -303,7 +303,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_resource_template_with_meta() {
|
||||
let resource_template =
|
||||
ResourceTemplate::new("file:///{path}", "template").with_meta(Meta::default());
|
||||
ResourceTemplate::new("file:///{path}", "template").with_meta(MetaObject::default());
|
||||
let json = serde_json::to_value(&resource_template).unwrap();
|
||||
assert!(json.get("_meta").is_some());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,19 @@ use std::borrow::Cow;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
CustomNotification, CustomRequest, Extensions, Meta, Notification, NotificationNoParam,
|
||||
Request, RequestNoParam, RequestOptionalParam,
|
||||
CustomNotification, CustomRequest, Extensions, JsonObject, MetaObject, Notification,
|
||||
NotificationMetaObject, NotificationNoParam, Request, RequestMetaObject, RequestNoParam,
|
||||
RequestOptionalParam,
|
||||
};
|
||||
|
||||
/// Wire-side view of `params`: the `_meta` map plus the remaining fields.
|
||||
///
|
||||
/// All metadata types are transparent wrappers over [`JsonObject`], so the
|
||||
/// serde plumbing works on the raw map; call sites wrap/unwrap the typed
|
||||
/// metadata ([`RequestMetaObject`] / [`NotificationMetaObject`]).
|
||||
#[derive(Deserialize)]
|
||||
struct WithMeta<'a, P> {
|
||||
_meta: Option<Cow<'a, Meta>>,
|
||||
_meta: Option<Cow<'a, JsonObject>>,
|
||||
#[serde(flatten)]
|
||||
_rest: P,
|
||||
}
|
||||
|
|
@ -25,7 +32,7 @@ impl<P: Serialize> Serialize for WithMeta<'_, P> {
|
|||
serde_json::to_value(&self._rest).map_err(serde::ser::Error::custom)?;
|
||||
|
||||
// Extract _meta from the serialized params (if it's an object containing one)
|
||||
let params_meta: Option<Meta> = rest_value
|
||||
let params_meta: Option<JsonObject> = rest_value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.remove("_meta"))
|
||||
.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
|
@ -78,6 +85,53 @@ struct ProxyNoParam<M> {
|
|||
method: M,
|
||||
}
|
||||
|
||||
/// Combine the message-specific `_meta` map with a legacy [`MetaObject`]
|
||||
/// extension (inserted through the deprecated `Meta` name), so pre-3.x code
|
||||
/// does not silently lose metadata on the wire. On key conflicts the
|
||||
/// message-specific map wins.
|
||||
fn merge_legacy_meta<'a>(
|
||||
typed: Option<&'a JsonObject>,
|
||||
extensions: &'a Extensions,
|
||||
) -> Option<Cow<'a, JsonObject>> {
|
||||
let legacy = extensions.get::<MetaObject>().map(|meta| &meta.0);
|
||||
match (typed, legacy) {
|
||||
(Some(typed), None) => Some(Cow::Borrowed(typed)),
|
||||
(None, Some(legacy)) => Some(Cow::Borrowed(legacy)),
|
||||
(Some(typed), Some(legacy)) => {
|
||||
let mut merged = legacy.clone();
|
||||
merged.extend(typed.clone());
|
||||
Some(Cow::Owned(merged))
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the request `_meta` map from extensions, if any.
|
||||
fn request_meta(extensions: &Extensions) -> Option<Cow<'_, JsonObject>> {
|
||||
let typed = extensions.get::<RequestMetaObject>().map(|meta| &meta.0.0);
|
||||
merge_legacy_meta(typed, extensions)
|
||||
}
|
||||
|
||||
/// Borrow the notification `_meta` map from extensions, if any.
|
||||
fn notification_meta(extensions: &Extensions) -> Option<Cow<'_, JsonObject>> {
|
||||
let typed = extensions
|
||||
.get::<NotificationMetaObject>()
|
||||
.map(|meta| &meta.0.0);
|
||||
merge_legacy_meta(typed, extensions)
|
||||
}
|
||||
|
||||
/// Build extensions holding a typed metadata map deserialized from `params._meta`.
|
||||
fn extensions_with_meta<T>(meta: Option<Cow<'_, JsonObject>>) -> Extensions
|
||||
where
|
||||
T: From<JsonObject> + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = meta {
|
||||
extensions.insert(T::from(meta.into_owned()));
|
||||
}
|
||||
extensions
|
||||
}
|
||||
|
||||
impl<M, R> Serialize for Request<M, R>
|
||||
where
|
||||
M: Serialize,
|
||||
|
|
@ -87,14 +141,12 @@ where
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
_meta: request_meta(&self.extensions),
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
|
|
@ -112,13 +164,8 @@ where
|
|||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = Proxy::deserialize(deserializer)?;
|
||||
let _meta = body.params._meta.map(|m| m.into_owned());
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
}
|
||||
Ok(Request {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<RequestMetaObject>(body.params._meta),
|
||||
method: body.method,
|
||||
params: body.params._rest,
|
||||
})
|
||||
|
|
@ -134,14 +181,12 @@ where
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
_meta: request_meta(&self.extensions),
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
|
|
@ -163,14 +208,10 @@ where
|
|||
let mut _meta = None;
|
||||
if let Some(body_params) = body.params {
|
||||
params = body_params._rest;
|
||||
_meta = body_params._meta.map(|m| m.into_owned());
|
||||
}
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
_meta = body_params._meta;
|
||||
}
|
||||
Ok(RequestOptionalParam {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
|
||||
method: body.method,
|
||||
params,
|
||||
})
|
||||
|
|
@ -185,14 +226,26 @@ where
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
// Emit `params` only when metadata is present, so the wire shape of
|
||||
// meta-less requests stays `{"method": ...}`.
|
||||
match request_meta(&self.extensions) {
|
||||
Some(_meta) => Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_meta: Some(_meta),
|
||||
_rest: (),
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
),
|
||||
None => ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,10 +257,10 @@ where
|
|||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
|
||||
let extensions = Extensions::new();
|
||||
let body = ProxyOptionalParam::<'_, _, Option<JsonObject>>::deserialize(deserializer)?;
|
||||
let _meta = body.params.and_then(|params| params._meta);
|
||||
Ok(RequestNoParam {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
|
||||
method: body.method,
|
||||
})
|
||||
}
|
||||
|
|
@ -222,14 +275,12 @@ where
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
_meta: notification_meta(&self.extensions),
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
|
|
@ -248,10 +299,7 @@ where
|
|||
{
|
||||
let body = ProxyOptionalParam::<'_, _, R>::deserialize(deserializer)?;
|
||||
let (_meta, params) = match body.params {
|
||||
Some(with_meta) => {
|
||||
let meta = with_meta._meta.map(|m| m.into_owned());
|
||||
(meta, with_meta._rest)
|
||||
}
|
||||
Some(with_meta) => (with_meta._meta, with_meta._rest),
|
||||
None => {
|
||||
// JSON-RPC 2.0: params is optional. Treat absent params as {}.
|
||||
let empty = serde_json::Value::Object(serde_json::Map::new());
|
||||
|
|
@ -259,12 +307,8 @@ where
|
|||
(None, r)
|
||||
}
|
||||
};
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
}
|
||||
Ok(Notification {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
|
||||
method: body.method,
|
||||
params,
|
||||
})
|
||||
|
|
@ -279,14 +323,26 @@ where
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
// Emit `params` only when metadata is present, so the wire shape of
|
||||
// meta-less notifications stays `{"method": ...}`.
|
||||
match notification_meta(&self.extensions) {
|
||||
Some(_meta) => Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_meta: Some(_meta),
|
||||
_rest: (),
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
),
|
||||
None => ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -298,10 +354,10 @@ where
|
|||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
|
||||
let extensions = Extensions::new();
|
||||
let body = ProxyOptionalParam::<'_, _, Option<JsonObject>>::deserialize(deserializer)?;
|
||||
let _meta = body.params.and_then(|params| params._meta);
|
||||
Ok(NotificationNoParam {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
|
||||
method: body.method,
|
||||
})
|
||||
}
|
||||
|
|
@ -312,8 +368,7 @@ impl Serialize for CustomRequest {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
let _meta = request_meta(&self.extensions);
|
||||
let params = self.params.as_ref();
|
||||
|
||||
let params = if _meta.is_some() || params.is_some() {
|
||||
|
|
@ -346,14 +401,10 @@ impl<'de> Deserialize<'de> for CustomRequest {
|
|||
let mut _meta = None;
|
||||
if let Some(body_params) = body.params {
|
||||
params = body_params._rest;
|
||||
_meta = body_params._meta.map(|m| m.into_owned());
|
||||
}
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
_meta = body_params._meta;
|
||||
}
|
||||
Ok(CustomRequest {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<RequestMetaObject>(_meta),
|
||||
method: body.method,
|
||||
params,
|
||||
})
|
||||
|
|
@ -365,8 +416,7 @@ impl Serialize for CustomNotification {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
let _meta = notification_meta(&self.extensions);
|
||||
let params = self.params.as_ref();
|
||||
|
||||
let params = if _meta.is_some() || params.is_some() {
|
||||
|
|
@ -399,14 +449,10 @@ impl<'de> Deserialize<'de> for CustomNotification {
|
|||
let mut _meta = None;
|
||||
if let Some(body_params) = body.params {
|
||||
params = body_params._rest;
|
||||
_meta = body_params._meta.map(|m| m.into_owned());
|
||||
}
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
_meta = body_params._meta;
|
||||
}
|
||||
Ok(CustomNotification {
|
||||
extensions,
|
||||
extensions: extensions_with_meta::<NotificationMetaObject>(_meta),
|
||||
method: body.method,
|
||||
params,
|
||||
})
|
||||
|
|
@ -418,7 +464,8 @@ mod test {
|
|||
use serde_json::json;
|
||||
|
||||
use crate::model::{
|
||||
CallToolRequest, CallToolRequestParams, CustomRequest, Extensions, ListToolsRequest, Meta,
|
||||
CallToolRequest, CallToolRequestParams, CustomRequest, Extensions, InitializedNotification,
|
||||
ListToolsRequest, NotificationMetaObject, PingRequest, RequestMetaObject,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -436,12 +483,12 @@ mod test {
|
|||
// When both extensions and params contain _meta, the output should have
|
||||
// a single merged _meta key (not two separate ones).
|
||||
let mut extensions = Extensions::new();
|
||||
let mut ext_meta = Meta::new();
|
||||
ext_meta.0.insert("traceId".to_string(), json!("abc"));
|
||||
let mut ext_meta = RequestMetaObject::new();
|
||||
ext_meta.insert("traceId".to_string(), json!("abc"));
|
||||
extensions.insert(ext_meta);
|
||||
|
||||
let mut params_meta = Meta::new();
|
||||
params_meta.0.insert("progressToken".to_string(), json!(1));
|
||||
let mut params_meta = RequestMetaObject::new();
|
||||
params_meta.insert("progressToken".to_string(), json!(1));
|
||||
|
||||
let req = CallToolRequest {
|
||||
extensions,
|
||||
|
|
@ -479,8 +526,8 @@ mod test {
|
|||
#[test]
|
||||
fn test_meta_only_from_extensions() {
|
||||
let mut extensions = Extensions::new();
|
||||
let mut ext_meta = Meta::new();
|
||||
ext_meta.0.insert("traceId".to_string(), json!("ext-only"));
|
||||
let mut ext_meta = RequestMetaObject::new();
|
||||
ext_meta.insert("traceId".to_string(), json!("ext-only"));
|
||||
extensions.insert(ext_meta);
|
||||
|
||||
let req = CallToolRequest {
|
||||
|
|
@ -503,8 +550,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_meta_only_from_params() {
|
||||
let mut params_meta = Meta::new();
|
||||
params_meta.0.insert("progressToken".to_string(), json!(42));
|
||||
let mut params_meta = RequestMetaObject::new();
|
||||
params_meta.insert("progressToken".to_string(), json!(42));
|
||||
|
||||
let req = CallToolRequest {
|
||||
extensions: Extensions::new(),
|
||||
|
|
@ -550,19 +597,13 @@ mod test {
|
|||
fn test_extensions_meta_takes_priority_on_conflict() {
|
||||
// When both sources have the same key, extensions should win.
|
||||
let mut extensions = Extensions::new();
|
||||
let mut ext_meta = Meta::new();
|
||||
ext_meta
|
||||
.0
|
||||
.insert("shared_key".to_string(), json!("from_extensions"));
|
||||
let mut ext_meta = RequestMetaObject::new();
|
||||
ext_meta.insert("shared_key".to_string(), json!("from_extensions"));
|
||||
extensions.insert(ext_meta);
|
||||
|
||||
let mut params_meta = Meta::new();
|
||||
params_meta
|
||||
.0
|
||||
.insert("shared_key".to_string(), json!("from_params"));
|
||||
params_meta
|
||||
.0
|
||||
.insert("params_only".to_string(), json!("kept"));
|
||||
let mut params_meta = RequestMetaObject::new();
|
||||
params_meta.insert("shared_key".to_string(), json!("from_params"));
|
||||
params_meta.insert("params_only".to_string(), json!("kept"));
|
||||
|
||||
let req = CallToolRequest {
|
||||
extensions,
|
||||
|
|
@ -586,10 +627,8 @@ mod test {
|
|||
#[test]
|
||||
fn test_round_trip_preserves_meta() {
|
||||
let mut extensions = Extensions::new();
|
||||
let mut ext_meta = Meta::new();
|
||||
ext_meta
|
||||
.0
|
||||
.insert("traceId".to_string(), json!("round-trip"));
|
||||
let mut ext_meta = RequestMetaObject::new();
|
||||
ext_meta.insert("traceId".to_string(), json!("round-trip"));
|
||||
extensions.insert(ext_meta);
|
||||
|
||||
let req = CallToolRequest {
|
||||
|
|
@ -609,8 +648,8 @@ mod test {
|
|||
let deserialized: CallToolRequest = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
// Extensions should have the meta after round-trip
|
||||
let meta = deserialized.extensions.get::<Meta>().unwrap();
|
||||
assert_eq!(meta.0.get("traceId").unwrap(), "round-trip");
|
||||
let meta = deserialized.extensions.get::<RequestMetaObject>().unwrap();
|
||||
assert_eq!(meta.get("traceId").unwrap(), "round-trip");
|
||||
|
||||
// Params should be preserved
|
||||
assert_eq!(deserialized.params.name, "my_tool");
|
||||
|
|
@ -630,10 +669,8 @@ mod test {
|
|||
fn test_custom_request_no_duplicate_meta() {
|
||||
// CustomRequest uses Option<Value> as params — verify no duplicate _meta.
|
||||
let mut extensions = Extensions::new();
|
||||
let mut ext_meta = Meta::new();
|
||||
ext_meta
|
||||
.0
|
||||
.insert("traceId".to_string(), json!("custom-ext"));
|
||||
let mut ext_meta = RequestMetaObject::new();
|
||||
ext_meta.insert("traceId".to_string(), json!("custom-ext"));
|
||||
extensions.insert(ext_meta);
|
||||
|
||||
let params = Some(json!({
|
||||
|
|
@ -660,4 +697,162 @@ mod test {
|
|||
assert_eq!(meta.get("traceId").unwrap(), "custom-ext");
|
||||
assert_eq!(meta.get("progressToken").unwrap(), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_no_param_meta_round_trip() {
|
||||
// Ping-shaped requests must carry `params._meta` on the wire.
|
||||
let mut extensions = Extensions::new();
|
||||
let mut meta = RequestMetaObject::new();
|
||||
meta.insert("traceId".to_string(), json!("ping-trace"));
|
||||
extensions.insert(meta);
|
||||
|
||||
let req = PingRequest {
|
||||
method: Default::default(),
|
||||
extensions,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&req).unwrap();
|
||||
assert_eq!(value["params"]["_meta"]["traceId"], json!("ping-trace"));
|
||||
|
||||
let deserialized: PingRequest = serde_json::from_value(value).unwrap();
|
||||
let meta = deserialized
|
||||
.extensions
|
||||
.get::<RequestMetaObject>()
|
||||
.expect("meta should survive the round-trip");
|
||||
assert_eq!(meta.get("traceId").unwrap(), &json!("ping-trace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_no_param_without_meta_has_no_params_key() {
|
||||
let req = PingRequest {
|
||||
method: Default::default(),
|
||||
extensions: Extensions::new(),
|
||||
};
|
||||
let value = serde_json::to_value(&req).unwrap();
|
||||
assert!(
|
||||
value.get("params").is_none(),
|
||||
"meta-less no-param requests must keep the historical wire shape: {value}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification_no_param_meta_round_trip() {
|
||||
// Initialized-shaped notifications must carry `params._meta` on the wire.
|
||||
let mut extensions = Extensions::new();
|
||||
let mut meta = NotificationMetaObject::new();
|
||||
meta.insert("traceId".to_string(), json!("init-trace"));
|
||||
extensions.insert(meta);
|
||||
|
||||
let notification = InitializedNotification {
|
||||
method: Default::default(),
|
||||
extensions,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¬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::<NotificationMetaObject>()
|
||||
.expect("meta should survive the round-trip");
|
||||
assert_eq!(meta.get("traceId").unwrap(), &json!("init-trace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification_no_param_without_meta_has_no_params_key() {
|
||||
let notification = InitializedNotification {
|
||||
method: Default::default(),
|
||||
extensions: Extensions::new(),
|
||||
};
|
||||
let value = serde_json::to_value(¬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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Meta;
|
||||
use super::MetaObject;
|
||||
|
||||
/// Metadata for augmenting a request with task execution (spec `TaskMetadata`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
|
|
@ -139,7 +139,7 @@ impl Task {
|
|||
pub struct CreateTaskResult {
|
||||
pub task: Task,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
impl CreateTaskResult {
|
||||
|
|
@ -149,7 +149,7 @@ impl CreateTaskResult {
|
|||
}
|
||||
|
||||
/// Sets the protocol-level metadata for this result.
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@ impl CreateTaskResult {
|
|||
#[non_exhaustive]
|
||||
pub struct GetTaskResult {
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
#[serde(flatten)]
|
||||
pub task: Task,
|
||||
}
|
||||
|
|
@ -222,7 +222,7 @@ impl<'de> serde::Deserialize<'de> for GetTaskPayloadResult {
|
|||
#[non_exhaustive]
|
||||
pub struct CancelTaskResult {
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
#[serde(flatten)]
|
||||
pub task: Task,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use schemars::JsonSchema;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{Icon, JsonObject, Meta};
|
||||
use super::{Icon, JsonObject, MetaObject};
|
||||
|
||||
/// A tool that can be used by a model.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -39,7 +39,7 @@ pub struct Tool {
|
|||
pub icons: Option<Vec<Icon>>,
|
||||
/// Optional additional metadata for this tool
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
pub meta: Option<MetaObject>,
|
||||
}
|
||||
|
||||
/// Per-tool task support mode as defined in the MCP specification.
|
||||
|
|
@ -286,7 +286,7 @@ impl Tool {
|
|||
}
|
||||
|
||||
/// Set the metadata
|
||||
pub fn with_meta(mut self, meta: Meta) -> Self {
|
||||
pub fn with_meta(mut self, meta: MetaObject) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue