[object Object]

← back to Cli Printing Press

fix(cli): handle binary-only response endpoints (Accept + base64 envelope) (#1574)

d8178077ede31c6873f63ac2ae192edeb0e18c97 · 2026-05-20 00:23:56 +0200 · andreasvainio

* fix(cli): handle binary-only response endpoints (Accept + base64 envelope)

Generated CLIs hardcoded `Accept: application/json` in the HTTP client
when no per-endpoint override was set, and ran every response through
the JSON sanitizer before returning it as `json.RawMessage`. Endpoints
whose only 2xx content type is non-JSON (e.g. `application/octet-stream`
PDF/file downloads) were therefore rejected by the server with HTTP 406,
and even with the right Accept the bytes would be mangled by the
sanitizer. This is a generic generator gap: it hits any spec with a
binary-only success response.

Fix, using existing plumbing with minimal surface:

- openapi parser: detect success responses whose media types are all
  non-JSON / non-`*/*` / non-`+json` and pin `Accept` to that type via
  the existing per-endpoint header-override path. New `upsertHeaderOverride`
  (and a merge in `applyHeaderOverrides`) keeps the Accept override stable
  when configured per-endpoint headers also apply. JSON and `*/*`
  endpoints are untouched.
- client.go.tmpl: content-type-gated base64 envelope
  (`{"_pp_binary":true,...,"data":"<b64>"}`) for genuinely binary
  success bodies so they survive the json.RawMessage contract. JSON,
  `*/*`, XML and every text/* type (incl. text/html, so
  response_format:html CLIs are unaffected) pass through unchanged. The
  error path is byte-identical (still sanitized + truncated).
- command_promoted.go.tmpl: promoted (top-level) GET commands now pass
  per-endpoint header overrides, matching typed commands. Without this
  the Accept override never reached the client for promoted commands.

Golden: extended testdata/golden/fixtures/golden-api.yaml with one
binary-only endpoint per docs/GOLDEN.md (general machine behavior),
froze its generated command file, and regenerated affected expected
fixtures. All 17 golden cases pass; every diff is attributable to the
envelope path or the one added endpoint.

Verification: go build ./..., go vet ./... (changed pkgs), go test ./...
(0 failures), 5 new parser tests, scripts/golden.sh verify (17/17).
Live-verified against a real binary endpoint: `Accept: application/json`
reproduces HTTP 406; `Accept: application/octet-stream` returns the
file (200, correct content-type).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): thread HeaderOverrides through MCP code-orchestration execute

The original binary-response fix covered typed CLI commands and promoted
commands, but the code-orchestration MCP path (<api>_execute) builds
requests from a slim endpoint table and called c.Get/Post/... directly,
so the per-endpoint Accept override never reached binary-only endpoints
— <api>_execute on a PDF/octet-stream endpoint still 406'd while the CLI
worked.

Add HeaderOverrides to codeOrchEndpoint, emit it in the generated
registry from endpoint.HeaderOverrides, and dispatch through
c.*WithHeaders when present. The client's content-type-gated base64
envelope (already in this branch) then handles the binary body.

Golden: generate-mcp-api/code_orch.go updated (struct field + WithHeaders
dispatch; table emission inert when no endpoint has overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): thread header overrides through typed MCP tools

* fix(cli): avoid text response Accept overrides

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>

Files touched

Diff

commit d8178077ede31c6873f63ac2ae192edeb0e18c97
Author: andreasvainio <vainio@panther.no>
Date:   Wed May 20 00:23:56 2026 +0200

    fix(cli): handle binary-only response endpoints (Accept + base64 envelope) (#1574)
    
    * fix(cli): handle binary-only response endpoints (Accept + base64 envelope)
    
    Generated CLIs hardcoded `Accept: application/json` in the HTTP client
    when no per-endpoint override was set, and ran every response through
    the JSON sanitizer before returning it as `json.RawMessage`. Endpoints
    whose only 2xx content type is non-JSON (e.g. `application/octet-stream`
    PDF/file downloads) were therefore rejected by the server with HTTP 406,
    and even with the right Accept the bytes would be mangled by the
    sanitizer. This is a generic generator gap: it hits any spec with a
    binary-only success response.
    
    Fix, using existing plumbing with minimal surface:
    
    - openapi parser: detect success responses whose media types are all
      non-JSON / non-`*/*` / non-`+json` and pin `Accept` to that type via
      the existing per-endpoint header-override path. New `upsertHeaderOverride`
      (and a merge in `applyHeaderOverrides`) keeps the Accept override stable
      when configured per-endpoint headers also apply. JSON and `*/*`
      endpoints are untouched.
    - client.go.tmpl: content-type-gated base64 envelope
      (`{"_pp_binary":true,...,"data":"<b64>"}`) for genuinely binary
      success bodies so they survive the json.RawMessage contract. JSON,
      `*/*`, XML and every text/* type (incl. text/html, so
      response_format:html CLIs are unaffected) pass through unchanged. The
      error path is byte-identical (still sanitized + truncated).
    - command_promoted.go.tmpl: promoted (top-level) GET commands now pass
      per-endpoint header overrides, matching typed commands. Without this
      the Accept override never reached the client for promoted commands.
    
    Golden: extended testdata/golden/fixtures/golden-api.yaml with one
    binary-only endpoint per docs/GOLDEN.md (general machine behavior),
    froze its generated command file, and regenerated affected expected
    fixtures. All 17 golden cases pass; every diff is attributable to the
    envelope path or the one added endpoint.
    
    Verification: go build ./..., go vet ./... (changed pkgs), go test ./...
    (0 failures), 5 new parser tests, scripts/golden.sh verify (17/17).
    Live-verified against a real binary endpoint: `Accept: application/json`
    reproduces HTTP 406; `Accept: application/octet-stream` returns the
    file (200, correct content-type).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix(cli): thread HeaderOverrides through MCP code-orchestration execute
    
    The original binary-response fix covered typed CLI commands and promoted
    commands, but the code-orchestration MCP path (<api>_execute) builds
    requests from a slim endpoint table and called c.Get/Post/... directly,
    so the per-endpoint Accept override never reached binary-only endpoints
    — <api>_execute on a PDF/octet-stream endpoint still 406'd while the CLI
    worked.
    
    Add HeaderOverrides to codeOrchEndpoint, emit it in the generated
    registry from endpoint.HeaderOverrides, and dispatch through
    c.*WithHeaders when present. The client's content-type-gated base64
    envelope (already in this branch) then handles the binary body.
    
    Golden: generate-mcp-api/code_orch.go updated (struct field + WithHeaders
    dispatch; table emission inert when no endpoint has overrides).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix(cli): thread header overrides through typed MCP tools
    
    * fix(cli): avoid text response Accept overrides
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Co-authored-by: Trevin Chow <trevin@trevinchow.com>
---
 .../generator/binary_paginated_promoted_test.go    |  45 ++++-
 internal/generator/generator_test.go               |   6 +-
 internal/generator/multipart_test.go               |   2 +-
 internal/generator/templates/client.go.tmpl        |  78 +++++++-
 .../generator/templates/command_promoted.go.tmpl   |  28 ++-
 internal/generator/templates/mcp_code_orch.go.tmpl |  44 ++++-
 internal/generator/templates/mcp_tools.go.tmpl     |  51 ++---
 internal/openapi/parser.go                         |  86 ++++++++-
 internal/openapi/parser_binary_response_test.go    | 207 +++++++++++++++++++++
 .../golden/cases/generate-golden-api/artifacts.txt |   1 +
 .../internal/client/client.go                      |  78 +++++++-
 .../printing-press-rich-auth/internal/mcp/tools.go |  25 ++-
 .../expected/generate-golden-api/dogfood.json      |   8 +-
 .../printing-press-golden/.printing-press.json     |   4 +-
 .../internal/cli/reports_export_report-year.go     |  59 ++++++
 .../printing-press-golden/internal/cli/sync.go     |   2 +
 .../printing-press-golden/internal/cli/which.go    |   1 +
 .../internal/client/client.go                      |  78 +++++++-
 .../printing-press-golden/internal/mcp/tools.go    |  59 ++++--
 .../expected/generate-golden-api/scorecard.json    |   2 +-
 .../mcp-cloudflare/internal/mcp/code_orch.go       |  38 +++-
 .../mcp-cloudflare/internal/mcp/tools.go           |  23 ++-
 .../public-param-golden/internal/mcp/tools.go      |  27 ++-
 .../tier-routing-golden/internal/client/client.go  |  78 +++++++-
 .../tier-routing-golden/internal/mcp/tools.go      |  29 ++-
 testdata/golden/fixtures/golden-api.yaml           |  20 ++
 26 files changed, 951 insertions(+), 128 deletions(-)

diff --git a/internal/generator/binary_paginated_promoted_test.go b/internal/generator/binary_paginated_promoted_test.go
index 1028e4d1..b307480f 100644
--- a/internal/generator/binary_paginated_promoted_test.go
+++ b/internal/generator/binary_paginated_promoted_test.go
@@ -55,8 +55,10 @@ func TestGenerateBinaryPaginatedPromotedThreadsHeader(t *testing.T) {
 	require.NoError(t, gen.Generate())
 
 	endpointSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_voices.go")
-	assert.Contains(t, endpointSrc, `headerOverrides := map[string]string{"X-Printing-Press-Binary-Response": "true"}`,
+	assert.Contains(t, endpointSrc, `headerOverrides := map[string]string{`,
 		"binary paginated promoted must declare headerOverrides")
+	assert.Contains(t, endpointSrc, `"X-Printing-Press-Binary-Response": "true",`,
+		"binary paginated promoted must include the binary sentinel")
 	assert.Contains(t, endpointSrc, `paginatedGet(c, path, map[string]string{`,
 		"non-HasStore pagination must use paginatedGet")
 	assert.NotContains(t, endpointSrc, `}, nil, flagAll,`,
@@ -94,8 +96,47 @@ func TestGenerateBinaryStoreBackedPromotedThreadsHeader(t *testing.T) {
 	require.NoError(t, gen.Generate())
 
 	endpointSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_voices.go")
-	assert.Contains(t, endpointSrc, `headerOverrides := map[string]string{"X-Printing-Press-Binary-Response": "true"}`,
+	assert.Contains(t, endpointSrc, `headerOverrides := map[string]string{`,
 		"store-backed binary GET must declare headerOverrides")
+	assert.Contains(t, endpointSrc, `"X-Printing-Press-Binary-Response": "true",`,
+		"store-backed binary GET must include the binary sentinel")
 	assert.Contains(t, endpointSrc, `resolveRead(cmd.Context(), c, flags, "voices", false, path, params, headerOverrides)`,
 		"store-backed binary GET must thread headerOverrides through resolveRead")
 }
+
+func TestGenerateBinaryMCPToolsThreadHeaderOverrides(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("audioapi")
+	apiSpec.Resources = map[string]spec.Resource{
+		"voices": {
+			Description: "Voices",
+			Endpoints: map[string]spec.Endpoint{
+				"download": {
+					Method:         "GET",
+					Path:           "/voices/{voice_id}/download",
+					Description:    "Download voice sample",
+					ResponseFormat: spec.ResponseFormatBinary,
+					HeaderOverrides: []spec.RequiredHeader{
+						{Name: "Accept", Value: "application/octet-stream"},
+					},
+					Params: []spec.Param{
+						{Name: "voice_id", Type: "string", Required: true, Positional: true, PathParam: true, Description: "Voice ID"},
+					},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
+	assert.Contains(t, mcpSrc, `makeAPIHandler("GET", "/voices/{voice_id}/download", true, map[string]string{"Accept": "application/octet-stream"},`,
+		"typed MCP tools must carry per-endpoint header overrides")
+	assert.Contains(t, mcpSrc, `headers[client.BinaryResponseHeader] = "true"`,
+		"typed MCP tools must still add the binary sentinel")
+	assert.Contains(t, mcpSrc, `data, err = c.GetWithHeaders(path, params, headers)`,
+		"typed MCP tools must dispatch through WithHeaders when headers are present")
+}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 37330043..a3d85a88 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -9389,7 +9389,7 @@ func TestGenerateOperationRoutingPathParamDefault(t *testing.T) {
 	mcpGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
 	require.NoError(t, err)
 	assert.Regexp(t,
-		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/graphql/\{pathQueryId\}/Followers",\s*false,\s*\[]mcpParamBinding\{.*WireName: "pathQueryId".*\},\s*\[]string\{[^}]*"pathQueryId"`),
+		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/graphql/\{pathQueryId\}/Followers",\s*false,\s*nil,\s*\[]mcpParamBinding\{.*WireName: "pathQueryId".*\},\s*\[]string\{[^}]*"pathQueryId"`),
 		string(mcpGo),
 		"MCP handler must receive the routing path param so it can substitute the URL")
 }
@@ -10899,11 +10899,11 @@ func TestGenerateMCPHandlerPreservesQueryPositionals(t *testing.T) {
 	// Call sites still pass both names — the upstream emit is unchanged;
 	// the fix lives entirely inside the handler body.
 	assert.Regexp(t,
-		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/search/movie",\s*false,\s*\[]mcpParamBinding\{.*WireName: "query".*\},\s*\[]string\{[^}]*"query"`),
+		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/search/movie",\s*false,\s*nil,\s*\[]mcpParamBinding\{.*WireName: "query".*\},\s*\[]string\{[^}]*"query"`),
 		tools,
 		"search call site must still pass `query` in positionalParams (handler decides path vs query at runtime)")
 	assert.Regexp(t,
-		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/movie/\{movieId\}",\s*false,\s*\[]mcpParamBinding\{.*WireName: "movieId".*\},\s*\[]string\{[^}]*"movieId"`),
+		regexp.MustCompile(`makeAPIHandler\("GET",\s*"/movie/\{movieId\}",\s*false,\s*nil,\s*\[]mcpParamBinding\{.*WireName: "movieId".*\},\s*\[]string\{[^}]*"movieId"`),
 		tools,
 		"get-by-id call site must pass `movieId` in positionalParams")
 
diff --git a/internal/generator/multipart_test.go b/internal/generator/multipart_test.go
index 833d1cf9..435f78da 100644
--- a/internal/generator/multipart_test.go
+++ b/internal/generator/multipart_test.go
@@ -78,7 +78,7 @@ func TestGenerateMultipartRequestBodyUsesMultipartClient(t *testing.T) {
 	assert.NotContains(t, promotedSrc, `"stdin"`)
 
 	mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
-	assert.Contains(t, mcpSrc, `makeAPIHandler("POST", "/assets", false, []mcpParamBinding`)
+	assert.Contains(t, mcpSrc, `makeAPIHandler("POST", "/assets", false, nil, []mcpParamBinding`)
 	assert.Contains(t, mcpSrc, `Format: "binary"`)
 	assert.Contains(t, mcpSrc, `RequestContentType: "multipart/form-data"`)
 	assert.Contains(t, mcpSrc, `multipartFileFields[binding.WireName] = fmt.Sprintf("%v", v)`)
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 5c909ca8..e630e732 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -9,6 +9,7 @@ import (
 	"crypto/tls"
 {{- end}}
 	"crypto/sha256"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
@@ -1179,9 +1180,6 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 		if err != nil {
 			return nil, 0, fmt.Errorf("reading response: %w", err)
 		}
-		if !binaryResponse {
-			respBody = sanitizeJSONResponse(respBody)
-		}
 
 		// Success
 		if resp.StatusCode < 400 {
@@ -1189,7 +1187,22 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 			if method != http.MethodGet && !c.DryRun {
 				c.invalidateCache()
 			}
-			return json.RawMessage(respBody), resp.StatusCode, nil
+			// Non-textual bodies (PDF, zip, image, octet-stream) must not be
+			// run through the JSON sanitizer or returned as raw json.RawMessage
+			// — return a self-describing base64 envelope instead. Textual and
+			// JSON responses fall through to the unchanged path.
+			if isBinaryResponseContentType(resp.Header.Get("Content-Type")) {
+				env, encErr := wrapBinaryResponse(resp.Header.Get("Content-Type"), respBody)
+				if encErr != nil {
+					return nil, 0, encErr
+				}
+				return env, resp.StatusCode, nil
+			}
+			return json.RawMessage(sanitizeJSONResponse(respBody)), resp.StatusCode, nil
+		}
+
+		if !binaryResponse {
+			respBody = sanitizeJSONResponse(respBody)
 		}
 
 		apiErr := &APIError{
@@ -1714,6 +1727,63 @@ func normalizeBasePath(p string) string {
 }
 
 {{end -}}
+// binaryResponseEnvelope wraps a non-textual success body so it survives the
+// json.RawMessage contract every consumer (CLI output, --json, MCP tools)
+// depends on. Without it, raw bytes (PDF, zip, image) are corrupted by
+// sanitizeJSONResponse and emitted as invalid JSON. The _pp_binary
+// discriminator lets callers and agents detect and base64-decode the payload.
+type binaryResponseEnvelope struct {
+	PPBinary    bool   `json:"_pp_binary"`
+	ContentType string `json:"content_type"`
+	Encoding    string `json:"encoding"`
+	Bytes       int    `json:"bytes"`
+	Data        string `json:"data"`
+}
+
+// isBinaryResponseContentType reports whether a successful response with this
+// Content-Type must be base64-wrapped instead of treated as text/JSON. It is
+// deliberately narrow: JSON, */*, XML, and every text/* type (including
+// text/html, so response_format:html CLIs are untouched) pass through
+// unchanged. Only genuinely binary payloads are wrapped.
+func isBinaryResponseContentType(ct string) bool {
+	mt := strings.ToLower(strings.TrimSpace(ct))
+	if i := strings.IndexByte(mt, ';'); i >= 0 {
+		mt = strings.TrimSpace(mt[:i])
+	}
+	if mt == "" {
+		return false
+	}
+	switch {
+	case mt == "application/json", mt == "text/json", mt == "*/*":
+		return false
+	case strings.HasPrefix(mt, "text/"):
+		return false
+	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
+		return false
+	case mt == "application/xml", mt == "application/xhtml+xml":
+		return false
+	case mt == "application/javascript", mt == "application/ecmascript",
+		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
+		return false
+	}
+	return true
+}
+
+// wrapBinaryResponse marshals body into a self-describing base64 envelope.
+func wrapBinaryResponse(ct string, body []byte) (json.RawMessage, error) {
+	out, err := json.Marshal(binaryResponseEnvelope{
+		PPBinary:    true,
+		ContentType: ct,
+		Encoding:    "base64",
+		Bytes:       len(body),
+		Data:        base64.StdEncoding.EncodeToString(body),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("encoding binary response: %w", err)
+	}
+	return json.RawMessage(out), nil
+}
+
 // sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
 // response bodies so that downstream JSON parsing succeeds. For clean JSON
 // responses these checks are no-ops.
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index 153bd68a..6ab59c74 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -131,9 +131,17 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- end}}
 {{- end}}
 
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
+			headerOverrides := map[string]string{
+{{- range .Endpoint.HeaderOverrides}}
+				"{{.Name}}": "{{.Value}}",
+{{- end}}
 {{- if .Endpoint.UsesBinaryResponse}}
-			headerOverrides := map[string]string{"X-Printing-Press-Binary-Response": "true"}
+				"X-Printing-Press-Binary-Response": "true",
+{{- end}}
+			}
 {{- end}}
+
 {{- if .Endpoint.Pagination}}
 {{- if .HasStore}}
 			data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", path, map[string]string{
@@ -145,7 +153,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 				"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
 {{- end}}
 {{- end}}
-			}, {{if .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
+			}, {{if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
 {{- else}}
 			data, err := paginatedGet(c, path, map[string]string{
 {{- range .Endpoint.Params}}
@@ -156,7 +164,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 				"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
 {{- end}}
 {{- end}}
-			}, {{if .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
+			}, {{if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
 {{- end}}
 {{- else}}
 			params := map[string]string{}
@@ -171,15 +179,15 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- end}}
 {{- end}}
 {{- if and .HasStore (eq (upper .Endpoint.Method) "GET")}}
-			data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, {{if .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}})
+			data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, {{if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}})
 {{- else if eq $method "GET"}}
-{{- if .Endpoint.UsesBinaryResponse}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
 			data, err := c.GetWithHeaders(path, params, headerOverrides)
 {{- else}}
 			data, err := c.Get(path, params)
 {{- end}}
 {{- else if eq (upper .Endpoint.Method) "DELETE"}}
-{{- if .Endpoint.UsesBinaryResponse}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
 			data, _, err := c.DeleteWithParamsAndHeaders(path, params, headerOverrides)
 {{- else}}
 			data, _, err := c.DeleteWithParams(path, params)
@@ -189,7 +197,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			fields := map[string]string{}
 			fileFields := map[string]string{}
 {{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if .Endpoint.UsesBinaryResponse}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
 			data, _, err := c.{{pascal (lower .Endpoint.Method)}}MultipartWithParamsAndHeaders(path, params, fields, fileFields, headerOverrides)
 {{- else}}
 			data, _, err := c.{{pascal (lower .Endpoint.Method)}}MultipartWithParams(path, params, fields, fileFields)
@@ -197,7 +205,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- else if $isForm}}
 			fields := url.Values{}
 {{formBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if .Endpoint.UsesBinaryResponse}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
 			data, _, err := c.{{pascal (lower .Endpoint.Method)}}FormWithParamsAndHeaders(path, params, fields, headerOverrides)
 {{- else}}
 			data, _, err := c.{{pascal (lower .Endpoint.Method)}}FormWithParams(path, params, fields)
@@ -208,7 +216,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			// body-aware cached read helper is filed as #425 for when a
 			// second store-backed POST-search consumer ships.
 			body := map[string]any{}
-{{bodyMapForEndpoint .Endpoint "\t\t\t"}}{{if .Endpoint.UsesBinaryResponse}}			data, _, err := c.{{pascal (lower .Endpoint.Method)}}WithParamsAndHeaders(path, params, body, headerOverrides)
+{{bodyMapForEndpoint .Endpoint "\t\t\t"}}{{if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}			data, _, err := c.{{pascal (lower .Endpoint.Method)}}WithParamsAndHeaders(path, params, body, headerOverrides)
 {{else}}			data, _, err := c.{{pascal (lower .Endpoint.Method)}}WithParams(path, params, body)
 {{end}}
 {{- end}}
@@ -216,7 +224,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 			// HEAD/OPTIONS and unknown verbs fall back to GET so generation
 			// stays compileable. Spec authors who genuinely need these verbs
 			// should add a typed client method and a dedicated template branch.
-{{- if .Endpoint.UsesBinaryResponse}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
 			data, err := c.GetWithHeaders(path, params, headerOverrides)
 {{- else}}
 			data, err := c.Get(path, params)
diff --git a/internal/generator/templates/mcp_code_orch.go.tmpl b/internal/generator/templates/mcp_code_orch.go.tmpl
index d6444ba1..dc1b70e5 100644
--- a/internal/generator/templates/mcp_code_orch.go.tmpl
+++ b/internal/generator/templates/mcp_code_orch.go.tmpl
@@ -59,7 +59,12 @@ type codeOrchEndpoint struct {
 	Tier        string
 	Summary     string
 	Positional  []string
-	keywords    []string
+	// HeaderOverrides carries per-endpoint request headers (e.g. an
+	// Accept override for binary-only response endpoints). Without
+	// threading these through, the code-orchestration execute path
+	// sends the client's default Accept and binary endpoints 406.
+	HeaderOverrides map[string]string
+	keywords        []string
 }
 
 // codeOrchEndpoints is the generator-populated registry covering every
@@ -78,6 +83,9 @@ var codeOrchEndpoints = []codeOrchEndpoint{
 {{- end}}
 		Summary: {{printf "%q" (oneline $endpoint.Description)}},
 		Positional: []string{ {{- range $endpoint.Params}}{{if .Positional}}"{{.Name}}",{{end}}{{end}} },
+{{- if $endpoint.HeaderOverrides}}
+		HeaderOverrides: map[string]string{ {{- range $endpoint.HeaderOverrides}}"{{.Name}}": "{{.Value}}",{{end}} },
+{{- end}}
 		keywords: codeOrchKeywords({{printf "%q" $name}}, {{printf "%q" $eName}}, {{printf "%q" (oneline $endpoint.Description)}}, {{printf "%q" $path}}),
 	},
 {{- end}}
@@ -93,6 +101,9 @@ var codeOrchEndpoints = []codeOrchEndpoint{
 {{- end}}
 		Summary: {{printf "%q" (oneline $endpoint.Description)}},
 		Positional: []string{ {{- range $endpoint.Params}}{{if .Positional}}"{{.Name}}",{{end}}{{end}} },
+{{- if $endpoint.HeaderOverrides}}
+		HeaderOverrides: map[string]string{ {{- range $endpoint.HeaderOverrides}}"{{.Name}}": "{{.Value}}",{{end}} },
+{{- end}}
 		keywords: codeOrchKeywords({{printf "%q" $name}}, {{printf "%q" $eName}}, {{printf "%q" (oneline $endpoint.Description)}}, {{printf "%q" $path}}),
 	},
 {{- end}}
@@ -243,18 +254,39 @@ func handleCodeOrchExecute(ctx context.Context, req mcplib.CallToolRequest) (*mc
 		}
 	}
 
+	hdrs := ep.HeaderOverrides
 	var data json.RawMessage
 	switch ep.Method {
 	case "GET":
-		data, err = c.Get(path, query)
+		if len(hdrs) > 0 {
+			data, err = c.GetWithHeaders(path, query, hdrs)
+		} else {
+			data, err = c.Get(path, query)
+		}
 	case "DELETE":
-		data, _, err = c.DeleteWithParams(path, query)
+		if len(hdrs) > 0 {
+			data, _, err = c.DeleteWithParamsAndHeaders(path, query, hdrs)
+		} else {
+			data, _, err = c.DeleteWithParams(path, query)
+		}
 	case "POST":
-		data, _, err = c.Post(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PostWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Post(path, params)
+		}
 	case "PUT":
-		data, _, err = c.Put(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PutWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Put(path, params)
+		}
 	case "PATCH":
-		data, _, err = c.Patch(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PatchWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Patch(path, params)
+		}
 	default:
 		return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
 	}
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index c97aea10..2d38143b 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -71,9 +71,9 @@ func RegisterTools(s *server.MCPServer) {
 {{- end}}
 		),
 {{- if $.HasTierRouting}}
-		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveEndpointPath $resource $endpoint)}}, {{printf "%q" (effectiveTier $.APISpec $resource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveEndpointPath $resource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
+		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveEndpointPath $resource $endpoint)}}, {{printf "%q" (effectiveTier $.APISpec $resource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, {{if $endpoint.HeaderOverrides}}map[string]string{ {{- range $endpoint.HeaderOverrides}}{{printf "%q" .Name}}: {{printf "%q" .Value}},{{end}} }{{else}}nil{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveEndpointPath $resource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
 {{- else}}
-		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveEndpointPath $resource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveEndpointPath $resource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
+		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveEndpointPath $resource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, {{if $endpoint.HeaderOverrides}}map[string]string{ {{- range $endpoint.HeaderOverrides}}{{printf "%q" .Name}}: {{printf "%q" .Value}},{{end}} }{{else}}nil{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveEndpointPath $resource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
 {{- end}}
 	)
 {{- end}}
@@ -106,9 +106,9 @@ func RegisterTools(s *server.MCPServer) {
 {{- end}}
 		),
 {{- if $.HasTierRouting}}
-		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveSubEndpointPath $resource $subResource $endpoint)}}, {{printf "%q" (effectiveSubTier $.APISpec $resource $subResource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveSubEndpointPath $resource $subResource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
+		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveSubEndpointPath $resource $subResource $endpoint)}}, {{printf "%q" (effectiveSubTier $.APISpec $resource $subResource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, {{if $endpoint.HeaderOverrides}}map[string]string{ {{- range $endpoint.HeaderOverrides}}{{printf "%q" .Name}}: {{printf "%q" .Value}},{{end}} }{{else}}nil{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveSubEndpointPath $resource $subResource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
 {{- else}}
-		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveSubEndpointPath $resource $subResource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveSubEndpointPath $resource $subResource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
+		makeAPIHandler({{printf "%q" (upper $endpoint.Method)}}, {{printf "%q" (effectiveSubEndpointPath $resource $subResource $endpoint)}}, {{if $endpoint.UsesBinaryResponse}}true{{else}}false{{end}}, {{if $endpoint.HeaderOverrides}}map[string]string{ {{- range $endpoint.HeaderOverrides}}{{printf "%q" .Name}}: {{printf "%q" .Value}},{{end}} }{{else}}nil{{end}}, []mcpParamBinding{ {{- range mcpParamBindings $endpoint (effectiveSubEndpointPath $resource $subResource $endpoint)}}{PublicName: {{printf "%q" .PublicName}}, WireName: {{printf "%q" .WireName}}, Location: {{printf "%q" .Location}}{{if .Format}}, Format: {{printf "%q" .Format}}{{end}}{{if .RequestContentType}}, RequestContentType: {{printf "%q" .RequestContentType}}{{end}}},{{end}} }, []string{ {{- range $endpoint.Params}}{{if or .Positional .PathParam}}{{printf "%q" .Name}},{{end}}{{end}} }),
 {{- end}}
 	)
 {{- end}}
@@ -200,9 +200,9 @@ func mcpFormFieldValue(v any) string {
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
 {{- if .HasTierRouting}}
-func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 {{- else}}
-func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 {{- end}}
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
@@ -227,8 +227,17 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 {{- if $hasMultipartRequest}}
 		multipartFields := make(map[string]string)
@@ -335,7 +344,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
@@ -343,7 +352,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		case "POST":
 {{- if $hasMultipartRequest}}
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PostMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
@@ -353,7 +362,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasFormRequest}}
 			if formEncoded {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PostFormWithParamsAndHeaders(path, params, formFields, headers)
 					break
 				}
@@ -363,7 +372,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasBodyJSONFallback}}
 			if len(bodyJSONOverride) > 0 {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PostWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
 					break
 				}
@@ -371,7 +380,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 				break
 			}
 {{- end}}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
@@ -379,7 +388,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		case "PUT":
 {{- if $hasMultipartRequest}}
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PutMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
@@ -389,7 +398,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasFormRequest}}
 			if formEncoded {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PutFormWithParamsAndHeaders(path, params, formFields, headers)
 					break
 				}
@@ -399,7 +408,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasBodyJSONFallback}}
 			if len(bodyJSONOverride) > 0 {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PutWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
 					break
 				}
@@ -407,7 +416,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 				break
 			}
 {{- end}}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
@@ -415,7 +424,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		case "PATCH":
 {{- if $hasMultipartRequest}}
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PatchMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
@@ -425,7 +434,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasFormRequest}}
 			if formEncoded {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PatchFormWithParamsAndHeaders(path, params, formFields, headers)
 					break
 				}
