← 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
M internal/pipeline/climanifest_test.goM internal/pipeline/mcpb_manifest.go
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 →