From d709eff64bcc99e2ee3975ef603f4e876c2a886b Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 03:58:04 +0530 Subject: [PATCH 1/2] Map text DataContent to an MCP text resource, not a binary blob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When exposing an agent as an MCP tool, agentContentToMCPContent converts a DataContent to an mcp.EmbeddedResource. Image and audio media types get dedicated content types, but every other media type — including text/* — fell through to the default case and was written to Resource.Blob as base64 binary. The reverse mapping (mcpContentToAgentContent) already branches on Resource.Text to reconstruct text, so the writer never producing Text is an asymmetry: a text DataContent round-tripped through MCP became an unreadable binary blob, and MCP clients reading the resource would treat text as binary. Add a text case that populates Resource.Text with the decoded string. Binary payloads (e.g. application/octet-stream) still use Blob. --- tool/mcptool/mcp.go | 9 +++++++++ tool/mcptool/mcp_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tool/mcptool/mcp.go b/tool/mcptool/mcp.go index ad76fb14..78a838d3 100644 --- a/tool/mcptool/mcp.go +++ b/tool/mcptool/mcp.go @@ -339,6 +339,15 @@ func agentContentToMCPContent(contentValue message.Content) mcp.Content { return &mcp.ImageContent{Data: data, MIMEType: contentValue.MediaType} case "audio": return &mcp.AudioContent{Data: data, MIMEType: contentValue.MediaType} + case "text": + // Text resources carry their payload in Text, not Blob. The reverse + // mapping (mcpContentToAgentContent) already reads Resource.Text for + // text; emitting Blob here would make text unreadable to MCP clients. + return &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{ + URI: contentValue.Name, + MIMEType: contentValue.MediaType, + Text: string(data), + }} default: return &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{ URI: contentValue.Name, diff --git a/tool/mcptool/mcp_test.go b/tool/mcptool/mcp_test.go index ef0f5d1d..8244cc6c 100644 --- a/tool/mcptool/mcp_test.go +++ b/tool/mcptool/mcp_test.go @@ -732,6 +732,35 @@ func TestAddToolReturnsBinaryDataAsEmbeddedResource(t *testing.T) { } } +// A text DataContent must be surfaced as an MCP text resource (Resource.Text), +// not a binary Blob; the reverse mapping reads Resource.Text for text resources. +func TestAddToolReturnsTextDataAsTextResource(t *testing.T) { + result := callAddedTool(t, stubFuncTool{ + name: "text-result", + description: "returns text content", + schema: map[string]any{"type": "object"}, + returnSchema: map[string]any{"type": "object"}, + call: func(context.Context, string) (any, error) { + // "hello" base64-encoded, with a text media type. + return &message.DataContent{Name: "note.txt", Data: "aGVsbG8=", MediaType: "text/plain"}, nil + }, + }) + + if len(result.Content) != 1 { + t.Fatalf("expected one content item, got %d", len(result.Content)) + } + embedded, ok := result.Content[0].(*mcp.EmbeddedResource) + if !ok { + t.Fatalf("content is %T, want *mcp.EmbeddedResource", result.Content[0]) + } + if embedded.Resource.Text != "hello" { + t.Errorf("resource Text = %q, want %q", embedded.Resource.Text, "hello") + } + if len(embedded.Resource.Blob) != 0 { + t.Errorf("text resource must not use Blob, got %#v", embedded.Resource.Blob) + } +} + func TestCallReturnsEmptyAndStructuredOnlyMCPResults(t *testing.T) { t.Run("empty", func(t *testing.T) { result := callMCPResult(t, &mcp.CallToolResult{}) From 368b45f89176d20a99b0814019b10b801c615f68 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 10:01:22 +0530 Subject: [PATCH 2/2] Fall back to Blob for non-UTF-8 text resources Address review feedback: a text-typed DataContent whose decoded bytes are not valid UTF-8 would be corrupted if placed in ResourceContents.Text (JSON transport replaces invalid sequences). Emit a binary Blob resource in that case. Adds a regression test. --- tool/mcptool/mcp.go | 12 +++++++++++- tool/mcptool/mcp_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tool/mcptool/mcp.go b/tool/mcptool/mcp.go index 78a838d3..ee0983ec 100644 --- a/tool/mcptool/mcp.go +++ b/tool/mcptool/mcp.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "strings" + "unicode/utf8" "github.com/microsoft/agent-framework-go/message" "github.com/microsoft/agent-framework-go/tool" @@ -343,10 +344,19 @@ func agentContentToMCPContent(contentValue message.Content) mcp.Content { // Text resources carry their payload in Text, not Blob. The reverse // mapping (mcpContentToAgentContent) already reads Resource.Text for // text; emitting Blob here would make text unreadable to MCP clients. + // Non-UTF-8 payloads cannot survive JSON transport as Text (invalid + // sequences are replaced), so fall back to Blob for those. + if utf8.Valid(data) { + return &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{ + URI: contentValue.Name, + MIMEType: contentValue.MediaType, + Text: string(data), + }} + } return &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{ URI: contentValue.Name, MIMEType: contentValue.MediaType, - Text: string(data), + Blob: data, }} default: return &mcp.EmbeddedResource{Resource: &mcp.ResourceContents{ diff --git a/tool/mcptool/mcp_test.go b/tool/mcptool/mcp_test.go index 8244cc6c..acafbba8 100644 --- a/tool/mcptool/mcp_test.go +++ b/tool/mcptool/mcp_test.go @@ -706,6 +706,35 @@ func TestAddToolReturnsDataAndMultipleContentResults(t *testing.T) { }) } +// A text-typed DataContent whose bytes are not valid UTF-8 must fall back to a +// binary Blob resource; putting invalid UTF-8 in Text corrupts it on transport. +func TestAddToolReturnsInvalidUTF8TextAsBlob(t *testing.T) { + result := callAddedTool(t, stubFuncTool{ + name: "bad-utf8-result", + description: "returns text media type with non-UTF-8 bytes", + schema: map[string]any{"type": "object"}, + returnSchema: map[string]any{"type": "object"}, + call: func(context.Context, string) (any, error) { + // "//4=" is base64 for {0xff, 0xfe}, which is not valid UTF-8. + return &message.DataContent{Name: "note.txt", Data: "//4=", MediaType: "text/plain"}, nil + }, + }) + + if len(result.Content) != 1 { + t.Fatalf("expected one content item, got %d", len(result.Content)) + } + embedded, ok := result.Content[0].(*mcp.EmbeddedResource) + if !ok { + t.Fatalf("content is %T, want *mcp.EmbeddedResource", result.Content[0]) + } + if embedded.Resource.Text != "" { + t.Errorf("Resource.Text = %q, want empty (non-UTF-8 must not be placed in Text)", embedded.Resource.Text) + } + if len(embedded.Resource.Blob) == 0 { + t.Error("Resource.Blob is empty, want the raw non-UTF-8 bytes") + } +} + func TestAddToolReturnsBinaryDataAsEmbeddedResource(t *testing.T) { result := callAddedTool(t, stubFuncTool{ name: "binary-result",