← back to Cli Printing Press
fix(cli): manifest emission no longer gated by stale mcp_ready label (#359)
2e892e368f3c3249a674b18da27402d47debbafa · 2026-04-28 00:26:52 -0700 · Trevin Chow
Generated CLIs now ship manifest.json regardless of mcp_ready, fixing
the Pagliacci-pizza class of bug where composed/cookie-auth CLIs with
unauthenticated tools (registration, login, public discovery) were
silently denied an MCPB install path.
Three-line root cause:
1. Spec authors rarely flag endpoints `no_auth: true` granularly.
2. computeMCPReady saw publicTools=0 for composed/cookie auth and
returned "cli-only".
3. WriteMCPBManifestFromStruct skipped emission whenever MCPReady was
"cli-only", suppressing the entire Claude Desktop install path even
for CLIs whose MCPs registered 67+ tools (Pagliacci had login,
registration, store/menu lookup tools that work without creds).
Two-pronged fix — machine plus skill — so future CLIs can't hit this:
Machine — internal/pipeline/mcpb_manifest.go:
- WriteMCPBManifestFromStruct's gate is now `m.MCPBinary == ""`. The
cli-only check is gone. Withholding the manifest from cli-only CLIs
only hurt users who could have installed the bundle and used the
unauthenticated surface; user_config.required already conveys
"needs creds" or "optional" via authRequiresCredential().
Machine — internal/pipeline/climanifest.go:
- computeMCPReady drops the publicTools-count gate entirely. cookie
and composed always return "partial". cli-only is no longer
produced by this function. Call signature dropped the now-unused
publicTools parameter; one caller in toolsmanifest.go updated.
- Comment block on the function explains why: the `publicTools > 0`
test relied on a tag spec authors rarely audit, producing systematic
false-negative readiness labels for composed/cookie APIs.
Skill — skills/printing-press/SKILL.md (Pre-Generation Auth Enrichment):
New "Tagging endpoints `no_auth: true`" subsection prompts the agent
to audit each endpoint when auth is composed/cookie/session_handshake.
Spells out the typical unauthenticated categories — auth-flow
primitives (register, login, password-reset), public discovery
(store/menu browse, catalog), health/metadata — and gives a worked
pizza-ordering example. Notes that the safety net (machine fix above)
makes this defense-in-depth rather than load-bearing.
Tests:
- TestComputeMCPReady simplified: composed/cookie always partial,
no more publicTools axis. Comment captures Pagliacci-pizza as the
canonical regression case.
- TestWriteMCPBManifest's "cli-only readiness → skipped" case
inverted to "cli-only readiness still emits manifest".
No golden update needed: golden-api.yaml uses api_key auth and is
unaffected by the composed/cookie path changes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M internal/pipeline/climanifest.goM internal/pipeline/climanifest_test.goM internal/pipeline/mcpb_manifest.goM internal/pipeline/toolsmanifest.goM skills/printing-press/SKILL.md
Diff
commit 2e892e368f3c3249a674b18da27402d47debbafa
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue Apr 28 00:26:52 2026 -0700
fix(cli): manifest emission no longer gated by stale mcp_ready label (#359)
Generated CLIs now ship manifest.json regardless of mcp_ready, fixing
the Pagliacci-pizza class of bug where composed/cookie-auth CLIs with
unauthenticated tools (registration, login, public discovery) were
silently denied an MCPB install path.
Three-line root cause:
1. Spec authors rarely flag endpoints `no_auth: true` granularly.
2. computeMCPReady saw publicTools=0 for composed/cookie auth and
returned "cli-only".
3. WriteMCPBManifestFromStruct skipped emission whenever MCPReady was
"cli-only", suppressing the entire Claude Desktop install path even
for CLIs whose MCPs registered 67+ tools (Pagliacci had login,
registration, store/menu lookup tools that work without creds).
Two-pronged fix — machine plus skill — so future CLIs can't hit this:
Machine — internal/pipeline/mcpb_manifest.go:
- WriteMCPBManifestFromStruct's gate is now `m.MCPBinary == ""`. The
cli-only check is gone. Withholding the manifest from cli-only CLIs
only hurt users who could have installed the bundle and used the
unauthenticated surface; user_config.required already conveys
"needs creds" or "optional" via authRequiresCredential().
Machine — internal/pipeline/climanifest.go:
- computeMCPReady drops the publicTools-count gate entirely. cookie
and composed always return "partial". cli-only is no longer
produced by this function. Call signature dropped the now-unused
publicTools parameter; one caller in toolsmanifest.go updated.
- Comment block on the function explains why: the `publicTools > 0`
test relied on a tag spec authors rarely audit, producing systematic
false-negative readiness labels for composed/cookie APIs.
Skill — skills/printing-press/SKILL.md (Pre-Generation Auth Enrichment):
New "Tagging endpoints `no_auth: true`" subsection prompts the agent
to audit each endpoint when auth is composed/cookie/session_handshake.
Spells out the typical unauthenticated categories — auth-flow
primitives (register, login, password-reset), public discovery
(store/menu browse, catalog), health/metadata — and gives a worked
pizza-ordering example. Notes that the safety net (machine fix above)
makes this defense-in-depth rather than load-bearing.
Tests:
- TestComputeMCPReady simplified: composed/cookie always partial,
no more publicTools axis. Comment captures Pagliacci-pizza as the
canonical regression case.
- TestWriteMCPBManifest's "cli-only readiness → skipped" case
inverted to "cli-only readiness still emits manifest".
No golden update needed: golden-api.yaml uses api_key auth and is
unaffected by the composed/cookie path changes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/pipeline/climanifest.go | 27 +++++++++++-----
internal/pipeline/climanifest_test.go | 50 ++++++++++++++++++-----------
internal/pipeline/mcpb_manifest.go | 16 +++++++---
internal/pipeline/toolsmanifest.go | 2 +-
skills/printing-press/SKILL.md | 60 +++++++++++++++++++++++++++++++++++
5 files changed, 123 insertions(+), 32 deletions(-)
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 15c1385f..893cef9f 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -158,17 +158,28 @@ func specChecksum(path string) (string, error) {
return "sha256:" + hex.EncodeToString(h[:]), nil
}
-// computeMCPReady determines the MCP readiness level based on the auth type
-// and the public/total tool split.
-func computeMCPReady(authType string, publicTools int) string {
+// computeMCPReady determines the MCP readiness label for scorecard /
+// SKILL prose. It does NOT gate manifest emission — that decision lives
+// in WriteMCPBManifestFromStruct and is purely "do we have an MCP binary
+// to ship?". The label exists to set user expectations: full = every
+// tool works without per-tool auth setup; partial = some tools work
+// without credentials, others need auth provided through the companion
+// CLI's flow (composed, cookie). cli-only is reserved for the
+// degenerate case of no MCP tools at all and is rarely set in practice.
+//
+// Why no `publicTools > 0` gate for composed/cookie any more: the count
+// relies on spec authors tagging endpoints `no_auth: true`, which most
+// generated specs don't audit carefully. A composed-auth CLI with zero
+// no_auth tags was previously labeled cli-only even when many endpoints
+// (registration, login, public discovery) actually work without auth.
+// Defaulting to "partial" matches the typical reality and avoids
+// suppressing manifest emission downstream.
+func computeMCPReady(authType string) string {
switch authType {
case "none", "api_key", "bearer_token":
return "full"
case "cookie", "composed":
- if publicTools > 0 {
- return "partial"
- }
- return "cli-only"
+ return "partial"
default:
return "full"
}
@@ -182,7 +193,7 @@ func populateMCPMetadata(m *CLIManifest, parsed *spec.APISpec) {
m.MCPBinary = naming.MCP(parsed.Name)
m.MCPToolCount = total
m.MCPPublicToolCount = public
- m.MCPReady = computeMCPReady(parsed.Auth.Type, public)
+ m.MCPReady = computeMCPReady(parsed.Auth.Type)
m.AuthType = parsed.Auth.Type
m.AuthEnvVars = parsed.Auth.EnvVars
m.AuthKeyURL = parsed.Auth.KeyURL
diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index ab6a4e63..9e8da30a 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -542,25 +542,30 @@ func TestArchiveRunArtifactsSkipsMissingDiscovery(t *testing.T) {
}
func TestComputeMCPReady(t *testing.T) {
+ // composed/cookie auth always reports "partial" — the older `if
+ // publicTools > 0` gate produced false-negative "cli-only" labels for
+ // CLIs whose spec authors hadn't yet tagged endpoints with `no_auth:
+ // true`. Pagliacci-pizza is the canonical case: composed auth, 67
+ // registered tools (account_register, account_login, store/menu
+ // lookups all unauthenticated), but mcp_public_tool_count was 0 so
+ // the readiness label was wrong and downstream manifest emission
+ // was suppressed.
tests := []struct {
- name string
- authType string
- publicTools int
- want string
+ name string
+ authType string
+ want string
}{
- {"none", "none", 0, "full"},
- {"api_key", "api_key", 0, "full"},
- {"bearer_token", "bearer_token", 0, "full"},
- {"oauth2 defaults to full", "oauth2", 0, "full"},
- {"cookie with public tools", "cookie", 3, "partial"},
- {"cookie no public tools", "cookie", 0, "cli-only"},
- {"composed with public tools", "composed", 5, "partial"},
- {"composed no public tools", "composed", 0, "cli-only"},
- {"empty auth type", "", 0, "full"},
+ {"none", "none", "full"},
+ {"api_key", "api_key", "full"},
+ {"bearer_token", "bearer_token", "full"},
+ {"oauth2 defaults to full", "oauth2", "full"},
+ {"cookie always partial", "cookie", "partial"},
+ {"composed always partial", "composed", "partial"},
+ {"empty auth type", "", "full"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := computeMCPReady(tt.authType, tt.publicTools)
+ got := computeMCPReady(tt.authType)
assert.Equal(t, tt.want, got)
})
}
@@ -575,14 +580,23 @@ func TestWriteMCPBManifest(t *testing.T) {
assert.True(t, os.IsNotExist(statErr))
})
- t.Run("cli-only readiness → skipped (host can't use bundle alone)", func(t *testing.T) {
+ t.Run("cli-only readiness still emits manifest", func(t *testing.T) {
+ // Earlier behavior: cli-only readiness skipped manifest emission
+ // entirely, on the theory that "the bundle won't work standalone."
+ // In practice that suppressed bundles for composed/cookie-auth CLIs
+ // with unauthenticated tools (registration, login, public lookups).
+ // The manifest now ships regardless; user_config's required flag
+ // communicates auth-required-or-optional. See Pagliacci-pizza.
dir := t.TempDir()
- m := CLIManifest{MCPBinary: "test-pp-mcp", MCPReady: "cli-only"}
- writeManifest(t, dir, m)
+ writeManifest(t, dir, CLIManifest{
+ APIName: "test",
+ MCPBinary: "test-pp-mcp",
+ MCPReady: "cli-only",
+ })
require.NoError(t, WriteMCPBManifest(dir))
_, statErr := os.Stat(filepath.Join(dir, MCPBManifestFilename))
- assert.True(t, os.IsNotExist(statErr))
+ require.NoError(t, statErr, "cli-only readiness should NOT skip manifest emission")
})
t.Run("missing MCP binary → skipped", func(t *testing.T) {
diff --git a/internal/pipeline/mcpb_manifest.go b/internal/pipeline/mcpb_manifest.go
index acbebc58..a2c9ce79 100644
--- a/internal/pipeline/mcpb_manifest.go
+++ b/internal/pipeline/mcpb_manifest.go
@@ -127,10 +127,16 @@ type MCPBCompat struct {
}
// WriteMCPBManifest emits manifest.json for a published CLI directory by
-// reading .printing-press.json. Skips silently when the CLI dir has no
-// manifest, no MCP binary, or cli-only readiness — none of those should
-// produce a draggable .mcpb. Callers that already have the CLIManifest
-// in memory should use WriteMCPBManifestFromStruct to avoid the re-read.
+// reading .printing-press.json. Skips silently only when the CLI dir has
+// no .printing-press.json or no MCP binary — every other CLI ships a
+// manifest, including composed/cookie-auth ones whose MCPReady label
+// says "partial" or "cli-only". The user_config block already conveys
+// auth-required-or-optional (per authRequiresCredential), so withholding
+// the manifest from cli-only readiness CLIs only hurt users who could
+// otherwise install the bundle and use any unauthenticated tools.
+//
+// Callers that already have the CLIManifest in memory should use
+// WriteMCPBManifestFromStruct to avoid the re-read.
func WriteMCPBManifest(dir string) error {
data, err := os.ReadFile(filepath.Join(dir, CLIManifestFilename))
if err != nil {
@@ -147,7 +153,7 @@ func WriteMCPBManifest(dir string) error {
// Use it when the CLIManifest was just built and writing it back to disk
// only to re-read it would be wasted work.
func WriteMCPBManifestFromStruct(dir string, m CLIManifest) error {
- if m.MCPBinary == "" || m.MCPReady == "cli-only" {
+ if m.MCPBinary == "" {
return nil
}
// SetEscapeHTML(false) so `>=1.0.0` stays readable instead of `>=1.0.0`.
diff --git a/internal/pipeline/toolsmanifest.go b/internal/pipeline/toolsmanifest.go
index 9f63685c..9b6cb037 100644
--- a/internal/pipeline/toolsmanifest.go
+++ b/internal/pipeline/toolsmanifest.go
@@ -85,7 +85,7 @@ func WriteToolsManifest(dir string, parsed *spec.APISpec) error {
}
total, public := parsed.CountMCPTools()
- mcpReady := computeMCPReady(parsed.Auth.Type, public)
+ mcpReady := computeMCPReady(parsed.Auth.Type)
// For cookie/composed auth, only include NoAuth endpoints.
cookieOrComposed := parsed.Auth.Type == "cookie" || parsed.Auth.Type == "composed"
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index ad545989..71bc221f 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1409,6 +1409,66 @@ template produces correct auth from the start.
APIs, public data feeds), don't invent auth. The signal must come from research — not
from guessing. No research mention of auth = no enrichment.
+#### Tagging endpoints `no_auth: true` (composed/cookie auth APIs)
+
+For APIs whose `auth.type` is `cookie`, `composed`, or `session_handshake` — i.e.,
+auth that requires interactive setup (browser cookie capture, multi-step token
+exchange) — audit each endpoint individually for whether it actually needs
+authentication. The default of `no_auth: false` means "auth required"; flip it to
+`no_auth: true` for endpoints that work without credentials.
+
+Typical unauthenticated endpoints worth tagging:
+
+- **Auth-flow primitives:** login, registration, password-reset, email-confirm,
+ refresh-token, OAuth callback. The user isn't authenticated when calling these —
+ they ARE the auth flow.
+- **Public discovery:** store/location finder, menu browse, public catalog,
+ category listing, public search, public product detail.
+- **Health/metadata:** health checks, version probes, capability flags, sitemap.
+
+Why this matters: the `no_auth` count drives downstream decisions. Specifically,
+a composed-auth API with zero `no_auth: true` tags previously got labeled
+`mcp_ready: cli-only` and was suppressed from MCPB manifest emission, which
+broke the Claude Desktop install path entirely. The current generator
+(post-2.5) ships a manifest regardless, but the readiness label, scorecard
+breadth dimension, and SKILL.md prose all read better when the count
+reflects reality.
+
+If unsure whether an endpoint requires auth, the safe default is `no_auth: false`
+(auth required) — over-tagging can mislead users to expect tools that won't work.
+
+**Example for a composed-auth pizza-ordering API:**
+
+```yaml
+resources:
+ account:
+ endpoints:
+ register:
+ method: POST
+ path: /account/register
+ no_auth: true # registering means you're not yet authenticated
+ login:
+ method: POST
+ path: /account/login
+ no_auth: true # the auth flow itself
+ profile:
+ method: GET
+ path: /account/profile
+ # no_auth defaults to false — needs auth to view your own profile
+ stores:
+ endpoints:
+ find:
+ method: GET
+ path: /stores/near
+ no_auth: true # public store finder
+ cart:
+ endpoints:
+ checkout:
+ method: POST
+ path: /cart/checkout
+ # no_auth defaults to false — placing an order needs auth
+```
+
### Lock and Generate
Before running any generate command, acquire the build lock:
← 9e260025 ci(ci): split fast and full test lanes (#357)
·
back to Cli Printing Press
·
fix(ci): speed up generated compile tests and caches (#360) ddacddba →