Skip to content

Commit 4eb413b

Browse files
authored
Spec conformance: meta support and spec updates (#415)
* feat: add _meta to content blocks and embedded resources; update schemas * feat: set default protocol version; add _meta to content blocks/resources; update schemas * chore: format content.rs via rustfmt * chore(protocol): keep LATEST at 2025-03-26 per review until full 2025-06-18 compliance * feat(prompt): add constructors with optional meta for image and resource - Keep text helper; meta is currently ignored for text until schema supports it. * refactor(prompt): simplify constructors so meta is optional; remove duplicate non-meta variants * fix: modify code comment about version * refactor(prompt): rename meta parameters in new_resource function for clarity
1 parent b514bc4 commit 4eb413b

File tree

9 files changed

+3427
-47
lines changed

9 files changed

+3427
-47
lines changed

crates/rmcp/src/model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ impl ProtocolVersion {
149149
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
150150
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
151151
pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05"));
152+
// Keep LATEST at 2025-03-26 until full 2025-06-18 compliance and automated testing are in place.
152153
pub const LATEST: Self = Self::V_2025_03_26;
153154
}
154155

crates/rmcp/src/model/annotated.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ pub struct Annotations {
1616
pub audience: Option<Vec<Role>>,
1717
#[serde(skip_serializing_if = "Option::is_none")]
1818
pub priority: Option<f32>,
19-
#[serde(skip_serializing_if = "Option::is_none")]
20-
pub timestamp: Option<DateTime<Utc>>,
19+
#[serde(skip_serializing_if = "Option::is_none", rename = "lastModified")]
20+
pub last_modified: Option<DateTime<Utc>>,
2121
}
2222

2323
impl Annotations {
@@ -30,7 +30,7 @@ impl Annotations {
3030
);
3131
Annotations {
3232
priority: Some(priority),
33-
timestamp: Some(timestamp),
33+
last_modified: Some(timestamp),
3434
audience: None,
3535
}
3636
}
@@ -72,7 +72,7 @@ impl<T: AnnotateAble> Annotated<T> {
7272
self.annotations.as_ref().and_then(|a| a.priority)
7373
}
7474
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
75-
self.annotations.as_ref().and_then(|a| a.timestamp)
75+
self.annotations.as_ref().and_then(|a| a.last_modified)
7676
}
7777
pub fn with_audience(self, audience: Vec<Role>) -> Annotated<T>
7878
where
@@ -92,7 +92,7 @@ impl<T: AnnotateAble> Annotated<T> {
9292
annotations: Some(Annotations {
9393
audience: Some(audience),
9494
priority: None,
95-
timestamp: None,
95+
last_modified: None,
9696
}),
9797
}
9898
}
@@ -114,7 +114,7 @@ impl<T: AnnotateAble> Annotated<T> {
114114
raw: self.raw,
115115
annotations: Some(Annotations {
116116
priority: Some(priority),
117-
timestamp: None,
117+
last_modified: None,
118118
audience: None,
119119
}),
120120
}
@@ -128,15 +128,15 @@ impl<T: AnnotateAble> Annotated<T> {
128128
Annotated {
129129
raw: self.raw,
130130
annotations: Some(Annotations {
131-
timestamp: Some(timestamp),
131+
last_modified: Some(timestamp),
132132
..annotations
133133
}),
134134
}
135135
} else {
136136
Annotated {
137137
raw: self.raw,
138138
annotations: Some(Annotations {
139-
timestamp: Some(timestamp),
139+
last_modified: Some(timestamp),
140140
priority: None,
141141
audience: None,
142142
}),
@@ -211,7 +211,7 @@ pub trait AnnotateAble: sealed::Sealed {
211211
Self: Sized,
212212
{
213213
self.annotate(Annotations {
214-
timestamp: Some(timestamp),
214+
last_modified: Some(timestamp),
215215
..Default::default()
216216
})
217217
}

crates/rmcp/src/model/content.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use super::{AnnotateAble, Annotated, resource::ResourceContents};
1111
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1212
pub struct RawTextContent {
1313
pub text: String,
14+
/// Optional protocol-level metadata for this content block
15+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
16+
pub meta: Option<super::Meta>,
1417
}
1518
pub type TextContent = Annotated<RawTextContent>;
1619
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -20,13 +23,17 @@ pub struct RawImageContent {
2023
/// The base64-encoded image
2124
pub data: String,
2225
pub mime_type: String,
26+
/// Optional protocol-level metadata for this content block
27+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
28+
pub meta: Option<super::Meta>,
2329
}
2430

2531
pub type ImageContent = Annotated<RawImageContent>;
2632
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2733
#[serde(rename_all = "camelCase")]
2834
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2935
pub struct RawEmbeddedResource {
36+
/// Optional protocol-level metadata for this content block
3037
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
3138
pub meta: Option<super::Meta>,
3239
pub resource: ResourceContents,
@@ -79,13 +86,17 @@ impl RawContent {
7986
}
8087

8188
pub fn text<S: Into<String>>(text: S) -> Self {
82-
RawContent::Text(RawTextContent { text: text.into() })
89+
RawContent::Text(RawTextContent {
90+
text: text.into(),
91+
meta: None,
92+
})
8393
}
8494

8595
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
8696
RawContent::Image(RawImageContent {
8797
data: data.into(),
8898
mime_type: mime_type.into(),
99+
meta: None,
89100
})
90101
}
91102

@@ -209,6 +220,7 @@ mod tests {
209220
let image_content = RawImageContent {
210221
data: "base64data".to_string(),
211222
mime_type: "image/png".to_string(),
223+
meta: None,
212224
};
213225

214226
let json = serde_json::to_string(&image_content).unwrap();

crates/rmcp/src/model/prompt.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,56 +114,76 @@ impl PromptMessage {
114114
content: PromptMessageContent::Text { text: text.into() },
115115
}
116116
}
117+
118+
/// Create a new image message. `meta` and `annotations` are optional.
117119
#[cfg(feature = "base64")]
118120
pub fn new_image(
119121
role: PromptMessageRole,
120122
data: &[u8],
121123
mime_type: &str,
124+
meta: Option<crate::model::Meta>,
122125
annotations: Option<Annotations>,
123126
) -> Self {
124-
let mime_type = mime_type.into();
125-
126127
let base64 = BASE64_STANDARD.encode(data);
127-
128128
Self {
129129
role,
130130
content: PromptMessageContent::Image {
131131
image: RawImageContent {
132132
data: base64,
133-
mime_type,
133+
mime_type: mime_type.into(),
134+
meta,
134135
}
135136
.optional_annotate(annotations),
136137
},
137138
}
138139
}
139140

140-
/// Create a new resource message
141+
/// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional.
141142
pub fn new_resource(
142143
role: PromptMessageRole,
143144
uri: String,
144-
mime_type: String,
145+
mime_type: Option<String>,
145146
text: Option<String>,
147+
resource_meta: Option<crate::model::Meta>,
148+
resource_content_meta: Option<crate::model::Meta>,
146149
annotations: Option<Annotations>,
147150
) -> Self {
148-
let resource_contents = ResourceContents::TextResourceContents {
149-
uri,
150-
mime_type: Some(mime_type),
151-
text: text.unwrap_or_default(),
152-
meta: None,
151+
let resource_contents = match text {
152+
Some(t) => ResourceContents::TextResourceContents {
153+
uri,
154+
mime_type,
155+
text: t,
156+
meta: resource_content_meta,
157+
},
158+
None => ResourceContents::BlobResourceContents {
159+
uri,
160+
mime_type,
161+
blob: String::new(),
162+
meta: resource_content_meta,
163+
},
153164
};
154-
155165
Self {
156166
role,
157167
content: PromptMessageContent::Resource {
158168
resource: RawEmbeddedResource {
159-
meta: None,
169+
meta: resource_meta,
160170
resource: resource_contents,
161171
}
162172
.optional_annotate(annotations),
163173
},
164174
}
165175
}
166176

177+
/// Note: PromptMessage text content does not carry protocol-level _meta per current schema.
178+
/// This function exists for API symmetry but ignores the meta parameter.
179+
pub fn new_text_with_meta<S: Into<String>>(
180+
role: PromptMessageRole,
181+
text: S,
182+
_meta: Option<crate::model::Meta>,
183+
) -> Self {
184+
Self::new_text(role, text)
185+
}
186+
167187
/// Create a new resource link message
168188
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
169189
Self {
@@ -184,6 +204,7 @@ mod tests {
184204
let image_content = RawImageContent {
185205
data: "base64data".to_string(),
186206
mime_type: "image/png".to_string(),
207+
meta: None,
187208
};
188209

189210
let json = serde_json::to_string(&image_content).unwrap();

crates/rmcp/tests/test_embedded_resource_meta.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ use serde_json::json;
44
#[test]
55
fn serialize_embedded_text_resource_with_meta() {
66
// Inner contents meta
7-
let mut inner_meta = Meta::new();
8-
inner_meta.insert("inner".to_string(), json!(2));
7+
let mut resource_content_meta = Meta::new();
8+
resource_content_meta.insert("inner".to_string(), json!(2));
99

1010
// Top-level embedded resource meta
11-
let mut top_meta = Meta::new();
12-
top_meta.insert("top".to_string(), json!(1));
11+
let mut resource_meta = Meta::new();
12+
resource_meta.insert("top".to_string(), json!(1));
1313

1414
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
15-
meta: Some(top_meta),
15+
meta: Some(resource_meta),
1616
resource: ResourceContents::TextResourceContents {
1717
uri: "str://example".to_string(),
1818
mime_type: Some("text/plain".to_string()),
1919
text: "hello".to_string(),
20-
meta: Some(inner_meta),
20+
meta: Some(resource_content_meta),
2121
},
2222
})
2323
.no_annotation();
@@ -97,19 +97,19 @@ fn deserialize_embedded_text_resource_with_meta() {
9797

9898
#[test]
9999
fn serialize_embedded_blob_resource_with_meta() {
100-
let mut inner_meta = Meta::new();
101-
inner_meta.insert("blob_inner".to_string(), json!(true));
100+
let mut resource_content_meta = Meta::new();
101+
resource_content_meta.insert("blob_inner".to_string(), json!(true));
102102

103-
let mut top_meta = Meta::new();
104-
top_meta.insert("blob_top".to_string(), json!("t"));
103+
let mut resource_meta = Meta::new();
104+
resource_meta.insert("blob_top".to_string(), json!("t"));
105105

106106
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
107-
meta: Some(top_meta),
107+
meta: Some(resource_meta),
108108
resource: ResourceContents::BlobResourceContents {
109109
uri: "str://blob".to_string(),
110110
mime_type: Some("application/octet-stream".to_string()),
111111
blob: "Zm9v".to_string(),
112-
meta: Some(inner_meta),
112+
meta: Some(resource_content_meta),
113113
},
114114
})
115115
.no_annotation();

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,19 @@
176176
"$ref": "#/definitions/Role"
177177
}
178178
},
179-
"priority": {
179+
"lastModified": {
180180
"type": [
181-
"number",
181+
"string",
182182
"null"
183183
],
184-
"format": "float"
184+
"format": "date-time"
185185
},
186-
"timestamp": {
186+
"priority": {
187187
"type": [
188-
"string",
188+
"number",
189189
"null"
190190
],
191-
"format": "date-time"
191+
"format": "float"
192192
}
193193
}
194194
},
@@ -859,6 +859,7 @@
859859
"type": "object",
860860
"properties": {
861861
"_meta": {
862+
"description": "Optional protocol-level metadata for this content block",
862863
"type": [
863864
"object",
864865
"null"
@@ -876,6 +877,14 @@
876877
"RawImageContent": {
877878
"type": "object",
878879
"properties": {
880+
"_meta": {
881+
"description": "Optional protocol-level metadata for this content block",
882+
"type": [
883+
"object",
884+
"null"
885+
],
886+
"additionalProperties": true
887+
},
879888
"data": {
880889
"description": "The base64-encoded image",
881890
"type": "string"
@@ -933,6 +942,14 @@
933942
"RawTextContent": {
934943
"type": "object",
935944
"properties": {
945+
"_meta": {
946+
"description": "Optional protocol-level metadata for this content block",
947+
"type": [
948+
"object",
949+
"null"
950+
],
951+
"additionalProperties": true
952+
},
936953
"text": {
937954
"type": "string"
938955
}

0 commit comments

Comments
 (0)