#1764

[Bug]: MCP endpoint returns 415 for legal Content-Type spellings (charset parameter, letter case) — go-sdk pinned at v1.5.0

OpenDudiPeretz-Orcaopened 2mo ago

Bug Description

The MCP endpoint (POST /api/mcp) rejects Content-Type values that are legal spellings of application/json, returning HTTP 415 Content-Type must be 'application/json'.

Two forms are refused that should be accepted per RFC 9110 §8.3:

  • application/json; charset=utf-8 — parameters are explicitly permitted
  • Application/JSON — media types are case-insensitive

This is not hoop's own code: the check comes from modelcontextprotocol/go-sdk, which gateway/go.mod pins at v1.5.0. Upstream fixed it in v1.6.0 (PR #878, "mcp: allow Content-Type parameters in streamable transport").

Why it matters in practice: Apache HttpClient (Java) appends charset=utf-8 to JSON bodies by default, so any Java-based MCP client is refused. We hit it with AWS Bedrock AgentCore, but AWS is incidental — it just happens to use that library. AgentCore sends a capability probe first (bare application/json, accepted), then falls back to a standard initialize carrying the charset parameter — and that fallback is refused, so MCP server registration never completes.

Steps to Reproduce

Any valid hoop API key against a hoop gateway (initialize is a read-only handshake):

export HOOP_API_KEY='<key>'
URL=https://<your-hoop-host>/api/mcp
BODY='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'

for CT in 'application/json' 'application/json; charset=utf-8' 'Application/JSON'; do
  printf '%-34s -> ' "$CT"
  curl -s -o /tmp/p.txt -w '%{http_code}  ' -X POST \
    -H "Authorization: Bearer $HOOP_API_KEY" \
    -H "Content-Type: $CT" \
    -H 'Accept: application/json, text/event-stream' \
    --data "$BODY" "$URL"
  head -c 80 /tmp/p.txt; echo
done

Expected Behavior

All three succeed. The media type is application/json in every case; a charset parameter and letter case are not semantic differences.

Actual Behavior

application/json                   -> 200  event: message
                                           data: {"jsonrpc":"2.0","id":1,"result":{...
application/json; charset=utf-8    -> 415  Content-Type must be 'application/json'
Application/JSON                   -> 415  Content-Type must be 'application/json'

Version

1.150.2 (latest release; also present on main)

Browser(s)

Other — not browser-related; server-side HTTP API

Operating System

Server-side (hoop gateway, Linux container); reproduced from a Linux client

Additional Context

Root cause. gateway/go.mod pins github.com/modelcontextprotocol/go-sdk v1.5.0, whose mcp/streamable.go does a literal string comparison:

contentType := req.Header.Get("Content-Type")
if contentType != "application/json" {
    http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType)
    return
}

v1.6.0 parses the media type instead, which fixes both the parameter and the case problem:

if req.Method == http.MethodPost && baseMediaType(req.Header.Get("Content-Type")) != "application/json" {

where baseMediaType is mime.ParseMediaType(value) returning the type without parameters (and normalized case).

Note on dates: PR #878 merged 2026-04-03 and v1.5.0 was released 2026-04-07, so it looks as though v1.5.0 should already contain the fix. It does not — v1.5.0 was cut from an earlier commit (the merge commit is 7 commits ahead of the v1.5.0 tag, 0 behind). Comparing the tagged sources directly is the reliable check:

for T in v1.5.0 v1.6.0; do
  echo "== $T"
  gh api "repos/modelcontextprotocol/go-sdk/contents/mcp/streamable.go?ref=$T" \
    --jq '.content' | base64 -d | grep -n -B2 "Content-Type must be"
done

Suggested fix. Bump modelcontextprotocol/go-sdk in gateway/go.mod to v1.6.1 or later (v1.7.0 is current). We suggest v1.6.1 rather than v1.6.0 because v1.6.1 adds the MCPGODEBUG=disablecontenttypecheck=1 escape hatch, which does not exist in earlier builds.

⚠️ One thing to be careful about when bumping — a security default changed. In v1.5.0, leaving CrossOriginProtection unset meant protection was on:

if h.opts.CrossOriginProtection == nil {
    h.opts.CrossOriginProtection = &http.CrossOriginProtection{}
}

In v1.6.0 the same line became conditional on an opt-in:

if h.opts.CrossOriginProtection == nil && enableoriginverification == "1" {

gateway/api/mcpserver/mcpserver.go constructs the handler as &mcp.StreamableHTTPOptions{SessionTimeout: mcpSessionTimeout} and never sets CrossOriginProtection — so a straight version bump would silently disable cross-origin protection on the gateway. Setting MCPGODEBUG=enableoriginverification=1, or configuring CrossOriginProtection explicitly (or wrapping the handler with http.NewCrossOriginProtection().Handler(...), as the v1.6.0 deprecation note suggests), preserves the current behavior.

Two smaller v1.6.0 behavior changes worth knowing while upgrading: CallToolResult.SetError now preserves existing content instead of overwriting it (if len(r.Content) == 0 || seterroroverwrite == "1"), and the jsonescaping option was removed.

Observability note. The api-audit log line records content-length, email, method, org, path and user-agent, but not content-type — so a rejected request looks unremarkable in the logs. Filtering path == "/api/mcp" shows the successful traffic; the 415s are distinguishable mainly by a user-agent such as Apache-HttpAsyncClient/5.6.1 (Java/21.0.12.1).

We searched existing issues for content-type, 415 and charset and found nothing, and no open PR bumping the SDK — apologies if we missed a duplicate.

Relevant Log Output

$ curl -s -o /dev/stderr -w '%{http_code}\n' -X POST \
    -H "Authorization: Bearer $HOOP_API_KEY" \
    -H "Content-Type: application/json; charset=utf-8" \
    -H 'Accept: application/json, text/event-stream' \
    --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
    https://<hoop-host>/api/mcp

Content-Type must be 'application/json'
415

Activity

1
github-actions[bot]2mo ago

🔗 Synced to Linear: SUP-226