[object Object]

← back to Cli Printing Press

fix(cli): manifest description no longer doubles 'API' when display_name ends in 'API' (#397)

fae7e6e09309dc5ca77ae063849fc60feee85a39 · 2026-04-29 15:34:42 -0700 · Trevin Chow

Spec authors commonly suffix " API" on info.title (and now on
x-display-name from #391). The derived description concatenated
"Stripe API" + " API surface as MCP tools." into "Stripe API API
surface as MCP tools." — and the env var help text concatenated
"Stripe API" + " MCP server." into "Stripe API MCP server." The
first reads as a typo; the second reads as describing a category
("API MCP server") instead of the brand's MCP server.

Single-source the trim at concat sites via displayNameForConcat,
which strips a trailing " API" with the leading space (so embedded
"API" in names like "API Gateway" stays untouched, and singular
"API" with no prefix isn't accidentally consumed). The manifest's
display_name field itself keeps the unmodified spec value — only
the concat sites trim.

Tests cover the cases that matter: trailing API trimmed (Stripe API,
Cal.com API), no trailing API unchanged (Stripe), embedded API not
trimmed (API Gateway), plural APIs not trimmed (Stripe APIs), and
the env var description's "<displayName> MCP server" surface.

Closes #393

Files touched

Diff

commit fae7e6e09309dc5ca77ae063849fc60feee85a39
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed Apr 29 15:34:42 2026 -0700

    fix(cli): manifest description no longer doubles 'API' when display_name ends in 'API' (#397)
    
    Spec authors commonly suffix " API" on info.title (and now on
    x-display-name from #391). The derived description concatenated
    "Stripe API" + " API surface as MCP tools." into "Stripe API API
    surface as MCP tools." — and the env var help text concatenated
    "Stripe API" + " MCP server." into "Stripe API MCP server." The
    first reads as a typo; the second reads as describing a category
    ("API MCP server") instead of the brand's MCP server.
    
    Single-source the trim at concat sites via displayNameForConcat,
    which strips a trailing " API" with the leading space (so embedded
    "API" in names like "API Gateway" stays untouched, and singular
    "API" with no prefix isn't accidentally consumed). The manifest's
    display_name field itself keeps the unmodified spec value — only
    the concat sites trim.
    
    Tests cover the cases that matter: trailing API trimmed (Stripe API,
    Cal.com API), no trailing API unchanged (Stripe), embedded API not
    trimmed (API Gateway), plural APIs not trimmed (Stripe APIs), and
    the env var description's "<displayName> MCP server" surface.
    
    Closes #393
---
 internal/pipeline/climanifest_test.go | 82 +++++++++++++++++++++++++++++++++++
 internal/pipeline/mcpb_manifest.go    | 25 ++++++++---
 2 files changed, 102 insertions(+), 5 deletions(-)

diff --git a/internal/pipeline/climanifest_test.go b/internal/pipeline/climanifest_test.go
index 3a3aa053..79ea17a5 100644
--- a/internal/pipeline/climanifest_test.go
+++ b/internal/pipeline/climanifest_test.go
@@ -808,6 +808,88 @@ func TestWriteMCPBManifestPreservesExistingDescription(t *testing.T) {
 	})
 }
 
+func TestWriteMCPBManifest_DerivedDescriptionTrimsAPIFromDisplayName(t *testing.T) {
+	// Spec authors commonly suffix " API" on info.title and
+	// x-display-name. Without trimming, the derived description
+	// concatenates "Stripe API" + " API surface as MCP tools." into
+	// "Stripe API API surface as MCP tools." Single-source the trim
+	// at concat sites; the manifest's display_name field still keeps
+	// the unmodified spec value.
+	tests := []struct {
+		name        string
+		displayName string
+		want        string
+	}{
+		{"trailing API trimmed", "Stripe API", "Stripe API surface as MCP tools."},
+		{"branded with punctuation+API trimmed", "Cal.com API", "Cal.com API surface as MCP tools."},
+		{"no trailing API unchanged", "Stripe", "Stripe API surface as MCP tools."},
+		{"embedded API not trimmed", "API Gateway", "API Gateway API surface as MCP tools."},
+		{"trailing APIs (plural) not trimmed", "Stripe APIs", "Stripe APIs API surface as MCP tools."},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			dir := t.TempDir()
+			writeManifest(t, dir, CLIManifest{
+				APIName:     "test",
+				DisplayName: tc.displayName,
+				MCPBinary:   "test-pp-mcp",
+				MCPReady:    "full",
+			})
+			require.NoError(t, WriteMCPBManifest(dir))
+			assert.Equal(t, tc.want, readMCPBManifest(t, dir).Description)
+			// display_name field itself stays unmodified — only concat
+			// sites trim the redundant " API" suffix.
+			assert.Equal(t, tc.displayName, readMCPBManifest(t, dir).DisplayName)
+		})
+	}
+}
+
+func TestWriteMCPBManifest_MigratesPriorDoubledAPIDescription(t *testing.T) {
+	// Manifests written before this fix carry the doubled form
+	// "Stripe API API surface as MCP tools." On the next mcp-sync,
+	// the new derived default ("Stripe API surface as MCP tools.")
+	// no longer matches; a naive "preserve when not matching"
+	// preserves the doubled form forever. Recognize the prior form
+	// too so the regen refreshes to the trimmed default.
+	dir := t.TempDir()
+	writeMCPBManifest(t, dir, MCPBManifest{
+		ManifestVersion: MCPBManifestVersion,
+		Name:            "stripe-pp-mcp",
+		DisplayName:     "Stripe API",
+		Description:     "Stripe API API surface as MCP tools.",
+	})
+	writeManifest(t, dir, CLIManifest{
+		APIName:     "stripe",
+		DisplayName: "Stripe API",
+		MCPBinary:   "stripe-pp-mcp",
+		MCPReady:    "full",
+	})
+	require.NoError(t, WriteMCPBManifest(dir))
+	assert.Equal(t, "Stripe API surface as MCP tools.", readMCPBManifest(t, dir).Description)
+}
+
+func TestWriteMCPBManifest_EnvVarDescriptionTrimsAPIFromDisplayName(t *testing.T) {
+	// The user_config env var description's "<displayName> MCP server"
+	// substring uses the same trim so "Stripe API MCP server" reads
+	// as "Stripe MCP server" — slightly more natural English and
+	// keeps the surfaces consistent.
+	dir := t.TempDir()
+	writeManifest(t, dir, CLIManifest{
+		APIName:     "stripe",
+		DisplayName: "Stripe API",
+		MCPBinary:   "stripe-pp-mcp",
+		MCPReady:    "full",
+		AuthType:    "bearer_token",
+		AuthEnvVars: []string{"STRIPE_TOKEN"},
+	})
+	require.NoError(t, WriteMCPBManifest(dir))
+	got := readMCPBManifest(t, dir)
+	require.NotNil(t, got.UserConfig)
+	stripe, ok := got.UserConfig["stripe_token"]
+	require.True(t, ok, "stripe_token user_config entry must exist")
+	assert.Equal(t, "Sets STRIPE_TOKEN for the Stripe MCP server.", stripe.Description)
+}
+
 func TestRefreshCLIManifestFromSpecRefreshesDisplayName(t *testing.T) {
 	// RefreshCLIManifestFromSpec must overwrite an existing
 	// DisplayName, not preserve it — otherwise stale slug-derived
diff --git a/internal/pipeline/mcpb_manifest.go b/internal/pipeline/mcpb_manifest.go
index 78e980ab..5a511841 100644
--- a/internal/pipeline/mcpb_manifest.go
+++ b/internal/pipeline/mcpb_manifest.go
@@ -220,11 +220,16 @@ func bundleVersion(m CLIManifest) string {
 
 // manifestDescription returns the existing hand-edited description over
 // the canonical one from .printing-press.json. The existing snapshot's
-// description is only treated as "hand-edited" when it differs from the
-// derived default — so a prior derived default still loses to canonical.
+// description is only treated as "hand-edited" when it differs from
+// every form the generator would have emitted — current and prior — so
+// a manifest written before the displayNameForConcat trim still gets
+// recognized as derived and refreshed from canonical.
 func manifestDescription(existing *existingMCPBManifest, m CLIManifest, displayName string) string {
-	derivedDefault := displayName + " API surface as MCP tools."
-	if existing != nil && existing.Description != "" && existing.Description != derivedDefault {
+	derivedDefault := displayNameForConcat(displayName) + " API surface as MCP tools."
+	priorDerivedDefault := displayName + " API surface as MCP tools."
+	if existing != nil && existing.Description != "" &&
+		existing.Description != derivedDefault &&
+		existing.Description != priorDerivedDefault {
 		return existing.Description
 	}
 	if m.Description != "" {
@@ -233,6 +238,16 @@ func manifestDescription(existing *existingMCPBManifest, m CLIManifest, displayN
 	return derivedDefault
 }
 
+// displayNameForConcat strips a trailing " API" from displayName so
+// concatenating with text that already names the API doesn't read
+// "Stripe API API surface as MCP tools." or "Stripe API MCP server."
+// Spec authors commonly include " API" as a suffix in info.title and
+// x-display-name; we let them keep that form for the manifest's
+// display_name field while removing the redundancy at concat sites.
+func displayNameForConcat(displayName string) string {
+	return strings.TrimSuffix(displayName, " API")
+}
+
 // existingMCPBManifest is the subset of manifest.json the manifest writer
 // reads when refreshing a published CLI's bundle. Single load site so the
 // display_name and description preservation rules don't each re-read the
@@ -319,7 +334,7 @@ func envVarDescription(m CLIManifest, envVar string, required bool) string {
 	b.WriteString(envVar)
 	b.WriteString(" for the ")
 	if m.DisplayName != "" {
-		b.WriteString(m.DisplayName)
+		b.WriteString(displayNameForConcat(m.DisplayName))
 	} else {
 		b.WriteString(m.APIName)
 	}

← 2b7a51e1 feat(cli,skills): generator-time MCP description enrichment  ·  back to Cli Printing Press  ·  feat(skills): add /printing-press-import to bring published 262ab876 →