← back to Cli Printing Press
feat(cli): add ElevenLabs generation support (#1307)
5f43de505252859e2ebbe846a4ecffb598e7655d · 2026-05-15 21:44:53 -0500 · Cathryn Lavery
* feat(cli): add ElevenLabs generation support
* fix(cli): share catalog metadata helpers
* fix(cli): scope headerOverrides to branches that use it in promoted commands
The promoted-command template declared `headerOverrides` at the top of
the handler when UsesBinaryResponse was set, but the paginated branch
(both HasStore and non-HasStore) passes nil to resolvePaginatedRead /
paginatedGet and never references the variable. The same was true for
the HasStore + GET branch that calls resolveRead. An endpoint with both
UsesBinaryResponse=true and pagination (or HasStore-GET) generated Go
that failed to compile with "headerOverrides declared and not used".
Move the declaration into each branch that actually consumes it (live
GET, DELETE, multipart, form, and JSON-body POST/PUT/PATCH). Regression
test pins a paginated binary GET and runs `go build` on the output.
Reported by Greptile on #1307.
* fix(cli): thread binary-response header through promoted pagination and store paths
The promoted-command template called paginatedGet, resolvePaginatedRead,
and resolveRead with nil headers regardless of UsesBinaryResponse. When
the live API call dispatched (cache miss for store-backed reads, or any
paginated binary GET), the BinaryResponseHeader sentinel never reached
the client, so the runtime ran sanitizeJSONResponse on raw binary bytes
and sent Accept: application/json — corrupting audio responses and
making strict APIs return JSON variants of binary endpoints.
Mirror the pattern already used in command_endpoint.go.tmpl: hoist
headerOverrides to the top of the handler when UsesBinaryResponse is
set, then thread headerOverrides (else nil) through every helper call
that accepts headers. Drops the per-branch headerOverrides declarations
the previous compile-fix introduced — all consumers now share one.
Two regression tests pin the wiring: the paginated path asserts
headerOverrides is passed to paginatedGet (forcing no-store via an
explicit VisionTemplateSet so IsZero() skips re-profiling), and the
store-backed path asserts resolveRead receives headerOverrides instead
of nil.
Reported by Greptile on #1307.
* chore(cli): refresh GraphQL shared-endpoint goldens after main merge
Mergify's update_method=merge brought main into this branch, which
includes the new PostWithParams helper. The GraphQL shared-endpoint
generator now threads a params map and calls PostWithParams instead
of Post for both list and get operations. Regenerated goldens to
reflect the merged behavior; no template change in this commit.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
Files touched
M .github/workflows/validate-catalog.ymlM AGENTS.mdA catalog/elevenlabs.yamlM docs/CATALOG.mdM internal/catalog/catalog.goM internal/catalog/catalog_test.goA internal/catalogmeta/catalogmeta.goM internal/cli/catalog.goM internal/cli/generate_test.goM internal/cli/root.goA internal/generator/binary_paginated_promoted_test.goM internal/generator/endpoint_is_write_test.goM internal/generator/form_test.goM internal/generator/generator_test.goM internal/generator/multipart_test.goA internal/generator/mutating_query_params_test.goM internal/generator/promoted_poststore_test.goM internal/generator/templates/client.go.tmplM internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/command_promoted.go.tmplM internal/generator/templates/mcp_tools.go.tmplM internal/generator/tier_routing_test.goM internal/openapi/parser.goM internal/openapi/parser_test.goM internal/pipeline/mcpsync/sync.goM internal/pipeline/mcpsync/sync_test.goM internal/pipeline/publish.goM internal/pipeline/publish_test.goM internal/spec/spec.goM testdata/golden/expected/catalog-list/stdout.txtM testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/client/client.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/mcp/tools.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar_upload-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.goM testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.goM testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.goM testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/tools.goM testdata/golden/expected/generate-public-param-names/public-param-golden/internal/cli/stores_create.goM testdata/golden/expected/generate-public-param-names/public-param-golden/internal/mcp/tools.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/client/client.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/mcp/tools.go
Diff
commit 5f43de505252859e2ebbe846a4ecffb598e7655d
Author: Cathryn Lavery <50469282+cathrynlavery@users.noreply.github.com>
Date: Fri May 15 21:44:53 2026 -0500
feat(cli): add ElevenLabs generation support (#1307)
* feat(cli): add ElevenLabs generation support
* fix(cli): share catalog metadata helpers
* fix(cli): scope headerOverrides to branches that use it in promoted commands
The promoted-command template declared `headerOverrides` at the top of
the handler when UsesBinaryResponse was set, but the paginated branch
(both HasStore and non-HasStore) passes nil to resolvePaginatedRead /
paginatedGet and never references the variable. The same was true for
the HasStore + GET branch that calls resolveRead. An endpoint with both
UsesBinaryResponse=true and pagination (or HasStore-GET) generated Go
that failed to compile with "headerOverrides declared and not used".
Move the declaration into each branch that actually consumes it (live
GET, DELETE, multipart, form, and JSON-body POST/PUT/PATCH). Regression
test pins a paginated binary GET and runs `go build` on the output.
Reported by Greptile on #1307.
* fix(cli): thread binary-response header through promoted pagination and store paths
The promoted-command template called paginatedGet, resolvePaginatedRead,
and resolveRead with nil headers regardless of UsesBinaryResponse. When
the live API call dispatched (cache miss for store-backed reads, or any
paginated binary GET), the BinaryResponseHeader sentinel never reached
the client, so the runtime ran sanitizeJSONResponse on raw binary bytes
and sent Accept: application/json — corrupting audio responses and
making strict APIs return JSON variants of binary endpoints.
Mirror the pattern already used in command_endpoint.go.tmpl: hoist
headerOverrides to the top of the handler when UsesBinaryResponse is
set, then thread headerOverrides (else nil) through every helper call
that accepts headers. Drops the per-branch headerOverrides declarations
the previous compile-fix introduced — all consumers now share one.
Two regression tests pin the wiring: the paginated path asserts
headerOverrides is passed to paginatedGet (forcing no-store via an
explicit VisionTemplateSet so IsZero() skips re-profiling), and the
store-backed path asserts resolveRead receives headerOverrides instead
of nil.
Reported by Greptile on #1307.
* chore(cli): refresh GraphQL shared-endpoint goldens after main merge
Mergify's update_method=merge brought main into this branch, which
includes the new PostWithParams helper. The GraphQL shared-endpoint
generator now threads a params map and calls PostWithParams instead
of Post for both list and get operations. Regenerated goldens to
reflect the merged behavior; no template change in this commit.
---------
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
---
.github/workflows/validate-catalog.yml | 2 +
AGENTS.md | 1 +
catalog/elevenlabs.yaml | 16 ++
docs/CATALOG.md | 2 +
internal/catalog/catalog.go | 4 +
internal/catalog/catalog_test.go | 9 +
internal/catalogmeta/catalogmeta.go | 68 +++++++
internal/cli/catalog.go | 3 +
internal/cli/generate_test.go | 22 +-
internal/cli/root.go | 7 +
.../generator/binary_paginated_promoted_test.go | 101 ++++++++++
internal/generator/endpoint_is_write_test.go | 10 +-
internal/generator/form_test.go | 4 +-
internal/generator/generator_test.go | 12 +-
internal/generator/multipart_test.go | 35 +++-
internal/generator/mutating_query_params_test.go | 56 ++++++
internal/generator/promoted_poststore_test.go | 8 +-
internal/generator/templates/client.go.tmpl | 96 +++++----
.../generator/templates/command_endpoint.go.tmpl | 222 +++++----------------
.../generator/templates/command_promoted.go.tmpl | 65 ++++--
internal/generator/templates/mcp_tools.go.tmpl | 81 +++++++-
internal/generator/tier_routing_test.go | 4 +-
internal/openapi/parser.go | 138 ++++++++++++-
internal/openapi/parser_test.go | 59 ++++++
internal/pipeline/mcpsync/sync.go | 16 ++
internal/pipeline/mcpsync/sync_test.go | 40 ++++
internal/pipeline/publish.go | 19 ++
internal/pipeline/publish_test.go | 41 ++++
internal/spec/spec.go | 13 +-
testdata/golden/expected/catalog-list/stdout.txt | 3 +
.../internal/client/client.go | 48 +++--
.../printing-press-rich-auth/internal/mcp/tools.go | 37 +++-
.../internal/cli/projects_avatar_upload-project.go | 8 +-
.../internal/cli/projects_create.go | 3 +-
.../internal/cli/projects_tasks_update-project.go | 8 +-
.../internal/client/client.go | 72 ++++---
.../printing-press-golden/internal/mcp/tools.go | 65 +++++-
.../internal/cli/things_get.go | 4 +-
.../internal/cli/things_list.go | 3 +-
.../mcp-cloudflare/internal/mcp/tools.go | 35 +++-
.../internal/cli/stores_create.go | 3 +-
.../public-param-golden/internal/mcp/tools.go | 39 +++-
.../tier-routing-golden/internal/client/client.go | 48 +++--
.../tier-routing-golden/internal/mcp/tools.go | 41 +++-
44 files changed, 1204 insertions(+), 367 deletions(-)
diff --git a/.github/workflows/validate-catalog.yml b/.github/workflows/validate-catalog.yml
index ae9ab8db..8373f39d 100644
--- a/.github/workflows/validate-catalog.yml
+++ b/.github/workflows/validate-catalog.yml
@@ -100,6 +100,8 @@ jobs:
./printing-press generate \
--spec "$SPEC_FILE" \
+ --spec-url "$SPEC_URL" \
+ --name "$NAME" \
--output "$OUT_DIR" \
--validate
diff --git a/AGENTS.md b/AGENTS.md
index e444a469..d013aaf3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -178,6 +178,7 @@ When adding or editing `catalog/*.yaml`, first decide whether the entry belongs
- `bearer_refresh`, when present, must include `bundle_url` and `pattern`; `bundle_url` must use HTTPS, and `pattern` must compile as a Go regexp.
- `auth_key_url`, when present, must use HTTPS. It overrides any URL inferred from the spec and surfaces in the printed CLI as `Get a key at: <URL>`.
- `auth_instructions`, when present, is a one-line string rendered under the URL. It overrides any `x-auth-instructions` value from the spec.
+- `base_url`, when present, must use HTTPS. Use it only when the upstream spec omits `servers:` and the correct API origin is known.
- Rebuild the binary after editing; `catalog.FS` is a Go embed.
See [`docs/CATALOG.md`](docs/CATALOG.md) for the inclusion rubric, evidence checklist, validation rationale, wrapper-only entry shape, and bearer-refresh metadata.
diff --git a/catalog/elevenlabs.yaml b/catalog/elevenlabs.yaml
new file mode 100644
index 00000000..99366304
--- /dev/null
+++ b/catalog/elevenlabs.yaml
@@ -0,0 +1,16 @@
+name: elevenlabs
+display_name: ElevenLabs
+description: Generate, transform, transcribe, dub, and manage AI audio, voices, music, and conversational agents with ElevenLabs.
+category: ai
+spec_url: https://api.elevenlabs.io/openapi.json
+spec_format: json
+openapi_version: "3.1"
+base_url: https://api.elevenlabs.io
+tier: official
+homepage: https://elevenlabs.io/docs
+spec_source: official
+auth_required: true
+auth_key_url: https://elevenlabs.io/app/settings/api-keys
+auth_instructions: Set ELEVENLABS_API_KEY to an ElevenLabs API key; requests send it as the xi-api-key header.
+client_pattern: rest
+http_transport: standard
diff --git a/docs/CATALOG.md b/docs/CATALOG.md
index 6e167285..304a5da3 100644
--- a/docs/CATALOG.md
+++ b/docs/CATALOG.md
@@ -96,3 +96,5 @@ Set `auth_key_url:` when the inference would land on a generic homepage and you
Catalog entries may also declare `auth_instructions:` — a one-line string of free-form guidance ("Settings → Personal access tokens → Generate new") that the printed CLI prints under the `Get a key at:` line. Use this when the URL lands on a docs page rather than the keys UI: the URL says where to start, the instruction says what to do once there.
Catalog `auth_instructions` overrides any value from the spec's [`x-auth-instructions`](SPEC-EXTENSIONS.md#x-auth-instructions) extension. The printed CLI surfaces it in auth prompts, `doctor`, and the new `auth setup` command (which also takes `--launch` to open the URL in a browser).
+
+Catalog entries may declare `base_url:` when the upstream spec intentionally omits `servers:` and the correct API origin is known. The value must be HTTPS and is used only when the parsed spec has no usable base URL.
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index 2fef309b..e48a8b27 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -115,6 +115,7 @@ type Entry struct {
SpecURL string `yaml:"spec_url"`
SpecFormat string `yaml:"spec_format"`
OpenAPIVersion string `yaml:"openapi_version"`
+ BaseURL string `yaml:"base_url,omitempty"`
Tier string `yaml:"tier"`
VerifiedDate string `yaml:"verified_date"`
Homepage string `yaml:"homepage"`
@@ -311,6 +312,9 @@ func (e *Entry) Validate() error {
return fmt.Errorf("http_transport must be one of: standard, browser-http, browser-chrome, browser-chrome-h3")
}
}
+ if e.BaseURL != "" && !strings.HasPrefix(e.BaseURL, "https://") {
+ return fmt.Errorf(`base_url must start with "https://"`)
+ }
if err := validateBearerRefresh(e.BearerRefresh); err != nil {
return err
}
diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go
index bad82ef9..942e8a3d 100644
--- a/internal/catalog/catalog_test.go
+++ b/internal/catalog/catalog_test.go
@@ -18,6 +18,7 @@ category: developer-tools
spec_url: https://example.com/openapi.yaml
spec_format: yaml
openapi_version: "3.0"
+base_url: https://api.example.com
tier: community
verified_date: "2026-03-23"
homepage: https://example.com
@@ -40,6 +41,7 @@ notes: Example fixture.
assert.Equal(t, "https://example.com/openapi.yaml", entry.SpecURL)
assert.Equal(t, "yaml", entry.SpecFormat)
assert.Equal(t, "3.0", entry.OpenAPIVersion)
+ assert.Equal(t, "https://api.example.com", entry.BaseURL)
assert.Equal(t, "community", entry.Tier)
assert.Equal(t, "2026-03-23", entry.VerifiedDate)
assert.Equal(t, "https://example.com", entry.Homepage)
@@ -144,6 +146,13 @@ func TestValidateEntry(t *testing.T) {
},
wantErr: "http_transport must be one of",
},
+ {
+ name: "non https base url",
+ mutate: func(e *Entry) {
+ e.BaseURL = "http://api.example.com"
+ },
+ wantErr: `base_url must start with "https://"`,
+ },
{
name: "bearer refresh missing bundle URL",
mutate: func(e *Entry) {
diff --git a/internal/catalogmeta/catalogmeta.go b/internal/catalogmeta/catalogmeta.go
new file mode 100644
index 00000000..845f6cb2
--- /dev/null
+++ b/internal/catalogmeta/catalogmeta.go
@@ -0,0 +1,68 @@
+package catalogmeta
+
+import (
+ "strings"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/naming"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+)
+
+func RebaseAuthEnvPrefix(auth *spec.AuthConfig, oldName, newName string) {
+ if auth == nil || oldName == "" || newName == "" || oldName == newName {
+ return
+ }
+ oldPrefix := naming.EnvPrefix(oldName) + "_"
+ newPrefix := naming.EnvPrefix(newName) + "_"
+ for i, envVar := range auth.EnvVars {
+ if suffix, ok := strings.CutPrefix(envVar, oldPrefix); ok {
+ auth.EnvVars[i] = newPrefix + suffix
+ }
+ }
+ for i := range auth.EnvVarSpecs {
+ if suffix, ok := strings.CutPrefix(auth.EnvVarSpecs[i].Name, oldPrefix); ok {
+ auth.EnvVarSpecs[i].Name = newPrefix + suffix
+ }
+ }
+}
+
+func IsReplaceableBaseURL(baseURL string, placeholder bool) bool {
+ switch strings.TrimRight(strings.TrimSpace(baseURL), "/") {
+ case "", strings.TrimRight(spec.PlaceholderBaseURL, "/"), "https://api.example.com":
+ return true
+ default:
+ return placeholder
+ }
+}
+
+func ApplyRuntimeMetadata(apiSpec *spec.APISpec, entry *catalog.Entry) {
+ if apiSpec == nil || entry == nil {
+ return
+ }
+ if entry.BaseURL != "" && IsReplaceableBaseURL(apiSpec.BaseURL, apiSpec.BaseURLIsPlaceholder) {
+ apiSpec.BaseURL = strings.TrimRight(entry.BaseURL, "/")
+ apiSpec.BaseURLIsPlaceholder = false
+ }
+ if entry.DisplayName != "" {
+ apiSpec.DisplayName = entry.DisplayName
+ apiSpec.DisplayNameDerivedFromTitle = false
+ }
+ if entry.Description != "" {
+ apiSpec.CLIDescription = entry.Description
+ }
+ if entry.AuthKeyURL != "" {
+ apiSpec.Auth.KeyURL = entry.AuthKeyURL
+ }
+ if entry.AuthInstructions != "" {
+ apiSpec.Auth.Instructions = entry.AuthInstructions
+ }
+ if entry.ClientPattern != "" {
+ apiSpec.ClientPattern = entry.ClientPattern
+ }
+ if entry.HTTPTransport != "" {
+ apiSpec.HTTPTransport = entry.HTTPTransport
+ }
+ if entry.SpecSource != "" {
+ apiSpec.SpecSource = entry.SpecSource
+ }
+}
diff --git a/internal/cli/catalog.go b/internal/cli/catalog.go
index 89728df8..0175efff 100644
--- a/internal/cli/catalog.go
+++ b/internal/cli/catalog.go
@@ -117,6 +117,9 @@ func newCatalogShowCmd() *cobra.Command {
if entry.OpenAPIVersion != "" {
fmt.Printf("OpenAPI: %s\n", entry.OpenAPIVersion)
}
+ if entry.BaseURL != "" {
+ fmt.Printf("Base URL: %s\n", entry.BaseURL)
+ }
if entry.Homepage != "" {
fmt.Printf("Homepage: %s\n", entry.Homepage)
}
diff --git a/internal/cli/generate_test.go b/internal/cli/generate_test.go
index a9a819ef..2bc057a4 100644
--- a/internal/cli/generate_test.go
+++ b/internal/cli/generate_test.go
@@ -11,6 +11,7 @@ import (
"testing"
"github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalogmeta"
"github.com/mvanhorn/cli-printing-press/v4/internal/pipeline"
"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
"github.com/stretchr/testify/assert"
@@ -1815,11 +1816,12 @@ resources:
}
func TestEnrichSpecFromCatalogCopiesGenerationMetadata(t *testing.T) {
- apiSpec := &spec.APISpec{Name: "test-api"}
+ apiSpec := &spec.APISpec{Name: "test-api", BaseURL: spec.PlaceholderBaseURL, BaseURLIsPlaceholder: true}
enrichSpecFromCatalogEntry(apiSpec, &catalog.Entry{
DisplayName: "Test.API",
OwnerName: "Trevin Chow",
+ BaseURL: "https://api.example.com/",
MCP: spec.MCPConfig{
Transport: []string{"stdio", "http"},
Orchestration: "code",
@@ -1829,11 +1831,29 @@ func TestEnrichSpecFromCatalogCopiesGenerationMetadata(t *testing.T) {
assert.Equal(t, "Test.API", apiSpec.DisplayName)
assert.Equal(t, "Trevin Chow", apiSpec.OwnerName)
+ assert.Equal(t, "https://api.example.com", apiSpec.BaseURL)
+ assert.False(t, apiSpec.BaseURLIsPlaceholder)
assert.Equal(t, []string{"stdio", "http"}, apiSpec.MCP.Transport)
assert.Equal(t, "code", apiSpec.MCP.Orchestration)
assert.Equal(t, "hidden", apiSpec.MCP.EndpointTools)
}
+func TestRebaseAuthEnvPrefix(t *testing.T) {
+ auth := spec.AuthConfig{
+ EnvVars: []string{"ELEVENLABS_DOCUMENTATION_API_KEY", "UNCHANGED_TOKEN"},
+ EnvVarSpecs: []spec.AuthEnvVar{
+ {Name: "ELEVENLABS_DOCUMENTATION_CLIENT_ID"},
+ {Name: "CUSTOM_SECRET"},
+ },
+ }
+
+ catalogmeta.RebaseAuthEnvPrefix(&auth, "elevenlabs-documentation", "elevenlabs")
+
+ assert.Equal(t, []string{"ELEVENLABS_API_KEY", "UNCHANGED_TOKEN"}, auth.EnvVars)
+ assert.Equal(t, "ELEVENLABS_CLIENT_ID", auth.EnvVarSpecs[0].Name)
+ assert.Equal(t, "CUSTOM_SECRET", auth.EnvVarSpecs[1].Name)
+}
+
func TestEnrichSpecFromCatalogMatchesSpecURLWhenSlugDiffers(t *testing.T) {
apiSpec := &spec.APISpec{
Name: "cloud-run-admin",
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 9debd614..b53c52a2 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -19,6 +19,7 @@ import (
"github.com/mvanhorn/cli-printing-press/v4/internal/artifacts"
"github.com/mvanhorn/cli-printing-press/v4/internal/browsersniff"
"github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalogmeta"
"github.com/mvanhorn/cli-printing-press/v4/internal/docspec"
"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
"github.com/mvanhorn/cli-printing-press/v4/internal/graphql"
@@ -294,6 +295,7 @@ func newGenerateCmd() *cobra.Command {
return &ExitError{Code: ExitSpecError, Err: fmt.Errorf("parsing spec %s: %w", specFile, err)}
}
+ enrichSpecFromCatalog(apiSpec, catalogSpecLookupRefs(specFiles, specURL)...)
if apiSpec.BaseURLIsPlaceholder {
return &ExitError{Code: ExitSpecError, Err: fmt.Errorf("spec %s declares no `servers:` block and no per-operation servers; the generator cannot resolve a real base URL and refuses to ship a CLI whose `doctor` would DNS-fail on every call. Add a `servers:` block with the real API host, or run via crowd-sniff with `--base-url` to supply one", specFile)}
}
@@ -312,6 +314,7 @@ func newGenerateCmd() *cobra.Command {
// cmd/<slug>-pp-cli matches what manifest/publish-validate look
// for. Explicit --name still wins.
if cliName != "" {
+ catalogmeta.RebaseAuthEnvPrefix(&apiSpec.Auth, apiSpec.Name, cliName)
apiSpec.Name = cliName
} else if researchName := pipeline.LoadAPINameFromResearchDir(researchDir); researchName != "" {
apiSpec.Name = researchName
@@ -1564,6 +1567,10 @@ func enrichSpecFromCatalogEntry(apiSpec *spec.APISpec, entry *catalog.Entry) {
if entry.Homepage != "" && apiSpec.WebsiteURL == "" {
apiSpec.WebsiteURL = entry.Homepage
}
+ if entry.BaseURL != "" && catalogmeta.IsReplaceableBaseURL(apiSpec.BaseURL, apiSpec.BaseURLIsPlaceholder) {
+ apiSpec.BaseURL = strings.TrimRight(entry.BaseURL, "/")
+ apiSpec.BaseURLIsPlaceholder = false
+ }
if entry.Category != "" && apiSpec.Category == "" {
apiSpec.Category = entry.Category
}
diff --git a/internal/generator/binary_paginated_promoted_test.go b/internal/generator/binary_paginated_promoted_test.go
new file mode 100644
index 00000000..1028e4d1
--- /dev/null
+++ b/internal/generator/binary_paginated_promoted_test.go
@@ -0,0 +1,101 @@
+package generator
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/naming"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Regression: promoted endpoints that combine UsesBinaryResponse with
+// either pagination or store-backed read previously called paginatedGet /
+// resolvePaginatedRead / resolveRead with nil headers. The live API call
+// then went out without the BinaryResponseHeader sentinel, so the client
+// ran sanitizeJSONResponse on raw binary bytes (corrupting audio) and
+// advertised Accept: application/json. The fix hoists headerOverrides
+// when UsesBinaryResponse is set and threads it into every helper call.
+func TestGenerateBinaryPaginatedPromotedThreadsHeader(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("audioapi")
+ apiSpec.Resources = map[string]spec.Resource{
+ "voices": {
+ Description: "Voices",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/voices",
+ Description: "List voices",
+ ResponseFormat: spec.ResponseFormatBinary,
+ Pagination: &spec.Pagination{
+ Type: "cursor",
+ LimitParam: "limit",
+ CursorParam: "after",
+ NextCursorPath: "next_cursor",
+ HasMoreField: "has_more",
+ },
+ Params: []spec.Param{
+ {Name: "limit", Type: "integer", Description: "Page size"},
+ {Name: "after", Type: "string", Description: "Cursor"},
+ },
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ gen := New(apiSpec, outputDir)
+ // Force no-store so the paginatedGet branch is exercised; resolvePaginatedRead
+ // would be wired the same way (both accept the headers map). Export keeps
+ // VisionSet.IsZero() false so the generator doesn't recompute from profile.
+ gen.VisionSet = VisionTemplateSet{Export: true}
+ 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"}`,
+ "binary paginated promoted must declare headerOverrides")
+ assert.Contains(t, endpointSrc, `paginatedGet(c, path, map[string]string{`,
+ "non-HasStore pagination must use paginatedGet")
+ assert.NotContains(t, endpointSrc, `}, nil, flagAll,`,
+ "paginated binary endpoint must pass headerOverrides, not nil")
+ assert.Contains(t, endpointSrc, `}, headerOverrides, flagAll,`,
+ "paginated binary endpoint must thread headerOverrides into paginatedGet")
+}
+
+// Regression: store-backed binary GET previously passed nil to resolveRead,
+// so the live API call dispatched without the binary sentinel header.
+func TestGenerateBinaryStoreBackedPromotedThreadsHeader(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("audioapi")
+ apiSpec.Resources = map[string]spec.Resource{
+ "voices": {
+ Description: "Voices",
+ Endpoints: map[string]spec.Endpoint{
+ "get": {
+ Method: "GET",
+ Path: "/voices/{voice_id}",
+ Description: "Get voice sample",
+ ResponseFormat: spec.ResponseFormatBinary,
+ 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)
+ gen.VisionSet = VisionTemplateSet{Store: true}
+ 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"}`,
+ "store-backed binary GET must declare headerOverrides")
+ assert.Contains(t, endpointSrc, `resolveRead(cmd.Context(), c, flags, "voices", false, path, params, headerOverrides)`,
+ "store-backed binary GET must thread headerOverrides through resolveRead")
+}
diff --git a/internal/generator/endpoint_is_write_test.go b/internal/generator/endpoint_is_write_test.go
index baa6af9d..18bc118a 100644
--- a/internal/generator/endpoint_is_write_test.go
+++ b/internal/generator/endpoint_is_write_test.go
@@ -249,7 +249,7 @@ func TestHasWriteCommands_PostAsQueryFlipsHasWriteFalse(t *testing.T) {
// TestPromotedCommandVerbBranching covers the integration path: the
// rendered promoted command emits the same HTTP verb the spec declared,
-// so a POST-only endpoint hits c.Post and a GET-only endpoint stays on
+// so a POST-only endpoint hits c.PostWithParams and a GET-only endpoint stays on
// c.Get/resolveRead.
func TestPromotedCommandVerbBranching(t *testing.T) {
cases := []struct {
@@ -262,7 +262,7 @@ func TestPromotedCommandVerbBranching(t *testing.T) {
mustNotHave []string
}{
{
- name: "POST endpoint emits c.Post",
+ name: "POST endpoint emits c.PostWithParams",
apiName: "post-promoted",
resourceName: "queries",
endpointName: "searchAll",
@@ -272,7 +272,7 @@ func TestPromotedCommandVerbBranching(t *testing.T) {
Description: "Search collections by free text",
Body: []spec.Param{{Name: "queryText", Type: "string"}},
},
- mustContain: []string{"c.Post("},
+ mustContain: []string{"c.PostWithParams("},
mustNotHave: []string{"c.Get(path, params)"},
},
{
@@ -561,8 +561,8 @@ func TestPromotedCommandPlumbsBodyFields(t *testing.T) {
"promoted command must build a body map from body flags")
require.Contains(t, src, `body["name"] = bodyName`,
"body map must use the spec-declared field name, not the camelCased flag var")
- require.Contains(t, src, `c.Post(path, body)`,
- "promoted command must pass the body map to c.Post, not the params map")
+ require.Contains(t, src, `c.PostWithParams(path, params, body)`,
+ "promoted command must pass the body map to c.PostWithParams, not the params map")
require.NotContains(t, src, `c.Post(path, params)`,
"promoted command must NOT pass params (URL/path params) as the request body — that was the bug")
}
diff --git a/internal/generator/form_test.go b/internal/generator/form_test.go
index 6c8940f5..1ce727c4 100644
--- a/internal/generator/form_test.go
+++ b/internal/generator/form_test.go
@@ -72,7 +72,7 @@ func TestGenerateFormRequestBodyUsesFormClient(t *testing.T) {
assert.Contains(t, endpointSrc, `fields := url.Values{}`)
assert.Contains(t, endpointSrc, `fields.Set("grant_type", bodyGrantType)`)
assert.Contains(t, endpointSrc, `fields.Set("client_id", bodyClientId)`)
- assert.Contains(t, endpointSrc, `c.PostForm(path, fields)`)
+ assert.Contains(t, endpointSrc, `c.PostFormWithParams(path, params, fields)`)
assert.NotContains(t, endpointSrc, `var stdinBody bool`)
assert.NotContains(t, endpointSrc, `c.Post(path, body)`)
// Required-flag check should fire at top-level, not inside `if !stdinBody`.
@@ -84,7 +84,7 @@ func TestGenerateFormRequestBodyUsesFormClient(t *testing.T) {
venuesSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_venues.go")
assert.Contains(t, venuesSrc, `if !json.Valid([]byte(bodyStructData))`)
assert.Contains(t, venuesSrc, `fields.Set("struct_data", bodyStructData)`)
- assert.Contains(t, venuesSrc, `c.PostForm(path, fields)`)
+ assert.Contains(t, venuesSrc, `c.PostFormWithParams(path, params, fields)`)
mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
assert.Contains(t, mcpSrc, `RequestContentType: "application/x-www-form-urlencoded"`)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 7019a313..96bc93be 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -8268,7 +8268,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*\[]mcpParamBinding\{.*WireName: "pathQueryId".*\},\s*\[]string\{[^}]*"pathQueryId"`),
+ regexp.MustCompile(`makeAPIHandler\("GET",\s*"/graphql/\{pathQueryId\}/Followers",\s*false,\s*\[]mcpParamBinding\{.*WireName: "pathQueryId".*\},\s*\[]string\{[^}]*"pathQueryId"`),
string(mcpGo),
"MCP handler must receive the routing path param so it can substitute the URL")
}
@@ -9141,9 +9141,9 @@ func TestGenerateResourceBaseURLOverrideRoutesToOverrideHost(t *testing.T) {
mcpTools, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
require.NoError(t, err)
mcpToolsStr := string(mcpTools)
- assert.Contains(t, mcpToolsStr, `makeAPIHandler("GET", "https://geocoding-api.example.com/v1/search"`,
+ assert.Contains(t, mcpToolsStr, `makeAPIHandler("GET", "https://geocoding-api.example.com/v1/search", false`,
"geocoding MCP endpoint mirror must emit the absolute override URL")
- assert.Contains(t, mcpToolsStr, `makeAPIHandler("GET", "/forecast"`,
+ assert.Contains(t, mcpToolsStr, `makeAPIHandler("GET", "/forecast", false`,
"forecast MCP endpoint mirror must keep the relative path when its resource has no override")
// Must compile.
@@ -9231,7 +9231,7 @@ func TestGenerateEndpointBaseURLOverrideRoutesToOverrideHost(t *testing.T) {
mcpTools, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
require.NoError(t, err)
- assert.Contains(t, string(mcpTools), `makeAPIHandler("GET", "https://geocoding-api.example.com/v1/search"`,
+ assert.Contains(t, string(mcpTools), `makeAPIHandler("GET", "https://geocoding-api.example.com/v1/search", false`,
"typed MCP endpoint tool must use the endpoint override URL")
}
@@ -9694,11 +9694,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*\[]mcpParamBinding\{.*WireName: "query".*\},\s*\[]string\{[^}]*"query"`),
+ regexp.MustCompile(`makeAPIHandler\("GET",\s*"/search/movie",\s*false,\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*\[]mcpParamBinding\{.*WireName: "movieId".*\},\s*\[]string\{[^}]*"movieId"`),
+ regexp.MustCompile(`makeAPIHandler\("GET",\s*"/movie/\{movieId\}",\s*false,\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 aba8d2e5..833d1cf9 100644
--- a/internal/generator/multipart_test.go
+++ b/internal/generator/multipart_test.go
@@ -67,18 +67,18 @@ func TestGenerateMultipartRequestBodyUsesMultipartClient(t *testing.T) {
assert.Contains(t, endpointSrc, `fileFields["assetData"] = bodyAssetData`)
assert.Contains(t, endpointSrc, `fields["filename"] = bodyFilename`)
assert.Contains(t, endpointSrc, `fields["metadata"] = bodyMetadata`)
- assert.Contains(t, endpointSrc, `c.PostMultipart(path, fields, fileFields)`)
+ assert.Contains(t, endpointSrc, `c.PostMultipartWithParams(path, params, fields, fileFields)`)
assert.NotContains(t, endpointSrc, `"stdin"`)
promotedSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_avatars.go")
assert.Contains(t, promotedSrc, `return fmt.Errorf("required flag \"%s\" not set", "image")`)
assert.Contains(t, promotedSrc, `fileFields["image"] = bodyImage`)
assert.Contains(t, promotedSrc, `fields["label"] = bodyLabel`)
- assert.Contains(t, promotedSrc, `c.PostMultipart(path, fields, fileFields)`)
+ assert.Contains(t, promotedSrc, `c.PostMultipartWithParams(path, params, fields, fileFields)`)
assert.NotContains(t, promotedSrc, `"stdin"`)
mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
- assert.Contains(t, mcpSrc, `makeAPIHandler("POST", "/assets", []mcpParamBinding`)
+ assert.Contains(t, mcpSrc, `makeAPIHandler("POST", "/assets", false, []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)`)
@@ -87,3 +87,32 @@ func TestGenerateMultipartRequestBodyUsesMultipartClient(t *testing.T) {
runGoCommand(t, outputDir, "mod", "tidy")
runGoCommand(t, outputDir, "build", "./...")
}
+
+func TestGenerateBinaryResponseWritesRawBytes(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("audioapi")
+ apiSpec.Resources = map[string]spec.Resource{
+ "audio": {
+ Description: "Audio",
+ Endpoints: map[string]spec.Endpoint{
+ "create": {
+ Method: "POST",
+ Path: "/audio",
+ Description: "Create audio",
+ ResponseFormat: spec.ResponseFormatBinary,
+ Body: []spec.Param{
+ {Name: "text", Type: "string", Required: true, Description: "Text"},
+ },
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ endpointSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_audio.go")
+ assert.Contains(t, endpointSrc, `binary response cannot be rendered as structured output`)
+ assert.Contains(t, endpointSrc, `cmd.OutOrStdout().Write(data)`)
+}
diff --git a/internal/generator/mutating_query_params_test.go b/internal/generator/mutating_query_params_test.go
new file mode 100644
index 00000000..09746868
--- /dev/null
+++ b/internal/generator/mutating_query_params_test.go
@@ -0,0 +1,56 @@
+package generator
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/naming"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGenerateMutatingEndpointPassesQueryParams(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("audioapi")
+ apiSpec.Resources = map[string]spec.Resource{
+ "text-to-speech": {
+ Description: "Text to speech",
+ Endpoints: map[string]spec.Endpoint{
+ "create": {
+ Method: "POST",
+ Path: "/text-to-speech/{voice_id}",
+ Description: "Create speech",
+ ResponseFormat: spec.ResponseFormatBinary,
+ Params: []spec.Param{
+ {Name: "voice_id", Type: "string", Required: true, Positional: true, PathParam: true, Description: "Voice ID"},
+ {Name: "output_format", Type: "string", Description: "Output format"},
+ },
+ Body: []spec.Param{
+ {Name: "text", Type: "string", Required: true, Description: "Text"},
+ },
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ clientSrc := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+ assert.Contains(t, clientSrc, `func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error)`)
+
+ endpointSrc := readGeneratedFile(t, outputDir, "internal", "cli", "promoted_text-to-speech.go")
+ assert.Contains(t, endpointSrc, `params := map[string]string{}`)
+ assert.Contains(t, endpointSrc, `params["output_format"] = fmt.Sprintf("%v", flagOutputFormat)`)
+ assert.Contains(t, endpointSrc, `c.PostWithParamsAndHeaders(path, params, body, headerOverrides)`)
+
+ mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
+ assert.Contains(t, mcpSrc, `PublicName: "output_format", WireName: "output_format", Location: "query"`)
+ assert.Contains(t, mcpSrc, `data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)`)
+ assert.Contains(t, mcpSrc, `"content_encoding": "base64"`)
+
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
+}
diff --git a/internal/generator/promoted_poststore_test.go b/internal/generator/promoted_poststore_test.go
index 9b5a463e..a93e9104 100644
--- a/internal/generator/promoted_poststore_test.go
+++ b/internal/generator/promoted_poststore_test.go
@@ -51,7 +51,7 @@ func TestPromotedHasStorePostRoutesThroughVerbBranch(t *testing.T) {
assert.NotContains(t, got, "resolveRead(",
"HasStore + POST must NOT route through resolveRead (GET-only internally)")
- assert.Contains(t, got, "c.Post(path, body)",
+ assert.Contains(t, got, "c.PostWithParams(path, params, body)",
"HasStore + POST must route through the verb branch with a built body")
assert.Contains(t, got, `attachFreshness(DataProvenance{Source: "live"}, flags)`,
"non-GET HasStore commands must synthesize a live-call prov so the downstream HasStore block compiles")
@@ -97,7 +97,7 @@ func TestPromotedHasStoreGetStillUsesResolveRead(t *testing.T) {
// other tests don't reach. The provenance synthesis lives in a single
// post-chain block, so a regression that drops it from one verb shape
// drops it from all of them — but DELETE has its own pre-existing
-// `data, _, err := c.Delete(path)` call site, so an explicit assertion
+// `data, _, err := c.DeleteWithParams(path, params)` call site, so an explicit assertion
// guards against template refactors that re-introduce the dup.
func TestPromotedHasStoreDeleteSynthesizesProv(t *testing.T) {
t.Parallel()
@@ -128,8 +128,8 @@ func TestPromotedHasStoreDeleteSynthesizesProv(t *testing.T) {
assert.NotContains(t, got, "resolveRead(",
"HasStore + DELETE must NOT route through resolveRead")
- assert.Contains(t, got, "c.Delete(path)",
- "HasStore + DELETE must route through c.Delete")
+ assert.Contains(t, got, "c.DeleteWithParams(path, params)",
+ "HasStore + DELETE must route through c.DeleteWithParams")
assert.Contains(t, got, `attachFreshness(DataProvenance{Source: "live"}, flags)`,
"HasStore + DELETE must synthesize a live-call prov for the downstream provenance block")
}
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 8fd8aa95..f1ca56ff 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -38,6 +38,8 @@ import (
"{{modulePath}}/internal/config"
)
+const BinaryResponseHeader = "X-Printing-Press-Binary-Response"
+
type Client struct {
BaseURL string
{{- if .BasePath}}
@@ -505,14 +507,14 @@ func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, nil, body, nil)
}
-func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, body, headers)
-}
-
func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, nil)
}
+func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, headers)
+}
+
func (c *Client) PostWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, headers)
}
@@ -522,14 +524,14 @@ func (c *Client) PostMultipart(path string, fields map[string]string, fileFields
return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PostMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PostMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PostMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PostMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -540,14 +542,14 @@ func (c *Client) PostForm(path string, fields url.Values) (json.RawMessage, int,
return c.do("POST", path, nil, formRequestBody{Fields: fields}, nil)
}
-func (c *Client) PostFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, formRequestBody{Fields: fields}, headers)
-}
-
func (c *Client) PostFormWithParams(path string, params map[string]string, fields url.Values) (json.RawMessage, int, error) {
return c.do("POST", path, params, formRequestBody{Fields: fields}, nil)
}
+func (c *Client) PostFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, formRequestBody{Fields: fields}, headers)
+}
+
func (c *Client) PostFormWithParamsAndHeaders(path string, params map[string]string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, formRequestBody{Fields: fields}, headers)
}
@@ -558,14 +560,14 @@ func (c *Client) Delete(path string) (json.RawMessage, int, error) {
return c.do("DELETE", path, nil, nil, nil)
}
-func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("DELETE", path, nil, nil, headers)
-}
-
func (c *Client) DeleteWithParams(path string, params map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, nil)
}
+func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, headers)
+}
+
func (c *Client) DeleteWithParamsAndHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, headers)
}
@@ -574,14 +576,14 @@ func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, nil, body, nil)
}
-func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, body, headers)
-}
-
func (c *Client) PutWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, nil)
}
+func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, headers)
+}
+
func (c *Client) PutWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, headers)
}
@@ -591,14 +593,14 @@ func (c *Client) PutMultipart(path string, fields map[string]string, fileFields
return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PutMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PutMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PutMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PutMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -609,14 +611,14 @@ func (c *Client) PutForm(path string, fields url.Values) (json.RawMessage, int,
return c.do("PUT", path, nil, formRequestBody{Fields: fields}, nil)
}
-func (c *Client) PutFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, formRequestBody{Fields: fields}, headers)
-}
-
func (c *Client) PutFormWithParams(path string, params map[string]string, fields url.Values) (json.RawMessage, int, error) {
return c.do("PUT", path, params, formRequestBody{Fields: fields}, nil)
}
+func (c *Client) PutFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, formRequestBody{Fields: fields}, headers)
+}
+
func (c *Client) PutFormWithParamsAndHeaders(path string, params map[string]string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, formRequestBody{Fields: fields}, headers)
}
@@ -627,14 +629,14 @@ func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, nil, body, nil)
}
-func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, body, headers)
-}
-
func (c *Client) PatchWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, nil)
}
+func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, headers)
+}
+
func (c *Client) PatchWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, headers)
}
@@ -644,14 +646,14 @@ func (c *Client) PatchMultipart(path string, fields map[string]string, fileField
return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PatchMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PatchMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PatchMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PatchMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -662,14 +664,14 @@ func (c *Client) PatchForm(path string, fields url.Values) (json.RawMessage, int
return c.do("PATCH", path, nil, formRequestBody{Fields: fields}, nil)
}
-func (c *Client) PatchFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, formRequestBody{Fields: fields}, headers)
-}
-
func (c *Client) PatchFormWithParams(path string, params map[string]string, fields url.Values) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, formRequestBody{Fields: fields}, nil)
}
+func (c *Client) PatchFormWithHeaders(path string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, formRequestBody{Fields: fields}, headers)
+}
+
func (c *Client) PatchFormWithParamsAndHeaders(path string, params map[string]string, fields url.Values, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, formRequestBody{Fields: fields}, headers)
}
@@ -992,6 +994,10 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
+ binaryResponse := strings.EqualFold(req.Header.Get(BinaryResponseHeader), "true")
+ if binaryResponse {
+ req.Header.Del(BinaryResponseHeader)
+ }
{{- if not .UsesBrowserManagedUserAgent}}
if req.Header.Get("User-Agent") == "" {
{{- if .UsesBrowserLikeUserAgent}}
@@ -1020,7 +1026,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
// per-endpoint headerOverrides, both of which run before this
// if-empty default.
if req.Header.Get("Accept") == "" {
- req.Header.Set("Accept", "application/json")
+ if binaryResponse {
+ req.Header.Set("Accept", "*/*")
+ } else {
+ req.Header.Set("Accept", "application/json")
+ }
}
{{- end}}
@@ -1035,7 +1045,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if err != nil {
return nil, 0, fmt.Errorf("reading response: %w", err)
}
- respBody = sanitizeJSONResponse(respBody)
+ if !binaryResponse {
+ respBody = sanitizeJSONResponse(respBody)
+ }
// Success
if resp.StatusCode < 400 {
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 65375db7..ab24938b 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -158,10 +158,13 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
headerOverrides := map[string]string{
{{- range .Endpoint.HeaderOverrides}}
"{{.Name}}": "{{.Value}}",
+{{- end}}
+{{- if .Endpoint.UsesBinaryResponse}}
+ "X-Printing-Press-Binary-Response": "true",
{{- end}}
}
{{- end}}
@@ -218,7 +221,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
- }, {{if .Endpoint.HeaderOverrides}}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}}
@@ -229,7 +232,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
- }, {{if .Endpoint.HeaderOverrides}}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{}
@@ -244,9 +247,9 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- end}}
{{- if .HasStore}}
- data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, {{if .Endpoint.HeaderOverrides}}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 .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, err := c.GetWithHeaders(path, params, headerOverrides)
{{- else}}
data, err := c.Get(path, params)
@@ -254,11 +257,6 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- end}}
{{- else if eq .Endpoint.Method "POST"}}
-{{- if $isMultipart}}
- fields := map[string]string{}
- fileFields := map[string]string{}
-{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
params := map[string]string{}
{{- range .Endpoint.Params}}
{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
@@ -270,45 +268,23 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if $isMultipart}}
+ fields := map[string]string{}
+ fileFields := map[string]string{}
+{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PostMultipartWithParamsAndHeaders(path, params, fields, fileFields, headerOverrides)
{{- else}}
data, statusCode, err := c.PostMultipartWithParams(path, params, fields, fileFields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PostMultipartWithHeaders(path, fields, fileFields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PostMultipart(path, fields, fileFields)
-{{- end}}
-{{- end}}
{{- else if $isForm}}
fields := url.Values{}
{{formBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PostFormWithParamsAndHeaders(path, params, fields, headerOverrides)
{{- else}}
data, statusCode, err := c.PostFormWithParams(path, params, fields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PostFormWithHeaders(path, fields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PostForm(path, fields)
-{{- end}}
-{{- end}}
{{- else}}
var body map[string]any
if stdinBody {
@@ -324,33 +300,13 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
} else {
body = map[string]any{}
{{bodyMapForEndpoint .Endpoint "\t\t\t\t"}} }
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PostWithParamsAndHeaders(path, params, body, headerOverrides)
{{- else}}
data, statusCode, err := c.PostWithParams(path, params, body)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PostWithHeaders(path, body, headerOverrides)
-{{- else}}
- data, statusCode, err := c.Post(path, body)
-{{- end}}
-{{- end}}
{{- end}}
{{- else if eq .Endpoint.Method "DELETE"}}
-{{- if endpointHasQueryFlags .Endpoint}}
params := map[string]string{}
{{- range .Endpoint.Params}}
{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
@@ -362,24 +318,12 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.DeleteWithParamsAndHeaders(path, params, headerOverrides)
{{- else}}
data, statusCode, err := c.DeleteWithParams(path, params)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.DeleteWithHeaders(path, headerOverrides)
-{{- else}}
- data, statusCode, err := c.Delete(path)
-{{- end}}
-{{- end}}
{{- else if eq .Endpoint.Method "PUT"}}
-{{- if $isMultipart}}
- fields := map[string]string{}
- fileFields := map[string]string{}
-{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
params := map[string]string{}
{{- range .Endpoint.Params}}
{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
@@ -391,45 +335,23 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if $isMultipart}}
+ fields := map[string]string{}
+ fileFields := map[string]string{}
+{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PutMultipartWithParamsAndHeaders(path, params, fields, fileFields, headerOverrides)
{{- else}}
data, statusCode, err := c.PutMultipartWithParams(path, params, fields, fileFields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PutMultipartWithHeaders(path, fields, fileFields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PutMultipart(path, fields, fileFields)
-{{- end}}
-{{- end}}
{{- else if $isForm}}
fields := url.Values{}
{{formBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PutFormWithParamsAndHeaders(path, params, fields, headerOverrides)
{{- else}}
data, statusCode, err := c.PutFormWithParams(path, params, fields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PutFormWithHeaders(path, fields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PutForm(path, fields)
-{{- end}}
-{{- end}}
{{- else}}
var body map[string]any
if stdinBody {
@@ -445,37 +367,13 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
} else {
body = map[string]any{}
{{bodyMapForEndpoint .Endpoint "\t\t\t\t"}} }
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PutWithParamsAndHeaders(path, params, body, headerOverrides)
{{- else}}
data, statusCode, err := c.PutWithParams(path, params, body)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PutWithHeaders(path, body, headerOverrides)
-{{- else}}
- data, statusCode, err := c.Put(path, body)
-{{- end}}
-{{- end}}
{{- end}}
{{- else if eq .Endpoint.Method "PATCH"}}
-{{- if $isMultipart}}
- fields := map[string]string{}
- fileFields := map[string]string{}
-{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
params := map[string]string{}
{{- range .Endpoint.Params}}
{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
@@ -487,45 +385,23 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if $isMultipart}}
+ fields := map[string]string{}
+ fileFields := map[string]string{}
+{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PatchMultipartWithParamsAndHeaders(path, params, fields, fileFields, headerOverrides)
{{- else}}
data, statusCode, err := c.PatchMultipartWithParams(path, params, fields, fileFields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PatchMultipartWithHeaders(path, fields, fileFields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PatchMultipart(path, fields, fileFields)
-{{- end}}
-{{- end}}
{{- else if $isForm}}
fields := url.Values{}
{{formBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PatchFormWithParamsAndHeaders(path, params, fields, headerOverrides)
{{- else}}
data, statusCode, err := c.PatchFormWithParams(path, params, fields)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PatchFormWithHeaders(path, fields, headerOverrides)
-{{- else}}
- data, statusCode, err := c.PatchForm(path, fields)
-{{- end}}
-{{- end}}
{{- else}}
var body map[string]any
if stdinBody {
@@ -541,30 +417,11 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
} else {
body = map[string]any{}
{{bodyMapForEndpoint .Endpoint "\t\t\t\t"}} }
-{{- if endpointHasQueryFlags .Endpoint}}
- params := map[string]string{}
-{{- range .Endpoint.Params}}
-{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
- params["{{.Name}}"] = args[{{positionalIndex $.Endpoint .Name}}]
-{{- end}}
-{{- if and (not .Positional) (not .PathParam)}}
- if flag{{camel (paramIdent .)}} != {{zeroValForParam .Name .Type}} {
- params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel (paramIdent .)}})
- }
-{{- end}}
-{{- end}}
-{{- if .Endpoint.HeaderOverrides}}
+{{- if or .Endpoint.HeaderOverrides .Endpoint.UsesBinaryResponse}}
data, statusCode, err := c.PatchWithParamsAndHeaders(path, params, body, headerOverrides)
{{- else}}
data, statusCode, err := c.PatchWithParams(path, params, body)
{{- end}}
-{{- else}}
-{{- if .Endpoint.HeaderOverrides}}
- data, statusCode, err := c.PatchWithHeaders(path, body, headerOverrides)
-{{- else}}
- data, statusCode, err := c.Patch(path, body)
-{{- end}}
-{{- end}}
{{- end}}
{{- end}}
{{- if eq .Endpoint.Method "DELETE"}}
@@ -576,6 +433,24 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
return classifyAPIError(err, flags)
}
{{- end}}
+{{- if .Endpoint.UsesBinaryResponse}}
+ _ = json.Valid
+ _ = os.Stderr
+{{- if or (eq .Endpoint.Method "POST") (eq .Endpoint.Method "PUT") (eq .Endpoint.Method "PATCH") (eq .Endpoint.Method "DELETE")}}
+ _ = statusCode
+{{- end}}
+{{- if and .HasStore (or (eq .Endpoint.Method "GET") (eq .Endpoint.Method "HEAD") .Endpoint.Pagination $isGraphQLEndpoint)}}
+ _ = prov
+{{- end}}
+ 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
+{{- else}}
{{- if endpointNeedsClientLimit .Endpoint}}
// Honor --limit when the API accepts but ignores ?limit=N.
data = truncateJSONArray(data, flagLimit)
@@ -753,6 +628,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+{{- end}}
},
}
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index e7811916..153bd68a 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -131,6 +131,9 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- end}}
{{- end}}
+{{- if .Endpoint.UsesBinaryResponse}}
+ headerOverrides := map[string]string{"X-Printing-Press-Binary-Response": "true"}
+{{- end}}
{{- if .Endpoint.Pagination}}
{{- if .HasStore}}
data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", path, map[string]string{
@@ -142,7 +145,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
- }, nil, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
+ }, {{if .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}}
@@ -153,10 +156,9 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
"{{paramWireName .}}": fmt.Sprintf("%v", flag{{camel (paramIdent .)}}),
{{- end}}
{{- end}}
- }, nil, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
+ }, {{if .Endpoint.UsesBinaryResponse}}headerOverrides{{else}}nil{{end}}, flagAll, "{{.Endpoint.Pagination.CursorParam}}", "{{.Endpoint.Pagination.NextCursorPath}}", "{{.Endpoint.Pagination.HasMoreField}}")
{{- end}}
{{- else}}
-{{- if or $isReadVerb (and $isWriteVerb $hasQueryFlags)}}
params := map[string]string{}
{{- range .Endpoint.Params}}
{{- if and .Positional (not (pathContainsParam $.Endpoint.Path .Name))}}
@@ -168,34 +170,37 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
}
{{- end}}
{{- end}}
-{{- end}}
-{{- if and .HasStore (eq $method "GET")}}
- data, prov, err := resolveRead(cmd.Context(), c, flags, "{{lower .ResourceName}}", {{if .Endpoint.Pagination}}true{{else}}false{{end}}, path, params, nil)
+{{- 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}})
{{- else if eq $method "GET"}}
+{{- if .Endpoint.UsesBinaryResponse}}
+ data, err := c.GetWithHeaders(path, params, headerOverrides)
+{{- else}}
data, err := c.Get(path, params)
-{{- else if eq $method "DELETE"}}
-{{- if $hasQueryFlags}}
- data, _, err := c.DeleteWithParams(path, params)
+{{- end}}
+{{- else if eq (upper .Endpoint.Method) "DELETE"}}
+{{- if .Endpoint.UsesBinaryResponse}}
+ data, _, err := c.DeleteWithParamsAndHeaders(path, params, headerOverrides)
{{- else}}
- data, _, err := c.Delete(path)
+ data, _, err := c.DeleteWithParams(path, params)
{{- end}}
-{{- else if or (eq $method "POST") (eq $method "PUT") (eq $method "PATCH")}}
+{{- else if or (eq (upper .Endpoint.Method) "POST") (eq (upper .Endpoint.Method) "PUT") (eq (upper .Endpoint.Method) "PATCH")}}
{{- if $isMultipart}}
fields := map[string]string{}
fileFields := map[string]string{}
{{multipartBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if $hasQueryFlags}}
- data, _, err := c.{{pascal (lower .Endpoint.Method)}}MultipartWithParams(path, params, fields, fileFields)
+{{- if .Endpoint.UsesBinaryResponse}}
+ data, _, err := c.{{pascal (lower .Endpoint.Method)}}MultipartWithParamsAndHeaders(path, params, fields, fileFields, headerOverrides)
{{- else}}
- data, _, err := c.{{pascal (lower .Endpoint.Method)}}Multipart(path, fields, fileFields)
+ data, _, err := c.{{pascal (lower .Endpoint.Method)}}MultipartWithParams(path, params, fields, fileFields)
{{- end}}
-{{- else if $isForm}}{{/* HeaderOverrides intentionally omitted on promoted form/multipart/JSON paths — parity with the existing multipart promoted branch above. */}}
+{{- else if $isForm}}
fields := url.Values{}
{{formBodyMaps .Endpoint.Body "\t\t\t"}}
-{{- if $hasQueryFlags}}
- data, _, err := c.{{pascal (lower .Endpoint.Method)}}FormWithParams(path, params, fields)
+{{- if .Endpoint.UsesBinaryResponse}}
+ data, _, err := c.{{pascal (lower .Endpoint.Method)}}FormWithParamsAndHeaders(path, params, fields, headerOverrides)
{{- else}}
- data, _, err := c.{{pascal (lower .Endpoint.Method)}}Form(path, fields)
+ data, _, err := c.{{pascal (lower .Endpoint.Method)}}FormWithParams(path, params, fields)
{{- end}}
{{- else}}
// HasStore + non-GET falls through to a live API call here
@@ -203,16 +208,20 @@ 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 $hasQueryFlags}} data, _, err := c.{{pascal (lower .Endpoint.Method)}}WithParams(path, params, body)
-{{else}} data, _, err := c.{{pascal (lower .Endpoint.Method)}}(path, body)
+{{bodyMapForEndpoint .Endpoint "\t\t\t"}}{{if .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}}
{{- else}}
// 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}}
+ data, err := c.GetWithHeaders(path, params, headerOverrides)
+{{- else}}
data, err := c.Get(path, params)
{{- end}}
+{{- end}}
{{- if and .HasStore (ne (upper .Endpoint.Method) "GET")}}
prov := attachFreshness(DataProvenance{Source: "live"}, flags)
{{- end}}
@@ -220,6 +229,21 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
if err != nil {
return classifyAPIError(err, flags)
}
+{{- if .Endpoint.UsesBinaryResponse}}
+ _ = json.Valid
+ _ = os.Stderr
+{{- if .HasStore}}
+ _ = prov
+{{- end}}
+ 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
+{{- else}}
{{- if endpointNeedsClientLimit .Endpoint}}
// Honor --limit when the API accepts but ignores ?limit=N.
data = truncateJSONArray(data, flagLimit)
@@ -297,6 +321,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
}
}
return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+{{- end}}
},
}
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index 46a507ec..c97aea10 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -9,6 +9,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
{{- if $hasFormRequest}}
@@ -70,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)}}, []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}}, []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)}}, []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}}, []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}}
@@ -105,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)}}, []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}}, []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)}}, []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}}, []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}}
@@ -199,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, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
{{- else}}
-func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
{{- end}}
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
@@ -225,6 +226,10 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
{{- if $hasMultipartRequest}}
multipartFields := make(map[string]string)
multipartFileFields := make(map[string]string)
@@ -330,68 +335,124 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
{{- if $hasMultipartRequest}}
if multipart {
+ if binaryResponse {
+ data, _, err = c.PostMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PostMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
{{- end}}
{{- if $hasFormRequest}}
if formEncoded {
+ if binaryResponse {
+ data, _, err = c.PostFormWithParamsAndHeaders(path, params, formFields, headers)
+ break
+ }
data, _, err = c.PostFormWithParams(path, params, formFields)
break
}
{{- end}}
{{- if $hasBodyJSONFallback}}
if len(bodyJSONOverride) > 0 {
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyJSONOverride)
break
}
{{- end}}
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
{{- if $hasMultipartRequest}}
if multipart {
+ if binaryResponse {
+ data, _, err = c.PutMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PutMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
{{- end}}
{{- if $hasFormRequest}}
if formEncoded {
+ if binaryResponse {
+ data, _, err = c.PutFormWithParamsAndHeaders(path, params, formFields, headers)
+ break
+ }
data, _, err = c.PutFormWithParams(path, params, formFields)
break
}
{{- end}}
{{- if $hasBodyJSONFallback}}
if len(bodyJSONOverride) > 0 {
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyJSONOverride)
break
}
{{- end}}
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
{{- if $hasMultipartRequest}}
if multipart {
+ if binaryResponse {
+ data, _, err = c.PatchMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PatchMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
{{- end}}
{{- if $hasFormRequest}}
if formEncoded {
+ if binaryResponse {
+ data, _, err = c.PatchFormWithParamsAndHeaders(path, params, formFields, headers)
+ break
+ }
data, _, err = c.PatchFormWithParams(path, params, formFields)
break
}
{{- end}}
{{- if $hasBodyJSONFallback}}
if len(bodyJSONOverride) > 0 {
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyJSONOverride, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyJSONOverride)
break
}
{{- end}}
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -490,6 +551,14 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
diff --git a/internal/generator/tier_routing_test.go b/internal/generator/tier_routing_test.go
index 522f0af5..7312d880 100644
--- a/internal/generator/tier_routing_test.go
+++ b/internal/generator/tier_routing_test.go
@@ -102,8 +102,8 @@ func TestTierRoutingEmitsTierAwareClientAndCommands(t *testing.T) {
require.Contains(t, paidCmd, `c = c.WithTier("paid")`)
mcpSrc := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
- require.Contains(t, mcpSrc, `makeAPIHandler("GET", "/items", "free"`)
- require.Contains(t, mcpSrc, `makeAPIHandler("GET", "/items/premium", "paid"`)
+ require.Contains(t, mcpSrc, `makeAPIHandler("GET", "/items", "free", false`)
+ require.Contains(t, mcpSrc, `makeAPIHandler("GET", "/items/premium", "paid", false`)
require.Contains(t, mcpSrc, `c = c.WithTier(tier)`)
require.Contains(t, mcpSrc, `"tier_routing": map[string]any`)
require.Regexp(t, `"items_premium":\s+"paid"`, mcpSrc)
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index c94c118b..1cfee019 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -548,6 +548,9 @@ func mapAuthWithDescriptionInference(doc *openapi3.T, name string, allowDescript
schemeName, scheme := selectSecurityScheme(doc)
if scheme == nil {
result := inferQueryParamAuth(doc, name, auth)
+ if result.Type == "none" {
+ result = inferHeaderParamAPIKeyAuth(doc, name, result)
+ }
if result.Type == "none" && allowDescriptionInference {
result = inferDescriptionAuth(doc, name, result)
}
@@ -1209,6 +1212,22 @@ var commonAuthQueryParams = map[string]bool{
"token": true,
}
+func authLikeHeaderName(name string) bool {
+ normalized := strings.ToLower(strings.TrimSpace(name))
+ if normalized == "" {
+ return false
+ }
+ switch normalized {
+ case "authorization", "proxy-authorization", "cookie", "set-cookie":
+ return false
+ }
+ compact := strings.NewReplacer("-", "", "_", "", " ", "").Replace(normalized)
+ if strings.Contains(compact, "apikey") || strings.Contains(compact, "authkey") {
+ return true
+ }
+ return strings.Contains(compact, "token") && (strings.Contains(compact, "auth") || strings.Contains(compact, "access"))
+}
+
// inferQueryParamAuth scans all operations for query parameters that look like
// API keys. If more than 30% of operations carry one, we infer query-param auth.
// This handles specs that omit securitySchemes but pass keys via query string.
@@ -1254,7 +1273,13 @@ func inferQueryParamAuth(doc *openapi3.T, name string, fallback spec.AuthConfig)
// Find the most common auth-like param name.
var best string
var bestCount int
- for pName, cnt := range paramCounts {
+ paramNames := make([]string, 0, len(paramCounts))
+ for pName := range paramCounts {
+ paramNames = append(paramNames, pName)
+ }
+ sort.Strings(paramNames)
+ for _, pName := range paramNames {
+ cnt := paramCounts[pName]
if cnt > bestCount {
best = pName
bestCount = cnt
@@ -1274,6 +1299,71 @@ func inferQueryParamAuth(doc *openapi3.T, name string, fallback spec.AuthConfig)
}
}
+// inferHeaderParamAPIKeyAuth handles OpenAPI specs that omit securitySchemes
+// but repeat an API-key-shaped header parameter across the operation surface.
+func inferHeaderParamAPIKeyAuth(doc *openapi3.T, name string, fallback spec.AuthConfig) spec.AuthConfig {
+ if doc == nil || doc.Paths == nil || hasTopLevelSecurityDeclaration(doc) {
+ return fallback
+ }
+
+ paramCounts := map[string]int{}
+ totalOps := 0
+
+ for _, pathKey := range doc.Paths.InMatchingOrder() {
+ pathItem := doc.Paths.Value(pathKey)
+ if pathItem == nil {
+ continue
+ }
+ for _, op := range pathItem.Operations() {
+ if op == nil {
+ continue
+ }
+ totalOps++
+ seen := false
+ for _, p := range mergeParameters(pathItem, op) {
+ if p == nil || p.In != openapi3.ParameterInHeader || seen {
+ continue
+ }
+ if authLikeHeaderName(p.Name) {
+ paramCounts[p.Name]++
+ seen = true
+ }
+ }
+ }
+ }
+
+ if totalOps == 0 {
+ return fallback
+ }
+
+ var best string
+ var bestCount int
+ paramNames := make([]string, 0, len(paramCounts))
+ for pName := range paramCounts {
+ paramNames = append(paramNames, pName)
+ }
+ sort.Strings(paramNames)
+ for _, pName := range paramNames {
+ cnt := paramCounts[pName]
+ if cnt > bestCount {
+ best = pName
+ bestCount = cnt
+ }
+ }
+ if bestCount == 0 || float64(bestCount)/float64(totalOps) <= 0.3 {
+ return fallback
+ }
+
+ envPrefix := naming.EnvPrefix(name)
+ return spec.AuthConfig{
+ Type: "api_key",
+ In: "header",
+ Header: best,
+ EnvVars: []string{envPrefix + "_API_KEY"},
+ Inferred: true,
+ }
+}
+
// detectRequiredHeaders scans all operations for required header parameters
// and returns those appearing on >80% of operations as global required headers.
// Auth-related and dynamic headers are excluded via case-insensitive matching.
@@ -2035,6 +2125,9 @@ func mapResources(doc *openapi3.T, out *spec.APISpec, basePath string) {
// response-schema inference. Resolution happens at parse time so
// the profiler sees a single resolved value per endpoint and
// templates do not re-walk schemas at generation time.
+ if responseUsesBinary(op) {
+ endpoint.ResponseFormat = spec.ResponseFormatBinary
+ }
if pathResourceIDOverride != "" {
endpoint.IDField = pathResourceIDOverride
} else {
@@ -3220,6 +3313,49 @@ func selectResponseSchema(response *openapi3.Response) *openapi3.SchemaRef {
return nil
}
+func responseUsesBinary(op *openapi3.Operation) bool {
+ if op == nil || op.Responses == nil {
+ return false
+ }
+ success := selectSuccessResponse(op.Responses)
+ if success == nil || success.Value == nil || success.Value.Content == nil {
+ return false
+ }
+ for _, contentType := range sortedContentTypes(success.Value.Content) {
+ media := success.Value.Content[contentType]
+ if media == nil {
+ continue
+ }
+ if schema := schemaRefValue(media.Schema); schema != nil {
+ if schema.Type != nil && schema.Type.Includes(openapi3.TypeString) && strings.EqualFold(schemaFormat(schema), "binary") {
+ return true
+ }
+ }
+ if binaryContentType(contentType) {
+ return true
+ }
+ }
+ return false
+}
+
+func binaryContentType(contentType string) bool {
+ base := strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
+ if base == "" {
+ return false
+ }
+ if strings.HasPrefix(base, "audio/") ||
+ strings.HasPrefix(base, "video/") ||
+ strings.HasPrefix(base, "image/") {
+ return true
+ }
+ switch base {
+ case "application/octet-stream", "application/zip", "application/x-zip", "application/x-zip-compressed", "application/pdf", "multipart/mixed":
+ return true
+ default:
+ return false
+ }
+}
+
// 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_test.go b/internal/openapi/parser_test.go
index 696d9639..ad92360c 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -3590,6 +3590,34 @@ func TestInferOperationLevelBearer(t *testing.T) {
})
}
+func TestInferHeaderParamAPIKeyAuth(t *testing.T) {
+ t.Parallel()
+
+ doc := &openapi3.T{
+ Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
+ Paths: &openapi3.Paths{},
+ }
+ for _, path := range []string{"/a", "/b", "/c", "/d"} {
+ doc.Paths.Set(path, &openapi3.PathItem{
+ Get: &openapi3.Operation{
+ Responses: openapi3.NewResponses(),
+ Parameters: openapi3.Parameters{
+ &openapi3.ParameterRef{Value: &openapi3.Parameter{
+ Name: "xi-api-key", In: "header", Required: false,
+ }},
+ },
+ },
+ })
+ }
+
+ result := mapAuth(doc, "elevenlabs")
+ assert.Equal(t, "api_key", result.Type)
+ assert.Equal(t, "header", result.In)
+ assert.Equal(t, "xi-api-key", result.Header)
+ assert.Equal(t, []string{"ELEVENLABS_API_KEY"}, result.EnvVars)
+ assert.True(t, result.Inferred)
+}
+
func TestAuthTierPrecedence(t *testing.T) {
t.Parallel()
@@ -5552,6 +5580,37 @@ paths:
assert.True(t, byName["filename"].Required)
}
+func TestParseBinaryResponseFormat(t *testing.T) {
+ t.Parallel()
+ data := []byte(`
+openapi: 3.1.0
+info:
+ title: Audio API
+ version: 1.0.0
+servers:
+ - url: https://api.example.com
+paths:
+ /v1/audio:
+ post:
+ operationId: createAudio
+ responses:
+ "200":
+ description: Audio bytes
+ content:
+ audio/mpeg:
+ schema:
+ type: string
+ format: binary
+`)
+
+ parsed, err := Parse(data)
+ require.NoError(t, err)
+
+ endpoint := findParsedEndpointByPath(t, parsed, "POST", "/v1/audio")
+ assert.Equal(t, spec.ResponseFormatBinary, endpoint.ResponseFormat)
+ assert.True(t, endpoint.UsesBinaryResponse())
+}
+
func TestParseFormUrlencodedRequestBodyPreservesContentType(t *testing.T) {
t.Parallel()
data := []byte(`
diff --git a/internal/pipeline/mcpsync/sync.go b/internal/pipeline/mcpsync/sync.go
index 0b440f5f..ca79c83e 100644
--- a/internal/pipeline/mcpsync/sync.go
+++ b/internal/pipeline/mcpsync/sync.go
@@ -10,6 +10,9 @@ import (
"regexp"
"strings"
+ catalogfs "github.com/mvanhorn/cli-printing-press/v4/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalogmeta"
"github.com/mvanhorn/cli-printing-press/v4/internal/generator"
"github.com/mvanhorn/cli-printing-press/v4/internal/graphql"
"github.com/mvanhorn/cli-printing-press/v4/internal/mcpoverrides"
@@ -80,6 +83,7 @@ func Sync(cliDir string, opts Options) (Result, error) {
if prior := applyManifestNameOverride(cliDir, parsed); prior != "" {
fmt.Fprintf(os.Stderr, "mcp-sync: using manifest api_name %q over spec-derived slug %q\n", parsed.Name, prior)
}
+ applyCatalogMetadata(parsed)
// Validate that spec.yaml.name matches the directory's basename.
// Older library CLIs sometimes have drift (weather-goat's
// spec.yaml.name = "weather"; open-meteo's name diverges similarly)
@@ -511,10 +515,22 @@ func applyManifestNameOverride(cliDir string, parsed *spec.APISpec) string {
if parsed.Config.Path == fmt.Sprintf(defaultConfigPathFormat, naming.CLI(prior)) {
parsed.Config.Path = fmt.Sprintf(defaultConfigPathFormat, naming.CLI(m.APIName))
}
+ catalogmeta.RebaseAuthEnvPrefix(&parsed.Auth, prior, m.APIName)
parsed.Name = m.APIName
return prior
}
+func applyCatalogMetadata(parsed *spec.APISpec) {
+ if parsed == nil {
+ return
+ }
+ entry, err := catalog.LookupFS(catalogfs.FS, parsed.Name)
+ if err != nil {
+ return
+ }
+ catalogmeta.ApplyRuntimeMetadata(parsed, entry)
+}
+
// readExistingManifestDisplayName returns the display_name from an
// existing manifest.json if it's a real brand name. The only form
// rejected is the bare lowercase slug we'd otherwise emit as last
diff --git a/internal/pipeline/mcpsync/sync_test.go b/internal/pipeline/mcpsync/sync_test.go
index bc7625f7..4eba6b26 100644
--- a/internal/pipeline/mcpsync/sync_test.go
+++ b/internal/pipeline/mcpsync/sync_test.go
@@ -1247,6 +1247,18 @@ func TestApplyManifestNameOverrideReplacesParsedName(t *testing.T) {
parsed := &spec.APISpec{
Name: "telegram-bot",
+ Auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "X-API-Key",
+ EnvVars: []string{"TELEGRAM_BOT_API_KEY"},
+ EnvVarSpecs: []spec.AuthEnvVar{{
+ Name: "TELEGRAM_BOT_API_KEY",
+ Kind: spec.AuthEnvVarKindPerCall,
+ Required: true,
+ Sensitive: true,
+ Inferred: true,
+ }},
+ },
Config: spec.ConfigSpec{
Format: "toml",
Path: "~/.config/telegram-bot-pp-cli/config.toml",
@@ -1257,6 +1269,34 @@ func TestApplyManifestNameOverrideReplacesParsedName(t *testing.T) {
assert.Equal(t, "telegram", parsed.Name, "parsed.Name should adopt the manifest's api_name")
assert.Equal(t, "~/.config/telegram-pp-cli/config.toml", parsed.Config.Path,
"spec-derived Config.Path must follow the override so README's Config file: line tracks the binary")
+ assert.Equal(t, []string{"TELEGRAM_API_KEY"}, parsed.Auth.EnvVars,
+ "inferred auth env vars should follow the manifest api_name")
+ require.Len(t, parsed.Auth.EnvVarSpecs, 1)
+ assert.Equal(t, "TELEGRAM_API_KEY", parsed.Auth.EnvVarSpecs[0].Name,
+ "inferred auth env var specs should follow the manifest api_name")
+}
+
+func TestApplyCatalogMetadataUsesCatalogBaseURL(t *testing.T) {
+ t.Parallel()
+
+ parsed := &spec.APISpec{
+ Name: "elevenlabs",
+ BaseURL: spec.PlaceholderBaseURL,
+ BaseURLIsPlaceholder: true,
+ Auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "xi-api-key",
+ },
+ }
+
+ applyCatalogMetadata(parsed)
+
+ assert.Equal(t, "https://api.elevenlabs.io", parsed.BaseURL)
+ assert.False(t, parsed.BaseURLIsPlaceholder)
+ assert.Equal(t, "ElevenLabs", parsed.DisplayName)
+ assert.Equal(t, "https://elevenlabs.io/app/settings/api-keys", parsed.Auth.KeyURL)
+ assert.Equal(t, "standard", parsed.HTTPTransport)
+ assert.Equal(t, "official", parsed.SpecSource)
}
// TestApplyManifestNameOverrideNoOpWhenManifestAgrees — most CLIs are
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index fee7801f..a81c676b 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -13,6 +13,7 @@ import (
"github.com/mvanhorn/cli-printing-press/v4/catalog"
catalogpkg "github.com/mvanhorn/cli-printing-press/v4/internal/catalog"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/catalogmeta"
"github.com/mvanhorn/cli-printing-press/v4/internal/graphql"
"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
"github.com/mvanhorn/cli-printing-press/v4/internal/openapi"
@@ -295,6 +296,7 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
parsed, parseErr = spec.ParseBytes(data)
}
if parseErr == nil {
+ applyPublishCatalogMetadata(parsed, state.APIName)
populateMCPMetadata(&m, parsed)
}
@@ -373,6 +375,23 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
return WriteCLIManifest(dir, m)
}
+func applyPublishCatalogMetadata(parsed *spec.APISpec, apiName string) {
+ if parsed == nil || apiName == "" {
+ return
+ }
+ priorName := parsed.Name
+ if priorName != "" && priorName != apiName {
+ catalogmeta.RebaseAuthEnvPrefix(&parsed.Auth, priorName, apiName)
+ }
+ parsed.Name = apiName
+
+ entry, err := catalogpkg.LookupFS(catalog.FS, apiName)
+ if err != nil {
+ return
+ }
+ catalogmeta.ApplyRuntimeMetadata(parsed, entry)
+}
+
// loadResearchForPromote returns the research.json relevant to the
// current promote/publish call, plus the path it was loaded from (used
// for non-canonical-source stderr visibility).
diff --git a/internal/pipeline/publish_test.go b/internal/pipeline/publish_test.go
index 5a6407af..09dc8e4f 100644
--- a/internal/pipeline/publish_test.go
+++ b/internal/pipeline/publish_test.go
@@ -269,6 +269,47 @@ paths: {}
assert.Equal(t, "Product Hunt", m.DisplayName)
}
+func TestWriteCLIManifestForPublishAppliesCatalogMetadataAfterNameOverride(t *testing.T) {
+ tmp := t.TempDir()
+ t.Setenv("PRINTING_PRESS_HOME", tmp)
+ t.Setenv("PRINTING_PRESS_SCOPE", "test-scope")
+ t.Setenv("PRINTING_PRESS_REPO_ROOT", tmp)
+
+ state := NewStateWithRun("elevenlabs", filepath.Join(tmp, "working", "elevenlabs-pp-cli"), "20260513-elevenlabs", "test-scope")
+ require.NoError(t, os.MkdirAll(state.WorkingDir, 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(state.WorkingDir, "spec.yaml"), []byte(`
+openapi: "3.1.0"
+info:
+ title: ElevenLabs API Documentation
+ version: "1.0"
+paths:
+ /v1/models:
+ get:
+ operationId: getModels
+ parameters:
+ - name: xi-api-key
+ in: header
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: ok
+`), 0o644))
+
+ require.NoError(t, writeCLIManifestForPublish(state, state.WorkingDir))
+
+ m := readPublishedManifest(t, state.WorkingDir)
+ assert.Equal(t, []string{"ELEVENLABS_API_KEY"}, m.AuthEnvVars)
+ assert.Equal(t, "https://elevenlabs.io/app/settings/api-keys", m.AuthKeyURL)
+
+ tools, err := ReadToolsManifest(state.WorkingDir)
+ require.NoError(t, err)
+ assert.Equal(t, "elevenlabs", tools.APIName)
+ assert.Equal(t, "https://api.elevenlabs.io", tools.BaseURL)
+ assert.Equal(t, []string{"ELEVENLABS_API_KEY"}, tools.Auth.EnvVars)
+}
+
func TestWriteCLIManifestForPublishPopulatesCategoryFromSpec(t *testing.T) {
tmp := t.TempDir()
t.Setenv("PRINTING_PRESS_HOME", tmp)
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index d522906b..3c00fb3e 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -32,8 +32,9 @@ const (
)
const (
- ResponseFormatJSON = "json"
- ResponseFormatHTML = "html"
+ ResponseFormatJSON = "json"
+ ResponseFormatHTML = "html"
+ ResponseFormatBinary = "binary"
)
const (
@@ -1270,6 +1271,10 @@ func (e Endpoint) UsesHTMLResponse() bool {
return e.EffectiveResponseFormat() == ResponseFormatHTML
}
+func (e Endpoint) UsesBinaryResponse() bool {
+ return e.EffectiveResponseFormat() == ResponseFormatBinary
+}
+
type HTMLExtract struct {
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` // page (default), links, or embedded-json
LinkPrefixes []string `yaml:"link_prefixes,omitempty" json:"link_prefixes,omitempty"` // URL path prefixes to keep when extracting links (mode: links)
@@ -2581,9 +2586,9 @@ func validateTierRoutingResource(s *APISpec, resourcePath string, resource Resou
func validateEndpointResponseFormat(e Endpoint) error {
switch e.ResponseFormat {
- case "", ResponseFormatJSON, ResponseFormatHTML:
+ case "", ResponseFormatJSON, ResponseFormatHTML, ResponseFormatBinary:
default:
- return fmt.Errorf("response_format must be one of: json, html")
+ return fmt.Errorf("response_format must be one of: json, html, binary")
}
if !e.UsesHTMLResponse() {
return nil
diff --git a/testdata/golden/expected/catalog-list/stdout.txt b/testdata/golden/expected/catalog-list/stdout.txt
index 86be8809..398e4f31 100644
--- a/testdata/golden/expected/catalog-list/stdout.txt
+++ b/testdata/golden/expected/catalog-list/stdout.txt
@@ -1,3 +1,6 @@
+ai:
+ elevenlabs Generate, transform, transcribe, dub, and manage AI audio, voices, music, and conversational agents with ElevenLabs.
+
auth:
stytch Authentication and user management API
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 278bf2e1..7ed57607 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
@@ -23,6 +23,8 @@ import (
"time"
)
+const BinaryResponseHeader = "X-Printing-Press-Binary-Response"
+
type Client struct {
BaseURL string
Config *config.Config
@@ -177,14 +179,14 @@ func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, nil, body, nil)
}
-func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, body, headers)
-}
-
func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, nil)
}
+func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, headers)
+}
+
func (c *Client) PostWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, headers)
}
@@ -193,14 +195,14 @@ func (c *Client) Delete(path string) (json.RawMessage, int, error) {
return c.do("DELETE", path, nil, nil, nil)
}
-func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("DELETE", path, nil, nil, headers)
-}
-
func (c *Client) DeleteWithParams(path string, params map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, nil)
}
+func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, headers)
+}
+
func (c *Client) DeleteWithParamsAndHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, headers)
}
@@ -209,14 +211,14 @@ func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, nil, body, nil)
}
-func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, body, headers)
-}
-
func (c *Client) PutWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, nil)
}
+func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, headers)
+}
+
func (c *Client) PutWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, headers)
}
@@ -225,14 +227,14 @@ func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, nil, body, nil)
}
-func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, body, headers)
-}
-
func (c *Client) PatchWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, nil)
}
+func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, headers)
+}
+
func (c *Client) PatchWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, headers)
}
@@ -306,6 +308,10 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
+ binaryResponse := strings.EqualFold(req.Header.Get(BinaryResponseHeader), "true")
+ if binaryResponse {
+ req.Header.Del(BinaryResponseHeader)
+ }
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "printing-press-oauth2-pp-cli/1.0.0")
}
@@ -321,7 +327,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
// per-endpoint headerOverrides, both of which run before this
// if-empty default.
if req.Header.Get("Accept") == "" {
- req.Header.Set("Accept", "application/json")
+ if binaryResponse {
+ req.Header.Set("Accept", "*/*")
+ } else {
+ req.Header.Set("Accept", "application/json")
+ }
}
resp, err := c.HTTPClient.Do(req)
@@ -335,7 +345,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if err != nil {
return nil, 0, fmt.Errorf("reading response: %w", err)
}
- respBody = sanitizeJSONResponse(respBody)
+ if !binaryResponse {
+ respBody = sanitizeJSONResponse(respBody)
+ }
// Success
if resp.StatusCode < 400 {
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 98853611..efe0916a 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
@@ -5,6 +5,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -31,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/items", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/items", false, []mcpParamBinding{}, []string{}),
)
// SQL tool — ad-hoc analysis on synced data without API calls
s.AddTool(
@@ -67,7 +68,7 @@ type mcpParamBinding struct {
}
// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
if err != nil {
@@ -87,6 +88,10 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
for _, binding := range bindings {
knownArgs[binding.PublicName] = true
v, ok := args[binding.PublicName]
@@ -130,14 +135,34 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -190,6 +215,14 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar_upload-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar_upload-project.go
index 9bf61373..3bddcf56 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar_upload-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar_upload-project.go
@@ -33,6 +33,10 @@ func newProjectsAvatarUploadProjectCmd(flags *rootFlags) *cobra.Command {
path := "/projects/{projectId}/avatar"
path = replacePathParam(path, "projectId", args[0])
+ params := map[string]string{}
+ if flagOverwrite != false {
+ params["overwrite"] = fmt.Sprintf("%v", flagOverwrite)
+ }
fields := map[string]string{}
fileFields := map[string]string{}
if bodyCaption != "" {
@@ -42,10 +46,6 @@ func newProjectsAvatarUploadProjectCmd(flags *rootFlags) *cobra.Command {
fileFields["file"] = bodyFile
}
- params := map[string]string{}
- if flagOverwrite != false {
- params["overwrite"] = fmt.Sprintf("%v", flagOverwrite)
- }
data, statusCode, err := c.PutMultipartWithParams(path, params, fields, fileFields)
if err != nil {
return classifyAPIError(err, flags)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
index 952292b0..4f1e7b79 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
@@ -38,6 +38,7 @@ func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
}
path := "/projects"
+ params := map[string]string{}
var body map[string]any
if stdinBody {
stdinData, err := io.ReadAll(os.Stdin)
@@ -61,7 +62,7 @@ func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
body["visibility"] = bodyVisibility
}
}
- data, statusCode, err := c.Post(path, body)
+ data, statusCode, err := c.PostWithParams(path, params, body)
if err != nil {
return classifyAPIError(err, flags)
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
index 86421865..0412e27f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -42,6 +42,10 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
return usageErr(fmt.Errorf("taskId is required\nUsage: %s <%s>", cmd.CommandPath(), "taskId"))
}
path = replacePathParam(path, "taskId", args[1])
+ params := map[string]string{}
+ if flagNotify != false {
+ params["notify"] = fmt.Sprintf("%v", flagNotify)
+ }
var body map[string]any
if stdinBody {
stdinData, err := io.ReadAll(os.Stdin)
@@ -65,10 +69,6 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
body["title"] = bodyTitle
}
}
- params := map[string]string{}
- if flagNotify != false {
- params["notify"] = fmt.Sprintf("%v", flagNotify)
- }
data, statusCode, err := c.PatchWithParams(path, params, body)
if err != nil {
return classifyAPIError(err, flags)
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 12a6aba0..62713388 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
@@ -22,6 +22,8 @@ import (
"time"
)
+const BinaryResponseHeader = "X-Printing-Press-Binary-Response"
+
type Client struct {
BaseURL string
Config *config.Config
@@ -171,14 +173,14 @@ func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, nil, body, nil)
}
-func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, body, headers)
-}
-
func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, nil)
}
+func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, headers)
+}
+
func (c *Client) PostWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, headers)
}
@@ -186,14 +188,14 @@ func (c *Client) PostMultipart(path string, fields map[string]string, fileFields
return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PostMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PostMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PostMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PostMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -202,14 +204,14 @@ func (c *Client) Delete(path string) (json.RawMessage, int, error) {
return c.do("DELETE", path, nil, nil, nil)
}
-func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("DELETE", path, nil, nil, headers)
-}
-
func (c *Client) DeleteWithParams(path string, params map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, nil)
}
+func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, headers)
+}
+
func (c *Client) DeleteWithParamsAndHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, headers)
}
@@ -218,14 +220,14 @@ func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, nil, body, nil)
}
-func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, body, headers)
-}
-
func (c *Client) PutWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, nil)
}
+func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, headers)
+}
+
func (c *Client) PutWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, headers)
}
@@ -233,14 +235,14 @@ func (c *Client) PutMultipart(path string, fields map[string]string, fileFields
return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PutMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PutMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PutMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PutMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -249,14 +251,14 @@ func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, nil, body, nil)
}
-func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, body, headers)
-}
-
func (c *Client) PatchWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, nil)
}
+func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, headers)
+}
+
func (c *Client) PatchWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, headers)
}
@@ -264,14 +266,14 @@ func (c *Client) PatchMultipart(path string, fields map[string]string, fileField
return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
-func (c *Client) PatchMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
-}
-
func (c *Client) PatchMultipartWithParams(path string, params map[string]string, fields map[string]string, fileFields map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, nil)
}
+func (c *Client) PatchMultipartWithHeaders(path string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
+}
+
func (c *Client) PatchMultipartWithParamsAndHeaders(path string, params map[string]string, fields map[string]string, fileFields map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, multipartRequestBody{Fields: fields, FileFields: fileFields}, headers)
}
@@ -399,6 +401,10 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
+ binaryResponse := strings.EqualFold(req.Header.Get(BinaryResponseHeader), "true")
+ if binaryResponse {
+ req.Header.Del(BinaryResponseHeader)
+ }
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
}
@@ -414,7 +420,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
// per-endpoint headerOverrides, both of which run before this
// if-empty default.
if req.Header.Get("Accept") == "" {
- req.Header.Set("Accept", "application/json")
+ if binaryResponse {
+ req.Header.Set("Accept", "*/*")
+ } else {
+ req.Header.Set("Accept", "application/json")
+ }
}
resp, err := c.HTTPClient.Do(req)
@@ -428,7 +438,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if err != nil {
return nil, 0, fmt.Errorf("reading response: %w", err)
}
- respBody = sanitizeJSONResponse(respBody)
+ if !binaryResponse {
+ respBody = sanitizeJSONResponse(respBody)
+ }
// Success
if resp.StatusCode < 400 {
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 67b50870..615a7001 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
@@ -5,6 +5,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -31,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/currencies", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/currencies", false, []mcpParamBinding{}, []string{}),
)
s.AddTool(
mcplib.NewTool("projects_create",
@@ -42,7 +43,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("POST", "/projects", []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, []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",
@@ -52,7 +53,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/projects/{projectId}", []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}}, []string{"projectId"}),
+ makeAPIHandler("GET", "/projects/{projectId}", false, []mcpParamBinding{{PublicName: "projectId", WireName: "projectId", Location: "path"}}, []string{"projectId"}),
)
s.AddTool(
mcplib.NewTool("projects_list",
@@ -64,7 +65,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/projects", []mcpParamBinding{{PublicName: "status", WireName: "status", Location: "query"}, {PublicName: "limit", WireName: "limit", Location: "query"}, {PublicName: "cursor", WireName: "cursor", Location: "query"}}, []string{}),
+ makeAPIHandler("GET", "/projects", false, []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",
@@ -75,7 +76,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithString("file", mcplib.Description("File")),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("PUT", "/projects/{projectId}/avatar", []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, []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",
@@ -88,7 +89,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/projects/{projectId}/tasks", []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, []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",
@@ -101,7 +102,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithString("title", mcplib.Description("Title")),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("PATCH", "/projects/{projectId}/tasks/{taskId}", []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, []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",
@@ -110,7 +111,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/public/status", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/public/status", false, []mcpParamBinding{}, []string{}),
)
s.AddTool(
mcplib.NewTool("reports_summary_get-report-year",
@@ -120,7 +121,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/reports/{year}/summary", []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
+ makeAPIHandler("GET", "/reports/{year}/summary", false, []mcpParamBinding{{PublicName: "year", WireName: "year", Location: "path"}}, []string{"year"}),
)
// Search tool — faster than iterating list endpoints for finding specific items
s.AddTool(
@@ -179,7 +180,7 @@ func mcpMultipartFieldValue(v any) string {
}
// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
if err != nil {
@@ -199,6 +200,10 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
multipartFields := make(map[string]string)
multipartFileFields := make(map[string]string)
multipart := false
@@ -258,26 +263,58 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
if multipart {
+ if binaryResponse {
+ data, _, err = c.PostMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PostMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
if multipart {
+ if binaryResponse {
+ data, _, err = c.PutMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PutMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
if multipart {
+ if binaryResponse {
+ data, _, err = c.PatchMultipartWithParamsAndHeaders(path, params, multipartFields, multipartFileFields, headers)
+ break
+ }
data, _, err = c.PatchMultipartWithParams(path, params, multipartFields, multipartFileFields)
break
}
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -330,6 +367,14 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go
index faed9b13..2f04632c 100644
--- a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_get.go
@@ -33,6 +33,8 @@ func newThingsGetCmd(flags *rootFlags) *cobra.Command {
path := "/graphql"
_ = path
+ params := map[string]string{}
+ params["thing_id"] = args[0]
var body map[string]any
if stdinBody {
stdinData, err := io.ReadAll(os.Stdin)
@@ -47,7 +49,7 @@ func newThingsGetCmd(flags *rootFlags) *cobra.Command {
} else {
body = map[string]any{}
}
- data, statusCode, err := c.Post(path, body)
+ data, statusCode, err := c.PostWithParams(path, params, body)
if err != nil {
return classifyAPIError(err, flags)
}
diff --git a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go
index 5263ff0d..b12420c0 100644
--- a/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go
+++ b/testdata/golden/expected/generate-graphql-shared-endpoint/graphql-shared-golden/internal/cli/things_list.go
@@ -30,6 +30,7 @@ func newThingsListCmd(flags *rootFlags) *cobra.Command {
path := "/graphql"
_ = path
+ params := map[string]string{}
var body map[string]any
if stdinBody {
stdinData, err := io.ReadAll(os.Stdin)
@@ -44,7 +45,7 @@ func newThingsListCmd(flags *rootFlags) *cobra.Command {
} else {
body = map[string]any{}
}
- data, statusCode, err := c.Post(path, body)
+ data, statusCode, err := c.PostWithParams(path, params, body)
if err != nil {
return classifyAPIError(err, flags)
}
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 e49b33b9..8264a877 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
@@ -5,6 +5,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -61,7 +62,7 @@ type mcpParamBinding struct {
}
// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
if err != nil {
@@ -81,6 +82,10 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
for _, binding := range bindings {
knownArgs[binding.PublicName] = true
v, ok := args[binding.PublicName]
@@ -124,14 +129,34 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -184,6 +209,14 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
diff --git a/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/cli/stores_create.go b/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/cli/stores_create.go
index 8fe13fd8..c8070688 100644
--- a/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/cli/stores_create.go
+++ b/testdata/golden/expected/generate-public-param-names/public-param-golden/internal/cli/stores_create.go
@@ -33,6 +33,7 @@ func newStoresCreateCmd(flags *rootFlags) *cobra.Command {
}
path := "/stores"
+ params := map[string]string{}
var body map[string]any
if stdinBody {
stdinData, err := io.ReadAll(os.Stdin)
@@ -50,7 +51,7 @@ func newStoresCreateCmd(flags *rootFlags) *cobra.Command {
body["store_code"] = bodyStoreCode
}
}
- data, statusCode, err := c.Post(path, body)
+ data, statusCode, err := c.PostWithParams(path, params, body)
if err != nil {
return classifyAPIError(err, flags)
}
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 25b3c856..4c14a760 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
@@ -5,6 +5,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -29,7 +30,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("POST", "/stores", []mcpParamBinding{{PublicName: "store-code", WireName: "store_code", Location: "body"}}, []string{}),
+ makeAPIHandler("POST", "/stores", false, []mcpParamBinding{{PublicName: "store-code", WireName: "store_code", Location: "body"}}, []string{}),
)
s.AddTool(
mcplib.NewTool("stores_find",
@@ -40,7 +41,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/power/store-locator", []mcpParamBinding{{PublicName: "address", WireName: "s", Location: "query"}, {PublicName: "city", WireName: "c", Location: "query"}}, []string{}),
+ makeAPIHandler("GET", "/power/store-locator", false, []mcpParamBinding{{PublicName: "address", WireName: "s", Location: "query"}, {PublicName: "city", WireName: "c", Location: "query"}}, []string{}),
)
// Context tool — front-loaded domain knowledge for agents.
@@ -66,7 +67,7 @@ type mcpParamBinding struct {
}
// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
if err != nil {
@@ -86,6 +87,10 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
for _, binding := range bindings {
knownArgs[binding.PublicName] = true
v, ok := args[binding.PublicName]
@@ -129,14 +134,34 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -182,6 +207,14 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
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 585769f7..e5f04e40 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
@@ -21,6 +21,8 @@ import (
"time"
)
+const BinaryResponseHeader = "X-Printing-Press-Binary-Response"
+
type Client struct {
BaseURL string
Config *config.Config
@@ -266,14 +268,14 @@ func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, nil, body, nil)
}
-func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("POST", path, nil, body, headers)
-}
-
func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, nil)
}
+func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, headers)
+}
+
func (c *Client) PostWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("POST", path, params, body, headers)
}
@@ -282,14 +284,14 @@ func (c *Client) Delete(path string) (json.RawMessage, int, error) {
return c.do("DELETE", path, nil, nil, nil)
}
-func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("DELETE", path, nil, nil, headers)
-}
-
func (c *Client) DeleteWithParams(path string, params map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, nil)
}
+func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, headers)
+}
+
func (c *Client) DeleteWithParamsAndHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, int, error) {
return c.do("DELETE", path, params, nil, headers)
}
@@ -298,14 +300,14 @@ func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, nil, body, nil)
}
-func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PUT", path, nil, body, headers)
-}
-
func (c *Client) PutWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, nil)
}
+func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, headers)
+}
+
func (c *Client) PutWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PUT", path, params, body, headers)
}
@@ -314,14 +316,14 @@ func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, nil, body, nil)
}
-func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
- return c.do("PATCH", path, nil, body, headers)
-}
-
func (c *Client) PatchWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, nil)
}
+func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, headers)
+}
+
func (c *Client) PatchWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
return c.do("PATCH", path, params, body, headers)
}
@@ -403,6 +405,10 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
for k, v := range headerOverrides {
req.Header.Set(k, v)
}
+ binaryResponse := strings.EqualFold(req.Header.Get(BinaryResponseHeader), "true")
+ if binaryResponse {
+ req.Header.Del(BinaryResponseHeader)
+ }
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
}
@@ -418,7 +424,11 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
// per-endpoint headerOverrides, both of which run before this
// if-empty default.
if req.Header.Get("Accept") == "" {
- req.Header.Set("Accept", "application/json")
+ if binaryResponse {
+ req.Header.Set("Accept", "*/*")
+ } else {
+ req.Header.Set("Accept", "application/json")
+ }
}
resp, err := c.HTTPClient.Do(req)
@@ -432,7 +442,9 @@ func (c *Client) do(method, path string, params map[string]string, body any, hea
if err != nil {
return nil, 0, fmt.Errorf("reading response: %w", err)
}
- respBody = sanitizeJSONResponse(respBody)
+ if !binaryResponse {
+ respBody = sanitizeJSONResponse(respBody)
+ }
// Success
if resp.StatusCode < 400 {
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 aa25d051..d9c8b975 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
@@ -5,6 +5,7 @@ package mcp
import (
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -31,7 +32,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/items/enterprise", "enterprise", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/items/enterprise", "enterprise", false, []mcpParamBinding{}, []string{}),
)
s.AddTool(
mcplib.NewTool("items_list",
@@ -40,7 +41,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/items", "free", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/items", "free", false, []mcpParamBinding{}, []string{}),
)
s.AddTool(
mcplib.NewTool("items_premium",
@@ -49,7 +50,7 @@ func RegisterTools(s *server.MCPServer) {
mcplib.WithDestructiveHintAnnotation(false),
mcplib.WithOpenWorldHintAnnotation(true),
),
- makeAPIHandler("GET", "/items/premium", "paid", []mcpParamBinding{}, []string{}),
+ makeAPIHandler("GET", "/items/premium", "paid", false, []mcpParamBinding{}, []string{}),
)
// SQL tool — ad-hoc analysis on synced data without API calls
s.AddTool(
@@ -85,7 +86,7 @@ type mcpParamBinding struct {
}
// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
-func makeAPIHandler(method, pathTemplate, tier string, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
+func makeAPIHandler(method, pathTemplate, tier string, binaryResponse bool, bindings []mcpParamBinding, positionalParams []string) server.ToolHandlerFunc {
return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
c, err := newMCPClient()
if err != nil {
@@ -106,6 +107,10 @@ func makeAPIHandler(method, pathTemplate, tier string, bindings []mcpParamBindin
pathParams := make(map[string]bool, len(positionalParams))
params := make(map[string]string)
bodyArgs := make(map[string]any)
+ var headers map[string]string
+ if binaryResponse {
+ headers = map[string]string{client.BinaryResponseHeader: "true"}
+ }
for _, binding := range bindings {
knownArgs[binding.PublicName] = true
v, ok := args[binding.PublicName]
@@ -149,14 +154,34 @@ func makeAPIHandler(method, pathTemplate, tier string, bindings []mcpParamBindin
var data json.RawMessage
switch method {
case "GET":
+ if binaryResponse {
+ data, err = c.GetWithHeaders(path, params, headers)
+ break
+ }
data, err = c.Get(path, params)
case "POST":
+ if binaryResponse {
+ data, _, err = c.PostWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PostWithParams(path, params, bodyArgs)
case "PUT":
+ if binaryResponse {
+ data, _, err = c.PutWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PutWithParams(path, params, bodyArgs)
case "PATCH":
+ if binaryResponse {
+ data, _, err = c.PatchWithParamsAndHeaders(path, params, bodyArgs, headers)
+ break
+ }
data, _, err = c.PatchWithParams(path, params, bodyArgs)
case "DELETE":
+ if binaryResponse {
+ data, _, err = c.DeleteWithParamsAndHeaders(path, params, headers)
+ break
+ }
data, _, err = c.DeleteWithParams(path, params)
default:
return mcplib.NewToolResultError("unsupported method: " + method), nil
@@ -209,6 +234,14 @@ func makeAPIHandler(method, pathTemplate, tier string, bindings []mcpParamBindin
}
}
}
+ if binaryResponse {
+ out, _ := json.Marshal(map[string]any{
+ "content_encoding": "base64",
+ "data_base64": base64.StdEncoding.EncodeToString(data),
+ "byte_count": len(data),
+ })
+ return mcplib.NewToolResultText(string(out)), nil
+ }
return mcplib.NewToolResultText(string(data)), nil
}
}
← 55f5b26b fix(cli): expand pm_stale timestamp-field list for non-updat
·
back to Cli Printing Press
·
fix(cli): short-circuit sync on dry-run sentinel response (# f8d3ba80 →