[object Object]

← back to Cli Printing Press

feat(skills): pre-generation MCP enrichment for large surfaces (#522)

53be12097d48f1b22fd05aab43c90fa6b52df4e8 · 2026-05-02 23:30:18 -0700 · Trevin Chow

* fix(cli): wire up promised generate-time MCP enrichment warning

spec.MCPConfig.OrchestrationThreshold's doc comment promised the
generator would warn when a spec's endpoint surface exceeded the
threshold without opting into mcp.orchestration: code, but nothing
implemented it.

Add the warning at the top of Generate(). Walks spec.Resources +
SubResources once per generation; emits an enrichment recommendation
to stderr when typed-endpoint count > threshold and the spec isn't
already opted into code orchestration. Informational — does not gate
generation, fail validation, or alter rendered output.

Verified silent on every existing golden fixture (none exceed the
default 50-endpoint threshold). End-to-end smoke confirmed the warning
fires for a fabricated 60-endpoint spec and goes silent once
mcp.orchestration: code is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): add Pre-Generation MCP Enrichment to Phase 2

Parallel in shape to the existing Pre-Generation Auth Enrichment
section. Decision table by tool count (<30 skip / 30-50 ask / >50
default to the Cloudflare pattern: transport [stdio,http], orchestration
code, endpoint_tools hidden) plus the exact mcp: spec block to add
before running generate.

Closes a recurring failure mode where agents emit a default
endpoint-mirror MCP surface for medium-to-large APIs and discover at
polish time that the architectural scorecard dimensions
(mcp_remote_transport, mcp_tool_design, mcp_surface_strategy) are weak —
by which point fixing requires re-running generation, which polish does
not do.

