openai/codex · error

MCP HTTP headers helper must output a JSON object of strings

Error message

MCP HTTP headers helper must output a JSON object of strings

What it means

After UTF-8 decoding, stdout must deserialize into a JSON object whose keys and values are all strings, as a single document with nothing after it (deserializer.end() rejects trailing tokens). Arrays, nested objects, numeric/boolean values, or extra text all fail here. The error does not echo the output, so malformed content with embedded secrets is not leaked.

Solutions

  1. Emit exactly one flat JSON object: {"Header-Name": "value", ...}
  2. Quote every value (numbers included) and keep values as strings
  3. Strip banners/debug lines so stdout contains only the JSON object
  4. Validate shape with jq before wiring the helper in: jq -e 'type == "object"'

Example fix

# before
{"X-Trace-Id": 123, "X-Org": {"id": 7}}

# after
{"X-Trace-Id": "123", "X-Org-Id": "7"}
Defensive patterns

Strategy: validation

Validate before calling

# Assert the exact contract: one JSON object, all values strings
cd "$MCP_CWD" && env -i PATH=/usr/bin:/bin sh -c "$HTTP_HEADERS_HELPER" | jq -e \
  'type == "object" and ([to_entries[].value | type] | all(. == "string"))' >/dev/null \
  && echo shape-ok || echo bad-shape

Type guard

fn is_helper_bad_json_shape(error: &anyhow::Error) -> bool {
    error.to_string().contains("must output a JSON object of strings")
}

Prevention

When it happens

Trigger: Helper output like ["a"], {"h": {"v": 1}}, {"n": 5}, a JSON object followed by extra text, multiple concatenated JSON documents, or JSONL lines.

Common situations: Helpers returning structured credential envelopes (nested tokens); scripts printing a banner before or after the JSON; tools emitting several JSON objects; YAML or pretty-printed non-JSON output.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/0e8448240da16149. Report an issue: GitHub.

Appendix: source

Thrown at codex-rs/rmcp-client/src/http_headers.rs:357

        }
        Ok(output)
    })
    .await
    .map_err(|_| anyhow!("MCP HTTP headers helper timed out after 10 seconds"))??;

    parse_helper_output(output)
}

fn parse_helper_output(stdout: Vec<u8>) -> Result<HeaderMap> {
    let stdout = String::from_utf8(stdout)
        .map_err(|_| anyhow!("MCP HTTP headers helper wrote non-UTF-8 data"))?;
    let mut deserializer = serde_json::Deserializer::from_str(stdout.trim());
    let headers = RawHeaderEntries::deserialize(&mut deserializer)
        .and_then(|headers| {
            deserializer.end()?;
            Ok(headers)
        })
        .map_err(|_| anyhow!("MCP HTTP headers helper must output a JSON object of strings"))?;
    if headers.has_exact_duplicate {
        return Err(anyhow!(
            "MCP HTTP headers helper returned duplicate header names"
        ));
    }
    let mut parsed = HeaderMap::with_capacity(headers.entries.len());
    for (name, value) in headers.entries {
        let name = HeaderName::from_bytes(name.as_bytes())
            .map_err(|_| anyhow!("MCP HTTP headers helper returned an invalid header name"))?;
        // Helper values replace same-name configured headers; bearer/OAuth owns Authorization.
        // Google IAP uses Proxy-Authorization alongside application Authorization. For HTTPS MCP
        // URLs it is sent through the forward-proxy tunnel to IAP, not used as CONNECT auth.
        if matches!(
            name.as_str(),
            "accept"
                | "authorization"
                | "connection"
                | "content-encoding"

View on GitHub (pinned to 339751715c)