← 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
M internal/generator/generator.goM internal/generator/generator_test.goA internal/generator/templates/mcp_code_orch.go.tmplM internal/generator/templates/mcp_tools.go.tmplM internal/spec/spec.goM internal/spec/spec_test.go
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 →