@@ -435,7 +444,7 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 {{- end}}
 {{- if $hasBodyJSONFallback}}
 			if len(bodyJSONOverride) > 0 {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
 					break
 				}
@@ -443,13 +452,13 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 				break
 			}
 {{- end}}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 0192be38..e1b76da6 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -1571,7 +1571,9 @@ func applyHeaderOverrides(s *spec.APISpec, perEndpoint map[string]map[string]str
 		for eName, e := range r.Endpoints {
 			overrides := headerOverridesForPath(e.Path, perEndpoint)
 			if len(overrides) > 0 {
-				e.HeaderOverrides = overrides
+				for _, o := range overrides {
+					e.HeaderOverrides = upsertHeaderOverride(e.HeaderOverrides, o.Name, o.Value)
+				}
 				r.Endpoints[eName] = e
 			}
 		}
@@ -1579,7 +1581,9 @@ func applyHeaderOverrides(s *spec.APISpec, perEndpoint map[string]map[string]str
 			for eName, e := range sub.Endpoints {
 				overrides := headerOverridesForPath(e.Path, perEndpoint)
 				if len(overrides) > 0 {
-					e.HeaderOverrides = overrides
+					for _, o := range overrides {
+						e.HeaderOverrides = upsertHeaderOverride(e.HeaderOverrides, o.Name, o.Value)
+					}
 					sub.Endpoints[eName] = e
 				}
 			}
@@ -2209,6 +2213,14 @@ func mapResources(doc *openapi3.T, out *spec.APISpec, basePath string) {
 			endpoint.Critical = pathCritical
 			endpoint.Walker = readWalkerExtension(op.Extensions, fmt.Sprintf("%s %q", strings.ToUpper(method), path))
 
+			// Binary-only success responses (e.g. PDF/octet-stream downloads)
+			// would otherwise receive the default Accept: application/json and
+			// be rejected with HTTP 406. Pin Accept to what the server
+			// produces; rides the existing per-endpoint header-override path.
+			if accept := binaryResponseAcceptType(op); accept != "" {
+				endpoint.HeaderOverrides = upsertHeaderOverride(endpoint.HeaderOverrides, "Accept", accept)
+			}
+
 			targetEndpoints[endpointName] = endpoint
 
 			// Update descriptions
@@ -3429,6 +3441,76 @@ func binaryContentType(contentType string) bool {
 	}
 }
 
+// binaryResponseAcceptType inspects the operation's selected success response.
+// When every declared media type is concrete and binary-enveloped by the
+// generated client, the server answers the client's default Accept:
+// application/json with HTTP 406. It returns the media type the client must
+// send instead (application/octet-stream when the response offers it, otherwise
+// the lexicographically-first concrete type). Returns "" for JSON, wildcard,
+// text, XML, empty, or mixed responses so the existing application/json default
+// and non-binary response path stay untouched — the vast majority of endpoints.
+func binaryResponseAcceptType(op *openapi3.Operation) string {
+	if op == nil || op.Responses == nil {
+		return ""
+	}
+	success := selectSuccessResponse(op.Responses)
+	if success == nil || success.Value == nil || len(success.Value.Content) == 0 {
+		return ""
+	}
+	concrete := make([]string, 0, len(success.Value.Content))
+	for ct := range success.Value.Content {
+		mt := strings.ToLower(strings.TrimSpace(strings.SplitN(ct, ";", 2)[0]))
+		if !binaryAcceptContentType(mt) {
+			return ""
+		}
+		concrete = append(concrete, mt)
+	}
+	if len(concrete) == 0 {
+		return ""
+	}
+	sort.Strings(concrete)
+	for _, mt := range concrete {
+		if mt == "application/octet-stream" {
+			return mt
+		}
+	}
+	return concrete[0]
+}
+
+func binaryAcceptContentType(mt string) bool {
+	if mt == "" {
+		return false
+	}
+	switch {
+	case mt == "application/json", mt == "text/json", mt == "*/*":
+		return false
+	case strings.HasPrefix(mt, "text/"):
+		return false
+	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
+		return false
+	case mt == "application/xml", mt == "application/xhtml+xml":
+		return false
+	case mt == "application/javascript", mt == "application/ecmascript",
+		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
+		return false
+	}
+	return true
+}
+
+// upsertHeaderOverride returns headers with name set to value: replacing an
+// existing case-insensitive match in place, or appending a new entry. Keeps a
+// binary-response Accept override stable when a later pass (applyHeaderOverrides)
+// merges per-endpoint configured headers onto the same endpoint.
+func upsertHeaderOverride(headers []spec.RequiredHeader, name, value string) []spec.RequiredHeader {
+	for i := range headers {
+		if strings.EqualFold(headers[i].Name, name) {
+			headers[i].Value = value
+			return headers
+		}
+	}
+	return append(headers, spec.RequiredHeader{Name: name, Value: value})
+}
+
 // readPathItemResourceID reads the `x-resource-id` extension from a path item
 // and returns the resolved field name. Accepts only string values; non-string
 // values (numbers, booleans, malformed YAML) emit a warning and return "".
diff --git a/internal/openapi/parser_binary_response_test.go b/internal/openapi/parser_binary_response_test.go
new file mode 100644
index 00000000..47ca2f94
--- /dev/null
+++ b/internal/openapi/parser_binary_response_test.go
@@ -0,0 +1,207 @@
+package openapi
+
+import (
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// binaryResponseSpec exercises the four shapes that matter for binary-only
+// response detection: JSON-only (no override), octet-stream-only (override),
+// pdf-only (override to the concrete type), text/XML-only (no override because
+// the client does not binary-wrap them), and a mixed octet-stream+JSON response
+// (no override — JSON is reachable with the default Accept).
+const binaryResponseSpec = `
+openapi: 3.0.0
+info:
+  title: binresp
+  version: "1"
+paths:
+  /widgets:
+    get:
+      operationId: listWidgets
+      responses:
+        "200":
+          description: ok
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    id: { type: string }
+  /widgets/{id}/content:
+    get:
+      operationId: downloadWidgetContent
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema: { type: string }
+      responses:
+        "200":
+          description: ok
+          content:
+            application/octet-stream:
+              schema:
+                type: string
+                format: byte
+  /widgets/{id}/report:
+    get:
+      operationId: widgetReport
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema: { type: string }
+      responses:
+        "200":
+          description: ok
+          content:
+            application/pdf:
+              schema:
+                type: string
+                format: byte
+  /widgets/{id}/export:
+    get:
+      operationId: exportWidget
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema: { type: string }
+      responses:
+        "200":
+          description: ok
+          content:
+            application/octet-stream:
+              schema: { type: string, format: byte }
+            application/json:
+              schema: { type: object }
+  /widgets/{id}/csv:
+    get:
+      operationId: exportWidgetCSV
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema: { type: string }
+      responses:
+        "200":
+          description: ok
+          content:
+            text/csv:
+              schema: { type: string }
+  /widgets/{id}/xml:
+    get:
+      operationId: exportWidgetXML
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema: { type: string }
+      responses:
+        "200":
+          description: ok
+          content:
+            application/xml:
+              schema: { type: string }
+`
+
+func acceptOverride(e spec.Endpoint) (string, bool) {
+	for _, h := range e.HeaderOverrides {
+		if h.Name == "Accept" {
+			return h.Value, true
+		}
+	}
+	return "", false
+}
+
+func endpointByPath(s *spec.APISpec, path string) (spec.Endpoint, bool) {
+	for _, r := range s.Resources {
+		for _, e := range r.Endpoints {
+			if e.Path == path {
+				return e, true
+			}
+		}
+		for _, sub := range r.SubResources {
+			for _, e := range sub.Endpoints {
+				if e.Path == path {
+					return e, true
+				}
+			}
+		}
+	}
+	return spec.Endpoint{}, false
+}
+
+func TestParseBinaryOnlyResponseEmitsAcceptOverride(t *testing.T) {
+	t.Parallel()
+
+	parsed, err := Parse([]byte(binaryResponseSpec))
+	require.NoError(t, err)
+
+	t.Run("octet-stream-only gets Accept override", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets/{id}/content")
+		require.True(t, ok, "expected /widgets/{id}/content endpoint")
+		v, has := acceptOverride(e)
+		require.True(t, has, "binary-only endpoint must carry an Accept header override")
+		assert.Equal(t, "application/octet-stream", v)
+	})
+
+	t.Run("non-octet binary type is pinned to the concrete type", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets/{id}/report")
+		require.True(t, ok)
+		v, has := acceptOverride(e)
+		require.True(t, has)
+		assert.Equal(t, "application/pdf", v)
+	})
+
+	t.Run("JSON-only endpoint gets no Accept override", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets")
+		require.True(t, ok)
+		_, has := acceptOverride(e)
+		assert.False(t, has, "JSON endpoints must keep the default application/json Accept")
+	})
+
+	t.Run("mixed octet-stream+JSON keeps the JSON default", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets/{id}/export")
+		require.True(t, ok)
+		_, has := acceptOverride(e)
+		assert.False(t, has, "a JSON-reachable response must not be forced to octet-stream")
+	})
+
+	t.Run("text response gets no Accept override", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets/{id}/csv")
+		require.True(t, ok)
+		_, has := acceptOverride(e)
+		assert.False(t, has, "text responses must not be forced into the binary response path")
+	})
+
+	t.Run("XML response gets no Accept override", func(t *testing.T) {
+		e, ok := endpointByPath(parsed, "/widgets/{id}/xml")
+		require.True(t, ok)
+		_, has := acceptOverride(e)
+		assert.False(t, has, "XML responses must not be forced into the binary response path")
+	})
+}
+
+func TestUpsertHeaderOverride(t *testing.T) {
+	t.Parallel()
+
+	base := []spec.RequiredHeader{{Name: "X-Api-Version", Value: "2"}}
+
+	appended := upsertHeaderOverride(base, "Accept", "application/octet-stream")
+	assert.Len(t, appended, 2)
+	v, ok := acceptOverride(spec.Endpoint{HeaderOverrides: appended})
+	assert.True(t, ok)
+	assert.Equal(t, "application/octet-stream", v)
+
+	replaced := upsertHeaderOverride(appended, "accept", "application/pdf")
+	assert.Len(t, replaced, 2, "case-insensitive name match must replace, not append")
+	v, _ = acceptOverride(spec.Endpoint{HeaderOverrides: replaced})
+	assert.Equal(t, "application/pdf", v)
+}
diff --git a/testdata/golden/cases/generate-golden-api/artifacts.txt b/testdata/golden/cases/generate-golden-api/artifacts.txt
index 80aa4946..e47fce8a 100644
--- a/testdata/golden/cases/generate-golden-api/artifacts.txt
+++ b/testdata/golden/cases/generate-golden-api/artifacts.txt
@@ -14,6 +14,7 @@ printing-press-golden/internal/cli/projects_tasks_list-project.go
 printing-press-golden/internal/cli/projects_tasks_update-project.go
 printing-press-golden/internal/cli/projects_avatar.go
 printing-press-golden/internal/cli/projects_avatar_upload-project.go
