Skip to content

Commit eb86473

Browse files
committed
fix(rmcp): flatten Resource variant of PromptMessageContent
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 014fb2e commit eb86473

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": {
@@ -1475,7 +1446,7 @@
14751446
"resourceTemplates": {
14761447
"type": "array",
14771448
"items": {
1478-
"$ref": "#/definitions/Annotated4"
1449+
"$ref": "#/definitions/Annotated3"
14791450
}
14801451
}
14811452
},
@@ -1502,7 +1473,7 @@
15021473
"resources": {
15031474
"type": "array",
15041475
"items": {
1505-
"$ref": "#/definitions/Annotated3"
1476+
"$ref": "#/definitions/Annotated2"
15061477
}
15071478
}
15081479
},
@@ -2142,8 +2113,26 @@
21422113
"description": "Embedded server-side resource",
21432114
"type": "object",
21442115
"properties": {
2116+
"_meta": {
2117+
"description": "Optional protocol-level metadata for this content block",
2118+
"type": [
2119+
"object",
2120+
"null"
2121+
],
2122+
"additionalProperties": true
2123+
},
2124+
"annotations": {
2125+
"anyOf": [
2126+
{
2127+
"$ref": "#/definitions/Annotations"
2128+
},
2129+
{
2130+
"type": "null"
2131+
}
2132+
]
2133+
},
21452134
"resource": {
2146-
"$ref": "#/definitions/Annotated2"
2135+
"$ref": "#/definitions/ResourceContents"
21472136
},
21482137
"type": {
21492138
"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": {
@@ -1475,7 +1446,7 @@
14751446
"resourceTemplates": {
14761447
"type": "array",
14771448
"items": {
1478-
"$ref": "#/definitions/Annotated4"
1449+
"$ref": "#/definitions/Annotated3"
14791450
}
14801451
}
14811452
},
@@ -1502,7 +1473,7 @@
15021473
"resources": {
15031474
"type": "array",
15041475
"items": {
1505-
"$ref": "#/definitions/Annotated3"
1476+
"$ref": "#/definitions/Annotated2"
15061477
}
15071478
}
15081479
},
@@ -2142,8 +2113,26 @@
21422113
"description": "Embedded server-side resource",
21432114
"type": "object",
21442115
"properties": {
2116+
"_meta": {
2117+
"description": "Optional protocol-level metadata for this content block",
2118+
"type": [
2119+
"object",
2120+
"null"
2121+
],
2122+
"additionalProperties": true
2123+
},
2124+
"annotations": {
2125+
"anyOf": [
2126+
{
2127+
"$ref": "#/definitions/Annotations"
2128+
},
2129+
{
2130+
"type": "null"
2131+
}
2132+
]
2133+
},
21452134
"resource": {
2146-
"$ref": "#/definitions/Annotated2"
2135+
"$ref": "#/definitions/ResourceContents"
21472136
},
21482137
"type": {
21492138
"type": "string",

0 commit comments

Comments
 (0)