[object Object]

← back to Cli Printing Press

fix(cli): drop MCP code-orch handler pre-marshal that base64-encoded mutating bodies (#1521)

ff562375ba7aaf413707b14d6debb0dd7bd11388 · 2026-05-16 11:45:53 -0700 · Trevin Chow

Sibling fix to #1357. The code-orchestration MCP handler template
(mcp_code_orch.go.tmpl) had the same defect the endpoint-mirror template
already fixed: POST/PUT/PATCH branches json.Marshaled `params` into a
[]byte, then handed the []byte to c.Post/Put/Patch as the body. The
generated client.do() then json.Marshaled the []byte again, which
encoding/json base64-encodes into a quoted string. Strict APIs reject
the payload with errors like 'Request data must be a JSON object, not a
string'.

Pass `params` (map[string]any) directly to c.Post/Put/Patch and let
client.do() do the single marshal pass. The intermediate marshal-error
branch is removed; client.do() already wraps any marshal error with the
same "marshaling body" prefix (client.go.tmpl:878).

Add TestMCPCodeOrchPassesParamsMap mirroring TestMCPHandlerPassesBodyArgsMap
to pin the orchestrator emission shape: no json.Marshal(params)
intermediate, and the literal forms c.Post(path, params),
c.Put(path, params), c.Patch(path, params).

Golden fixture mcp-cloudflare/internal/mcp/code_orch.go updated in
lockstep per AGENTS.md "Generator Output Stability".

Closes #1426.

Files touched

Diff

commit ff562375ba7aaf413707b14d6debb0dd7bd11388
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 16 11:45:53 2026 -0700

    fix(cli): drop MCP code-orch handler pre-marshal that base64-encoded mutating bodies (#1521)
    
    Sibling fix to #1357. The code-orchestration MCP handler template
    (mcp_code_orch.go.tmpl) had the same defect the endpoint-mirror template
    already fixed: POST/PUT/PATCH branches json.Marshaled `params` into a
    []byte, then handed the []byte to c.Post/Put/Patch as the body. The
    generated client.do() then json.Marshaled the []byte again, which
    encoding/json base64-encodes into a quoted string. Strict APIs reject
    the payload with errors like 'Request data must be a JSON object, not a
    string'.
    
    Pass `params` (map[string]any) directly to c.Post/Put/Patch and let
    client.do() do the single marshal pass. The intermediate marshal-error
    branch is removed; client.do() already wraps any marshal error with the
    same "marshaling body" prefix (client.go.tmpl:878).
    
    Add TestMCPCodeOrchPassesParamsMap mirroring TestMCPHandlerPassesBodyArgsMap
    to pin the orchestrator emission shape: no json.Marshal(params)
    intermediate, and the literal forms c.Post(path, params),
    c.Put(path, params), c.Patch(path, params).
    
    Golden fixture mcp-cloudflare/internal/mcp/code_orch.go updated in
    lockstep per AGENTS.md "Generator Output Stability".
    
    Closes #1426.
---
 internal/generator/generator_test.go               | 63 ++++++++++++++++++++++
 internal/generator/templates/mcp_code_orch.go.tmpl | 21 +++-----
 .../mcp-cloudflare/internal/mcp/code_orch.go       | 21 +++-----
 3 files changed, 77 insertions(+), 28 deletions(-)

diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index e7335a11..7750aff1 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -10056,6 +10056,69 @@ func TestMCPHandlerPassesBodyArgsMap(t *testing.T) {
 		"PATCH branch must forward bodyArgs directly to the client")
 }
 
+// TestMCPCodeOrchPassesParamsMap pins the same body-passthrough contract for
+// the code-orchestration handler template (mcp_code_orch.go.tmpl). Sibling of
+// TestMCPHandlerPassesBodyArgsMap: an earlier pre-marshal stored []byte in the
+// body slot, which client.do() then json.Marshaled into a base64-encoded
+// string that strict APIs reject.
+func TestMCPCodeOrchPassesParamsMap(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("mcp-orch-body-passthrough")
+	apiSpec.MCP = spec.MCPConfig{Orchestration: "code", EndpointTools: "hidden"}
+	apiSpec.Resources["widgets"] = spec.Resource{
+		Description: "Widgets",
+		Endpoints: map[string]spec.Endpoint{
+			"create": {
+				Method:      "POST",
+				Path:        "/widgets",
+				Description: "Create a widget",
+				Body: []spec.Param{
+					{Name: "label", Type: "string", Required: true, Description: "Widget label"},
+				},
+			},
+			"replace": {
+				Method:      "PUT",
+				Path:        "/widgets/{id}",
+				Description: "Replace a widget",
+				Params: []spec.Param{
+					{Name: "id", Type: "string", Required: true, Description: "Widget id"},
+				},
+				Body: []spec.Param{
+					{Name: "label", Type: "string", Required: true, Description: "Widget label"},
+				},
+			},
+			"update": {
+				Method:      "PATCH",
+				Path:        "/widgets/{id}",
+				Description: "Update a widget",
+				Params: []spec.Param{
+					{Name: "id", Type: "string", Required: true, Description: "Widget id"},
+				},
+				Body: []spec.Param{
+					{Name: "label", Type: "string", Required: true, Description: "Widget label"},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	mcpSource := readGeneratedFile(t, outputDir, "internal", "mcp", "code_orch.go")
+
+	assert.NotContains(t, mcpSource, "json.Marshal(params)",
+		"code-orch handler must not pre-marshal params: client.do() json.Marshals what it receives, "+
+			"so a []byte arrives base64-encoded on the wire and strict APIs reject the payload")
+
+	assert.Contains(t, mcpSource, "c.Post(path, params)",
+		"POST branch must forward params directly to the client")
+	assert.Contains(t, mcpSource, "c.Put(path, params)",
+		"PUT branch must forward params directly to the client")
+	assert.Contains(t, mcpSource, "c.Patch(path, params)",
+		"PATCH branch must forward params 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 /
diff --git a/internal/generator/templates/mcp_code_orch.go.tmpl b/internal/generator/templates/mcp_code_orch.go.tmpl
index d334784b..d6444ba1 100644
--- a/internal/generator/templates/mcp_code_orch.go.tmpl
+++ b/internal/generator/templates/mcp_code_orch.go.tmpl
@@ -249,21 +249,14 @@ func handleCodeOrchExecute(ctx context.Context, req mcplib.CallToolRequest) (*mc
 		data, err = c.Get(path, query)
 	case "DELETE":
 		data, _, err = c.DeleteWithParams(path, query)
+	case "POST":
+		data, _, err = c.Post(path, params)
+	case "PUT":
+		data, _, err = c.Put(path, params)
+	case "PATCH":
+		data, _, err = c.Patch(path, params)
 	default:
-		body, mErr := json.Marshal(params)
-		if mErr != nil {
-			return mcplib.NewToolResultError(fmt.Sprintf("marshaling body: %v", mErr)), nil
-		}
-		switch ep.Method {
-		case "POST":
-			data, _, err = c.Post(path, body)
-		case "PUT":
-			data, _, err = c.Put(path, body)
-		case "PATCH":
-			data, _, err = c.Patch(path, body)
-		default:
-			return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
-		}
+		return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
 	}
 	if err != nil {
 		return mcplib.NewToolResultError(err.Error()), nil
diff --git a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
index a67ac1ef..07aa5132 100644
--- a/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
+++ b/testdata/golden/expected/generate-mcp-api/mcp-cloudflare/internal/mcp/code_orch.go
@@ -219,21 +219,14 @@ func handleCodeOrchExecute(ctx context.Context, req mcplib.CallToolRequest) (*mc
 		data, err = c.Get(path, query)
 	case "DELETE":
 		data, _, err = c.DeleteWithParams(path, query)
+	case "POST":
+		data, _, err = c.Post(path, params)
+	case "PUT":
+		data, _, err = c.Put(path, params)
+	case "PATCH":
+		data, _, err = c.Patch(path, params)
 	default:
-		body, mErr := json.Marshal(params)
-		if mErr != nil {
-			return mcplib.NewToolResultError(fmt.Sprintf("marshaling body: %v", mErr)), nil
-		}
-		switch ep.Method {
-		case "POST":
-			data, _, err = c.Post(path, body)
-		case "PUT":
-			data, _, err = c.Put(path, body)
-		case "PATCH":
-			data, _, err = c.Patch(path, body)
-		default:
-			return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
-		}
+		return mcplib.NewToolResultError(fmt.Sprintf("unsupported method %q", ep.Method)), nil
 	}
 	if err != nil {
 		return mcplib.NewToolResultError(err.Error()), nil

← 95417401 fix(cli): filter auth login --chrome cookies by spec auth.co  ·  back to Cli Printing Press  ·  fix(cli): detect integer page paginator and advance numerica 0c3c8bcc →