Skip to content

Commit da444aa

Browse files
manusaclaude
andcommitted
fix(mcp): wrap slice structuredContent in object per MCP spec
The MCP specification requires structuredContent to marshal to a JSON object, but NewStructuredResult could receive slice/array values. Add ensureStructuredObject to automatically wrap slices in {"items": [...]} so that the result always complies with the spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Marc Nuri <marc@marcnuri.com>
1 parent c1f81f6 commit da444aa

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

pkg/mcp/mcp.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"os"
8+
"reflect"
89
"slices"
910
"sync"
1011
"time"
@@ -399,6 +400,10 @@ func NewTextResult(content string, err error) *mcp.CallToolResult {
399400
// The Content field contains the JSON-serialized form of structuredContent
400401
// for backward compatibility with MCP clients that don't support structuredContent.
401402
//
403+
// Per the MCP specification, structuredContent must marshal to a JSON object.
404+
// If structuredContent is a slice/array, it is automatically wrapped in
405+
// {"items": [...]} to satisfy this requirement.
406+
//
402407
// Per the MCP specification:
403408
// "For backwards compatibility, a tool that returns structured content SHOULD
404409
// also return the serialized JSON in a TextContent block."
@@ -425,7 +430,19 @@ func NewStructuredResult(content string, structuredContent any, err error) *mcp.
425430
},
426431
}
427432
if structuredContent != nil {
428-
result.StructuredContent = structuredContent
433+
result.StructuredContent = ensureStructuredObject(structuredContent)
429434
}
430435
return result
431436
}
437+
438+
// ensureStructuredObject wraps slice/array values in a {"items": ...} object
439+
// because the MCP specification requires structuredContent to be a JSON object.
440+
// Note: this checks the top-level reflect.Kind, so a pointer-to-slice (*[]T)
441+
// would not be wrapped. All current callers pass value types.
442+
func ensureStructuredObject(v any) any {
443+
rv := reflect.ValueOf(v)
444+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
445+
return map[string]any{"items": v}
446+
}
447+
return v
448+
}

pkg/mcp/text_result_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ func (s *TextResultSuite) TestNewStructuredResult() {
4848
s.Equal(`{"pods":["pod-1","pod-2"]}`, tc.Text)
4949
s.Equal(structured, result.StructuredContent)
5050
})
51+
s.Run("wraps slice in object for MCP spec compliance", func() {
52+
items := []map[string]any{{"name": "ns-1"}, {"name": "ns-2"}}
53+
result := NewStructuredResult("text", items, nil)
54+
s.False(result.IsError)
55+
wrapped, ok := result.StructuredContent.(map[string]any)
56+
s.Require().True(ok, "expected map[string]any wrapper")
57+
s.Equal(items, wrapped["items"])
58+
})
59+
s.Run("does not wrap map structured content", func() {
60+
structured := map[string]any{"key": "value"}
61+
result := NewStructuredResult("text", structured, nil)
62+
s.Equal(structured, result.StructuredContent)
63+
})
5164
s.Run("omits structured content when nil", func() {
5265
result := NewStructuredResult("text output", nil, nil)
5366
s.False(result.IsError)

0 commit comments

Comments
 (0)