[object Object]

← back to Cli Printing Press

feat(cli): code-orchestration thin surface for large-surface APIs (#244)

a4207be8bec5cc7a4f32bf039a6ef2b3d2aa3e87 · 2026-04-22 21:02:35 -0700 · Matt Van Horn

Very large APIs (Cloudflare, Salesforce-360, anything past ~50 endpoints)
overflow context even with intent-grouped tools. Code-orchestration
collapses the whole surface into two MCP tools: <api>_search finds the
right endpoint by natural-language query, <api>_execute invokes it with a
params object. Pattern source: Anthropic's 2026-04-22 post, with Cloudflare
as the reference case (~2,500 endpoints in ~1K tokens).

Spec surface: mcp.orchestration: endpoint-mirror (default) | intent | code.
When code is set, the generator emits internal/mcp/code_orch.go with the
two tools + a generator-populated endpoint registry covering every endpoint
declared in the spec (including sub-resources). The endpoint-mirror tools
are fully suppressed in this mode; VisionSet and context tools still
register as before.

Also adds mcp.orchestration_threshold (default 50) so future auto-warnings
can flag large-surface specs that should consider code mode.

Plan: docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md (U3)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a4207be8bec5cc7a4f32bf039a6ef2b3d2aa3e87
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date:   Wed Apr 22 21:02:35 2026 -0700

    feat(cli): code-orchestration thin surface for large-surface APIs (#244)
    
    Very large APIs (Cloudflare, Salesforce-360, anything past ~50 endpoints)
    overflow context even with intent-grouped tools. Code-orchestration
    collapses the whole surface into two MCP tools: <api>_search finds the
    right endpoint by natural-language query, <api>_execute invokes it with a
    params object. Pattern source: Anthropic's 2026-04-22 post, with Cloudflare
    as the reference case (~2,500 endpoints in ~1K tokens).
    
    Spec surface: mcp.orchestration: endpoint-mirror (default) | intent | code.
    When code is set, the generator emits internal/mcp/code_orch.go with the
    two tools + a generator-populated endpoint registry covering every endpoint
    declared in the spec (including sub-resources). The endpoint-mirror tools
    are fully suppressed in this mode; VisionSet and context tools still
    register as before.
    
    Also adds mcp.orchestration_threshold (default 50) so future auto-warnings
    can flag large-surface specs that should consider code mode.
    
    Plan: docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md (U3)
    
    Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator.go                    |   5 +
 internal/generator/generator_test.go               |  67 ++++++
 internal/generator/templates/mcp_code_orch.go.tmpl | 237 +++++++++++++++++++++
 internal/generator/templates/mcp_tools.go.tmpl     |   6 +
 internal/spec/spec.go                              |  40 +++-
 internal/spec/spec_test.go                         |  53 +++++
 6 files changed, 404 insertions(+), 4 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 247919cc..9182abd7 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1180,6 +1180,11 @@ func (g *Generator) Generate() error {
 				return fmt.Errorf("rendering MCP intents: %w", err)
 			}
 		}
+		if g.Spec.MCP.IsCodeOrchestration() {
+			if err := g.renderTemplate("mcp_code_orch.go.tmpl", filepath.Join("internal", "mcp", "code_orch.go"), mcpData); err != nil {
+				return fmt.Errorf("rendering MCP code-orchestration: %w", err)
+			}
+		}
 	}
 
 	// Generate api discovery command when promoted commands exist (lets users browse hidden interfaces)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 733a17d0..8ec5f85a 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -2179,6 +2179,73 @@ func TestGenerateMCPMainRemoteOptIn(t *testing.T) {
 	}
 }
 
