[object Object]

← 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

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 →