Pairs with the generator's new stderr warning: the skill prompts the
agent before generate, the binary catches misses regardless of which
skill drove generation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 53be12097d48f1b22fd05aab43c90fa6b52df4e8
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 2 23:30:18 2026 -0700

    feat(skills): pre-generation MCP enrichment for large surfaces (#522)
    
    * fix(cli): wire up promised generate-time MCP enrichment warning
    
    spec.MCPConfig.OrchestrationThreshold's doc comment promised the
    generator would warn when a spec's endpoint surface exceeded the
    threshold without opting into mcp.orchestration: code, but nothing
    implemented it.
    
    Add the warning at the top of Generate(). Walks spec.Resources +
    SubResources once per generation; emits an enrichment recommendation
    to stderr when typed-endpoint count > threshold and the spec isn't
    already opted into code orchestration. Informational — does not gate
    generation, fail validation, or alter rendered output.
    
    Verified silent on every existing golden fixture (none exceed the
    default 50-endpoint threshold). End-to-end smoke confirmed the warning
    fires for a fabricated 60-endpoint spec and goes silent once
    mcp.orchestration: code is set.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * feat(skills): add Pre-Generation MCP Enrichment to Phase 2
    
    Parallel in shape to the existing Pre-Generation Auth Enrichment
    section. Decision table by tool count (<30 skip / 30-50 ask / >50
    default to the Cloudflare pattern: transport [stdio,http], orchestration
    code, endpoint_tools hidden) plus the exact mcp: spec block to add
    before running generate.
    
    Closes a recurring failure mode where agents emit a default
    endpoint-mirror MCP surface for medium-to-large APIs and discover at
    polish time that the architectural scorecard dimensions
    (mcp_remote_transport, mcp_tool_design, mcp_surface_strategy) are weak —
    by which point fixing requires re-running generation, which polish does
    not do.
    
    Pairs with the generator's new stderr warning: the skill prompts the
    agent before generate, the binary catches misses regardless of which
    skill drove generation.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator.go        |   1 +
 internal/generator/mcp_warning.go      |  50 +++++++++++++
 internal/generator/mcp_warning_test.go | 131 +++++++++++++++++++++++++++++++++
 skills/printing-press/SKILL.md         |  73 ++++++++++++++++++
 4 files changed, 255 insertions(+)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index f9f2dcc2..4b949293 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1356,6 +1356,7 @@ func (g *Generator) renderOptionalSupportFiles() error {
 }
 
 func (g *Generator) Generate() error {
+	warnUnenrichedLargeMCPSurface(g.Spec, os.Stderr)
 	if err := g.prepareOutput(); err != nil {
 		return err
 	}
diff --git a/internal/generator/mcp_warning.go b/internal/generator/mcp_warning.go
new file mode 100644
index 00000000..07438b11
--- /dev/null
+++ b/internal/generator/mcp_warning.go
@@ -0,0 +1,50 @@
+package generator
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/mvanhorn/cli-printing-press/v3/internal/spec"
+)
+
+const largeMCPSurfaceWarning = `warning: spec exposes %d MCP endpoint tools (>%d threshold). The default
+         endpoint-mirror surface burns agent context at this scale and will
+         score poorly on the scorecard's MCP architectural dimensions. Consider
+         enriching the spec's mcp: block before generation:
+           mcp:
+             transport: [stdio, http]    # remote-capable; reaches hosted agents
+             orchestration: code         # thin <api>_search + <api>_execute pair
+             endpoint_tools: hidden      # suppress raw per-endpoint mirrors
+         See docs/SPEC-EXTENSIONS.md for the full mcp: schema.
+`
+
+// warnUnenrichedLargeMCPSurface honors the contract on
+// spec.MCPConfig.OrchestrationThreshold: when the typed-endpoint surface
+// exceeds the effective threshold and the spec hasn't opted into code
+// orchestration, recommend the enrichment pattern. Informational only —
+// does not gate generation or alter rendered output.
+func warnUnenrichedLargeMCPSurface(s *spec.APISpec, w io.Writer) {
+	if s == nil {
+		return
+	}
+	threshold := s.MCP.EffectiveOrchestrationThreshold()
+	total := countTypedEndpoints(s)
+	if total <= threshold || s.MCP.IsCodeOrchestration() {
+		return
+	}
+	fmt.Fprintf(w, largeMCPSurfaceWarning, total, threshold)
+}
+
+func countTypedEndpoints(s *spec.APISpec) int {
+	if s == nil {
+		return 0
+	}
+	n := 0
+	for _, r := range s.Resources {
+		n += len(r.Endpoints)
+		for _, sub := range r.SubResources {
+			n += len(sub.Endpoints)
+		}
+	}
+	return n
+}
diff --git a/internal/generator/mcp_warning_test.go b/internal/generator/mcp_warning_test.go
new file mode 100644
index 00000000..9e2cc143
--- /dev/null
+++ b/internal/generator/mcp_warning_test.go
@@ -0,0 +1,131 @@
+package generator
+
+import (
+	"bytes"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v3/internal/spec"
+)
+
+func TestWarnUnenrichedLargeMCPSurface(t *testing.T) {
+	tests := []struct {
+		name           string
+		spec           *spec.APISpec
+		wantWarning    bool
+		wantContaining string
+	}{
+		{
+			name:        "nil spec emits no warning",
+			spec:        nil,
+			wantWarning: false,
+		},
+		{
+			name:        "small surface emits no warning",
+			spec:        buildSpecWithEndpoints(10, spec.MCPConfig{}),
+			wantWarning: false,
+		},
+		{
+			name:        "exactly-at-threshold emits no warning",
+			spec:        buildSpecWithEndpoints(50, spec.MCPConfig{}),
+			wantWarning: false,
+		},
+		{
+			name:           "above-threshold endpoint-mirror surface warns",
+			spec:           buildSpecWithEndpoints(60, spec.MCPConfig{}),
+			wantWarning:    true,
+			wantContaining: "warning: spec exposes 60 MCP endpoint tools (>50",
+		},
+		{
+			name: "above-threshold but already opted into code orchestration is silent",
+			spec: buildSpecWithEndpoints(60, spec.MCPConfig{
+				Orchestration: "code",
+			}),
+			wantWarning: false,
+		},
+		{
+			name: "above-threshold with intent-only enrichment still warns (intents do not solve scale)",
+			spec: buildSpecWithEndpoints(60, spec.MCPConfig{
+				Intents: []spec.Intent{{Name: "lookup", Description: "x"}},
+			}),
+			wantWarning:    true,
+			wantContaining: "endpoint-mirror surface burns agent context",
+		},
+		{
+			name: "custom orchestration_threshold respected (raised above surface = silent)",
+			spec: buildSpecWithEndpoints(60, spec.MCPConfig{
+				OrchestrationThreshold: 100,
+			}),
+			wantWarning: false,
+		},
+		{
+			name: "custom orchestration_threshold respected (lowered below surface = warns)",
+			spec: buildSpecWithEndpoints(20, spec.MCPConfig{
+				OrchestrationThreshold: 10,
+			}),
+			wantWarning:    true,
+			wantContaining: "warning: spec exposes 20 MCP endpoint tools (>10",
+		},
+		{
+			name:           "sub-resource endpoints counted toward the total",
+			spec:           buildSpecWithSubResourceEndpoints(30, 30),
+			wantWarning:    true,
+			wantContaining: "warning: spec exposes 60 MCP endpoint tools",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			warnUnenrichedLargeMCPSurface(tt.spec, &buf)
+			got := buf.String()
+			if tt.wantWarning {
+				if got == "" {
+					t.Fatalf("expected a warning, got none")
+				}
+				if !strings.Contains(got, tt.wantContaining) {
+					t.Errorf("warning missing expected substring %q\nfull output:\n%s", tt.wantContaining, got)
+				}
+			} else {
+				if got != "" {
+					t.Errorf("expected no warning, got:\n%s", got)
+				}
+			}
+		})
+	}
+}
+
+func buildSpecWithEndpoints(n int, mcp spec.MCPConfig) *spec.APISpec {
+	endpoints := make(map[string]spec.Endpoint, n)
+	for i := range n {
+		endpoints["ep_"+strconv.Itoa(i)] = spec.Endpoint{Method: "GET", Path: "/x"}
+	}
+	return &spec.APISpec{
+		Resources: map[string]spec.Resource{
+			"items": {Endpoints: endpoints},
+		},
+		MCP: mcp,
+	}
+}
+
+func buildSpecWithSubResourceEndpoints(top, sub int) *spec.APISpec {
+	topEndpoints := make(map[string]spec.Endpoint, top)
+	for i := range top {
+		topEndpoints["ep_"+strconv.Itoa(i)] = spec.Endpoint{Method: "GET", Path: "/x"}
+	}
+	subEndpoints := make(map[string]spec.Endpoint, sub)
+	for i := range sub {
+		subEndpoints["ep_"+strconv.Itoa(i)] = spec.Endpoint{Method: "GET", Path: "/y"}
+	}
+	return &spec.APISpec{
+		Resources: map[string]spec.Resource{
+			"items": {
+				Endpoints: topEndpoints,
+				SubResources: map[string]spec.Resource{
+					"children": {Endpoints: subEndpoints},
+				},
+			},
+		},
+	}
+}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 1463dd4c..1e11724a 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1536,6 +1536,79 @@ resources:
         # no_auth defaults to false — placing an order needs auth
 ```
 
+### Pre-Generation MCP Enrichment
+
+Before generating, count the spec's MCP tool surface and decide whether to opt
+into the spec's `mcp:` enrichment fields. This matters most for medium-to-large
+APIs (>30 tools) where the default endpoint-mirror surface scores poorly on the
+scorecard's MCP architectural dimensions and burns agent context at runtime.
+
+**Why before generation, not after:** the generator emits the MCP server's
+`main.go`, `tools.go`, `intents.go`, `code_orch.go`, `tools-manifest.json`, and
+README MCP section from the spec at generate-time. Patching after generation
+fragments across 4+ files, won't be byte-identical, and the polish skill cannot
+fix it (polish doesn't re-run generation). Enriching the spec means every
+template emits the right surface from the start.
+
+**Count the tool surface.** Two parts:
+
+1. **Typed endpoints** — count `endpoints` across all `resources` (and
+   `sub_resources`) in the spec. These become per-endpoint MCP tools at
+   generate-time.
+2. **Cobratree-walked tools** — the runtime walker registers user-facing Cobra
+   commands as MCP tools. Estimate as: `extra_commands` count + ~13 framework
+   tools that ship by default (sql, search, context, sync, stale, doctor,
+   reconcile, etc., minus framework-skipped). When novel features are planned,
+   add their estimated command count.
+
+The total is what an agent loads at MCP server start.
+
+**Decision table:**
+
+| Total tools | Action |
+|-------------|--------|
+| <30 | Skip — default endpoint-mirror surface is fine. |
+| 30–50 | Ask the user. Suggest `mcp.transport: [stdio, http]` for remote reach; suggest `mcp.intents` if there are clear multi-step workflows. |
+| >50 | Default to recommending the Cloudflare pattern (transport + code orchestration + hidden endpoint tools). The generator will also print a warning at this size. |
+
+**The Cloudflare pattern** (recommended for large surfaces) — edit the spec's
+`mcp:` block before running `generate`:
+
+```yaml
+mcp:
+  transport: [stdio, http]    # remote-capable; reaches hosted agents
+  orchestration: code         # thin <api>_search + <api>_execute pair
+  endpoint_tools: hidden      # suppress raw per-endpoint mirrors
+  intents:                    # optional; named multi-step intents
+    - name: <intent_name>
+      description: <agent-facing intent description>
+      params: [...]
+      steps: [...]
+```
+
+`mcp.transport: [stdio, http]` adds HTTP streamable transport so cloud-hosted
+agents (Managed Agents, web clients) can connect. `mcp.orchestration: code`
+emits the thin search+execute pair that covers the full surface in ~1K tokens.
+`mcp.endpoint_tools: hidden` removes the raw per-endpoint tools that would
+otherwise still show up alongside the orchestration pair.
+
+**Smaller-surface variants:**
+
+- Just want remote reach? `mcp.transport: [stdio, http]` alone is fine.
+- Have 3–5 obvious multi-step workflows but <50 endpoints? Add `mcp.intents`
+  without code orchestration; leave `endpoint_tools` at default (visible).
+
+**When to skip entirely:** small APIs (<30 tools), one-shot specs that won't
+be installed as MCP servers, or APIs where the user explicitly opts out of MCP
+enrichment.
+
+**Verifying after generation:** the scorecard's `mcp_remote_transport`,
+`mcp_tool_design`, and `mcp_surface_strategy` dimensions reflect the choices
+above. A correctly enriched spec for a >50 tool API should score 10/10 on all
+three. If polish later reports these dims weak, that's a sign this enrichment
+step was skipped — re-run generation with the enriched spec rather than
+trying to fix it in polish.
+
 ### Lock and Generate
 
 Before running any generate command, acquire the build lock:

← d5594764 chore(main): release 3.6.2 (#520)  ·  back to Cli Printing Press  ·  feat(skills): retro defaults to don't-file; adversarial tria 3c04a065 →