+// TestGenerateMCPCodeOrchestrationEmitsSearchExecute proves that when the
+// spec opts into code-orchestration, the generator emits only
+// <api>_search and <api>_execute as MCP tools, covering every endpoint via
+// a single registry. This is the thin surface pattern referenced by
+// Anthropic's 2026-04-22 post (Cloudflare's ~2,500-endpoint server in ~1K
+// tokens).
+func TestGenerateMCPCodeOrchestrationEmitsSearchExecute(t *testing.T) {
+	t.Parallel()
+
+	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "loops.yaml"))
+	require.NoError(t, err)
+	apiSpec.MCP = spec.MCPConfig{Orchestration: "code"}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	codeOrchPath := filepath.Join(outputDir, "internal", "mcp", "code_orch.go")
+	data, err := os.ReadFile(codeOrchPath)
+	require.NoError(t, err, "code_orch.go must be emitted when orchestration is code")
+	body := string(data)
+
+	for _, want := range []string{
+		`func RegisterCodeOrchestrationTools(`,
+		`mcplib.NewTool("loops_search"`,
+		`mcplib.NewTool("loops_execute"`,
+		`codeOrchEndpoints = []codeOrchEndpoint`,
+		`func handleCodeOrchSearch(`,
+		`func handleCodeOrchExecute(`,
+	} {
+		assert.Contains(t, body, want, "code_orch.go missing expected snippet %q", want)
+	}
+
+	toolsPath := filepath.Join(outputDir, "internal", "mcp", "tools.go")
+	toolsData, err := os.ReadFile(toolsPath)
+	require.NoError(t, err)
+	toolsBody := string(toolsData)
+	assert.Contains(t, toolsBody, "RegisterCodeOrchestrationTools(s)",
+		"code-orchestration RegisterTools must call RegisterCodeOrchestrationTools")
+	assert.NotContains(t, toolsBody, `mcplib.NewTool("contacts_list"`,
+		"endpoint-mirror tools must be fully suppressed in code-orch mode")
+
+	// End-to-end: the generated project must compile.
+	runGoCommand(t, outputDir, "mod", "tidy")
+	runGoCommand(t, outputDir, "build", "./...")
+}
+
+// TestGenerateMCPCodeOrchestrationSkippedByDefault guards against the
+// template accidentally emitting code_orch.go for specs that didn't opt in.
+// Small APIs should keep today's endpoint-mirror shape; the thin surface
+// costs a discovery round-trip the agent doesn't need when there are only
+// a handful of tools.
+func TestGenerateMCPCodeOrchestrationSkippedByDefault(t *testing.T) {
+	t.Parallel()
+
+	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "loops.yaml"))
+	require.NoError(t, err)
+	require.False(t, apiSpec.MCP.IsCodeOrchestration())
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	_, err = os.Stat(filepath.Join(outputDir, "internal", "mcp", "code_orch.go"))
+	assert.True(t, os.IsNotExist(err), "code_orch.go must not be emitted without orchestration: code")
+}
+
 // TestGenerateMCPIntentsEmittedWhenDeclared proves that a spec with mcp.intents
 // emits internal/mcp/intents.go, wires the intent handler into RegisterTools
 // via RegisterIntents, and keeps endpoint-mirror tools by default.
