[object Object]

← back to Cli Printing Press

fix(cli): emit WithNumber/WithBoolean for OpenAPI-parsed numeric MCP params (#1372)

1f1b0bdb20325f512dce35283d724602c72ebcdd · 2026-05-14 00:29:58 -0700 · Trevin Chow

* fix(cli): normalize numeric/bool spec types in MCP binding emission

The MCP tool and intent templates picked the `mcplib.With*` binder with
a literal `eq .Type "integer"` / `eq .Type "boolean"` check. That worked
for internal-spec literals but missed every OpenAPI-parsed numeric and
bool: `mapSchemaType` normalizes to `"int"`, `"float"`, `"bool"`, and
the templates fell through to `WithString` for those. The JSON-RPC
input then arrived as a quoted string, and any type-strict endpoint
(Pushover's update_highest_message.json was the trigger case) rejected
the payload with HTTP 400.

Funnel through a new `mcpBindingFunc(t)` helper that runs `primitiveKind`
and returns the `With*` name, so OpenAPI shapes and internal-spec
literals both produce the same binding. Collapses three duplicated
3-branch switches in the templates into one call each.

Refs #1362.

* docs(cli): learning doc for MCP template literal type-match miss

Captures the failure mode that #1362 fixed: a Go normalizer helper
(primitiveKind) existed but the MCP templates did their own literal
comparison against Param.Type, so OpenAPI-parser canonical shapes
("int"/"float"/"bool") missed the WithNumber/WithBoolean branches.
Includes the grep checks to spot the same pattern elsewhere in the
template tree.

Refs #1362.

Files touched

Diff

commit 1f1b0bdb20325f512dce35283d724602c72ebcdd
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Thu May 14 00:29:58 2026 -0700

    fix(cli): emit WithNumber/WithBoolean for OpenAPI-parsed numeric MCP params (#1372)
    
    * fix(cli): normalize numeric/bool spec types in MCP binding emission
    
    The MCP tool and intent templates picked the `mcplib.With*` binder with
    a literal `eq .Type "integer"` / `eq .Type "boolean"` check. That worked
    for internal-spec literals but missed every OpenAPI-parsed numeric and
    bool: `mapSchemaType` normalizes to `"int"`, `"float"`, `"bool"`, and
    the templates fell through to `WithString` for those. The JSON-RPC
    input then arrived as a quoted string, and any type-strict endpoint
    (Pushover's update_highest_message.json was the trigger case) rejected
    the payload with HTTP 400.
    
    Funnel through a new `mcpBindingFunc(t)` helper that runs `primitiveKind`
    and returns the `With*` name, so OpenAPI shapes and internal-spec
    literals both produce the same binding. Collapses three duplicated
    3-branch switches in the templates into one call each.
    
    Refs #1362.
    
    * docs(cli): learning doc for MCP template literal type-match miss
    
    Captures the failure mode that #1362 fixed: a Go normalizer helper
    (primitiveKind) existed but the MCP templates did their own literal
    comparison against Param.Type, so OpenAPI-parser canonical shapes
    ("int"/"float"/"bool") missed the WithNumber/WithBoolean branches.
    Includes the grep checks to spot the same pattern elsewhere in the
    template tree.
    
    Refs #1362.
---
 ...-type-match-missed-openapi-shapes-2026-05-14.md | 109 +++++++++++++++++++++
 internal/generator/cursor_param_test.go            |  29 ++++++
 internal/generator/generator.go                    |  19 ++++
 internal/generator/generator_test.go               |  51 ++++++++++
 internal/generator/templates/mcp_intents.go.tmpl   |   8 +-
 internal/generator/templates/mcp_tools.go.tmpl     |  32 +-----
 .../printing-press-golden/internal/mcp/tools.go    |  12 +--
 7 files changed, 219 insertions(+), 41 deletions(-)

diff --git a/docs/solutions/logic-errors/mcp-template-literal-type-match-missed-openapi-shapes-2026-05-14.md b/docs/solutions/logic-errors/mcp-template-literal-type-match-missed-openapi-shapes-2026-05-14.md
new file mode 100644
index 00000000..79166b53
--- /dev/null
+++ b/docs/solutions/logic-errors/mcp-template-literal-type-match-missed-openapi-shapes-2026-05-14.md
@@ -0,0 +1,109 @@
+---
+title: "MCP template's literal `eq .Type \"integer\"` check missed OpenAPI-parsed `\"int\"` shape — every numeric body/query param bound as JSON string"
+date: 2026-05-14
+category: logic-errors
+module: internal/generator/templates
+problem_type: logic_error
+component: tooling
+symptoms:
+  - "Pushover's update_highest_message.json endpoint rejected every direct CLI invocation and every MCP tool call with HTTP 400 (`\"message\":\"789\"` instead of `\"message\":789`)."
+  - "Surfaced by Greptile on printing-press-library#511; local fixes shipped on commits 19bc3147 (inline novel-cmd) and 49156f77 (standalone CLI + MCP), filed upstream as #1362."
+  - "Visible in golden output: `WithString(\"limit\", ...)`, `WithString(\"year\", ...)`, `WithString(\"overwrite\", ...)`, `WithString(\"notify\", ...)` against an OpenAPI golden spec that declared those params as `type: integer` / `type: boolean`."
+root_cause: logic_error
+resolution_type: code_fix
+severity: high
+tags:
+  - generator-template
+  - mcp
+  - openapi-parser
+  - type-normalization
+  - primitive-kind
+  - withnumber
+  - withboolean
+  - integer-body-fields
+  - golden-harness
+---
+
+# MCP template's literal `eq .Type "integer"` check missed OpenAPI-parsed `"int"` shape
+
+## Problem
+
+`mcp_tools.go.tmpl` and `mcp_intents.go.tmpl` decided which `mcplib.With*` binder to emit using literal string comparison on `spec.Param.Type`:
+
+```gotemplate
+{{- if eq .Type "integer"}}
+            mcplib.WithNumber(...)
+{{- else if eq .Type "boolean"}}
+            mcplib.WithBoolean(...)
+{{- else}}
+            mcplib.WithString(...)
+{{- end}}
+```
+
+Two parser code paths populate `Param.Type`:
+
+- The **internal-spec** YAML parser preserves whatever the author wrote, so `type: integer` flows through verbatim.
+- The **OpenAPI** parser at `internal/openapi/parser.go:3616` (`mapSchemaType`) normalizes to canonical Go-shaped tokens: `openapi3.TypeInteger -> "int"`, `openapi3.TypeNumber -> "float"`, `openapi3.TypeBoolean -> "bool"`.
+
+The `if eq .Type "integer"` branch only matched the internal-spec literal. Every OpenAPI-parsed numeric body/query/path param fell through to `WithString`, so the JSON-RPC client sent `{"message":"789"}` and any type-strict endpoint (Pushover, several Stripe write paths, Linear filters, anything declaring `type: integer` body fields) returned HTTP 400.
+
+The CLI surface was *not* affected: `bodyVarDecls` / `bodyFlagRegs` / `bodyMapForEndpoint` route through `cobraFlagFunc` and `goType`, which both call `primitiveKind` — that helper already collapses `"integer"`/`"int"` to the same kind. The MCP template was the only emitter doing literal-match without normalization.
+
+## Symptoms
+
+- Pushover `update_highest_message.json` rejected `"message":"789"` (the StringVar flag value flowed straight into the body map and into the MCP wire payload).
+- Five sibling Pushover fields exhibited the same shape: `glances_update` `count` and `percent`; `messages_send` `priority`/`retry`/`expire`/`timestamp`; the `html`/`monospace`/`encrypted` integer-as-bool flags.
+- Visible in the golden harness once the fix landed: `testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go` flipped 6 lines from `WithString` to `WithNumber` (`limit`, `year`) and `WithBoolean` (`overwrite`, `notify`, `completed`).
+- Sampling against the published library by grep: every published CLI generated from an OpenAPI vendor spec emitted `WithString` for `type: integer` query/path params, so the bug was uniform across the catalog, not specific to a single API.
+
+## What Didn't Work
+
+- Looking only at the CLI emission. A first reading of the issue assumed the CLI `StringVar` shape needed a typed `Int64Var` patch; closer inspection of `cobraFlagFunc` / `primitiveKind` showed the CLI side was already correct for both spec shapes. The Pushover CLI's `var bodyMessage string` was a spec-author bug (the internal YAML declared `type: string` for `message`), not a generator bug. The generator bug was confined to the MCP template.
+- Special-casing the Pushover-named field. The fix needed to land in the generator (one place), not in the printed CLI (one site per affected field per CLI). The `printing-press-library#511` patches `19bc3147` and `49156f77` are local printed-CLI workarounds that the upstream fix renders unnecessary on regen.
+
+## Solution
+
+Add a single `mcpBindingFunc(t string) string` helper that funnels the type through the existing `primitiveKind` normalizer and returns the `mcplib.With*` name. Replace the three duplicated inline switches in the MCP templates with one call each.
+
+**Helper** (`internal/generator/generator.go`, alongside `cobraFlagFunc`):
+
+```go
+func mcpBindingFunc(t string) string {
+    switch primitiveKind(t) {
+    case "int", "float":
+        return "WithNumber"
+    case "bool":
+        return "WithBoolean"
+    default:
+        return "WithString"
+    }
+}
+```
+
+Registered in the template FuncMap immediately after `cobraFlagFunc` / `cobraFlagFuncForParam` (same "kind-mapping helper" cluster).
+
+**Template change** (`mcp_tools.go.tmpl`, `mcp_intents.go.tmpl`) — five call sites total: top-level params, top-level body, sub-resource params, sub-resource body in `mcp_tools.go.tmpl`, and the intent params loop in `mcp_intents.go.tmpl`:
+
+```gotemplate
+{{- range $endpoint.Body}}
+            mcplib.{{mcpBindingFunc .Type}}({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
+{{- end}}
+```
+
+**Tests**:
+
+- `TestMCPBindingFunc` (`cursor_param_test.go`, table-driven): asserts every cross-product of `{integer, int, number, float, boolean, bool, string, "", object, array, INTEGER}` maps to the expected `With*` name.
+- `TestMCPBindingNumericTypesAcrossSpecShapes` (`generator_test.go`, integration): generates a two-endpoint spec — one with OpenAPI-shape types (`int`, `bool`) and one with internal-spec literals (`integer`, `boolean`) — and asserts both shapes emit `WithNumber` / `WithBoolean` on body and query surfaces.
+
+**Golden update**: one expected file (`generate-golden-api/.../mcp/tools.go`), 6 lines. The fixture was already an OpenAPI spec with `type: integer` and `type: boolean` params, so the golden diff *is* the bug shape rendered visible.
+
+## Why this matters / how to spot the pattern
+
+This is the same shape as any "literal-match in a template against a typed-but-non-canonical input field" bug. The signal is: a Go helper (here `primitiveKind`) that accepts multiple aliases for the same kind, used everywhere *except* the templates, which do their own literal compares instead.
+
+Two checks to apply on adjacent code:
+
+1. Grep template tree for `eq .Type "<literal>"` clauses. Each one is a candidate for the same bug if the source field can carry parser-canonical synonyms (`int`/`integer`, `float`/`number`, `bool`/`boolean`, `array`/`list`, etc.).
+2. When a normalizer helper exists in Go (`primitiveKind`, `oneOf`, `canonicalize…`), templates calling raw `.Field` instead of `(normalize .Field)` are smells. Push the normalization through a one-line FuncMap helper rather than duplicating the switch in template-language.
+
+The CLI surface stayed correct here because `cobraFlagFunc` and `goType` both call `primitiveKind` internally. The MCP template was the outlier — that's worth a project-wide grep next time someone changes a `Param.Type` consumer.
diff --git a/internal/generator/cursor_param_test.go b/internal/generator/cursor_param_test.go
index 67233be9..ed7ebebd 100644
--- a/internal/generator/cursor_param_test.go
+++ b/internal/generator/cursor_param_test.go
@@ -110,6 +110,35 @@ func TestCobraFlagFuncAcceptsSpecScalarAliases(t *testing.T) {
 	assert.Equal(t, "StringVar", cobraFlagFuncForParam("cursor", "integer"))
 }
 
+// TestMCPBindingFunc pins that OpenAPI-parsed shapes ("int", "float",
+// "bool") and internal-spec literals ("integer", "number", "boolean")
+// produce the same MCP binding.
+func TestMCPBindingFunc(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		typ  string
+		want string
+	}{
+		{"integer", "WithNumber"},
+		{"int", "WithNumber"},
+		{"number", "WithNumber"},
+		{"float", "WithNumber"},
+		{"boolean", "WithBoolean"},
+		{"bool", "WithBoolean"},
+		{"string", "WithString"},
+		{"", "WithString"},
+		{"object", "WithString"},
+		{"array", "WithString"},
+		{"INTEGER", "WithNumber"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.typ, func(t *testing.T) {
+			t.Parallel()
+			assert.Equal(t, tt.want, mcpBindingFunc(tt.typ))
+		})
+	}
+}
+
 func TestDefaultAndZeroValuesAcceptSpecScalarAliases(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 68d2e38e..3024e054 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -207,6 +207,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"goStoreType":           goStoreType,
 		"cobraFlagFunc":         cobraFlagFunc,
 		"cobraFlagFuncForParam": cobraFlagFuncForParam,
+		"mcpBindingFunc":        mcpBindingFunc,
 		"defaultVal":            defaultVal,
 		"defaultValForParam":    defaultValForParam,
 		"isConstDefault":        paramIsConstDefault,
@@ -3054,6 +3055,24 @@ func cobraFlagFunc(t string) string {
 	}
 }
 
+// mcpBindingFunc returns the mcplib.With* function name used in MCP tool
+// input-schema registration so numeric fields bind as JSON numbers (not
+// quoted strings) and pass through the generic makeAPIHandler intact.
+// Funnels through primitiveKind so OpenAPI-parsed shapes ("int", "float",
+// "bool") and internal-spec literals ("integer", "number", "boolean")
+// produce the same binding. Object/array bodies fall back to WithString
+// because they ride the --body-json fallback or a JSON-string flag.
+func mcpBindingFunc(t string) string {
+	switch primitiveKind(t) {
+	case "int", "float":
+		return "WithNumber"
+	case "bool":
+		return "WithBoolean"
+	default:
+		return "WithString"
+	}
+}
+
 // goTypeForParam returns the Go type for a parameter, overriding int→string
 // for ID-like parameters to avoid overflow and zero-value confusion,
 // numeric→string for pagination cursors so they survive scientific-notation
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 182469b8..afb3263d 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -9340,6 +9340,57 @@ func TestMCPHandlerPassesBodyArgsMap(t *testing.T) {
 		"PATCH branch must forward bodyArgs directly to the client")
 }
 
+// TestMCPBindingNumericTypesAcrossSpecShapes pins template wiring: both
+// OpenAPI-parsed shapes ("int", "bool") and internal-spec literals
+// ("integer", "boolean") flow through mcpBindingFunc to WithNumber /
+// WithBoolean across body and query surfaces.
+func TestMCPBindingNumericTypesAcrossSpecShapes(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("mcp-numeric-binding")
+	apiSpec.Resources["messages"] = spec.Resource{
+		Description: "Messages",
+		Endpoints: map[string]spec.Endpoint{
+			"openapi-shape": {
+				Method:      "POST",
+				Path:        "/messages",
+				Description: "Send a message (OpenAPI-parsed types)",
+				Params: []spec.Param{
+					{Name: "silent", Type: "bool", Description: "Suppress notifications"},
+				},
+				Body: []spec.Param{
+					{Name: "priority", Type: "int", Required: true, Description: "Priority level"},
+				},
+			},
+			"internal-shape": {
+				Method:      "POST",
+				Path:        "/messages/legacy",
+				Description: "Send a message (internal-spec types)",
+				Params: []spec.Param{
+					{Name: "enabled", Type: "boolean", Description: "Enabled"},
+				},
+				Body: []spec.Param{
+					{Name: "count", Type: "integer", Required: true, Description: "Count"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	mcpSource := readGeneratedFile(t, outputDir, "internal", "mcp", "tools.go")
+
+	assert.Contains(t, mcpSource, `mcplib.WithBoolean("silent"`,
+		"OpenAPI bool query param must bind WithBoolean")
+	assert.Contains(t, mcpSource, `mcplib.WithNumber("priority", mcplib.Required()`,
+		"OpenAPI int body field must bind WithNumber (Pushover trigger case)")
+	assert.Contains(t, mcpSource, `mcplib.WithBoolean("enabled"`,
+		"internal-spec boolean query param must bind WithBoolean")
+	assert.Contains(t, mcpSource, `mcplib.WithNumber("count", mcplib.Required()`,
+		"internal-spec integer body field must bind WithNumber")
+}
+
 func TestGeneratePublicParamNamesInPromotedExamples(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/generator/templates/mcp_intents.go.tmpl b/internal/generator/templates/mcp_intents.go.tmpl
index f26dadb7..8931a7e0 100644
--- a/internal/generator/templates/mcp_intents.go.tmpl
+++ b/internal/generator/templates/mcp_intents.go.tmpl
@@ -33,13 +33,7 @@ func RegisterIntents(s *server.MCPServer) {
 		mcplib.NewTool("{{.Name}}",
 			mcplib.WithDescription({{printf "%q" .Description}}),
 {{- range .Params}}
-{{- if eq .Type "integer"}}
-			mcplib.WithNumber("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" .Description}})),
-{{- else if eq .Type "boolean"}}
-			mcplib.WithBoolean("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" .Description}})),
-{{- else}}
-			mcplib.WithString("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" .Description}})),
-{{- end}}
+			mcplib.{{mcpBindingFunc .Type}}("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" .Description}})),
 {{- end}}
 		),
 		handle{{pascal .Name}},
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index eec0339d..46a507ec 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -47,22 +47,10 @@ func RegisterTools(s *server.MCPServer) {
 		mcplib.NewTool({{printf "%q" (printf "%s_%s" (snake $name) (snake $eName))}},
 			mcplib.WithDescription({{printf "%q" (composeMCPDesc $.APISpec $resource $endpoint $.MCPPublicCount $.MCPTotalCount)}}),
 {{- range $endpoint.Params}}
-{{- if eq .Type "integer"}}
-			mcplib.WithNumber({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else if eq .Type "boolean"}}
-			mcplib.WithBoolean({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else}}
-			mcplib.WithString({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- end}}
+			mcplib.{{mcpBindingFunc .Type}}({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
 {{- end}}
 {{- range $endpoint.Body}}
-{{- if eq .Type "integer"}}
-			mcplib.WithNumber({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else if eq .Type "boolean"}}
-			mcplib.WithBoolean({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else}}
-			mcplib.WithString({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- end}}
+			mcplib.{{mcpBindingFunc .Type}}({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
 {{- end}}
 {{- if $endpoint.BodyJSONFallback}}
 			mcplib.WithString("body_json"{{if $endpoint.BodyRequired}}, mcplib.Required(){{end}}, mcplib.Description("Request body as a JSON object string (this endpoint accepts a polymorphic schema: oneOf/anyOf)")),
@@ -94,22 +82,10 @@ func RegisterTools(s *server.MCPServer) {
 		mcplib.NewTool({{printf "%q" (printf "%s_%s_%s" (snake $name) (snake $subName) (snake $eName))}},
 			mcplib.WithDescription({{printf "%q" (composeMCPSubDesc $.APISpec $resource $subResource $endpoint $.MCPPublicCount $.MCPTotalCount)}}),
 {{- range $endpoint.Params}}
-{{- if eq .Type "integer"}}
-			mcplib.WithNumber({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else if eq .Type "boolean"}}
-			mcplib.WithBoolean({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else}}
-			mcplib.WithString({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- end}}
+			mcplib.{{mcpBindingFunc .Type}}({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
 {{- end}}
 {{- range $endpoint.Body}}
-{{- if eq .Type "integer"}}
-			mcplib.WithNumber({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else if eq .Type "boolean"}}
-			mcplib.WithBoolean({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- else}}
-			mcplib.WithString({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
-{{- end}}
+			mcplib.{{mcpBindingFunc .Type}}({{printf "%q" (mcpInputName .)}}{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description({{printf "%q" (mcpParamDesc .)}})),
 {{- end}}
 {{- if $endpoint.BodyJSONFallback}}
 			mcplib.WithString("body_json"{{if $endpoint.BodyRequired}}, mcplib.Required(){{end}}, mcplib.Description("Request body as a JSON object string (this endpoint accepts a polymorphic schema: oneOf/anyOf)")),
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 560ef840..67b50870 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
@@ -58,7 +58,7 @@ func RegisterTools(s *server.MCPServer) {
 		mcplib.NewTool("projects_list",
 			mcplib.WithDescription("List projects. Optional: status, limit (default: 25), cursor. Returns array of Project."),
 			mcplib.WithString("status", mcplib.Description("Status")),
-			mcplib.WithString("limit", mcplib.Description("Limit")),
+			mcplib.WithNumber("limit", mcplib.Description("Limit")),
 			mcplib.WithString("cursor", mcplib.Description("Cursor")),
 			mcplib.WithReadOnlyHintAnnotation(true),
 			mcplib.WithDestructiveHintAnnotation(false),
@@ -70,7 +70,7 @@ func RegisterTools(s *server.MCPServer) {
 		mcplib.NewTool("projects_avatar_upload-project",
 			mcplib.WithDescription("Upload project avatar. Required: projectId. Optional: overwrite, caption, file."),
 			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
-			mcplib.WithString("overwrite", mcplib.Description("Overwrite")),
+			mcplib.WithBoolean("overwrite", mcplib.Description("Overwrite")),
 			mcplib.WithString("caption", mcplib.Description("Caption")),
 			mcplib.WithString("file", mcplib.Description("File")),
 			mcplib.WithOpenWorldHintAnnotation(true),
@@ -82,7 +82,7 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDescription("List project tasks. Required: projectId. Optional: priority, limit (default: 50), cursor. Returns array of Task."),
 			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
 			mcplib.WithString("priority", mcplib.Description("Priority")),
-			mcplib.WithString("limit", mcplib.Description("Limit")),
+			mcplib.WithNumber("limit", mcplib.Description("Limit")),
 			mcplib.WithString("cursor", mcplib.Description("Cursor")),
 			mcplib.WithReadOnlyHintAnnotation(true),
 			mcplib.WithDestructiveHintAnnotation(false),
@@ -95,8 +95,8 @@ func RegisterTools(s *server.MCPServer) {
 			mcplib.WithDescription("Update project task. Required: projectId, taskId. Optional: notify, completed, priority (plus 1 more). Partial update."),
 			mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
 			mcplib.WithString("taskId", mcplib.Required(), mcplib.Description("Task id")),
-			mcplib.WithString("notify", mcplib.Description("Notify")),
-			mcplib.WithString("completed", mcplib.Description("Completed")),
+			mcplib.WithBoolean("notify", mcplib.Description("Notify")),
+			mcplib.WithBoolean("completed", mcplib.Description("Completed")),
 			mcplib.WithString("priority", mcplib.Description("Priority")),
 			mcplib.WithString("title", mcplib.Description("Title")),
 			mcplib.WithOpenWorldHintAnnotation(true),
@@ -115,7 +115,7 @@ func RegisterTools(s *server.MCPServer) {
 	s.AddTool(
 		mcplib.NewTool("reports_summary_get-report-year",
 			mcplib.WithDescription("Get a report summary for a year. Required: year."),
-			mcplib.WithString("year", mcplib.Required(), mcplib.Description("Year")),
+			mcplib.WithNumber("year", mcplib.Required(), mcplib.Description("Year")),
 			mcplib.WithReadOnlyHintAnnotation(true),
 			mcplib.WithDestructiveHintAnnotation(false),
 			mcplib.WithOpenWorldHintAnnotation(true),

← 725e063f chore(main): release 4.6.1 (#1363)  ·  back to Cli Printing Press  ·  test(cli): regression tests for body-flag identifier collisi d44587e9 →