Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion gateway/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,45 @@ func projectMCPMarkedValue(value reflect.Value) (interface{}, bool) {
}
marker := value.FieldByName("Has")
if !marker.IsValid() || marker.Kind() != reflect.Ptr || marker.IsNil() || marker.Elem().Kind() != reflect.Struct {
return mcpJSONValue(value), false
// A discriminated wrapper can contain marked patches without owning
// a marker itself. Preserve the wrapper's JSON shape while projecting
// its marked children before omitempty can erase explicit clears.
if implementsMCPJSONMarshaler(value.Interface()) {
return mcpJSONValue(value), false
}
normalized, ok := mcpJSONValue(value).(map[string]interface{})
if !ok {
return mcpJSONValue(value), false
}
marked := false
for index := 0; index < value.NumField(); index++ {
field := value.Type().Field(index)
if !field.IsExported() {
continue
}
name := strings.Split(field.Tag.Get("json"), ",")[0]
if name == "-" {
continue
}
projected, childMarked := projectMCPMarkedValue(value.Field(index))
if !childMarked {
continue
}
if field.Anonymous && name == "" {
if embedded, ok := projected.(map[string]interface{}); ok {
for key, item := range embedded {
normalized[key] = item
}
}
} else {
if name == "" {
name = field.Name
}
normalized[name] = projected
}
marked = true
}
return normalized, marked
}
marker = marker.Elem()
return projectMCPMarkedStruct(value, marker), true
Expand Down
36 changes: 36 additions & 0 deletions gateway/mcp_wrapped_presence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gateway

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestMCPBodyPreservesWrappedPatchClears(t *testing.T) {
type presence struct {
ID, Tags, Nullable, Omitted bool
}
type patch struct {
ID int `json:"id"`
Tags []string `json:"tags,omitempty"`
Nullable *string `json:"nullable,omitempty"`
Omitted string `json:"omitted,omitempty"`
Has *presence `json:"-"`
}
type wrapper struct {
Kind string `json:"kind"`
Patch *patch `json:"patch,omitempty"`
}
input := []wrapper{{Kind: "native", Patch: &patch{
ID: 7, Tags: []string{}, Has: &presence{ID: true, Tags: true, Nullable: true},
}}}
encoded, err := json.Marshal(preserveMCPBodyPresence(nil, input))
require.NoError(t, err)
require.JSONEq(t, `[{"kind":"native","patch":{"id":7,"tags":[],"nullable":null}}]`, string(encoded))
// Unmarked models retain their existing omitempty behavior.
input[0].Patch.Has = nil
encoded, err = json.Marshal(preserveMCPBodyPresence(nil, input))
require.NoError(t, err)
require.JSONEq(t, `[{"kind":"native","patch":{"id":7}}]`, string(encoded))
}
Loading