fix: do not omitempty ReadOnlyHint in ToolAnnotations (#908)

`ReadOnlyHint` is a `bool` with omitempty, so the zero value (false) is
indistinguishable from unset, and **drops out** of marshaled JSON.

The current behavior causes an issue with the OpenAI MCP app submission
because they require explicit hints.

Consumers that explicitly set `ReadOnlyHint: false` on write tools lose
the field on the wire. Removing `omitempty` ensures `false` is always
serialized, which matches the MCP spec default.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
Co-authored-by: guglielmoc <guglielmoc@google.com>
This commit is contained in:
Pavel Bazin 2026-05-25 10:52:15 -03:00 • committed by GitHub
commit c044cdb734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1346,6 +1346,13 @@ type Tool struct {
Icons []Icon `json:"icons,omitempty"`
}
// hintomitempty is a compatibility parameter that restores the pre-1.7.0
// behavior of [ToolAnnotations] JSON marshaling, where false-valued bare bool
// fields (ReadOnlyHint, IdempotentHint) were omitted from the output.
// See the documentation for the mcpgodebug package for instructions on how to
// enable it.
var hintomitempty = mcpgodebug.Value("hintomitempty")
// Additional properties describing a Tool to clients.
//
// NOTE: all properties in ToolAnnotations are hints. They are not
@ -1368,7 +1375,7 @@ type ToolAnnotations struct {
// (This property is meaningful only when ReadOnlyHint == false.)
//
// Default: false
IdempotentHint bool `json:"idempotentHint,omitempty"`
IdempotentHint bool `json:"idempotentHint"`
// If true, this tool may interact with an "open world" of external entities. If
// false, the tool's domain of interaction is closed. For example, the world of
// a web search tool is open, whereas that of a memory tool is not.
@ -1378,11 +1385,30 @@ type ToolAnnotations struct {
// If true, the tool does not modify its environment.
//
// Default: false
ReadOnlyHint bool `json:"readOnlyHint,omitempty"`
ReadOnlyHint bool `json:"readOnlyHint"`
// A human-readable title for the tool.
Title string `json:"title,omitempty"`
}
// MarshalJSON implements [json.Marshaler] for ToolAnnotations.
//
// To restore the previous behavior where false-valued ReadOnlyHint and
// IdempotentHint were omitted, set MCPGODEBUG=hintomitempty=1.
func (t ToolAnnotations) MarshalJSON() ([]byte, error) {
if hintomitempty == "1" {
type compat struct {
DestructiveHint *bool `json:"destructiveHint,omitempty"`
IdempotentHint bool `json:"idempotentHint,omitempty"`
OpenWorldHint *bool `json:"openWorldHint,omitempty"`
ReadOnlyHint bool `json:"readOnlyHint,omitempty"`
Title string `json:"title,omitempty"`
}
return json.Marshal(compat(t))
}
type nomethod ToolAnnotations
return json.Marshal(nomethod(t))
}
type ToolListChangedParams struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.