← 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
M internal/generator/generator.goA internal/generator/mcp_warning.goA internal/generator/mcp_warning_test.goM skills/printing-press/SKILL.md
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 →