From b136041461809888b2e4637d74ff595bf642f93b Mon Sep 17 00:00:00 2001 From: Jeremy Schoemaker Date: Wed, 5 Aug 2026 23:33:55 -0500 Subject: [PATCH] mcp: preserve required text field when marshaling empty ResourceContents ResourceContents.Text carries json:"text,omitempty", so a text resource with empty content marshals without a text key and matches neither branch of the spec's anyOf[TextResourceContents, BlobResourceContents] union. Strict clients reject the response even though it round-trips fine between two Go peers, since the Go unmarshaler is lenient about the missing field. Add MarshalJSON on ResourceContents, branching on Blob so nil Blob always emits text (even empty) and omits blob, while a non-nil Blob (including an empty one) emits blob and omits text entirely. _meta is carried on both branches. This mirrors TextContent.MarshalJSON and finishes the series from the TODO at the top of content.go: TextContent (#91) and ImageContent/AudioContent (#95) already got this treatment; ResourceContents was the one left. --- mcp/content.go | 33 +++++++++++++++++++++++++++++++++ mcp/content_test.go | 4 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mcp/content.go b/mcp/content.go index 7af90fe7..adb2048e 100644 --- a/mcp/content.go +++ b/mcp/content.go @@ -307,6 +307,39 @@ type ResourceContents struct { Meta Meta `json:"_meta,omitempty"` } +func (r *ResourceContents) MarshalJSON() ([]byte, error) { + // Custom wire format to ensure the required "text" or "blob" field is + // always included, even when empty, so a text resource with empty + // content still matches the TextResourceContents branch of the spec's + // anyOf union instead of matching neither branch. + if r.Blob != nil { + wire := struct { + URI string `json:"uri"` + MIMEType string `json:"mimeType,omitempty"` + Blob []byte `json:"blob"` + Meta Meta `json:"_meta,omitempty"` + }{ + URI: r.URI, + MIMEType: r.MIMEType, + Blob: r.Blob, + Meta: r.Meta, + } + return json.Marshal(wire) + } + wire := struct { + URI string `json:"uri"` + MIMEType string `json:"mimeType,omitempty"` + Text string `json:"text"` + Meta Meta `json:"_meta,omitempty"` + }{ + URI: r.URI, + MIMEType: r.MIMEType, + Text: r.Text, + Meta: r.Meta, + } + return json.Marshal(wire) +} + // wireContent is the wire format for content. // It represents the protocol types TextContent, ImageContent, AudioContent, // ResourceLink, EmbeddedResource, ToolUseContent, and ToolResultContent. diff --git a/mcp/content_test.go b/mcp/content_test.go index 094950eb..39af5e20 100644 --- a/mcp/content_test.go +++ b/mcp/content_test.go @@ -160,8 +160,10 @@ func TestEmbeddedResource(t *testing.T) { `{"uri":"u","mimeType":"m","text":"t","_meta":{"key":"value"}}`, }, { + // Text is required by TextResourceContents in the spec, so it must + // always be present, even when empty. &mcp.ResourceContents{URI: "u"}, - `{"uri":"u"}`, + `{"uri":"u","text":""}`, }, { &mcp.ResourceContents{URI: "u", Blob: []byte{}},