[object Object]

← back to Cli Printing Press

fix(cli): MCP template ports — NoCache=true + codeOrch stopword filter (refs #515 F1, F4) (#521)

734e00d14d73c55de921c8840910900b6ff041cd · 2026-05-02 19:42:11 -0700 · Trevin Chow

* fix(cli): MCP template ports — NoCache=true + codeOrch stopword filter

Two template fixes from the Dub session retro #515 that landed in Dub's
library copy as polish but had not propagated to generator templates.
Without these template ports, every newly-generated CLI ships with the
same bugs.

NoCache=true (F1):
  newMCPClient() did not disable the on-disk response cache. Agents
  driving the API through MCP need fresh data every call — the cache
  survives across MCP server invocations, so a DELETE/PATCH followed
  by a GET would return the pre-mutation snapshot for up to the cache
  TTL. Verified end-to-end during Dub's MCP smoke test on 2026-05-02:
  delete-via-MCP returned success, then get-via-MCP returned the stale
  cached record while a direct curl confirmed the upstream had 404'd.
  Fix: c.NoCache = true after client.New(...). Interactive CLI commands
  construct their own client and are unaffected.

codeOrch stopword filter (F4):
  codeOrchKeywords() filtered tokens with len < 2 and no stopword set.
  Two-letter words like "is"/"in" inside endpoint descriptions matched
  every query token via the substring-contains rank rule, polluting
  results. Search for "list links" against Dub's 53 endpoints ranked
  partners_create-link above links_get because "links" matched both
  paths' keywords AND the substring "is" inside "is enrolled" matched
  "list". Fix: 28-word stopword set + bump min-length to 3. After fix,
  all 7 representative test queries return the correct top endpoint.

Both fixes are universal — every new CLI gets them automatically.
Existing library CLIs need backfill: F1 affects 38 CLIs (every CLI
with an MCP server except dub which got the polish fix in this run);
F4 affects 0 CLIs (dub is the only one with code_orch.go and it
already has the polish fix). The library backfill ships as a separate
PR against printing-press-library.

Tests:
  - TestGenerateMCPNewClientSkipsCache: positive (NoCache present) +
    negative (old inline-return shape is gone)
  - TestGenerateMCPCodeOrchKeywordsHasStopwordFilter: positive
    (stopword set + 3-char min) + negative (old len < 2 filter is gone)
  - Golden generate-golden-api updated to reflect NoCache injection
  - Full internal/generator test suite + golden verify pass

Refs #515 F1, #515 F4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): simplify MCP template tests per /simplify review

- Remove dated/incident references from test docstrings (AGENTS.md
  forbids dates, ticket numbers in code comments — those belong in
  the PR description and commit message).
- Drop brittle negative assertions ("NotContains" of exact prior-shape
  strings). The positive assertions already prove the new shape; the
  negatives would false-trigger on innocuous template tweaks like a
  timeout-literal change.
- Loosen stopword key assertions from `"is": true` to just `"is"` so
  a future gofmt reflow of the map literal doesn't break the tests.

No behavioral change. Same tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): address /review findings on PR 521

Four fixes from the parallel review pass:

- mcp_tools.go.tmpl comment: drop the macOS-specific cache path
  (~/Library/Caches/...) since Linux/XDG users have a different path
  and the WHY doesn't need the literal location. Also tighten the
  "Skip the cache for the MCP path" sentence which lightly restated
  what c.NoCache = true already says.

- mcp_code_orch.go.tmpl comments: drop the trailing "Combined with a
  3-character minimum, the stopword filter removes the worst-of-class
  noise" and the "Tokens shorter than 3 characters and stopwords
  (above) are filtered out" — both restate WHAT the adjacent code
  visibly does. The concrete failure-mode example (kw.contains(t)
  || t.contains(kw) substring matcher) is the load-bearing WHY and
  stays.

- TestGenerateMCPNewClientSkipsCache: split into 3 subtests covering
  the SpecSource=sniffed branch (rate-limited client.New(...,2)
  variant) and a negative assertion that the interactive CLI's
  newClient drives cache from --no-cache flag (c.NoCache = f.noCache),
  not statically. Refactored shared scaffolding into
  assertNewMCPClientSkipsCache helper.

