Summary
PromptMessageContent::Resource currently serializes with a doubly-nested resource object, producing a wire shape that does not conform to the MCP specification's Prompts → Embedded Resources definition.
Affected versions: at least rmcp-v1.4.0, rmcp-v1.5.0, rmcp-v1.6.0, and the current main branch.
Actual (current) JSON
{
"type": "resource",
"resource": {
"resource": {
"uri": "file:///example.md",
"mimeType": "text/markdown",
"text": "..."
}
}
}
Expected (MCP spec) JSON
Per the MCP spec, Prompts → Embedded Resources:
https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
{
"type": "resource",
"resource": {
"uri": "file:///example.md",
"mimeType": "text/markdown",
"text": "..."
}
}
Root cause
In crates/rmcp/src/model/prompt.rs, the Resource variant of PromptMessageContent is the only variant missing #[serde(flatten)]:
pub enum PromptMessageContent {
Text { text: String },
Image {
#[serde(flatten)] // ← present
image: ImageContent,
},
Resource { resource: EmbeddedResource }, // ← #[serde(flatten)] missing
ResourceLink {
#[serde(flatten)] // ← present
link: super::resource::Resource,
},
}
Because EmbeddedResource = Annotated<RawEmbeddedResource> and Annotated<T> itself uses #[serde(flatten)] on its inner T, the inner RawEmbeddedResource field resource: ResourceContents is exposed inside the outer Resource { resource: ... } field, producing the doubly-nested wire shape.
The other three variants (Text, Image, ResourceLink) serialize flat. Resource is the only outlier.
Impact
Spec-conformant MCP clients (e.g. Claude Code, which uses Zod schema validation against the spec) reject prompts/get responses containing an embedded resource message:
ZodError: invalid_union at messages[N].content
- expected resource.uri (string), received undefined
This breaks the embedded-resource path of prompts/get end-to-end with conformant clients. Other content types (text, image, resource_link) are unaffected.
Minimal reproducer
use rmcp::model::{PromptMessage, PromptMessageRole};
fn main() {
let m = PromptMessage::new_resource(
PromptMessageRole::User,
"file:///example.md".to_string(),
Some("text/markdown".to_string()),
Some("# Hello".to_string()),
None,
None,
None,
);
println!("{}", serde_json::to_string_pretty(&m).unwrap());
}
Observed output shows content.resource.resource.{uri,mimeType,text} instead of content.resource.{uri,mimeType,text}.
Notes on related history
Proposed fix
Add #[serde(flatten)] to the Resource variant's inner field, matching the existing pattern used by Image and ResourceLink in the same enum. Single-line change. Public Rust API (variant name, field name, field type) is unchanged; only the serde representation is corrected.
I have a fix ready on a fork branch (commits include a regression test and a regenerated schema snapshot) and will open a PR linked to this issue.
Summary
PromptMessageContent::Resourcecurrently serializes with a doubly-nestedresourceobject, producing a wire shape that does not conform to the MCP specification's Prompts → Embedded Resources definition.Affected versions: at least
rmcp-v1.4.0,rmcp-v1.5.0,rmcp-v1.6.0, and the currentmainbranch.Actual (current) JSON
{ "type": "resource", "resource": { "resource": { "uri": "file:///example.md", "mimeType": "text/markdown", "text": "..." } } }Expected (MCP spec) JSON
Per the MCP spec, Prompts → Embedded Resources:
https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
{ "type": "resource", "resource": { "uri": "file:///example.md", "mimeType": "text/markdown", "text": "..." } }Root cause
In
crates/rmcp/src/model/prompt.rs, theResourcevariant ofPromptMessageContentis the only variant missing#[serde(flatten)]:Because
EmbeddedResource = Annotated<RawEmbeddedResource>andAnnotated<T>itself uses#[serde(flatten)]on its innerT, the innerRawEmbeddedResourcefieldresource: ResourceContentsis exposed inside the outerResource { resource: ... }field, producing the doubly-nested wire shape.The other three variants (
Text,Image,ResourceLink) serialize flat.Resourceis the only outlier.Impact
Spec-conformant MCP clients (e.g. Claude Code, which uses Zod schema validation against the spec) reject
prompts/getresponses containing an embedded resource message:This breaks the embedded-resource path of
prompts/getend-to-end with conformant clients. Other content types (text,image,resource_link) are unaffected.Minimal reproducer
Observed output shows
content.resource.resource.{uri,mimeType,text}instead ofcontent.resource.{uri,mimeType,text}.Notes on related history
feat(rmcp): add optional _meta to CallToolResult, EmbeddedResource, and ResourceContents) — the PR description illustrates the expected (flat) JSON shape, but the implementation still produces the doubly-nested shape. The discrepancy appears to be a review oversight rather than an intentional change.feat: add resource_link support to tools and prompts) — correctly uses#[serde(flatten)]on theResourceLinkvariant. The same pattern needs to apply toResource.Proposed fix
Add
#[serde(flatten)]to theResourcevariant's inner field, matching the existing pattern used byImageandResourceLinkin the same enum. Single-line change. Public Rust API (variant name, field name, field type) is unchanged; only the serde representation is corrected.I have a fix ready on a fork branch (commits include a regression test and a regenerated schema snapshot) and will open a PR linked to this issue.