+printing-press-golden/internal/cli/reports_export_report-year.go
 printing-press-golden/internal/cli/promoted_public.go
 printing-press-golden/internal/cliutil/text.go
 printing-press-golden/internal/cliutil/ratelimit.go
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
index 3884fc83..fee0d978 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.go
@@ -6,6 +6,7 @@ package client
 import (
 	"bytes"
 	"crypto/sha256"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
@@ -458,9 +459,6 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 		if err != nil {
 			return nil, 0, fmt.Errorf("reading response: %w", err)
 		}
-		if !binaryResponse {
-			respBody = sanitizeJSONResponse(respBody)
-		}
 
 		// Success
 		if resp.StatusCode < 400 {
@@ -468,7 +466,22 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 			if method != http.MethodGet && !c.DryRun {
 				c.invalidateCache()
 			}
-			return json.RawMessage(respBody), resp.StatusCode, nil
+			// Non-textual bodies (PDF, zip, image, octet-stream) must not be
+			// run through the JSON sanitizer or returned as raw json.RawMessage
+			// — return a self-describing base64 envelope instead. Textual and
+			// JSON responses fall through to the unchanged path.
+			if isBinaryResponseContentType(resp.Header.Get("Content-Type")) {
+				env, encErr := wrapBinaryResponse(resp.Header.Get("Content-Type"), respBody)
+				if encErr != nil {
+					return nil, 0, encErr
+				}
+				return env, resp.StatusCode, nil
+			}
+			return json.RawMessage(sanitizeJSONResponse(respBody)), resp.StatusCode, nil
+		}
+
+		if !binaryResponse {
+			respBody = sanitizeJSONResponse(respBody)
 		}
 
 		apiErr := &APIError{
@@ -721,6 +734,63 @@ func (c *Client) refreshAccessToken() error {
 	return nil
 }
 
+// binaryResponseEnvelope wraps a non-textual success body so it survives the
+// json.RawMessage contract every consumer (CLI output, --json, MCP tools)
+// depends on. Without it, raw bytes (PDF, zip, image) are corrupted by
+// sanitizeJSONResponse and emitted as invalid JSON. The _pp_binary
+// discriminator lets callers and agents detect and base64-decode the payload.
+type binaryResponseEnvelope struct {
+	PPBinary    bool   `json:"_pp_binary"`
+	ContentType string `json:"content_type"`
+	Encoding    string `json:"encoding"`
+	Bytes       int    `json:"bytes"`
+	Data        string `json:"data"`
+}
+
+// isBinaryResponseContentType reports whether a successful response with this
+// Content-Type must be base64-wrapped instead of treated as text/JSON. It is
+// deliberately narrow: JSON, */*, XML, and every text/* type (including
+// text/html, so response_format:html CLIs are untouched) pass through
+// unchanged. Only genuinely binary payloads are wrapped.
+func isBinaryResponseContentType(ct string) bool {
+	mt := strings.ToLower(strings.TrimSpace(ct))
+	if i := strings.IndexByte(mt, ';'); i >= 0 {
+		mt = strings.TrimSpace(mt[:i])
+	}
+	if mt == "" {
+		return false
+	}
+	switch {
+	case mt == "application/json", mt == "text/json", mt == "*/*":
+		return false
+	case strings.HasPrefix(mt, "text/"):
+		return false
+	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
+		return false
+	case mt == "application/xml", mt == "application/xhtml+xml":
+		return false
+	case mt == "application/javascript", mt == "application/ecmascript",
+		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
+		return false
+	}
+	return true
+}
+
+// wrapBinaryResponse marshals body into a self-describing base64 envelope.
+func wrapBinaryResponse(ct string, body []byte) (json.RawMessage, error) {
+	out, err := json.Marshal(binaryResponseEnvelope{
+		PPBinary:    true,
+		ContentType: ct,
+		Encoding:    "base64",
+		Bytes:       len(body),
+		Data:        base64.StdEncoding.EncodeToString(body),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("encoding binary response: %w", err)
+	}
+	return json.RawMessage(out), nil
+}
+
 // sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
 // response bodies so that downstream JSON parsing succeeds. For clean JSON
 // responses these checks are no-ops.
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/mcp/tools.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/mcp/tools.go
index efe0916a..0d67d533 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/mcp/tools.go
@@ -32,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/items", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/items", false, nil, []mcpParamBinding{}, []string{}),
 	)
 	// SQL tool — ad-hoc analysis on synced data without API calls
 	s.AddTool(
@@ -68,7 +68,7 @@ type mcpParamBinding struct {
 }
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
 		if err != nil {
@@ -89,8 +89,17 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 		for _, binding := range bindings {
 			knownArgs[binding.PublicName] = true
@@ -135,31 +144,31 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
 			data, err = c.Get(path, params)
 		case "POST":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PostWithParams(path, params, bodyArgs)
 		case "PUT":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PutWithParams(path, params, bodyArgs)
 		case "PATCH":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
index 0f4c8cb0..cfa577e0 100644
--- a/testdata/golden/expected/generate-golden-api/dogfood.json
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -35,7 +35,7 @@
     "state": "runtime_walking"
   },
   "naming_check": {
-    "checked": 34
+    "checked": 36
   },
   "novel_features_check": {
     "found": 0,
@@ -56,7 +56,7 @@
     "sync_resources_present": true
   },
   "print_json_filtered_check": {
-    "checked": 34
+    "checked": 36
   },
   "reimplementation_check": {
     "checked": 0,
@@ -75,8 +75,8 @@
   "verdict": "WARN",
   "wiring_check": {
     "command_tree": {
-      "defined": 43,
-      "registered": 43
+      "defined": 45,
+      "registered": 45
     },
     "config_consistency": {
       "consistent": true
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
index e37a040d..0c5fd65b 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
@@ -11,14 +11,14 @@
   "mcp_binary": "printing-press-golden-pp-mcp",
   "mcp_public_tool_count": 1,
   "mcp_ready": "full",
-  "mcp_tool_count": 9,
+  "mcp_tool_count": 10,
   "owner": "printing-press-golden",
   "printer": "printing-press-golden",
   "printer_name": "printing-press-golden",
   "printing_press_version": "<PRINTING_PRESS_VERSION>",
   "run_id": "<RUN_ID>",
   "schema_version": 1,
-  "spec_checksum": "sha256:2ea5c937580b3011c022fa9fcbbd19cba6be4a38a5303c66905cb9bb736991fb",
+  "spec_checksum": "sha256:edbb503a934acade9d2455b09e41c7750ec04985dbc0bb2e5611872736009565",
   "spec_format": "openapi3",
   "spec_path": "testdata/golden/fixtures/golden-api.yaml",
   "spec_url": "file://testdata/golden/fixtures/golden-api.yaml"
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/reports_export_report-year.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/reports_export_report-year.go
new file mode 100644
index 00000000..0d03c458
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/reports_export_report-year.go
@@ -0,0 +1,59 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+func newReportsExportReportYearCmd(flags *rootFlags) *cobra.Command {
+	var flagYear int
+
+	cmd := &cobra.Command{
+		Use:         "report-year",
+		Aliases:     []string{"get"},
+		Short:       "Download the annual report as a binary file",
+		Example:     "  printing-press-golden-pp-cli reports export report-year --year 42",
+		Annotations: map[string]string{"pp:endpoint": "export.report-year", "pp:method": "GET", "pp:path": "/reports/{year}/export", "mcp:read-only": "true"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if !cmd.Flags().Changed("year") && !flags.dryRun {
+				return fmt.Errorf("required flag \"%s\" not set", "year")
+			}
+			c, err := flags.newClient()
+			if err != nil {
+				return err
+			}
+
+			path := "/reports/{year}/export"
+			path = replacePathParam(path, "year", fmt.Sprintf("%v", flagYear))
+			headerOverrides := map[string]string{
+				"Accept":                           "application/octet-stream",
+				"X-Printing-Press-Binary-Response": "true",
+			}
+			params := map[string]string{}
+			data, prov, err := resolveRead(cmd.Context(), c, flags, "export", false, path, params, headerOverrides)
+			if err != nil {
+				return classifyAPIError(err, flags)
+			}
+			_ = json.Valid
+			_ = os.Stderr
+			_ = prov
+			if flags.quiet {
+				return nil
+			}
+			if flags.asJSON || flags.csv || flags.compact || flags.plain || flags.selectFields != "" {
+				return fmt.Errorf("binary response cannot be rendered as structured output; redirect stdout or use --deliver file:<path>")
+			}
+			_, err = cmd.OutOrStdout().Write(data)
+			return err
+		},
+	}
+	cmd.Flags().IntVar(&flagYear, "year", 0, "Year")
+
+	return cmd
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
index 33651807..a295f141 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
@@ -1022,6 +1022,8 @@ func upsertSingleObject(db *store.Store, resource string, data json.RawMessage)
 		return db.UpsertAvatar(data)
 	case "tasks":
 		return db.UpsertTasks(data)
+	case "export":
+		return db.UpsertExport(data)
 	case "summary":
 		return db.UpsertSummary(data)
 	default:
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go
index 562f0a79..c55720e1 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/which.go
@@ -35,6 +35,7 @@ var whichIndex = []whichEntry{
 	{Command: "projects tasks list-project", Description: "List project tasks", Group: "projects"},
 	{Command: "projects tasks update-project", Description: "Update project task", Group: "projects"},
 	{Command: "public get-status", Description: "Get public service status", Group: "public"},
+	{Command: "reports export report-year", Description: "Download the annual report as a binary file", Group: "reports"},
 	{Command: "reports summary get-report-year", Description: "Get a report summary for a year", Group: "reports"},
 }
 
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
index 9d260005..21cb2f31 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -6,6 +6,7 @@ package client
 import (
 	"bytes"
 	"crypto/sha256"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
@@ -551,9 +552,6 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 		if err != nil {
 			return nil, 0, fmt.Errorf("reading response: %w", err)
 		}
-		if !binaryResponse {
-			respBody = sanitizeJSONResponse(respBody)
-		}
 
 		// Success
 		if resp.StatusCode < 400 {
@@ -561,7 +559,22 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 			if method != http.MethodGet && !c.DryRun {
 				c.invalidateCache()
 			}
-			return json.RawMessage(respBody), resp.StatusCode, nil
+			// Non-textual bodies (PDF, zip, image, octet-stream) must not be
+			// run through the JSON sanitizer or returned as raw json.RawMessage
+			// — return a self-describing base64 envelope instead. Textual and
+			// JSON responses fall through to the unchanged path.
+			if isBinaryResponseContentType(resp.Header.Get("Content-Type")) {
+				env, encErr := wrapBinaryResponse(resp.Header.Get("Content-Type"), respBody)
+				if encErr != nil {
+					return nil, 0, encErr
+				}
+				return env, resp.StatusCode, nil
+			}
+			return json.RawMessage(sanitizeJSONResponse(respBody)), resp.StatusCode, nil
+		}
+
+		if !binaryResponse {
+			respBody = sanitizeJSONResponse(respBody)
 		}
 
 		apiErr := &APIError{
@@ -651,6 +664,63 @@ func (c *Client) authHeader() (string, error) {
 	return c.Config.AuthHeader(), nil
 }
 
+// binaryResponseEnvelope wraps a non-textual success body so it survives the
+// json.RawMessage contract every consumer (CLI output, --json, MCP tools)
+// depends on. Without it, raw bytes (PDF, zip, image) are corrupted by
+// sanitizeJSONResponse and emitted as invalid JSON. The _pp_binary
+// discriminator lets callers and agents detect and base64-decode the payload.
+type binaryResponseEnvelope struct {
+	PPBinary    bool   `json:"_pp_binary"`
+	ContentType string `json:"content_type"`
+	Encoding    string `json:"encoding"`
+	Bytes       int    `json:"bytes"`
+	Data        string `json:"data"`
+}
+
+// isBinaryResponseContentType reports whether a successful response with this
+// Content-Type must be base64-wrapped instead of treated as text/JSON. It is
+// deliberately narrow: JSON, */*, XML, and every text/* type (including
+// text/html, so response_format:html CLIs are untouched) pass through
+// unchanged. Only genuinely binary payloads are wrapped.
+func isBinaryResponseContentType(ct string) bool {
+	mt := strings.ToLower(strings.TrimSpace(ct))
+	if i := strings.IndexByte(mt, ';'); i >= 0 {
+		mt = strings.TrimSpace(mt[:i])
+	}
+	if mt == "" {
+		return false
+	}
+	switch {
+	case mt == "application/json", mt == "text/json", mt == "*/*":
+		return false
+	case strings.HasPrefix(mt, "text/"):
+		return false
+	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
+		return false
+	case mt == "application/xml", mt == "application/xhtml+xml":
+		return false
+	case mt == "application/javascript", mt == "application/ecmascript",
+		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
+		return false
+	}
+	return true
+}
+
+// wrapBinaryResponse marshals body into a self-describing base64 envelope.
+func wrapBinaryResponse(ct string, body []byte) (json.RawMessage, error) {
+	out, err := json.Marshal(binaryResponseEnvelope{
+		PPBinary:    true,
+		ContentType: ct,
+		Encoding:    "base64",
+		Bytes:       len(body),
+		Data:        base64.StdEncoding.EncodeToString(body),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("encoding binary response: %w", err)
+	}
+	return json.RawMessage(out), nil
+}
+
 // sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
 // response bodies so that downstream JSON parsing succeeds. For clean JSON
 // responses these checks are no-ops.
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
index 615a7001..aa2007f4 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
@@ -32,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/currencies", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/currencies", false, nil, []mcpParamBinding{}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_create",
@@ -43,7 +43,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("POST", "/projects", false, []mcpParamBinding{{PublicName: "name", WireName: "name", Location: "body"}, {PublicName: "owner_email", WireName: "owner_email", Location: "body"}, {PublicName: "visibility", WireName: "visibility", Location: "body"}}, []string{}),
+		makeAPIHandler("POST", "/projects", false, nil, []mcpParamBinding{{PublicName: "name", WireName: "name", Location: "body"}, {PublicName: "owner_email", WireName: "owner_email", Location: "body"}, {PublicName: "visibility", WireName: "visibility", Location: "body"}}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_get",
@@ -53,7 +53,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/projects/{projectId}", false, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}}, []string{"projectId"}),
+		makeAPIHandler("GET", "/projects/{projectId}", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}}, []string{"projectId"}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_list",
@@ -65,7 +65,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/projects", false, []mcpParamBinding{{PublicName: "status", WireName: "status", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{}),
+		makeAPIHandler("GET", "/projects", false, nil, []mcpParamBinding{{PublicName: "status", WireName: "status", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_avatar_upload-project",
@@ -76,7 +76,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithString("file", mcplib.Description("File")),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("PUT", "/projects/{projectId}/avatar", false, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path", RequestContentType: "multipart/form-data"}, {PublicName: "overwrite", WireName: "overwrite", Location: "query", RequestContentType: "multipart/form-data"}, {PublicName: "caption", WireName: "caption", Location: "body", RequestContentType: "multipart/form-data"}, {PublicName: "file", WireName: "file", Location: "body", Format: "binary", RequestContentType: "multipart/form-data"}}, []string{"projectId"}),
+		makeAPIHandler("PUT", "/projects/{projectId}/avatar", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path", RequestContentType: "multipart/form-data"}, {PublicName: "overwrite", WireName: "overwrite", Location: "query", RequestContentType: "multipart/form-data"}, {PublicName: "caption", WireName: "caption", Location: "body", RequestContentType: "multipart/form-data"}, {PublicName: "file", WireName: "file", Location: "body", Format: "binary", RequestContentType: "multipart/form-data"}}, []string{"projectId"}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_tasks_list-project",
@@ -89,7 +89,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/projects/{projectId}/tasks", false, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "priority", WireName: "priority", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{"projectId"}),
+		makeAPIHandler("GET", "/projects/{projectId}/tasks", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "priority", WireName: "priority", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{"projectId"}),
 	)
 	s.AddTool(
 		mcplib.NewTool("projects_tasks_update-project",
@@ -102,7 +102,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithString("title", mcplib.Description("Title")),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("PATCH", "/projects/{projectId}/tasks/{taskId}", false, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "taskId", WireName: "taskId", Location: "path"}, {PublicName: "notify", WireName: "notify", Location: "query"}, {PublicName: "completed", WireName: "completed", Location: "body"}, {PublicName: "priority", WireName: "priority", Location: "body"}, {PublicName: "title", WireName: "title", Location: "body"}}, []string{"projectId", "taskId"}),
+		makeAPIHandler("PATCH", "/projects/{projectId}/tasks/{taskId}", false, nil, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}, {PublicName: "taskId", WireName: "taskId", Location: "path"}, {PublicName: "notify", WireName: "notify", Location: "query"}, {PublicName: "completed", WireName: "completed", Location: "body"}, {PublicName: "priority", WireName: "priority", Location: "body"}, {PublicName: "title", WireName: "title", Location: "body"}}, []string{"projectId", "taskId"}),
 	)
 	s.AddTool(
 		mcplib.NewTool("public_get-status",
@@ -111,7 +111,17 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/public/status", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/public/status", false, nil, []mcpParamBinding{}, []string{}),
+	)
+	s.AddTool(
+		mcplib.NewTool("reports_export_report-year",
+			mcplib.WithDescription("Download the annual report as a binary file. Required: year."),
+			mcplib.WithNumber("year", mcplib.Required(), mcplib.Description("Year")),
+			mcplib.WithReadOnlyHintAnnotation(true),
+			mcplib.WithDestructiveHintAnnotation(false),
+			mcplib.WithOpenWorldHintAnnotation(true),
+		),
+		makeAPIHandler("GET", "/reports/{year}/export", true, map[string]string{"Accept": "application/octet-stream"}, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
 	)
 	s.AddTool(
 		mcplib.NewTool("reports_summary_get-report-year",
@@ -121,7 +131,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/reports/{year}/summary", false, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
+		makeAPIHandler("GET", "/reports/{year}/summary", false, nil, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
 	)
 	// Search tool — faster than iterating list endpoints for finding specific items
 	s.AddTool(
@@ -180,7 +190,7 @@ func mcpMultipartFieldValue(v any) string {
 }
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
 		if err != nil {
@@ -201,8 +211,17 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 		multipartFields := make(map[string]string)
 		multipartFileFields := make(map[string]string)
@@ -263,55 +282,55 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
 			data, err = c.Get(path, params)
 		case "POST":
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PostMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
 				data, _, err = c.PostMultipartWithParams(path, params, multipartFields, multipartFileFields)
 				break
 			}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PostWithParams(path, params, bodyArgs)
 		case "PUT":
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PutMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
 				data, _, err = c.PutMultipartWithParams(path, params, multipartFields, multipartFileFields)
 				break
 			}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PutWithParams(path, params, bodyArgs)
 		case "PATCH":
 			if multipart {
-				if binaryResponse {
+				if len(headers) > 0 {
 					data, _, err = c.PatchMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
 					break
 				}
 				data, _, err = c.PatchMultipartWithParams(path, params, multipartFields, multipartFileFields)
 				break
 			}
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
@@ -533,7 +552,7 @@ func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToo
 		"api":         "printing-press-golden",
 		"description": "Purpose-built fixture for golden generation coverage.",
 		"archetype":   "project-management",
-		"tool_count":  9,
+		"tool_count":  10,
 		// tool_surface tells agents which surface a capability lives on.
 		"tool_surface": "MCP exposes typed endpoint tools plus a runtime mirror of user-facing CLI commands. Endpoint tools keep typed schemas; command-mirror tools shell out to the companion printing-press-golden-pp-cli binary.",
 		"auth": map[string]any{
diff --git a/testdata/golden/expected/generate-golden-api/scorecard.json b/testdata/golden/expected/generate-golden-api/scorecard.json
index 94d88284..8a385895 100644
--- a/testdata/golden/expected/generate-golden-api/scorecard.json
+++ b/testdata/golden/expected/generate-golden-api/scorecard.json
@@ -3,7 +3,7 @@
     "api_name": "printing-press-golden",
     "competitor_scores": null,
     "gap_report": [
-      "MCP: 9 tools (1 public, 8 auth-required) — readiness: full"
+      "MCP: 10 tools (1 public, 9 auth-required) — readiness: full"
     ],
     "overall_grade": "A",
     "steinberger": {
diff --git a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
index 07aa5132..fadf6179 100644
--- a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
+++ b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
@@ -59,7 +59,12 @@ type codeOrchEndpoint struct {
 	Tier       string
 	Summary    string
 	Positional []string
-	keywords   []string
+	// HeaderOverrides carries per-endpoint request headers (e.g. an
+	// Accept override for binary-only response endpoints). Without
+	// threading these through, the code-orchestration execute path
+	// sends the client's default Accept and binary endpoints 406.
+	HeaderOverrides map[string]string
+	keywords        []string
 }
 
 // codeOrchEndpoints is the generator-populated registry covering every
@@ -213,18 +218,39 @@ func handleCodeOrchExecute(ctx context.Context, req mcplib.CallToolRequest) (*mc
 		}
 	}
 
+	hdrs := ep.HeaderOverrides
 	var data json.RawMessage
 	switch ep.Method {
 	case "GET":
-		data, err = c.Get(path, query)
+		if len(hdrs) > 0 {
+			data, err = c.GetWithHeaders(path, query, hdrs)
+		} else {
+			data, err = c.Get(path, query)
+		}
 	case "DELETE":
-		data, _, err = c.DeleteWithParams(path, query)
+		if len(hdrs) > 0 {
+			data, _, err = c.DeleteWithParamsAndHeaders(path, query, hdrs)
+		} else {
+			data, _, err = c.DeleteWithParams(path, query)
+		}
 	case "POST":
-		data, _, err = c.Post(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PostWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Post(path, params)
+		}
 	case "PUT":
-		data, _, err = c.Put(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PutWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Put(path, params)
+		}
 	case "PATCH":
-		data, _, err = c.Patch(path, params)
+		if len(hdrs) > 0 {
+			data, _, err = c.PatchWithHeaders(path, params, hdrs)
+		} else {
+			data, _, err = c.Patch(path, params)
+		}
 	default:
 		return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
 	}
diff --git a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/tools.go b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/tools.go
index 8264a877..5fd9a50b 100644
--- a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/tools.go
@@ -62,7 +62,7 @@ type mcpParamBinding struct {
 }
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
 		if err != nil {
@@ -83,8 +83,17 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 		for _, binding := range bindings {
 			knownArgs[binding.PublicName] = true
@@ -129,31 +138,31 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
 			data, err = c.Get(path, params)
 		case "POST":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PostWithParams(path, params, bodyArgs)
 		case "PUT":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PutWithParams(path, params, bodyArgs)
 		case "PATCH":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/mcp/tools.go b/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/mcp/tools.go
index 4c14a760..ccfe183f 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/mcp/tools.go
@@ -30,7 +30,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("POST", "/stores", false, []mcpParamBinding{{PublicName: "store-code", WireName: "store_code", Location: "body"}}, []string{}),
+		makeAPIHandler("POST", "/stores", false, nil, []mcpParamBinding{{PublicName: "store-code", WireName: "store_code", Location: "body"}}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("stores_find",
@@ -41,7 +41,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/power/store-locator", false, []mcpParamBinding{{PublicName: "address", WireName: "s", Location: "query"}, {PublicName: "city", WireName: "c", Location: "query"}}, []string{}),
+		makeAPIHandler("GET", "/power/store-locator", false, nil, []mcpParamBinding{{PublicName: "address", WireName: "s", Location: "query"}, {PublicName: "city", WireName: "c", Location: "query"}}, []string{}),
 	)
 
 	// Context tool — front-loaded domain knowledge for agents.
@@ -67,7 +67,7 @@ type mcpParamBinding struct {
 }
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
 		if err != nil {
@@ -88,8 +88,17 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 		for _, binding := range bindings {
 			knownArgs[binding.PublicName] = true
@@ -134,31 +143,31 @@ func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings [
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
 			data, err = c.Get(path, params)
 		case "POST":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PostWithParams(path, params, bodyArgs)
 		case "PUT":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PutWithParams(path, params, bodyArgs)
 		case "PATCH":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
index 57bd9595..a7ef5dc1 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.go
@@ -6,6 +6,7 @@ package client
 import (
 	"bytes"
 	"crypto/sha256"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
@@ -549,9 +550,6 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 		if err != nil {
 			return nil, 0, fmt.Errorf("reading response: %w", err)
 		}
-		if !binaryResponse {
-			respBody = sanitizeJSONResponse(respBody)
-		}
 
 		// Success
 		if resp.StatusCode < 400 {
@@ -559,7 +557,22 @@ func (c *Client) doInternal(method, path string, params map[string]string, body
 			if method != http.MethodGet && !c.DryRun {
 				c.invalidateCache()
 			}
-			return json.RawMessage(respBody), resp.StatusCode, nil
+			// Non-textual bodies (PDF, zip, image, octet-stream) must not be
+			// run through the JSON sanitizer or returned as raw json.RawMessage
+			// — return a self-describing base64 envelope instead. Textual and
+			// JSON responses fall through to the unchanged path.
+			if isBinaryResponseContentType(resp.Header.Get("Content-Type")) {
+				env, encErr := wrapBinaryResponse(resp.Header.Get("Content-Type"), respBody)
+				if encErr != nil {
+					return nil, 0, encErr
+				}
+				return env, resp.StatusCode, nil
+			}
+			return json.RawMessage(sanitizeJSONResponse(respBody)), resp.StatusCode, nil
+		}
+
+		if !binaryResponse {
+			respBody = sanitizeJSONResponse(respBody)
 		}
 
 		apiErr := &APIError{
@@ -659,6 +672,63 @@ func (c *Client) authHeader() (string, error) {
 	return c.Config.AuthHeader(), nil
 }
 
+// binaryResponseEnvelope wraps a non-textual success body so it survives the
+// json.RawMessage contract every consumer (CLI output, --json, MCP tools)
+// depends on. Without it, raw bytes (PDF, zip, image) are corrupted by
+// sanitizeJSONResponse and emitted as invalid JSON. The _pp_binary
+// discriminator lets callers and agents detect and base64-decode the payload.
+type binaryResponseEnvelope struct {
+	PPBinary    bool   `json:"_pp_binary"`
+	ContentType string `json:"content_type"`
+	Encoding    string `json:"encoding"`
+	Bytes       int    `json:"bytes"`
+	Data        string `json:"data"`
+}
+
+// isBinaryResponseContentType reports whether a successful response with this
+// Content-Type must be base64-wrapped instead of treated as text/JSON. It is
+// deliberately narrow: JSON, */*, XML, and every text/* type (including
+// text/html, so response_format:html CLIs are untouched) pass through
+// unchanged. Only genuinely binary payloads are wrapped.
+func isBinaryResponseContentType(ct string) bool {
+	mt := strings.ToLower(strings.TrimSpace(ct))
+	if i := strings.IndexByte(mt, ';'); i >= 0 {
+		mt = strings.TrimSpace(mt[:i])
+	}
+	if mt == "" {
+		return false
+	}
+	switch {
+	case mt == "application/json", mt == "text/json", mt == "*/*":
+		return false
+	case strings.HasPrefix(mt, "text/"):
+		return false
+	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
+		return false
+	case mt == "application/xml", mt == "application/xhtml+xml":
+		return false
+	case mt == "application/javascript", mt == "application/ecmascript",
+		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
+		return false
+	}
+	return true
+}
+
+// wrapBinaryResponse marshals body into a self-describing base64 envelope.
+func wrapBinaryResponse(ct string, body []byte) (json.RawMessage, error) {
+	out, err := json.Marshal(binaryResponseEnvelope{
+		PPBinary:    true,
+		ContentType: ct,
+		Encoding:    "base64",
+		Bytes:       len(body),
+		Data:        base64.StdEncoding.EncodeToString(body),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("encoding binary response: %w", err)
+	}
+	return json.RawMessage(out), nil
+}
+
 // sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
 // response bodies so that downstream JSON parsing succeeds. For clean JSON
 // responses these checks are no-ops.
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/mcp/tools.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/mcp/tools.go
index d9c8b975..f1957530 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/mcp/tools.go
@@ -32,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/items/enterprise", "enterprise", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/items/enterprise", "enterprise", false, nil, []mcpParamBinding{}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("items_list",
@@ -41,7 +41,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/items", "free", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/items", "free", false, nil, []mcpParamBinding{}, []string{}),
 	)
 	s.AddTool(
 		mcplib.NewTool("items_premium",
@@ -50,7 +50,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),
 		),
-		makeAPIHandler("GET", "/items/premium", "paid", false, []mcpParamBinding{}, []string{}),
+		makeAPIHandler("GET", "/items/premium", "paid", false, nil, []mcpParamBinding{}, []string{}),
 	)
 	// SQL tool — ad-hoc analysis on synced data without API calls
 	s.AddTool(
@@ -86,7 +86,7 @@ type mcpParamBinding struct {
 }
 
 // makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, headerOverrides map[string]string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
 	return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
 		c, err := newMCPClient()
 		if err != nil {
@@ -108,8 +108,17 @@ func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bind
 		params := make(map[string]string)
 		bodyArgs := make(map[string]any)
 		var headers map[string]string
+		if len(headerOverrides) > 0 {
+			headers = make(map[string]string, len(headerOverrides)+1)
+			for k, v := range headerOverrides {
+				headers[k] = v
+			}
+		}
 		if binaryResponse {
-			headers = map[string]string{client.BinaryResponseHeader: "true"}
+			if headers == nil {
+				headers = map[string]string{}
+			}
+			headers[client.BinaryResponseHeader] = "true"
 		}
 		for _, binding := range bindings {
 			knownArgs[binding.PublicName] = true
@@ -154,31 +163,31 @@ func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bind
 		var data json.RawMessage
 		switch method {
 		case "GET":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, err = c.GetWithHeaders(path, params, headers)
 				break
 			}
 			data, err = c.Get(path, params)
 		case "POST":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PostWithParams(path, params, bodyArgs)
 		case "PUT":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PutWithParams(path, params, bodyArgs)
 		case "PATCH":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
 				break
 			}
 			data, _, err = c.PatchWithParams(path, params, bodyArgs)
 		case "DELETE":
-			if binaryResponse {
+			if len(headers) > 0 {
 				data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
 				break
 			}
diff --git a/testdata/golden/fixtures/golden-api.yaml b/testdata/golden/fixtures/golden-api.yaml
index ab2891db..9fa5b96c 100644
--- a/testdata/golden/fixtures/golden-api.yaml
+++ b/testdata/golden/fixtures/golden-api.yaml
@@ -335,3 +335,23 @@ paths:
       responses:
         "200":
           description: OK
+  /reports/{year}/export:
+    get:
+      tags: [reports]
+      operationId: exportReportYear
+      summary: Download the annual report as a binary file
+      parameters:
+        - $ref: "#/components/parameters/ApiVersion"
+        - name: year
+          in: path
+          required: true
+          schema:
+            type: integer
+      responses:
+        "200":
+          description: OK
+          content:
+            application/octet-stream:
+              schema:
+                type: string
+                format: byte

← 1a243810 feat(cli): add Quo (formerly OpenPhone) catalog entry and Op  ·  back to Cli Printing Press  ·  fix(cli): redact shipcheck api key in json (#1673) 4093ef3e →