From eb86473d4b0088f7b9e103d127104a11aa3fd6e1 Mon Sep 17 00:00:00 2001 From: Yutaka Nishimura Date: Mon, 11 May 2026 18:51:50 +0900 Subject: [PATCH] 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) 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. --- crates/rmcp/src/model/prompt.rs | 56 ++++++++++++++++++- .../server_json_rpc_message_schema.json | 55 ++++++++---------- ...erver_json_rpc_message_schema_current.json | 55 ++++++++---------- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 72ea0e46..e3bf4061 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -158,7 +158,10 @@ pub enum PromptMessageContent { image: ImageContent, }, /// Embedded server-side resource - Resource { resource: EmbeddedResource }, + Resource { + #[serde(flatten)] + resource: EmbeddedResource, + }, /// A link to a resource that can be fetched separately ResourceLink { #[serde(flatten)] @@ -321,6 +324,57 @@ mod tests { assert!(json.contains("\"name\":\"test.txt\"")); } + #[test] + fn test_prompt_message_resource_serialization_is_flat() { + // Regression test: PromptMessageContent::Resource must serialize to + // the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }` + // and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`. + // See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts + let message = PromptMessage::new_resource( + PromptMessageRole::User, + "alc://packages/sc/narrative".to_string(), + Some("text/markdown".to_string()), + Some("# Hello".to_string()), + None, + None, + None, + ); + + let value: serde_json::Value = serde_json::to_value(&message).unwrap(); + + // Drill into content + let content = value.get("content").expect("content present"); + assert_eq!( + content.get("type").and_then(|v| v.as_str()), + Some("resource") + ); + + let resource = content + .get("resource") + .expect("resource field present at content level"); + + // Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat + assert_eq!( + resource.get("uri").and_then(|v| v.as_str()), + Some("alc://packages/sc/narrative"), + "expected flat resource.uri, got: {resource:#?}" + ); + assert_eq!( + resource.get("mimeType").and_then(|v| v.as_str()), + Some("text/markdown") + ); + assert_eq!( + resource.get("text").and_then(|v| v.as_str()), + Some("# Hello") + ); + + // Regression guard: content.resource MUST NOT contain a nested `resource` key. + assert!( + resource.get("resource").is_none(), + "double-nested resource detected (regression): {resource:#?}" + ); + } + #[test] fn test_prompt_message_content_resource_link_deserialization() { let json = r#"{ diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index 405b3e02..07dd3e14 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -140,35 +140,6 @@ ] }, "Annotated2": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "Annotated3": { "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { @@ -244,7 +215,7 @@ "name" ] }, - "Annotated4": { + "Annotated3": { "type": "object", "properties": { "annotations": { @@ -1475,7 +1446,7 @@ "resourceTemplates": { "type": "array", "items": { - "$ref": "#/definitions/Annotated4" + "$ref": "#/definitions/Annotated3" } } }, @@ -1502,7 +1473,7 @@ "resources": { "type": "array", "items": { - "$ref": "#/definitions/Annotated3" + "$ref": "#/definitions/Annotated2" } } }, @@ -2142,8 +2113,26 @@ "description": "Embedded server-side resource", "type": "object", "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, "resource": { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/ResourceContents" }, "type": { "type": "string", diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index 405b3e02..07dd3e14 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -140,35 +140,6 @@ ] }, "Annotated2": { - "type": "object", - "properties": { - "_meta": { - "description": "Optional protocol-level metadata for this content block", - "type": [ - "object", - "null" - ], - "additionalProperties": true - }, - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/ResourceContents" - } - }, - "required": [ - "resource" - ] - }, - "Annotated3": { "description": "Represents a resource in the extension with metadata", "type": "object", "properties": { @@ -244,7 +215,7 @@ "name" ] }, - "Annotated4": { + "Annotated3": { "type": "object", "properties": { "annotations": { @@ -1475,7 +1446,7 @@ "resourceTemplates": { "type": "array", "items": { - "$ref": "#/definitions/Annotated4" + "$ref": "#/definitions/Annotated3" } } }, @@ -1502,7 +1473,7 @@ "resources": { "type": "array", "items": { - "$ref": "#/definitions/Annotated3" + "$ref": "#/definitions/Annotated2" } } }, @@ -2142,8 +2113,26 @@ "description": "Embedded server-side resource", "type": "object", "properties": { + "_meta": { + "description": "Optional protocol-level metadata for this content block", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, "resource": { - "$ref": "#/definitions/Annotated2" + "$ref": "#/definitions/ResourceContents" }, "type": { "type": "string",