diff --git a/internal/generator/templates/mcp_code_orch.go.tmpl b/internal/generator/templates/mcp_code_orch.go.tmpl
new file mode 100644
index 00000000..a879ac1f
--- /dev/null
+++ b/internal/generator/templates/mcp_code_orch.go.tmpl
@@ -0,0 +1,237 @@
+// Copyright {{currentYear}} {{.Owner}}. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+// Package mcp — code-orchestration thin surface.
+//
+// Two tools cover the entire API: <api>_search to discover endpoints, and
+// <api>_execute to invoke one. This collapses a large API (50+ endpoints)
+// to ~1K tokens of tool definitions while preserving full coverage — the
+// agent writes the composition logic in its own sandbox.
+//
+// Pattern source: Anthropic 2026-04-22 "Building agents that reach
+// production systems with MCP" — Cloudflare's MCP server covers ~2,500
+// endpoints in roughly 1K tokens via the same search+execute shape.
+
+package mcp
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"sort"
+	"strings"
+
+	mcplib "github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+// RegisterCodeOrchestrationTools registers the two agent-facing tools that
+// cover the whole API surface. Called from RegisterTools in place of the
+// per-endpoint registrations when MCP.Orchestration is "code".
+func RegisterCodeOrchestrationTools(s *server.MCPServer) {
+	s.AddTool(
+		mcplib.NewTool("{{snake .Name}}_search",
+			mcplib.WithDescription("Search the {{.Name}} API for endpoints matching a natural-language query. Returns a ranked list of {endpoint_id, method, path, summary} entries. Call this first to find the endpoint to execute."),
+			mcplib.WithString("query", mcplib.Required(), mcplib.Description("Natural-language description of what you want to do.")),
+			mcplib.WithNumber("limit", mcplib.Description("Max endpoints to return (default 10).")),
+		),
+		handleCodeOrchSearch,
+	)
+
+	s.AddTool(
+		mcplib.NewTool("{{snake .Name}}_execute",
+			mcplib.WithDescription("Execute one {{.Name}} API endpoint by its endpoint_id (from {{snake .Name}}_search). Params are passed as a JSON object; path placeholders and query strings are resolved automatically."),
+			mcplib.WithString("endpoint_id", mcplib.Required(), mcplib.Description("Endpoint identifier returned by {{snake .Name}}_search (e.g., \"users.list\").")),
+			mcplib.WithObject("params", mcplib.Description("Parameters for the endpoint. Path placeholders match by name; remaining entries become query string on GET/DELETE or JSON body on POST/PUT/PATCH.")),
+		),
+		handleCodeOrchExecute,
+	)
+}
+
+// codeOrchEndpoint captures the small slice of endpoint metadata the
+// search+execute pair needs at runtime. `keywords` is a precomputed
+// lowercase stream of description + path tokens used for naive ranking;
+// anything more sophisticated belongs on the agent side.
+type codeOrchEndpoint struct {
+	ID          string
+	Method      string
+	Path        string
+	Summary     string
+	Positional  []string
+	keywords    []string
+}
+
+// codeOrchEndpoints is the generator-populated registry covering every
+// endpoint declared in the spec. Kept flat on purpose — the agent queries
+// via <api>_search, so hierarchy shows up as dotted IDs, not nested maps.
+var codeOrchEndpoints = []codeOrchEndpoint{
+{{- range $name, $resource := .Resources}}
+{{- range $eName, $endpoint := $resource.Endpoints}}
+	{
+		ID:      "{{$name}}.{{$eName}}",
+		Method:  "{{upper $endpoint.Method}}",
+		Path:    "{{$endpoint.Path}}",
+		Summary: {{printf "%q" (oneline $endpoint.Description)}},
+		Positional: []string{ {{- range $endpoint.Params}}{{if .Positional}}"{{.Name}}",{{end}}{{end}} },
+		keywords: codeOrchKeywords({{printf "%q" $name}}, {{printf "%q" $eName}}, {{printf "%q" (oneline $endpoint.Description)}}, {{printf "%q" $endpoint.Path}}),
+	},
+{{- end}}
+{{- range $subName, $subResource := $resource.SubResources}}
+{{- range $eName, $endpoint := $subResource.Endpoints}}
+	{
+		ID:      "{{$name}}.{{$subName}}.{{$eName}}",
+		Method:  "{{upper $endpoint.Method}}",
+		Path:    "{{$endpoint.Path}}",
+		Summary: {{printf "%q" (oneline $endpoint.Description)}},
+		Positional: []string{ {{- range $endpoint.Params}}{{if .Positional}}"{{.Name}}",{{end}}{{end}} },
+		keywords: codeOrchKeywords({{printf "%q" $name}}, {{printf "%q" $eName}}, {{printf "%q" (oneline $endpoint.Description)}}, {{printf "%q" $endpoint.Path}}),
+	},
+{{- end}}
+{{- end}}
+{{- end}}
+}
+
+// codeOrchKeywords produces the lowercase token stream used for search
+// ranking. Defined at package level so the registry initializer can call it
+// inline above without pulling in a separate precompute step.
+func codeOrchKeywords(resource, endpoint, summary, path string) []string {
+	raw := strings.ToLower(resource + " " + endpoint + " " + summary + " " + path)
+	raw = strings.Map(func(r rune) rune {
+		switch r {
+		case '_', '-', '/', '{', '}', '.', ',', ':', ';':
+			return ' '
+		}
+		return r
+	}, raw)
+	out := make([]string, 0, 8)
+	for _, tok := range strings.Fields(raw) {
+		if len(tok) < 2 {
+			continue
+		}
+		out = append(out, tok)
+	}
+	return out
+}
+
+func handleCodeOrchSearch(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+	args := req.GetArguments()
+	query, ok := args["query"].(string)
+	if !ok || strings.TrimSpace(query) == "" {
+		return mcplib.NewToolResultError("query is required"), nil
+	}
+	limit := 10
+	if v, ok := args["limit"].(float64); ok && v > 0 {
+		limit = int(v)
+	}
+
+	terms := codeOrchKeywords("", "", query, "")
+	type scored struct {
+		ep    *codeOrchEndpoint
+		score int
+	}
+	results := make([]scored, 0, len(codeOrchEndpoints))
+	for i := range codeOrchEndpoints {
+		ep := &codeOrchEndpoints[i]
+		score := 0
+		for _, t := range terms {
+			for _, kw := range ep.keywords {
+				if kw == t {
+					score += 2
+				} else if strings.Contains(kw, t) || strings.Contains(t, kw) {
+					score++
+				}
+			}
+		}
+		if score > 0 {
+			results = append(results, scored{ep: ep, score: score})
+		}
+	}
+	sort.SliceStable(results, func(i, j int) bool { return results[i].score > results[j].score })
+	if len(results) > limit {
+		results = results[:limit]
+	}
+
+	out := make([]map[string]any, 0, len(results))
+	for _, r := range results {
+		out = append(out, map[string]any{
+			"endpoint_id": r.ep.ID,
+			"method":      r.ep.Method,
+			"path":        r.ep.Path,
+			"summary":     r.ep.Summary,
+			"score":       r.score,
+		})
+	}
+	data, _ := json.Marshal(map[string]any{"count": len(out), "results": out})
+	return mcplib.NewToolResultText(string(data)), nil
+}
+
+func handleCodeOrchExecute(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+	args := req.GetArguments()
+	id, ok := args["endpoint_id"].(string)
+	if !ok || id == "" {
+		return mcplib.NewToolResultError("endpoint_id is required (call {{snake .Name}}_search first)"), nil
+	}
+
+	var ep *codeOrchEndpoint
+	for i := range codeOrchEndpoints {
+		if codeOrchEndpoints[i].ID == id {
+			ep = &codeOrchEndpoints[i]
+			break
+		}
+	}
+	if ep == nil {
+		return mcplib.NewToolResultError(fmt.Sprintf("unknown endpoint_id %q — call {{snake .Name}}_search to discover valid ids", id)), nil
+	}
+
+	params, _ := args["params"].(map[string]any)
+	if params == nil {
+		params = map[string]any{}
+	}
+
+	c, err := newMCPClient()
+	if err != nil {
+		return mcplib.NewToolResultError(err.Error()), nil
+	}
+
+	path := ep.Path
+	for _, p := range ep.Positional {
+		if v, ok := params[p]; ok {
+			path = strings.ReplaceAll(path, "{"+p+"}", fmt.Sprintf("%v", v))
+			delete(params, p)
+		}
+	}
+
+	query := map[string]string{}
+	if ep.Method == "GET" || ep.Method == "DELETE" {
+		for k, v := range params {
+			query[k] = fmt.Sprintf("%v", v)
+		}
+	}
+
+	var data json.RawMessage
+	switch ep.Method {
+	case "GET":
+		data, err = c.Get(path, query)
+	case "DELETE":
+		data, _, err = c.Delete(path)
+	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
+		}
+	}
+	if err != nil {
+		return mcplib.NewToolResultError(err.Error()), nil
+	}
+	return mcplib.NewToolResultText(string(data)), nil
+}
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index 47b2936d..51f74827 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -60,6 +60,11 @@ func sanitizeErrorBody(msg string) string {
 
 // RegisterTools registers all API operations as MCP tools.
 func RegisterTools(s *server.MCPServer) {
+{{- if .MCP.IsCodeOrchestration}}
+	// Code-orchestration mode — the full surface is covered by two tools
+	// (<api>_search + <api>_execute). Endpoint-mirror tools are suppressed.
+	RegisterCodeOrchestrationTools(s)
+{{- else}}
 {{- if ne .MCP.EndpointTools "hidden"}}
 {{- range $name, $resource := .Resources}}
 {{- range $eName, $endpoint := $resource.Endpoints}}
@@ -104,6 +109,7 @@ func RegisterTools(s *server.MCPServer) {
 	// Intent tools — higher-level compositions declared in the spec.
 	RegisterIntents(s)
 {{- end}}
+{{- end}}
 
 {{- if .VisionSet.Sync}}
 	// Sync tool — populates local database for offline search and sql queries
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 4a56c5ef..c08ca157 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -153,10 +153,12 @@ type ShareConfig struct {
 // ["stdio"] for backward compatibility. Unknown values are rejected at spec
 // load; this prevents silent drift when new transports are introduced.
 type MCPConfig struct {
-	Transport     []string `yaml:"transport,omitempty" json:"transport,omitempty"`           // allowed transports the generated binary compiles support for; empty == [stdio]. Runtime transport is chosen via the --transport flag and PP_MCP_TRANSPORT env.
-	Addr          string   `yaml:"addr,omitempty" json:"addr,omitempty"`                     // default bind address for the http transport (e.g., ":7777"). Blank means runtime default (":7777"). Ignored unless http is in Transport.
-	Intents       []Intent `yaml:"intents,omitempty" json:"intents,omitempty"`               // higher-level MCP tools that compose multiple endpoint calls. The agent sees one intent tool; the generator emits a handler that fans out to the declared endpoints sequentially. Anti-pattern to fight: one-tool-per-endpoint mirrors that force agents to stitch primitives.
-	EndpointTools string   `yaml:"endpoint_tools,omitempty" json:"endpoint_tools,omitempty"` // "visible" (default) keeps the per-endpoint MCP tools; "hidden" suppresses them so only intents + generator-emitted tools appear. Use "hidden" when intents fully cover the surface and raw endpoints would be noise.
+	Transport              []string `yaml:"transport,omitempty" json:"transport,omitempty"`                             // allowed transports the generated binary compiles support for; empty == [stdio]. Runtime transport is chosen via the --transport flag and PP_MCP_TRANSPORT env.
+	Addr                   string   `yaml:"addr,omitempty" json:"addr,omitempty"`                                       // default bind address for the http transport (e.g., ":7777"). Blank means runtime default (":7777"). Ignored unless http is in Transport.
+	Intents                []Intent `yaml:"intents,omitempty" json:"intents,omitempty"`                                 // higher-level MCP tools that compose multiple endpoint calls. The agent sees one intent tool; the generator emits a handler that fans out to the declared endpoints sequentially. Anti-pattern to fight: one-tool-per-endpoint mirrors that force agents to stitch primitives.
+	EndpointTools          string   `yaml:"endpoint_tools,omitempty" json:"endpoint_tools,omitempty"`                   // "visible" (default) keeps the per-endpoint MCP tools; "hidden" suppresses them so only intents + generator-emitted tools appear. Use "hidden" when intents fully cover the surface and raw endpoints would be noise.
+	Orchestration          string   `yaml:"orchestration,omitempty" json:"orchestration,omitempty"`                     // "endpoint-mirror" (default), "intent", or "code". Code-orchestration emits a thin <api>_search + <api>_execute pair covering the full surface in ~1K tokens; used for very large APIs where even intent-grouped tools would overflow context. Mutually exclusive with endpoint-mirror at emission time.
+	OrchestrationThreshold int      `yaml:"orchestration_threshold,omitempty" json:"orchestration_threshold,omitempty"` // endpoint count above which the generator warns that code-orchestration would be a better default. Zero means use the built-in default (50).
 }
 
 // Intent declares an MCP tool that composes multiple endpoint calls into a
@@ -619,9 +621,39 @@ func validateMCP(m MCPConfig, resources map[string]Resource) error {
 	if m.EndpointTools != "" && m.EndpointTools != "visible" && m.EndpointTools != "hidden" {
 		return fmt.Errorf("mcp.endpoint_tools: %q must be \"visible\" or \"hidden\"", m.EndpointTools)
 	}
+	switch m.Orchestration {
+	case "", "endpoint-mirror", "intent", "code":
+	default:
+		return fmt.Errorf("mcp.orchestration: %q must be one of endpoint-mirror, intent, code", m.Orchestration)
+	}
+	if m.OrchestrationThreshold < 0 {
+		return fmt.Errorf("mcp.orchestration_threshold: %d must be non-negative", m.OrchestrationThreshold)
+	}
 	return validateIntents(m.Intents, resources)
 }
 
+// DefaultOrchestrationThreshold is the endpoint-count above which the
+// generator recommends (but does not require) code-orchestration. At 50+
+// endpoints, even intent-grouped tools tend to overflow an agent's usable
+// context; code-orchestration covers the full surface in a pair of tools.
+const DefaultOrchestrationThreshold = 50
+
+// EffectiveOrchestrationThreshold returns the resolved threshold, applying
+// the built-in default when the spec leaves it unset.
+func (m MCPConfig) EffectiveOrchestrationThreshold() int {
+	if m.OrchestrationThreshold <= 0 {
+		return DefaultOrchestrationThreshold
+	}
+	return m.OrchestrationThreshold
+}
+
+// IsCodeOrchestration reports whether this MCP config opts into the
+// code-orchestration thin surface. Templates branch on this to emit only
+// <api>_search + <api>_execute instead of the endpoint-mirror.
+func (m MCPConfig) IsCodeOrchestration() bool {
+	return m.Orchestration == "code"
+}
+
 // intentNameRe enforces snake_case for MCP intent tool names so they line up
 // with the snake_case convention used for endpoint-mirror tool names.
 var intentNameRe = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go
index 55c406e1..bb0cfbe7 100644
--- a/internal/spec/spec_test.go
+++ b/internal/spec/spec_test.go
@@ -1229,6 +1229,59 @@ func TestMCPIntentsValidation(t *testing.T) {
 	}
 }
 
+func TestMCPOrchestrationValidation(t *testing.T) {
+	base := func(mcp MCPConfig) APISpec {
+		return APISpec{
+			Name:      "demo",
+			BaseURL:   "http://x",
+			Resources: map[string]Resource{"items": {Endpoints: map[string]Endpoint{"list": {Method: "GET", Path: "/items"}}}},
+			MCP:       mcp,
+		}
+	}
+
+	tests := []struct {
+		name    string
+		mcp     MCPConfig
+		wantErr string
+	}{
+		{
+			name:    "unknown orchestration rejected",
+			mcp:     MCPConfig{Orchestration: "magic"},
+			wantErr: "must be one of endpoint-mirror, intent, code",
+		},
+		{
+			name:    "negative threshold rejected",
+			mcp:     MCPConfig{OrchestrationThreshold: -1},
+			wantErr: "must be non-negative",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := base(tt.mcp)
+			err := s.Validate()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tt.wantErr)
+		})
+	}
+
+	// Happy paths for orchestration modes and threshold fallback.
+	ok := []MCPConfig{
+		{}, // absent = endpoint-mirror default
+		{Orchestration: "endpoint-mirror"},
+		{Orchestration: "intent"},
+		{Orchestration: "code"},
+		{Orchestration: "code", OrchestrationThreshold: 100},
+	}
+	for i, mcp := range ok {
+		s := base(mcp)
+		require.NoError(t, s.Validate(), "case %d", i)
+	}
+	assert.Equal(t, 50, MCPConfig{}.EffectiveOrchestrationThreshold())
+	assert.Equal(t, 100, MCPConfig{OrchestrationThreshold: 100}.EffectiveOrchestrationThreshold())
+	assert.True(t, MCPConfig{Orchestration: "code"}.IsCodeOrchestration())
+	assert.False(t, MCPConfig{Orchestration: "intent"}.IsCodeOrchestration())
+}
+
 func TestMCPConfigAcceptsValidShapes(t *testing.T) {
 	tests := []struct {
 		name string

← 85d4ace9 feat(cli): declare intent-grouped MCP tools in the spec (#24  ·  back to Cli Printing Press  ·  feat(cli): scorecard dimensions for remote transport, tool d 2d07a021 →