← 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
M internal/generator/generator_test.goM internal/generator/templates/mcp_code_orch.go.tmplM internal/generator/templates/mcp_tools.go.tmplM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
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 →