- Golden fixture refreshed to match the simplified comment block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 734e00d14d73c55de921c8840910900b6ff041cd
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Sat May 2 19:42:11 2026 -0700

    fix(cli): MCP template ports — NoCache=true + codeOrch stopword filter (refs #515 F1, F4) (#521)
    
    * fix(cli): MCP template ports — NoCache=true + codeOrch stopword filter
    
    Two template fixes from the Dub session retro #515 that landed in Dub's
    library copy as polish but had not propagated to generator templates.
    Without these template ports, every newly-generated CLI ships with the
    same bugs.
    
    NoCache=true (F1):
      newMCPClient() did not disable the on-disk response cache. Agents
      driving the API through MCP need fresh data every call — the cache
      survives across MCP server invocations, so a DELETE/PATCH followed
      by a GET would return the pre-mutation snapshot for up to the cache
      TTL. Verified end-to-end during Dub's MCP smoke test on 2026-05-02:
      delete-via-MCP returned success, then get-via-MCP returned the stale
      cached record while a direct curl confirmed the upstream had 404'd.
      Fix: c.NoCache = true after client.New(...). Interactive CLI commands
      construct their own client and are unaffected.
    
    codeOrch stopword filter (F4):
      codeOrchKeywords() filtered tokens with len < 2 and no stopword set.
      Two-letter words like "is"/"in" inside endpoint descriptions matched
      every query token via the substring-contains rank rule, polluting
      results. Search for "list links" against Dub's 53 endpoints ranked
      partners_create-link above links_get because "links" matched both
      paths' keywords AND the substring "is" inside "is enrolled" matched
      "list". Fix: 28-word stopword set + bump min-length to 3. After fix,
      all 7 representative test queries return the correct top endpoint.
    
    Both fixes are universal — every new CLI gets them automatically.
    Existing library CLIs need backfill: F1 affects 38 CLIs (every CLI
    with an MCP server except dub which got the polish fix in this run);
    F4 affects 0 CLIs (dub is the only one with code_orch.go and it
    already has the polish fix). The library backfill ships as a separate
    PR against printing-press-library.
    
    Tests:
      - TestGenerateMCPNewClientSkipsCache: positive (NoCache present) +
        negative (old inline-return shape is gone)
      - TestGenerateMCPCodeOrchKeywordsHasStopwordFilter: positive
        (stopword set + 3-char min) + negative (old len < 2 filter is gone)
      - Golden generate-golden-api updated to reflect NoCache injection
      - Full internal/generator test suite + golden verify pass
    
    Refs #515 F1, #515 F4.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * refactor(cli): simplify MCP template tests per /simplify review
    
    - Remove dated/incident references from test docstrings (AGENTS.md
      forbids dates, ticket numbers in code comments — those belong in
      the PR description and commit message).
    - Drop brittle negative assertions ("NotContains" of exact prior-shape
      strings). The positive assertions already prove the new shape; the
      negatives would false-trigger on innocuous template tweaks like a
      timeout-literal change.
    - Loosen stopword key assertions from `"is": true` to just `"is"` so
      a future gofmt reflow of the map literal doesn't break the tests.
    
    No behavioral change. Same tests pass.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * refactor(cli): address /review findings on PR 521
    
    Four fixes from the parallel review pass:
    
    - mcp_tools.go.tmpl comment: drop the macOS-specific cache path
      (~/Library/Caches/...) since Linux/XDG users have a different path
      and the WHY doesn't need the literal location. Also tighten the
      "Skip the cache for the MCP path" sentence which lightly restated
      what c.NoCache = true already says.
    
    - mcp_code_orch.go.tmpl comments: drop the trailing "Combined with a
      3-character minimum, the stopword filter removes the worst-of-class
      noise" and the "Tokens shorter than 3 characters and stopwords
      (above) are filtered out" — both restate WHAT the adjacent code
      visibly does. The concrete failure-mode example (kw.contains(t)
      || t.contains(kw) substring matcher) is the load-bearing WHY and
      stays.
    
    - TestGenerateMCPNewClientSkipsCache: split into 3 subtests covering
      the SpecSource=sniffed branch (rate-limited client.New(...,2)
      variant) and a negative assertion that the interactive CLI's
      newClient drives cache from --no-cache flag (c.NoCache = f.noCache),
      not statically. Refactored shared scaffolding into
      assertNewMCPClientSkipsCache helper.
    
    - Golden fixture refreshed to match the simplified comment block.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator_test.go               | 87 ++++++++++++++++++++++
 internal/generator/templates/mcp_code_orch.go.tmpl | 20 ++++-
 internal/generator/templates/mcp_tools.go.tmpl     | 13 +++-
 .../printing-press-golden/internal/mcp/tools.go    |  9 ++-
 4 files changed, 123 insertions(+), 6 deletions(-)

diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 3c1cad90..17432509 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -5549,6 +5549,93 @@ func TestGenerateMCPCodeOrchestrationSkippedByDefault(t *testing.T) {
 	assert.True(t, os.IsNotExist(err), "code_orch.go must not be emitted without orchestration: code")
 }
 
+// TestGenerateMCPNewClientSkipsCache proves that newMCPClient sets
+// c.NoCache = true. Agents calling through MCP need fresh data every call;
+// the on-disk response cache survives across MCP server invocations, so
+// without this the next GET after a DELETE/PATCH returns the pre-mutation
+// snapshot for up to the cache TTL. Interactive CLI commands construct
+// their own client and are unaffected.
+func TestGenerateMCPNewClientSkipsCache(t *testing.T) {
+	t.Parallel()
+
+	t.Run("default spec source", func(t *testing.T) {
+		t.Parallel()
+		assertNewMCPClientSkipsCache(t, "")
+	})
+
+	// SpecSource=sniffed takes the rate-limited branch in the template
+	// (client.New(cfg, 30*time.Second, 2) instead of ..., 0). The cache
+	// bypass must apply on both branches; this guards against a future
+	// edit moving NoCache=true inside one of the if/else arms.
+	t.Run("sniffed spec source", func(t *testing.T) {
+		t.Parallel()
+		assertNewMCPClientSkipsCache(t, "sniffed")
+	})
+
+	t.Run("interactive CLI client is not statically NoCache=true", func(t *testing.T) {
+		t.Parallel()
+		apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "loops.yaml"))
+		require.NoError(t, err)
+
+		outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+		require.NoError(t, New(apiSpec, outputDir).Generate())
+
+		rootData, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
+		require.NoError(t, err)
+		rootBody := string(rootData)
+
+		assert.Contains(t, rootBody, "c.NoCache = f.noCache",
+			"interactive CLI's newClient must drive cache from --no-cache flag, not statically")
+	})
+}
+
+// assertNewMCPClientSkipsCache generates a CLI from loops.yaml with the
+// given SpecSource and asserts the MCP server's newMCPClient bypasses the
+// response cache.
+func assertNewMCPClientSkipsCache(t *testing.T, specSource string) {
+	t.Helper()
+	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "loops.yaml"))
+	require.NoError(t, err)
+	apiSpec.SpecSource = specSource
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	toolsData, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "tools.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(toolsData), "c.NoCache = true",
+		"newMCPClient must disable the response cache so MCP-driven reads see fresh state across mutations")
+}
+
+// TestGenerateMCPCodeOrchKeywordsHasStopwordFilter proves the keyword
+// extractor in the code-orchestration thin surface filters short tokens
+// and a stopword set. Without this, two-letter substrings like "is"/"in"
+// inside endpoint descriptions match every query token via the
+// substring-contains rank rule, polluting search results.
+func TestGenerateMCPCodeOrchKeywordsHasStopwordFilter(t *testing.T) {
+	t.Parallel()
+
+	apiSpec, err := spec.Parse(filepath.Join("..", "..", "testdata", "loops.yaml"))
+	require.NoError(t, err)
+	apiSpec.MCP = spec.MCPConfig{Orchestration: "code"}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	codeOrchData, err := os.ReadFile(filepath.Join(outputDir, "internal", "mcp", "code_orch.go"))
+	require.NoError(t, err)
+	body := string(codeOrchData)
+
+	assert.Contains(t, body, "var codeOrchStopwords = map[string]bool{",
+		"code_orch.go must declare codeOrchStopwords")
+	assert.Contains(t, body, `len(tok) < 3 || codeOrchStopwords[tok]`,
+		"keyword extractor must filter short tokens and stopwords")
+	for _, sw := range []string{`"is"`, `"in"`, `"the"`, `"and"`} {
+		assert.Contains(t, body, sw, "stopword set missing %q", sw)
+	}
+}
+
 // TestGenerateMCPIntentsEmittedWhenDeclared proves that a spec with mcp.intents
 // emits internal/mcp/intents.go, wires the intent handler into RegisterTools
 // via RegisterIntents, and keeps endpoint-mirror tools by default.
