← back to Cli Printing Press
feat(cli): mcp-audit subcommand + docs for the new MCP surface (#246)
38db061099b28c1d7dd307e0a3aedb83bb1601d1 · 2026-04-22 21:14:49 -0700 · Matt Van Horn
Adds printing-press mcp-audit, a diagnostic subcommand that walks every
CLI under ~/printing-press/library/ and reports transport, tool design,
endpoint count, and a one-line recommendation for each. Recommendations
map directly to the mcp: spec fields introduced in U1-U3 so the operator
can turn audit output straight into a spec change.
Output is a human-readable table by default; --json for machine-readable
output (suitable for emboss/regenerate loops). Exit 0 regardless of
findings — diagnostic, not gating.
Also documents the new spec surface in README.md, AGENTS.md glossary, and
docs/PIPELINE.md's review-phase gate description.
Plan: docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md (U5)
Closes the plan — all 5 units shipped.
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 AGENTS.mdM README.mdM docs/PIPELINE.mdA internal/cli/mcp_audit.goA internal/cli/mcp_audit_test.goM internal/cli/root.go
Diff
commit 38db061099b28c1d7dd307e0a3aedb83bb1601d1
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date: Wed Apr 22 21:14:49 2026 -0700
feat(cli): mcp-audit subcommand + docs for the new MCP surface (#246)
Adds printing-press mcp-audit, a diagnostic subcommand that walks every
CLI under ~/printing-press/library/ and reports transport, tool design,
endpoint count, and a one-line recommendation for each. Recommendations
map directly to the mcp: spec fields introduced in U1-U3 so the operator
can turn audit output straight into a spec change.
Output is a human-readable table by default; --json for machine-readable
output (suitable for emboss/regenerate loops). Exit 0 regardless of
findings — diagnostic, not gating.
Also documents the new spec surface in README.md, AGENTS.md glossary, and
docs/PIPELINE.md's review-phase gate description.
Plan: docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md (U5)
Closes the plan — all 5 units shipped.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
AGENTS.md | 2 +
README.md | 27 +++++
docs/PIPELINE.md | 2 +-
internal/cli/mcp_audit.go | 225 +++++++++++++++++++++++++++++++++++++++++
internal/cli/mcp_audit_test.go | 133 ++++++++++++++++++++++++
internal/cli/root.go | 1 +
6 files changed, 389 insertions(+), 1 deletion(-)
diff --git a/AGENTS.md b/AGENTS.md
index f999970f..ad7a628f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -92,6 +92,8 @@ Key terms used throughout this repo. Several have overloaded meanings — the gl
| **scorecard** / **scoring** | Two-tier quality assessment with a 50/50 weighted composite. Tier 1: infrastructure (16 string-matching dimensions, raw max 160, normalized to 0-50). Tier 2: domain correctness (7 semantic dimensions, raw max 60 when live verify ran, normalized to 0-50). Total /100 with letter grades. Source of truth: `internal/pipeline/scorecard.go` (tier1Max / tier2Max). Subcommand: `printing-press scorecard`. |
| **doctor** | Self-diagnostic command shipped inside every printed CLI for end-users to run. Checks environment, auth config, and connectivity at the user's runtime. Unlike dogfood (which validates at generation time), doctor runs post-install. |
| **auth doctor** | Subcommand on the printing-press binary (`printing-press auth doctor`). Scans every installed printed CLI's `tools-manifest.json` under `~/printing-press/library/<api>/` and reports env-var status (ok / suspicious / not_set / no_auth / unknown) with redacted fingerprints. Diagnostic only — never gates, never probes the network. Lives in `internal/authdoctor/`. |
+| **mcp-audit** | Subcommand on the printing-press binary (`printing-press mcp-audit`). Walks every library CLI and reports transport, tool-design, and per-CLI recommendations for the `mcp:` spec surface introduced in the U1-U3 machine work (remote transport, intent tools, code-orchestration). Diagnostic only — exit 0 regardless of findings. Supports `--json` for machine-readable output. |
+| **mcp spec surface** | Opt-in fields on the spec's `mcp:` block introduced April 2026 to reach production agent-hosts: `transport: [stdio, http]` (remote-capable via streamable HTTP), `intents:` (multi-step composed MCP tools), `orchestration: code` (Cloudflare-style thin `<api>_search` + `<api>_execute` surface for 50+ endpoint APIs), `endpoint_tools: hidden` (suppress raw per-endpoint tools). Empty `mcp:` keeps today's stdio-only endpoint-mirror emission byte-compatible. |
| **local library** | `~/printing-press/library/<api-slug>/` — where printed CLIs land after a successful run. Directory is keyed by API slug (e.g., `notion`), not CLI name. Local directory, not a git repo. |
| **public library repo** | The GitHub repo [`mvanhorn/printing-press-library`](https://github.com/mvanhorn/printing-press-library) — public catalog of finished CLIs organized by category. `/printing-press-publish` pushes here. |
| **publish (pipeline)** | The pipeline step that moves a working CLI into the local library and writes the `.printing-press.json` provenance manifest. |
diff --git a/README.md b/README.md
index 61a41194..13a265a4 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,33 @@ One spec --> printing-press generate --> <api>-pp-cli (cobra) + <api>-pp-m
Every API that gets a CLI+MCP becomes instantly accessible to every AI coding tool. The printing press is the factory.
+### MCP spec surface (U1-U3)
+
+The generator ships three opt-in knobs on the spec's `mcp:` block, aligned with Anthropic's [2026-04-22 MCP production-agents guidance](https://www.anthropic.com/news/building-agents-that-reach-production-systems-with-mcp):
+
+```yaml
+mcp:
+ transport: [stdio, http] # U1 — remote-capable for cloud-hosted agents; default [stdio]
+ addr: ":7777" # U1 — default bind for the http transport
+ orchestration: code # U3 — "endpoint-mirror" (default), "intent", or "code"
+ endpoint_tools: hidden # U2 — suppress raw endpoint tools when intents cover the surface
+ intents: # U2 — compose multi-step tools declaratively
+ - name: create_issue_from_thread
+ description: "Create an issue from a Slack thread."
+ params:
+ - { name: thread_id, type: string, required: true, description: "slack thread id" }
+ steps:
+ - endpoint: messages.get_thread
+ bind: { thread_id: "${input.thread_id}" }
+ capture: thread
+ - endpoint: issues.create
+ bind: { title: "${thread.subject}", description: "${thread.body}" }
+ capture: issue
+ returns: issue
+```
+
+Run `printing-press mcp-audit` after changes to see which library CLIs would benefit from the new surface.
+
## Domain Archetypes
The profiler classifies every API into a domain archetype and auto-generates the right workflow + insight commands:
diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md
index 7a46b7c1..6db5e4f5 100644
--- a/docs/PIPELINE.md
+++ b/docs/PIPELINE.md
@@ -164,7 +164,7 @@ Outputs:
Gates:
- Dogfood: structural checks (path validity, auth, dead flags, wiring) pass at the configured tier
- Verify: runtime behavioral checks against the real API or a mock server return PASS (or WARN after an auto-remediation pass)
-- Scorecard: overall grade clears the operator's configured threshold
+- Scorecard: overall grade clears the operator's configured threshold. Tier-1 dimensions include three MCP-shape dimensions (`mcp_remote_transport`, `mcp_tool_design`, `mcp_surface_strategy`) added in the April 2026 MCP production-readiness pass — opt-in via the `(score, bool)` pattern so CLIs without the relevant surface remain unscored.
Artifacts:
- `pipeline/dogfood-results.json`
diff --git a/internal/cli/mcp_audit.go b/internal/cli/mcp_audit.go
new file mode 100644
index 00000000..482ce56c
--- /dev/null
+++ b/internal/cli/mcp_audit.go
@@ -0,0 +1,225 @@
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+// newMCPAuditCmd reports the MCP surface strategy for every CLI in the
+// local library, so the operator can see at a glance which printed CLIs
+// are ready for production agents and which would benefit from a
+// regenerate under the new spec surface (remote transport, intent tools,
+// code-orchestration).
+//
+// Diagnostic only — never exits non-zero for findings. Use the scorecard
+// (gated by the ship pipeline) when you need a gating signal.
+func newMCPAuditCmd() *cobra.Command {
+ var asJSON bool
+ var libraryPath string
+
+ cmd := &cobra.Command{
+ Use: "mcp-audit",
+ Short: "Report MCP surface shape for every installed printed CLI",
+ Long: `Walks each CLI under ~/printing-press/library/<api>/ and reports the
+current MCP surface strategy — transport, tool design, whether the shape
+matches the API's size. Useful after a machine change to see which CLIs
+would benefit from a regenerate.
+
+Diagnostic only. Exit 0 regardless of findings.`,
+ Example: ` printing-press mcp-audit
+ printing-press mcp-audit --library ~/printing-press/library --json`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ path := libraryPath
+ if path == "" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return fmt.Errorf("resolving home: %w", err)
+ }
+ path = filepath.Join(home, "printing-press", "library")
+ }
+ findings, err := runMCPAudit(path)
+ if err != nil {
+ return err
+ }
+ if asJSON {
+ data, err := json.MarshalIndent(findings, "", " ")
+ if err != nil {
+ return fmt.Errorf("encoding findings: %w", err)
+ }
+ fmt.Fprintln(cmd.OutOrStdout(), string(data))
+ return nil
+ }
+ renderMCPAuditTable(cmd.OutOrStdout(), findings)
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&libraryPath, "library", "", "library root (default ~/printing-press/library)")
+ cmd.Flags().BoolVar(&asJSON, "json", false, "emit JSON instead of a human-readable table")
+ return cmd
+}
+
+// MCPAuditFinding summarizes one CLI's MCP shape. Stable JSON field names
+// so downstream tools (ci scripts, the future emboss-audit loop) can
+// consume this without depending on go struct ordering.
+type MCPAuditFinding struct {
+ API string `json:"api"` // library directory basename
+ HasMCP bool `json:"has_mcp"` // the CLI emits an MCP server
+ Transport string `json:"transport"` // "stdio", "http", "both", "unknown", "n/a"
+ ToolDesign string `json:"tool_design"` // "endpoint-mirror", "intent", "code-orch", "n/a"
+ EndpointCt int `json:"endpoint_count"`
+ IntentCt int `json:"intent_count"`
+ Recommend string `json:"recommend"` // short, actionable suggestion
+}
+
+// runMCPAudit walks every immediate subdirectory of libraryPath and reports
+// one finding per CLI. Missing MCP surfaces are reported, not skipped, so
+// the operator sees which CLIs lack one entirely.
+func runMCPAudit(libraryPath string) ([]MCPAuditFinding, error) {
+ entries, err := os.ReadDir(libraryPath)
+ if err != nil {
+ return nil, fmt.Errorf("reading library %s: %w", libraryPath, err)
+ }
+
+ var findings []MCPAuditFinding
+ for _, e := range entries {
+ if !e.IsDir() {
+ continue
+ }
+ findings = append(findings, auditLibraryCLI(filepath.Join(libraryPath, e.Name()), e.Name()))
+ }
+ sort.Slice(findings, func(i, j int) bool { return findings[i].API < findings[j].API })
+ return findings, nil
+}
+
+// auditLibraryCLI inspects a single library entry. It relies on string
+// heuristics over file contents rather than requiring a manifest file
+// because older printed CLIs predate the tools-manifest schema.
+func auditLibraryCLI(dir, api string) MCPAuditFinding {
+ f := MCPAuditFinding{API: api}
+
+ var mainPath string
+ cmdDir := filepath.Join(dir, "cmd")
+ cmdEntries, err := os.ReadDir(cmdDir)
+ if err == nil {
+ for _, ce := range cmdEntries {
+ if ce.IsDir() && strings.HasSuffix(ce.Name(), "-pp-mcp") {
+ mainPath = filepath.Join(cmdDir, ce.Name(), "main.go")
+ break
+ }
+ }
+ }
+ if mainPath == "" {
+ f.Transport = "n/a"
+ f.ToolDesign = "n/a"
+ f.Recommend = "no MCP surface — consider adding mcp: block to enable an MCP server"
+ return f
+ }
+ f.HasMCP = true
+
+ if data, err := os.ReadFile(mainPath); err == nil {
+ body := string(data)
+ hasStdio := strings.Contains(body, "server.ServeStdio")
+ hasHTTP := strings.Contains(body, "NewStreamableHTTPServer") || strings.Contains(body, "ServeStreamableHTTP")
+ switch {
+ case hasStdio && hasHTTP:
+ f.Transport = "both"
+ case hasHTTP:
+ f.Transport = "http"
+ case hasStdio:
+ f.Transport = "stdio"
+ default:
+ f.Transport = "unknown"
+ }
+ } else {
+ f.Transport = "unknown"
+ }
+
+ if data, err := os.ReadFile(filepath.Join(dir, "internal", "mcp", "tools.go")); err == nil {
+ f.EndpointCt = strings.Count(string(data), "mcplib.NewTool(")
+ }
+ intentsPresent := false
+ if data, err := os.ReadFile(filepath.Join(dir, "internal", "mcp", "intents.go")); err == nil {
+ intentsPresent = true
+ f.IntentCt = strings.Count(string(data), "mcplib.NewTool(")
+ }
+ codeOrch := false
+ if _, err := os.Stat(filepath.Join(dir, "internal", "mcp", "code_orch.go")); err == nil {
+ codeOrch = true
+ }
+
+ switch {
+ case codeOrch:
+ f.ToolDesign = "code-orch"
+ case intentsPresent:
+ f.ToolDesign = "intent"
+ default:
+ f.ToolDesign = "endpoint-mirror"
+ }
+
+ f.Recommend = recommendForFinding(f)
+ return f
+}
+
+// recommendForFinding produces a one-line action hint. The hints match the
+// spec surface introduced in U1-U3 so the operator can map directly from
+// audit output to the mcp: fields they should add.
+func recommendForFinding(f MCPAuditFinding) string {
+ if !f.HasMCP {
+ return "no MCP surface — add mcp: block to enable"
+ }
+ var hints []string
+ if f.Transport == "stdio" {
+ hints = append(hints, "set mcp.transport: [stdio, http] for cloud-agent reach")
+ }
+ switch f.ToolDesign {
+ case "endpoint-mirror":
+ if f.EndpointCt > 50 {
+ hints = append(hints, "set mcp.orchestration: code (endpoint count past threshold)")
+ } else if f.EndpointCt >= 10 {
+ hints = append(hints, "declare mcp.intents for common workflows")
+ }
+ case "intent":
+ if f.EndpointCt > 50 {
+ hints = append(hints, "consider mcp.orchestration: code for the full surface")
+ }
+ }
+ if len(hints) == 0 {
+ return "ok"
+ }
+ return strings.Join(hints, "; ")
+}
+
+// renderMCPAuditTable produces the human-readable table. Kept narrow on
+// purpose — operators run this interactively, and wide tables stop being
+// readable past ~80 columns. Recommend is the last column so long hints
+// wrap cleanly in terminals.
+func renderMCPAuditTable(w interface{ Write(p []byte) (int, error) }, findings []MCPAuditFinding) {
+ if len(findings) == 0 {
+ fmt.Fprintln(w, "(no CLIs found in library)")
+ return
+ }
+ header := fmt.Sprintf("%-20s %-10s %-14s %6s %6s %s", "API", "Transport", "ToolDesign", "Epts", "Itnts", "Recommend")
+ fmt.Fprintln(w, header)
+ fmt.Fprintln(w, strings.Repeat("-", len(header)))
+ for _, f := range findings {
+ fmt.Fprintf(w, "%-20s %-10s %-14s %6d %6d %s\n",
+ truncate(f.API, 20), f.Transport, f.ToolDesign, f.EndpointCt, f.IntentCt, f.Recommend)
+ }
+}
+
+func truncate(s string, n int) string {
+ if len(s) <= n {
+ return s
+ }
+ if n <= 1 {
+ return s[:n]
+ }
+ return s[:n-1] + "…"
+}
diff --git a/internal/cli/mcp_audit_test.go b/internal/cli/mcp_audit_test.go
new file mode 100644
index 00000000..00ed8f4d
--- /dev/null
+++ b/internal/cli/mcp_audit_test.go
@@ -0,0 +1,133 @@
+package cli
+
+import (
+ "bytes"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// mustWrite is the table-test friendly helper for this file. Keeps each
+// fixture construction compact since the audit reads multiple file paths
+// per CLI.
+func mustWrite(t *testing.T, root, rel, body string) {
+ t.Helper()
+ full := filepath.Join(root, rel)
+ require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
+ require.NoError(t, os.WriteFile(full, []byte(body), 0o644))
+}
+
+// makeMCPTools returns a tools.go body with n endpoint-tool registrations.
+func makeMCPTools(n int) string {
+ var b strings.Builder
+ b.WriteString("package mcp\nfunc RegisterTools() {\n")
+ for i := 0; i < n; i++ {
+ b.WriteString("mcplib.NewTool(\"x\",)\n")
+ }
+ b.WriteString("}\n")
+ return b.String()
+}
+
+func TestRunMCPAuditNoMCPSurface(t *testing.T) {
+ lib := t.TempDir()
+ // CLI without cmd/*-pp-mcp — audit should report no MCP.
+ require.NoError(t, os.MkdirAll(filepath.Join(lib, "bare-cli"), 0o755))
+
+ findings, err := runMCPAudit(lib)
+ require.NoError(t, err)
+ require.Len(t, findings, 1)
+ assert.Equal(t, "bare-cli", findings[0].API)
+ assert.False(t, findings[0].HasMCP)
+ assert.Equal(t, "n/a", findings[0].Transport)
+ assert.Equal(t, "n/a", findings[0].ToolDesign)
+ assert.Contains(t, findings[0].Recommend, "mcp: block")
+}
+
+func TestRunMCPAuditStdioEndpointMirror(t *testing.T) {
+ lib := t.TempDir()
+ cli := filepath.Join(lib, "small-cli")
+ mustWrite(t, cli, "cmd/small-cli-pp-mcp/main.go", "package main\nfunc main() { server.ServeStdio(s) }\n")
+ mustWrite(t, cli, "internal/mcp/tools.go", makeMCPTools(6))
+
+ findings, err := runMCPAudit(lib)
+ require.NoError(t, err)
+ require.Len(t, findings, 1)
+ f := findings[0]
+ assert.True(t, f.HasMCP)
+ assert.Equal(t, "stdio", f.Transport)
+ assert.Equal(t, "endpoint-mirror", f.ToolDesign)
+ assert.Equal(t, 6, f.EndpointCt)
+ assert.Contains(t, f.Recommend, "[stdio, http]", "small stdio mirror still recommends remote")
+ assert.NotContains(t, f.Recommend, "intents", "6 endpoints is below intent recommendation threshold")
+}
+
+func TestRunMCPAuditLargeMirrorRecommendsCodeOrch(t *testing.T) {
+ lib := t.TempDir()
+ cli := filepath.Join(lib, "huge-cli")
+ // Both transports + endpoint mirror with many tools.
+ mustWrite(t, cli, "cmd/huge-cli-pp-mcp/main.go",
+ "package main\nfunc main() { server.ServeStdio(s); server.NewStreamableHTTPServer(s) }\n")
+ mustWrite(t, cli, "internal/mcp/tools.go", makeMCPTools(60))
+
+ findings, err := runMCPAudit(lib)
+ require.NoError(t, err)
+ require.Len(t, findings, 1)
+ f := findings[0]
+ assert.Equal(t, "both", f.Transport)
+ assert.Equal(t, "endpoint-mirror", f.ToolDesign)
+ assert.Equal(t, 60, f.EndpointCt)
+ assert.Contains(t, f.Recommend, "orchestration: code")
+ assert.NotContains(t, f.Recommend, "[stdio, http]", "transport is already both — no remote recommendation")
+}
+
+func TestRunMCPAuditCodeOrchSurface(t *testing.T) {
+ lib := t.TempDir()
+ cli := filepath.Join(lib, "cloudy-cli")
+ mustWrite(t, cli, "cmd/cloudy-cli-pp-mcp/main.go",
+ "package main\nfunc main() { server.ServeStdio(s); server.NewStreamableHTTPServer(s) }\n")
+ mustWrite(t, cli, "internal/mcp/tools.go", "package mcp\n")
+ mustWrite(t, cli, "internal/mcp/code_orch.go", "package mcp\n")
+
+ findings, err := runMCPAudit(lib)
+ require.NoError(t, err)
+ require.Len(t, findings, 1)
+ f := findings[0]
+ assert.Equal(t, "both", f.Transport)
+ assert.Equal(t, "code-orch", f.ToolDesign)
+ assert.Equal(t, "ok", f.Recommend, "fully-modernized CLI has nothing to recommend")
+}
+
+func TestRunMCPAuditJSONRoundtrip(t *testing.T) {
+ lib := t.TempDir()
+ mustWrite(t, lib, "alpha/cmd/alpha-pp-mcp/main.go", "package main\nfunc main() { server.ServeStdio(s) }\n")
+ mustWrite(t, lib, "alpha/internal/mcp/tools.go", makeMCPTools(3))
+ mustWrite(t, lib, "beta/cmd/beta-pp-mcp/main.go", "package main\nfunc main() { server.NewStreamableHTTPServer(s); server.ServeStdio(s) }\n")
+ mustWrite(t, lib, "beta/internal/mcp/tools.go", makeMCPTools(12))
+ mustWrite(t, lib, "beta/internal/mcp/intents.go", makeMCPTools(4))
+
+ cmd := newMCPAuditCmd()
+ var out bytes.Buffer
+ cmd.SetOut(&out)
+ cmd.SetArgs([]string{"--library", lib, "--json"})
+ require.NoError(t, cmd.Execute())
+
+ var findings []MCPAuditFinding
+ require.NoError(t, json.Unmarshal(out.Bytes(), &findings))
+ require.Len(t, findings, 2)
+ assert.Equal(t, "alpha", findings[0].API)
+ assert.Equal(t, "stdio", findings[0].Transport)
+ assert.Equal(t, "beta", findings[1].API)
+ assert.Equal(t, "both", findings[1].Transport)
+ assert.Equal(t, "intent", findings[1].ToolDesign)
+ assert.Equal(t, 4, findings[1].IntentCt)
+}
+
+func TestRunMCPAuditMissingLibraryErrors(t *testing.T) {
+ _, err := runMCPAudit(filepath.Join(t.TempDir(), "does-not-exist"))
+ assert.Error(t, err, "missing library path should surface as a clear error")
+}
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 71f7eb7c..a826153e 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -57,6 +57,7 @@ func Execute() error {
rootCmd.AddCommand(newPolishCmd())
rootCmd.AddCommand(newWorkflowVerifyCmd())
rootCmd.AddCommand(newLockCmd())
+ rootCmd.AddCommand(newMCPAuditCmd())
return rootCmd.Execute()
}
← 2d07a021 feat(cli): scorecard dimensions for remote transport, tool d
·
back to Cli Printing Press
·
feat(cli): generate replayable website CLIs (#241) e741db80 →