← back to Cli Printing Press
fix(cli): OpenAPI parser prefers description over summary on operations (#411)
cb91024d4b6a216ad15f4f8f4683d1f375c2ce50 · 2026-04-29 21:35:43 -0700 · Trevin Chow
selectDescription's prior rule "if summary contains spaces, use it"
inverted the priority for the common case where a multi-word summary
sits alongside a rich long-form description. The OpenAPI convention
is summary=title, description=prose; when both exist, description
carries the agent-useful detail and should win.
Concrete cost: scrape-creators's spec carries rich descriptions on
nearly every endpoint (e.g., the credit-balance route's description
is "Returns the number of API credits remaining on your Scrape
Creators account..."), but the parser surfaced the summary "Get
credit balance" instead. tools-audit then flagged 47 endpoints as
thin-mcp-description even though the source material was rich.
After this change a single mcp-sync rerun lifts all 47 endpoints
above the threshold using the descriptions the spec already had.
The fix flips the priority: description wins when present and at
least as long as summary; otherwise fall back to summary (with
humanization for single-word/mangled operationID cases). The
length comparison guards against the rare placeholder case
(description = "TODO" with a longer summary).
A new TestSelectDescription locks the behavior with the
scrape-creators concrete case ("Get credit balance" vs. the long
description), single-word summary case (TikTok "Profile"), the
fallback path (description empty), the placeholder case, and the
mangled-operationID humanization path.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M internal/openapi/parser.goM internal/openapi/parser_test.go
Diff
commit cb91024d4b6a216ad15f4f8f4683d1f375c2ce50
Author: Trevin Chow <trevin@trevinchow.com>
Date: Wed Apr 29 21:35:43 2026 -0700
fix(cli): OpenAPI parser prefers description over summary on operations (#411)
selectDescription's prior rule "if summary contains spaces, use it"
inverted the priority for the common case where a multi-word summary
sits alongside a rich long-form description. The OpenAPI convention
is summary=title, description=prose; when both exist, description
carries the agent-useful detail and should win.
Concrete cost: scrape-creators's spec carries rich descriptions on
nearly every endpoint (e.g., the credit-balance route's description
is "Returns the number of API credits remaining on your Scrape
Creators account..."), but the parser surfaced the summary "Get
credit balance" instead. tools-audit then flagged 47 endpoints as
thin-mcp-description even though the source material was rich.
After this change a single mcp-sync rerun lifts all 47 endpoints
above the threshold using the descriptions the spec already had.
The fix flips the priority: description wins when present and at
least as long as summary; otherwise fall back to summary (with
humanization for single-word/mangled operationID cases). The
length comparison guards against the rare placeholder case
(description = "TODO" with a longer summary).
A new TestSelectDescription locks the behavior with the
scrape-creators concrete case ("Get credit balance" vs. the long
description), single-word summary case (TikTok "Profile"), the
fallback path (description empty), the placeholder case, and the
mangled-operationID humanization path.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
internal/openapi/parser.go | 23 ++++++-------
internal/openapi/parser_test.go | 73 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+), 13 deletions(-)
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 21c15680..cf0ee5fb 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -2918,20 +2918,18 @@ func isRequired(required map[string]struct{}, name string) bool {
return ok
}
+// selectDescription chooses between an OpenAPI operation's summary and
+// description. OpenAPI convention has summary as a one-line title and
+// description as long-form prose; when both exist, description carries
+// the richer agent-useful detail and wins. Fall back to summary when
+// description is empty, or when description is shorter than summary
+// (rare: placeholder strings like "TODO" or a truncated migration
+// artifact). Single-word/mangled summaries get humanized in the
+// fallback path.
func selectDescription(summary, description string) string {
- summaryHasSpaces := summary != "" && strings.ContainsAny(summary, " \t")
-
- // If summary is a real sentence (has spaces), prefer it
- if summaryHasSpaces {
- return summary
- }
-
- // Summary is a single word or empty - prefer the description if available
- if description != "" {
+ if description != "" && len(description) >= len(summary) {
return description
}
-
- // No description - use summary, humanizing if it looks like a mangled operationID
if summary != "" {
if shouldHumanizeDescription(summary) {
return humanizeDescription(summary)
@@ -2941,8 +2939,7 @@ func selectDescription(summary, description string) string {
}
return summary
}
-
- return ""
+ return description
}
func detectPagination(params []spec.Param, op *openapi3.Operation) *spec.Pagination {
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index ef5b2bbe..c4620edf 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -1148,3 +1148,76 @@ func mustMarshalJSON(t *testing.T, v any) []byte {
require.NoError(t, err)
return data
}
+
+// TestSelectDescription locks in that the OpenAPI parser prefers
+// the long-form `description` over the short `summary` when both are
+// present. The earlier rule ("if summary has spaces, use it")
+// inverted the priority for the common case where a multi-word
+// summary sits alongside a rich description, and was the root cause
+// behind 47 thin-mcp-description findings on the scrape-creators
+// CLI even though every endpoint had rich source description text.
+func TestSelectDescription(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ summary string
+ description string
+ want string
+ }{
+ {
+ name: "rich description wins over multi-word summary",
+ summary: "Get credit balance",
+ description: "Returns the number of API credits remaining on your Scrape Creators account.",
+ want: "Returns the number of API credits remaining on your Scrape Creators account.",
+ },
+ {
+ name: "rich description wins over single-word summary",
+ summary: "Profile",
+ description: "Fetches public profile data for a TikTok user by their handle.",
+ want: "Fetches public profile data for a TikTok user by their handle.",
+ },
+ {
+ name: "summary used when description empty",
+ summary: "Get the user",
+ description: "",
+ want: "Get the user",
+ },
+ {
+ name: "shorter description (placeholder) falls back to summary",
+ summary: "Returns the order with full line items and shipping address",
+ description: "TODO",
+ want: "Returns the order with full line items and shipping address",
+ },
+ {
+ name: "mangled operationID summary is humanized when alone",
+ summary: "GetUserById",
+ description: "",
+ want: "Get user by id",
+ },
+ {
+ name: "both empty returns empty",
+ summary: "",
+ description: "",
+ want: "",
+ },
+ {
+ name: "description-only case",
+ summary: "",
+ description: "Returns recent orders.",
+ want: "Returns recent orders.",
+ },
+ {
+ name: "description equal length to summary still prefers description",
+ summary: "abc",
+ description: "xyz",
+ want: "xyz",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ got := selectDescription(tt.summary, tt.description)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
← 0683ef1f fix(skills): drop context: fork from polish so AskUserQuesti
·
back to Cli Printing Press
·
fix(skills): polish syncs novel_features; publish detects sq fe5d65c5 →