diff --git a/internal/generator/templates/mcp_code_orch.go.tmpl b/internal/generator/templates/mcp_code_orch.go.tmpl
index 59ea86df..7aa8cf02 100644
--- a/internal/generator/templates/mcp_code_orch.go.tmpl
+++ b/internal/generator/templates/mcp_code_orch.go.tmpl
@@ -93,6 +93,20 @@ var codeOrchEndpoints = []codeOrchEndpoint{
 {{- end}}
 }
 
+// codeOrchStopwords filters two-letter and short common-word substrings
+// that pollute ranking via the substring-contains rule. Without them, a
+// search for "list links" matches every endpoint whose description
+// contains "is enrolled" because "is" is two chars and the matcher
+// accepts kw.contains(t) || t.contains(kw).
+var codeOrchStopwords = map[string]bool{
+	"a": true, "an": true, "and": true, "are": true, "as": true,
+	"at": true, "be": true, "by": true, "for": true, "from": true,
+	"has": true, "in": true, "is": true, "it": true, "its": true,
+	"of": true, "on": true, "or": true, "that": true, "the": true,
+	"this": true, "to": true, "was": true, "will": true, "with": true,
+	"your": true, "you": true, "any": true, "all": true,
+}
+
 // codeOrchKeywords produces the lowercase token stream used for search
 // ranking. Defined at package level so the registry initializer can call it
 // inline above without pulling in a separate precompute step.
