Skip to content

Commit d83b156

Browse files
authored
fix(rmcp): flatten Resource variant of PromptMessageContent (modelcontextprotocol#843)
The Resource variant of PromptMessageContent was missing #[serde(flatten)], causing the embedded resource content block to serialize as a double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }` instead of the spec-compliant flat shape `{ "type": "resource", "resource": {uri, mimeType, text} }`. This caused Zod-based MCP clients (e.g. Claude Code) to reject prompts/get responses containing embedded resource messages with InvalidUnion errors. The Image and ResourceLink variants already use #[serde(flatten)] correctly; only Resource was missing it. Fix: add #[serde(flatten)] so EmbeddedResource (=Annotated<RawEmbeddedResource>) fields _meta / annotations / resource are flattened to the content-block level, matching the MCP spec for prompts embedded resources. Regression test: test_prompt_message_resource_serialization_is_flat verifies content.resource.uri is reachable and content.resource.resource is absent. Schema snapshots regenerated via UPDATE_SCHEMA=1.
1 parent 321ab14 commit d83b156

3 files changed

Lines changed: 99 additions & 67 deletions

File tree

crates/rmcp/src/model/prompt.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ pub enum PromptMessageContent {
158158
image: ImageContent,
159159
},
160160
/// Embedded server-side resource
161-
Resource { resource: EmbeddedResource },
161+
Resource {
162+
#[serde(flatten)]
163+
resource: EmbeddedResource,
164+
},
162165
/// A link to a resource that can be fetched separately
163166
ResourceLink {
164167
#[serde(flatten)]
@@ -321,6 +324,57 @@ mod tests {
321324
assert!(json.contains("\"name\":\"test.txt\""));
322325
}
323326

327+
#[test]
328+
fn test_prompt_message_resource_serialization_is_flat() {
329+
// Regression test: PromptMessageContent::Resource must serialize to
330+
// the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }`
331+
// and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`.
332+
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
333+
let message = PromptMessage::new_resource(
334+
PromptMessageRole::User,
335+
"alc://packages/sc/narrative".to_string(),
336+
Some("text/markdown".to_string()),
337+
Some("# Hello".to_string()),
338+
None,
339+
None,
340+
None,
341+
);
342+
343+
let value: serde_json::Value = serde_json::to_value(&message).unwrap();
344+
345+
// Drill into content
346+
let content = value.get("content").expect("content present");
347+
assert_eq!(
348+
content.get("type").and_then(|v| v.as_str()),
349+
Some("resource")
350+
);
351+
352+
let resource = content
353+
.get("resource")
354+
.expect("resource field present at content level");
355+
356+
// Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat
357+
assert_eq!(
358+
resource.get("uri").and_then(|v| v.as_str()),
359+
Some("alc://packages/sc/narrative"),
360+
"expected flat resource.uri, got: {resource:#?}"
361+
);
362+
assert_eq!(
363+
resource.get("mimeType").and_then(|v| v.as_str()),
364+
Some("text/markdown")
365+
);
366+
assert_eq!(
367+
resource.get("text").and_then(|v| v.as_str()),
368+
Some("# Hello")
369+
);
370+
371+
// Regression guard: content.resource MUST NOT contain a nested `resource` key.
372+
assert!(
373+
resource.get("resource").is_none(),
374+
"double-nested resource detected (regression): {resource:#?}"
375+
);
376+
}
377+
324378
#[test]
325379
fn test_prompt_message_content_resource_link_deserialization() {
326380
let json = r#"{

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -140,35 +140,6 @@
140140
]
141141
},
142142
"Annotated2": {
143-
"type": "object",
144-
"properties": {
145-
"_meta": {
146-
"description": "Optional protocol-level metadata for this content block",
147-
"type": [
148-
"object",
149-
"null"
150-
],
151-
"additionalProperties": true
152-
},
153-
"annotations": {
154-
"anyOf": [
155-
{
156-
"$ref": "#/definitions/Annotations"
157-
},
158-
{
159-
"type": "null"
160-
}
161-
]
162-
},
163-
"resource": {
164-
"$ref": "#/definitions/ResourceContents"
165-
}
166-
},
167-
"required": [
168-
"resource"
169-
]
170-
},
171-
"Annotated3": {
172143
"description": "Represents a resource in the extension with metadata",
173144
"type": "object",
174145
"properties": {
@@ -244,7 +215,7 @@
244215
"name"
245216
]
246217
},
247-
"Annotated4": {
218+
"Annotated3": {
248219
"type": "object",
249220
"properties": {
250221
"annotations": {
@@ -1481,7 +1452,7 @@
14811452
"resourceTemplates": {
14821453
"type": "array",
14831454
"items": {
1484-
"$ref": "#/definitions/Annotated4"
1455+
"$ref": "#/definitions/Annotated3"
14851456
}
14861457
}
14871458
},
@@ -1508,7 +1479,7 @@
15081479
"resources": {
15091480
"type": "array",
15101481
"items": {
1511-
"$ref": "#/definitions/Annotated3"
1482+
"$ref": "#/definitions/Annotated2"
15121483
}
15131484
}
15141485
},
@@ -2148,8 +2119,26 @@
21482119
"description": "Embedded server-side resource",
21492120
"type": "object",
21502121
"properties": {
2122+
"_meta": {
2123+
"description": "Optional protocol-level metadata for this content block",
2124+
"type": [
2125+
"object",
2126+
"null"
2127+
],
2128+
"additionalProperties": true
2129+
},
2130+
"annotations": {
2131+
"anyOf": [
2132+
{
2133+
"$ref": "#/definitions/Annotations"
2134+
},
2135+
{
2136+
"type": "null"
2137+
}
2138+
]
2139+
},
21512140
"resource": {
2152-
"$ref": "#/definitions/Annotated2"
2141+
"$ref": "#/definitions/ResourceContents"
21532142
},
21542143
"type": {
21552144
"type": "string",

crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -140,35 +140,6 @@
140140
]
141141
},
142142
"Annotated2": {
143-
"type": "object",
144-
"properties": {
145-
"_meta": {
146-
"description": "Optional protocol-level metadata for this content block",
147-
"type": [
148-
"object",
149-
"null"
150-
],
151-
"additionalProperties": true
152-
},
153-
"annotations": {
154-
"anyOf": [
155-
{
156-
"$ref": "#/definitions/Annotations"
157-
},
158-
{
159-
"type": "null"
160-
}
161-
]
162-
},
163-
"resource": {
164-
"$ref": "#/definitions/ResourceContents"
165-
}
166-
},
167-
"required": [
168-
"resource"
169-
]
170-
},
171-
"Annotated3": {
172143
"description": "Represents a resource in the extension with metadata",
173144
"type": "object",
174145
"properties": {
@@ -244,7 +215,7 @@
244215
"name"
245216
]
246217
},
247-
"Annotated4": {
218+
"Annotated3": {
248219
"type": "object",
249220
"properties": {
250221
"annotations": {
@@ -1481,7 +1452,7 @@
14811452
"resourceTemplates": {
14821453
"type": "array",
14831454
"items": {
1484-
"$ref": "#/definitions/Annotated4"
1455+
"$ref": "#/definitions/Annotated3"
14851456
}
14861457
}
14871458
},
@@ -1508,7 +1479,7 @@
15081479
"resources": {
15091480
"type": "array",
15101481
"items": {
1511-
"$ref": "#/definitions/Annotated3"
1482+
"$ref": "#/definitions/Annotated2"
15121483
}
15131484
}
15141485
},
@@ -2148,8 +2119,26 @@
21482119
"description": "Embedded server-side resource",
21492120
"type": "object",
21502121
"properties": {
2122+
"_meta": {
2123+
"description": "Optional protocol-level metadata for this content block",
2124+
"type": [
2125+
"object",
2126+
"null"
2127+
],
2128+
"additionalProperties": true
2129+
},
2130+
"annotations": {
2131+
"anyOf": [
2132+
{
2133+
"$ref": "#/definitions/Annotations"
2134+
},
2135+
{
2136+
"type": "null"
2137+
}
2138+
]
2139+
},
21512140
"resource": {
2152-
"$ref": "#/definitions/Annotated2"
2141+
"$ref": "#/definitions/ResourceContents"
21532142
},
21542143
"type": {
21552144
"type": "string",

0 commit comments

Comments
 (0)