mcp: update JSON marshaling to not HTML-escape messages (#769)

It's causing some servers to fail: #767. I checked with Typescript
implementation and it doesn't do any escaping.

It's still a behavior change, so we'll expose a way to bring back the previous behavior via an env var.

Fixes #238 
Fixes #767
This commit is contained in:
Maciej Kisiel 2026-01-30 17:13:30 +01:00 • committed by GitHub
commit bba5acdc8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,9 +5,12 @@
package jsonrpc2
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug"
)
// ID is a Request identifier, which is defined by the spec to be a string, integer, or null.
@ -145,9 +148,9 @@ func toWireError(err error) *WireError {
func EncodeMessage(msg Message) ([]byte, error) {
wire := wireCombined{VersionTag: wireVersion}
msg.marshal(&wire)
data, err := json.Marshal(&wire)
data, err := jsonMarshal(&wire)
if err != nil {
return data, fmt.Errorf("marshaling jsonrpc message: %w", err)
return nil, fmt.Errorf("marshaling jsonrpc message: %w", err)
}
return data, nil
}
@ -158,11 +161,14 @@ func EncodeMessage(msg Message) ([]byte, error) {
func EncodeIndent(msg Message, prefix, indent string) ([]byte, error) {
wire := wireCombined{VersionTag: wireVersion}
msg.marshal(&wire)
data, err := json.MarshalIndent(&wire, prefix, indent)
if err != nil {
return data, fmt.Errorf("marshaling jsonrpc message: %w", err)
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent(prefix, indent)
if err := enc.Encode(&wire); err != nil {
return nil, fmt.Errorf("marshaling jsonrpc message: %w", err)
}
return data, nil
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
func DecodeMessage(data []byte) (Message, error) {
@ -204,9 +210,31 @@ func marshalToRaw(obj any) (json.RawMessage, error) {
if obj == nil {
return nil, nil
}
data, err := json.Marshal(obj)
data, err := jsonMarshal(obj)
if err != nil {
return nil, err
}
return json.RawMessage(data), nil
}
// jsonescaping is a compatibility parameter that allows to restore
// JSON escaping in the JSON marshaling, which stopped being the default
// in the 1.4.0 version of the SDK. See the documentation for the
// mcpgodebug package for instructions how to enable it.
// The option will be removed in the 1.6.0 version of the SDK.
var jsonescaping = mcpgodebug.Value("jsonescaping")
// jsonMarshal marshals obj to JSON like json.Marshal but without HTML escaping.
func jsonMarshal(obj any) ([]byte, error) {
if jsonescaping == "1" {
return json.Marshal(obj)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(obj); err != nil {
return nil, err
}
// json.Encoder.Encode adds a trailing newline. Trim it to be consistent with json.Marshal.
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}