From 1634c62a8ba48404d20cb5628f268d3db1890680 Mon Sep 17 00:00:00 2001 From: Dmitriy Fedoryshchev Date: Thu, 13 Aug 2026 15:41:24 +0100 Subject: [PATCH] fix(mcp): accept empty text and blob resource contents ParseResourceContents chose the TextResourceContents or BlobResourceContents variant by testing the decoded field for emptiness, so a resource whose text or blob is the empty string matched neither branch and fell through to the "unsupported resource type" error. Both fields marshal unconditionally, so a resources/read response carrying an empty resource is rejected by Client.ReadResource even when the server produced it with this library. Select the variant on the presence of a string field instead; absent, null and non-string values still fall through as before. --- mcp/utils.go | 8 +++-- mcp/utils_additional_test.go | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/mcp/utils.go b/mcp/utils.go index b7bbf6e0c..320b5f6d8 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -832,7 +832,11 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error) return nil, fmt.Errorf("_meta must be an object") } - if text := ExtractString(contentMap, "text"); text != "" { + // Select the variant on the presence of a string "text" or "blob" field, + // not on its emptiness. An empty resource is still a resource, and both + // fields marshal unconditionally, so treating "" as absent rejected + // payloads this library itself produces. + if text, ok := contentMap["text"].(string); ok { return TextResourceContents{ Meta: meta, URI: uri, @@ -841,7 +845,7 @@ func ParseResourceContents(contentMap map[string]any) (ResourceContents, error) }, nil } - if blob := ExtractString(contentMap, "blob"); blob != "" { + if blob, ok := contentMap["blob"].(string); ok { return BlobResourceContents{ Meta: meta, URI: uri, diff --git a/mcp/utils_additional_test.go b/mcp/utils_additional_test.go index ad9c10412..effe40259 100644 --- a/mcp/utils_additional_test.go +++ b/mcp/utils_additional_test.go @@ -223,6 +223,40 @@ func TestParseResourceContents(t *testing.T) { assert.Contains(t, err.Error(), "uri is missing") }) + t.Run("empty text resource", func(t *testing.T) { + contentMap := map[string]any{ + "uri": "file:///empty.txt", + "mimeType": "text/plain", + "text": "", + } + + result, err := ParseResourceContents(contentMap) + require.NoError(t, err) + + textRes, ok := result.(TextResourceContents) + require.True(t, ok) + assert.Equal(t, "file:///empty.txt", textRes.URI) + assert.Equal(t, "text/plain", textRes.MIMEType) + assert.Empty(t, textRes.Text) + }) + + t.Run("empty blob resource", func(t *testing.T) { + contentMap := map[string]any{ + "uri": "file:///empty.bin", + "mimeType": "application/octet-stream", + "blob": "", + } + + result, err := ParseResourceContents(contentMap) + require.NoError(t, err) + + blobRes, ok := result.(BlobResourceContents) + require.True(t, ok) + assert.Equal(t, "file:///empty.bin", blobRes.URI) + assert.Equal(t, "application/octet-stream", blobRes.MIMEType) + assert.Empty(t, blobRes.Blob) + }) + t.Run("no text or blob", func(t *testing.T) { contentMap := map[string]any{ "uri": "file:///test", @@ -232,6 +266,40 @@ func TestParseResourceContents(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported resource type") }) + + t.Run("non-string text falls through", func(t *testing.T) { + contentMap := map[string]any{ + "uri": "file:///test", + "text": 42, + } + + _, err := ParseResourceContents(contentMap) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported resource type") + }) +} + +// Test that a resources/read response carrying an empty text resource round +// trips through ParseReadResourceResult, which is the path Client.ReadResource +// takes. + +func TestParseReadResourceResultEmptyText(t *testing.T) { + payload, err := json.Marshal(ReadResourceResult{ + Contents: []ResourceContents{ + TextResourceContents{URI: "file:///empty.txt", MIMEType: "text/plain"}, + }, + }) + require.NoError(t, err) + + raw := json.RawMessage(payload) + result, err := ParseReadResourceResult(&raw) + require.NoError(t, err) + require.Len(t, result.Contents, 1) + + textRes, ok := result.Contents[0].(TextResourceContents) + require.True(t, ok) + assert.Equal(t, "file:///empty.txt", textRes.URI) + assert.Empty(t, textRes.Text) } // Test ParseGetPromptResult with malformed JSON