@@ -105,11 +119,13 @@ func codeOrchKeywords(resource, endpoint, summary, path string) []string {
 		}
 		return r
 	}, raw)
-	out := make([]string, 0, 8)
+	out := make([]string, 0, 16)
+	seen := map[string]bool{}
 	for _, tok := range strings.Fields(raw) {
-		if len(tok) < 2 {
+		if len(tok) < 3 || codeOrchStopwords[tok] || seen[tok] {
 			continue
 		}
+		seen[tok] = true
 		out = append(out, tok)
 	}
 	return out
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index 06ddfb62..5227f699 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -291,10 +291,17 @@ func newMCPClient() (*client.Client, error) {
 		return nil, fmt.Errorf("loading config: %w", err)
 	}
 {{- if eq .SpecSource "sniffed"}}
-	return client.New(cfg, 30*time.Second, 2), nil
+	c := client.New(cfg, 30*time.Second, 2)
 {{- else}}
-	return client.New(cfg, 30*time.Second, 0), nil
-{{- end}}
+	c := client.New(cfg, 30*time.Second, 0)
+{{- end}}
+	// Agents calling through MCP need fresh data every call. The on-disk
+	// response cache survives across MCP server invocations, so a
+	// DELETE/PATCH followed by a GET would otherwise return the
+	// pre-mutation snapshot for up to the cache TTL. The interactive CLI
+	// constructs its own client and is unaffected.
+	c.NoCache = true
+	return c, nil
 }
 
 func dbPath() string {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
index a46e683a..93032f94 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
@@ -255,7 +255,14 @@ func newMCPClient() (*client.Client, error) {
 	if err != nil {
 		return nil, fmt.Errorf("loading config: %w", err)
 	}
-	return client.New(cfg, 30*time.Second, 0), nil
+	c := client.New(cfg, 30*time.Second, 0)
+	// Agents calling through MCP need fresh data every call. The on-disk
+	// response cache survives across MCP server invocations, so a
+	// DELETE/PATCH followed by a GET would otherwise return the
+	// pre-mutation snapshot for up to the cache TTL. The interactive CLI
+	// constructs its own client and is unaffected.
+	c.NoCache = true
+	return c, nil
 }
 
 func dbPath() string {

← 358e46a6 fix(cli): scorer respects runtime MCP surface selection (ref  ·  back to Cli Printing Press  ·  chore(main): release 3.6.2 (#520) d5594764 →