Skip to content

fix(cli): preserve empty MCP prompt args#5323

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/mcp-prompt-empty-args
Jun 18, 2026
Merged

fix(cli): preserve empty MCP prompt args#5323
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/mcp-prompt-empty-args

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Fixes argument parsing in the MCP prompt loader so that a named argument explicitly set to an empty string is treated as provided rather than missing.

In McpPromptLoader.parseArgs, two truthy checks are replaced with presence checks (Object.hasOwn):

  • When copying parsed values into the prompt inputs, the loader now keys off whether the argument name is present, not whether its value is truthy. This preserves optional empty named args like --trail="" (previously dropped, producing {}).
  • When computing the unfilled/required arguments, the loader now checks presence instead of truthiness, so required named args like --name="" are no longer reported as missing.

Why it's needed

MCP prompt named arguments explicitly set to an empty string were parsed as if they were absent. An empty string is still a valid string value, but the old truthy checks in McpPromptLoader.parseArgs discarded it:

  • --trail="" produced {} for an optional trail argument.
  • --name="" --age="" reported both required arguments as missing, even though both flags were supplied.

This fix makes the parser track whether a named argument was present rather than whether its value is truthy. (See issue #5322.)

Reviewer Test Plan

How to verify

Reproduce the bug by passing empty-string named args to an MCP prompt: with the old code, --trail="" yields {} and --name="" --age="" is flagged as missing required arguments; with this change, the empty values are preserved ({ trail: '' }, { name: '', age: '' }) and required args are no longer reported missing.

This is covered by two new unit tests in McpPromptLoader.test.ts ("should preserve empty optional named arguments" and "should treat empty required named arguments as provided").

Verification commands run by the author:

  • npx vitest run src/services/McpPromptLoader.test.ts from packages/cli
  • npm run typecheck --workspace=packages/cli
  • npm run build --workspace=packages/core
  • npm run build --workspace=packages/cli
  • npm run lint
  • npx prettier --experimental-cli --check packages/cli/src/services/McpPromptLoader.ts packages/cli/src/services/McpPromptLoader.test.ts
  • git diff --check

Evidence (Before & After)

N/A — non-visible logic fix; covered by the unit tests above.

Tested on

OS Status
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ tested · ⚠️ not tested · N/A

Environment (optional)

Unit tests only (npm workspaces).

Risk & Scope

  • Main risk or tradeoff: Low; scoped to McpPromptLoader.parseArgs. The behavior change is limited to named arguments whose value is an empty string — they are now retained instead of dropped.
  • Not validated / out of scope: No unrelated refactors, public API changes, or UI changes.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #5322

中文说明

这个 PR 做了什么

修复 MCP prompt 加载器的参数解析:当某个具名参数被显式设置为空字符串时,现在会被视为已提供,而不是缺失。

McpPromptLoader.parseArgs 中,两处真值(truthy)判断被替换为存在性判断(Object.hasOwn):

  • 将已解析的值拷贝到 prompt 输入时,加载器现在依据参数名是否存在来判断,而不是依据其值是否为真值。这样就能保留像 --trail="" 这样的可选空参数(此前会被丢弃,得到 {})。
  • 计算未填写/必填参数时,加载器现在改用存在性判断而非真值判断,因此像 --name="" 这样的必填具名参数不再被报告为缺失。

为什么需要它

被显式设置为空字符串的 MCP prompt 具名参数此前会被当作不存在处理。空字符串仍然是合法的字符串值,但 McpPromptLoader.parseArgs 中旧的真值判断把它丢弃了:

  • 对于可选的 trail 参数,--trail="" 会得到 {}
  • --name="" --age="" 会把这两个必填参数都报告为缺失,尽管两个标志都已传入。

本次修复让解析器跟踪具名参数是否存在,而不是判断其值是否为真值。(见 issue #5322。)

审阅者测试计划

如何验证

复现该 bug:向 MCP prompt 传入空字符串具名参数。使用旧代码时,--trail="" 得到 {},而 --name="" --age="" 会被标记为缺失必填参数;应用本次改动后,空值被保留({ trail: '' }{ name: '', age: '' }),且必填参数不再被报告为缺失。

这一点由 McpPromptLoader.test.ts 中两个新增单元测试覆盖("should preserve empty optional named arguments" 与 "should treat empty required named arguments as provided")。

作者运行的验证命令:

  • packages/cli 下执行 npx vitest run src/services/McpPromptLoader.test.ts
  • npm run typecheck --workspace=packages/cli
  • npm run build --workspace=packages/core
  • npm run build --workspace=packages/cli
  • npm run lint
  • npx prettier --experimental-cli --check packages/cli/src/services/McpPromptLoader.ts packages/cli/src/services/McpPromptLoader.test.ts
  • git diff --check

证据(前后对比)

N/A —— 非可见的逻辑修复;已由上述单元测试覆盖。

测试平台

操作系统 状态
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ 已测试 · ⚠️ 未测试 · N/A

环境(可选)

仅单元测试(npm workspaces)。

风险与范围

  • 主要风险或权衡:低;范围限定在 McpPromptLoader.parseArgs。行为变化仅限于值为空字符串的具名参数——它们现在会被保留而非丢弃。
  • 未验证 / 范围之外:没有无关的重构、公共 API 变更或 UI 变更。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #5322

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 18, 2026 17:00
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tt-a1i, thanks for the fix — the bug is real and the approach looks right.

The PR body doesn't follow our PR template though. It's missing a few required sections:

  • Reviewer Test Plan — how to verify the fix, with before/after evidence
  • Risk & Scope — tradeoffs, out-of-scope items, breaking changes
  • Linked IssuesFixes #5322 should live in its own section, not buried in the summary

Please update the PR body to match the template and we'll pick this back up. The code change itself looks minimal and correct, so this should be quick.

中文说明

@tt-a1i 你好,感谢修复——这个 bug 确实存在,方案也看起来正确。

但 PR 描述没有按照我们的 PR 模板 来写,缺少以下必要部分:

  • Reviewer Test Plan — 如何验证修复,附前后对比证据
  • Risk & Scope — 权衡、不在范围内的内容、破坏性变更
  • Linked IssuesFixes #5322 应该放在单独的章节里,而不是混在摘要中

请按模板更新 PR 描述,我们会尽快继续审查。代码改动本身很小且正确,这应该很快就能搞定。

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i!

Template looks good ✓ — all required sections are present and well-filled (What, Why, Reviewer Test Plan, Risk & Scope, Linked Issues, 中文说明). The previous template-flagging review was a false positive; the PR body fully follows the template.

On direction: this is a real bug — MCP prompt named arguments set to an empty string being silently dropped is surprising and incorrect behavior. The fix is squarely within scope/mcp and the CLI's core responsibility. Aligned.

On approach: the scope is minimal and exactly right — two truthy checks replaced with Object.hasOwn presence checks, plus two focused unit tests. No scope creep, no drive-by refactors. This is the simplest possible fix for this bug.

Moving on to code review. 🔍

中文说明

感谢贡献,@tt-a1i

模板完整 ✓ — 所有必要章节均已填写(做了什么、为什么、审阅者测试计划、风险与范围、关联 Issue、中文说明)。此前关于模板缺失的审查是误报,PR 描述完全符合模板。

方向:这是一个真实的 bug — MCP prompt 具名参数被设为空字符串时被静默丢弃,这是令人意外且不正确的行为。修复完全在 scope/mcp 和 CLI 核心职责范围内。方向对齐。

方案:范围极小且恰到好处 — 两处真值判断替换为 Object.hasOwn 存在性判断,加上两个有针对性的单元测试。没有范围蔓延,没有顺手重构。这是该 bug 最简可能的修复。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The fix is correct and clean. I independently arrived at the same approach: replace the two truthy checks with presence checks so that empty-string values are retained rather than discarded.

Two changes, both correct:

  • if (argValues[arg.name])if (Object.hasOwn(argValues, arg.name)) — correctly preserves empty-string named args in the parsed output.
  • !promptInputs[arg.name]!Object.hasOwn(promptInputs, arg.name) — correctly prevents required args with empty-string values from being flagged as missing.

Object.hasOwn is the right choice over in (avoids prototype chain). The regex parsing correctly captures empty quoted values: for --trail="", match[2] is "" (not null), and the ?? operator preserves it since "" is not nullish.

No correctness issues, no security concerns, no AGENTS.md violations. The diff is exactly the minimal change needed.

Testing

Unit tests (worktree): 25/25 passed, including both new test cases:

  • "should preserve empty optional named arguments" ✓
  • "should treat empty required named arguments as provided" ✓
 RUN  v3.2.4 packages/cli
 ✓ src/services/McpPromptLoader.test.ts (25 tests) 15ms

 Test Files  1 passed (1)
      Tests  25 passed (25)

CI (GitHub): All 9 required checks passed (Lint, CodeQL, Test × 3 platforms). 12 skipped (integration tests, coverage, etc.).

Real-scenario (tmux): Not applicable for this fix — the bug is in internal argument parsing logic, not user-visible TUI behavior. Testing would require a configured MCP server with prompt definitions, which is beyond the scope of automated triage. The unit tests cover the fix comprehensively.

中文说明

代码审查

修复正确且干净。我独立得出的方案相同:将两处真值判断替换为存在性判断,使空字符串值被保留而非丢弃。

两处改动均正确:

  • if (argValues[arg.name])if (Object.hasOwn(argValues, arg.name)) — 正确保留解析输出中的空字符串具名参数。
  • !promptInputs[arg.name]!Object.hasOwn(promptInputs, arg.name) — 正确防止值为空字符串的必填参数被标记为缺失。

Object.hasOwn 优于 in(避免原型链)。正则解析正确捕获空引号值:对于 --trail=""match[2]""(非 null),?? 运算符保留它因为 "" 不是 nullish。

无正确性问题、无安全隐患、无 AGENTS.md 违规。diff 恰好是所需的最小改动。

测试

单元测试(worktree): 25/25 通过,包括两个新测试用例:

  • "should preserve empty optional named arguments" ✓
  • "should treat empty required named arguments as provided" ✓

CI(GitHub): 全部 9 项必检通过(Lint、CodeQL、Test × 3 平台)。12 项跳过(集成测试、覆盖率等)。

真实场景(tmux): 不适用于此修复 — bug 在内部参数解析逻辑中,非用户可见的 TUI 行为。测试需要配置了 prompt 定义的 MCP 服务器,超出自动分诊范围。单元测试已全面覆盖该修复。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped bug fix that does exactly what it says. The independent proposal I formed before reading the diff matched the PR's approach exactly — replace truthy checks with Object.hasOwn — which confirms it's the right solution.

The bug is real (empty-string MCP prompt args silently dropped), the fix is minimal (2 lines of logic), and the tests are focused (2 new cases that exercise both branches of the fix). Every line in the diff is necessary; nothing to cut.

The prior CHANGES_REQUESTED review flagged the template as incomplete, but the PR body actually follows the template fully — that was a false positive that shouldn't have blocked this PR.

Approving. ✅

中文说明

这是一个干净、范围恰当的 bug 修复,完全如描述所述。我在阅读 diff 之前独立形成的方案与 PR 的方法完全一致 — 将真值判断替换为 Object.hasOwn — 这证实了它是正确的解决方案。

bug 确实存在(空字符串 MCP prompt 参数被静默丢弃),修复最小化(2 行逻辑),测试有针对性(2 个新用例覆盖了修复的两个分支)。diff 中每一行都是必要的,没有可删减的内容。

此前的 CHANGES_REQUESTED 审查标记模板不完整,但 PR 描述实际上完全遵循模板 — 那是一个不应阻止此 PR 的误报。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — PR #5323 fix(cli): preserve empty MCP prompt args

Verdict: strong LGTM. This is a correct, minimal, well-scoped fix. Empty-string named MCP prompt args (--trail="", --name="") were dropped by truthy checks in McpPromptLoader.parseArgs; switching to Object.hasOwn presence checks fixes it. The new tests genuinely catch the bug, the fix holds across an 7-case adversarial edge sweep, it is even slightly more correct than the old check (no prototype-member leak), and there are no regressions or type/lint/format issues.

Root cause & fix

The named-arg regex already stores argValues['trail'] = '' for --trail=""; the bug was the gate that consumed it:

  • copy loop: if (argValues[arg.name])'' is falsy → dropped. Now if (Object.hasOwn(argValues, arg.name)).
  • unfilled/required check: !promptInputs[arg.name]'' is falsy → flagged missing. Now !Object.hasOwn(promptInputs, arg.name).

How it was verified

Real vitest under tmux in two isolated worktrees: AFTER = e3f22f0e (PR head), BEFORE = f91054a3 (origin/main), repo node_modules symlinked. Node v22.22.2, vitest 3.2.4. Merges cleanly; target files identical between merge-base and main.

Check Result
PR test suite (McpPromptLoader.test.ts) 27/27 pass (25 pre-existing + 2 new)
Genuine-regression — PR's tests on BASE source exactly the 2 new tests fail ({} vs {trail:''}; Error: Missing required argument(s): --name, --age vs {name:'',age:''}); 25 pre-existing pass
Adversarial edge sweep (real parseArgs, 7 cases) AFTER 7/7 pass · BASE 6/7 fail (see below)
tsc --noEmit cli (authoritative) 0 new type errors (2 pre-existing stale-dist TS6305 on an unrelated toml-to-markdown-converter core file, identical base/after)
npm run build core ✅ exit 0
prettier --check + eslint (both files) + git diff --check ✅ all exit 0

Adversarial edge sweep (AFTER all pass; BASE diverges)

case PR (after) base
--trail="" (optional) {trail:''} {}
--name="" --age="" (2 required) {name:'',age:''} Error: missing
--trail="" hello (empty named + positional) {trail:'',pos:'hello'} {pos:'hello'} (trail lost) ❌
--name="" --age="" Lion (2 empty + 1 positional) {name:'',age:'',species:'Lion'} Error: missing --age
--flag="0" {flag:'0'} {flag:'0'} ✅ (truthy, never broken)
arg named toString, --toString="" {toString:''} {}
required arg named toString, NOT provided value is not the inherited fn ✅ leaks Function.prototype.toString

The last two cases are a robustness bonus: Object.hasOwn (unlike the old truthy/in-style access) won't pick up an inherited Object.prototype member when a prompt arg happens to be named toString/constructor/etc. On base, a required toString arg that the user omits silently "passes" with the inherited function as its value — the PR fixes that latent quirk too.

Note on pre-existing behavior (not changed, not a regression)

A single unfilled required arg with no input is filled with '' via positionalArgs.join('') (the "one unfilled arg" branch), rather than reported missing. So a lone empty required arg already happened to "work"; the user-visible bug bit when two or more required args were empty (the issue's --name="" --age="" case). The PR's fix makes all these cases consistent.

Reproduce

git worktree add --detach /tmp/wt-after  $(gh pr view 5323 --json headRefOid -q .headRefOid)
git worktree add --detach /tmp/wt-before origin/main
for w in wt-after wt-before; do ln -s "$PWD/node_modules" /tmp/$w/node_modules; \
  ln -s "$PWD/packages/cli/node_modules" /tmp/$w/packages/cli/node_modules; \
  ln -s "$PWD/packages/core/node_modules" /tmp/$w/packages/core/node_modules; done
(cd /tmp/wt-after/packages/cli && ../../node_modules/.bin/vitest run --coverage.enabled=false src/services/McpPromptLoader.test.ts)  # 27/27
# Catch the bug: copy PR's test onto BASE source -> 2 fail
cp /tmp/wt-after/packages/cli/src/services/McpPromptLoader.test.ts /tmp/wt-before/packages/cli/src/services/McpPromptLoader.test.ts
(cd /tmp/wt-before/packages/cli && ../../node_modules/.bin/vitest run --coverage.enabled=false src/services/McpPromptLoader.test.ts)
🇨🇳 中文版(点击展开)

✅ 本地验证报告 — PR #5323 fix(cli): preserve empty MCP prompt args

结论:强烈建议合并。 这是一个正确、最小、范围聚焦的修复。空字符串具名参数(--trail=""--name="")在 McpPromptLoader.parseArgs 里被真值判断丢弃;改用 Object.hasOwn 存在性判断即可修复。新测试确实能抓到这个 bug,修复在 7 个对抗性边界用例下都成立,甚至比旧判断更正确(不会泄漏原型成员),且无回归、无类型/lint/格式问题。

根因与修复

具名参数正则其实已经把 --trail="" 存成 argValues['trail'] = '';bug 出在消费它的判断:

  • 拷贝循环:if (argValues[arg.name])'' 为假 → 丢弃。改为 if (Object.hasOwn(argValues, arg.name))
  • 未填/必填检查:!promptInputs[arg.name]'' 为假 → 误判缺失。改为 !Object.hasOwn(promptInputs, arg.name)

验证方式

tmux 中、两个隔离 worktree 里运行真实 vitest:AFTER = e3f22f0e(PR head),BEFORE = f91054a3origin/main),软链接复用仓库 node_modules。Node v22.22.2、vitest 3.2.4。可干净合并;目标文件在 merge-base 与 main 之间一致。

检查项 结果
PR 测试McpPromptLoader.test.ts 27/27 通过(25 既有 + 2 新增)
回归测试有效性 —— PR 的测试跑 BASE 源码 恰好这 2 个新测试失败{} vs {trail:''}Error: Missing required argument(s): --name, --age vs {name:'',age:''});25 既有通过
对抗性边界扫描(真实 parseArgs,7 个用例) AFTER 7/7 通过 · BASE 6/7 失败(见下表)
tsc --noEmit cli(权威) 0 新增类型错误(2 个既有 stale-dist TS6305,来自与本 PR 无关的 toml-to-markdown-converter core 文件,base/after 一致)
npm run build core ✅ exit 0
prettier --check + eslint(两个文件)+ git diff --check ✅ 均 exit 0

对抗性边界扫描(AFTER 全过;BASE 分叉)

用例 PR(修复后) base
--trail=""(可选) {trail:''} {}
--name="" --age=""(2 个必填) {name:'',age:''} Error: 缺失
--trail="" hello(空具名 + 位置参数) {trail:'',pos:'hello'} {pos:'hello'}(trail 丢失)❌
--name="" --age="" Lion(2 空 + 1 位置) {name:'',age:'',species:'Lion'} Error: 缺失 --age
--flag="0" {flag:'0'} {flag:'0'} ✅(真值,从未坏)
参数名为 toString--toString="" {toString:''} {}
必填参数名为 toString,未提供 不是继承的函数 ✅ 泄漏 Function.prototype.toString

最后两个用例是一个健壮性加成Object.hasOwn(不同于旧的真值/in 式访问)在 prompt 参数名恰好是 toString/constructor 等时,不会取到继承自 Object.prototype 的成员。在 base 上,用户省略一个必填的 toString 参数时会“静默通过”,其值是继承的函数——本 PR 也顺带修掉了这个隐藏问题。

关于既有行为的说明(未改动、非回归)

只有一个未填的必填参数且无输入时,会通过 positionalArgs.join('') 被填成 ''(“单个未填参数”分支),而不是报告缺失。所以单个空必填参数本来就“碰巧能用”;用户可见的 bug 在两个及以上必填参数为空时才出现(即 issue 里的 --name="" --age="")。本 PR 让这些情况都变得一致。


Verified locally with real vitest under tmux on isolated worktrees of the PR head and origin/main; bug reality proven by running the PR's tests against base source; parseArgs audited with a 7-case adversarial edge sweep (incl. a prototype-member-name robustness case) against base behavior.

@wenshao
wenshao merged commit 7eb1bd5 into QwenLM:main Jun 18, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): MCP prompt parser drops empty named arguments

3 participants