mcp: preserve required text field when marshaling empty ResourceContents - #1149
Open
shoemoney wants to merge 1 commit into
Open
mcp: preserve required text field when marshaling empty ResourceContents#1149shoemoney wants to merge 1 commit into
shoemoney wants to merge 1 commit into
Conversation
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 (modelcontextprotocol#91) and ImageContent/AudioContent (modelcontextprotocol#95) already got this treatment; ResourceContents was the one left.
guglielmo-san
approved these changes
Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
ResourceContents(mcp/content.go) carriesText string \json:"text,omitempty"`. One struct backs both branches of the spec'sanyOf[TextResourceContents, BlobResourceContents], andtextis **required** onTextResourceContents. A text resource with empty content (e.g.&ResourceContents{URI: "u"}) therefore marshals to{"uri":"u"}— notext` key at all — whichmatches neither branch of the union. Strict clients validating against the schema reject the
response outright.
This doesn't show up in Go↔Go round-trips because the Go unmarshaler is lenient about the
missing field, so nothing caught it until now.
The fix
Added
MarshalJSONonResourceContents, mirroring the existingTextContent.MarshalJSON(same mechanism, no new pattern introduced). It branches on
Blob:Blob == nil→ text resource:textis always emitted, even when empty;blobis omitted.Blob != nil(including[]byte{}) → blob resource:blobis emitted;textis omittedentirely.
_metais carried through on both branches — flagging this explicitly because #96 previouslyhad to go back and repair a dropped
Metafield after #95, so it's an easy thing to miss whensplitting a marshal method into two wire structs.
Struct tags on the fields are left untouched, since they're still what
UnmarshalJSONrelies on(there's no custom unmarshaler for
ResourceContents).This finishes the series the TODO at the top of
content.goasks for(
// TODO(findleyr): update JSON marshalling of all content types to preserve required fields.):TextContentlanded in #91,ImageContent/AudioContentin #95, andResourceContentswas theone left.
Test change — please look closely here
mcp/content_test.go'sTestEmbeddedResourcepinned{"uri":"u"}as the expected output for&ResourceContents{URI: "u"}(empty text, nil blob) — that expectation was the bug. Updated itto
{"uri":"u","text":""}with a comment explaining why. Calling this out explicitly sincechanging a test to make your own fix pass is exactly the kind of diff that deserves a second set
of eyes.
The existing
&ResourceContents{URI: "u", Blob: []byte{}}→{"uri":"u","blob":""}case isunchanged and still passes, confirming the blob branch wasn't touched.
Verification
gofmt -l ./— cleango build ./...— passesgo vet ./...— passesgo test ./...— all packages passDiscrimination check: reverted only the source change (kept the new test expectation) and ran
go test ./mcp/ -run TestEmbeddedResource -v. It failed exactly as expected:Restored the fix and reran the full suite — passes again.
Dedupe check
Searched open/closed issues and PRs for
ResourceContents/MarshalJSON/text. #781 and #782moved
Blobtojson:"blob,omitzero"(Go 1.24) — already landed, which is whyBlobhas thattag today — but neither touched
Text. No open PR currently touchescontent.go. This isn't aduplicate of prior work; it's the same series continued.