Comparing a live tools/list against --help on each subcommand, seven arguments exist on the CLI and not on the MCP tool that mirrors it. Every one of them is a lever an agent cannot reach — and the MCP schema is the only surface an agent has, with no --help to fall back on.
| Tool |
On the CLI |
Over MCP |
digest |
--detail, --no-private / -fields / -docs / -attrs / -lines, --preset |
still the pre-#37 pair include_private / include_fields |
map |
--preset, --include-private, --include-fields |
absent |
callers |
--tests, --exclude-tests |
absent |
reverse-deps |
--tests, --exclude-tests |
absent |
search |
--rebuild |
absent |
context |
--rebuild |
absent |
impact |
--rebuild |
absent |
show |
<TARGET>..., several targets |
path is a single string |
digest is the widest gap: #37 made map and digest one command with three orthogonal axes, and the MCP schema kept the two-flag form the change replaced. An agent asking for digest over MCP cannot request --detail signatures, which is the middle ground between a bare-name digest and a full map.
show may be a decision rather than an omission — a single path still accepts a glob, so the multi-target grammar is reachable in spirit. Worth confirming either way, since #49 established that a schema which under-promises costs an agent a capability it has no way to discover.
Drift runs the other way too, and #49 already fixed one instance: rebuild was declared on callers / callees / trace but absent from their args structs, so serde dropped the key and the tools advertised a lever they did not have.
Why nothing catches this
The two surfaces are declared in different languages in different files: clap derives in src/lib.rs, a hand-written JSON literal in src/mcp/tools.rs (plus src/impact.rs, src/context.rs, and src/search/mcp.rs, which declare their own args). #49's guards check that an argument which exists describes itself. Neither one asks whether it exists on both sides.
#57 closes the value half — defaults now come from src/defaults.rs, so a retuned number cannot land on one surface alone, and three guards fail the build on a default declared anywhere else. Argument existence is untouched by that.
Proposed guard
A cli_mcp_parity test comparing the argument names of Cli::command() against tools::list(), with an explicit allowlist of intended differences:
- the six maintenance subcommands (
install, uninstall, status, hook, prompt, mcp), which have no MCP tool by design;
--compact, which shapes text output that MCP does not render;
--help;
- whatever the
show target grammar turns out to be.
The allowlist is the deliverable as much as the test is: it turns "these two happen to differ" into "these two differ on purpose, and here is the list", which is what a reviewer needs to judge the next change.
Related: #56 records the convention in AGENTS.md, #57 shares the defaults.
Comparing a live
tools/listagainst--helpon each subcommand, seven arguments exist on the CLI and not on the MCP tool that mirrors it. Every one of them is a lever an agent cannot reach — and the MCP schema is the only surface an agent has, with no--helpto fall back on.digest--detail,--no-private/-fields/-docs/-attrs/-lines,--presetinclude_private/include_fieldsmap--preset,--include-private,--include-fieldscallers--tests,--exclude-testsreverse-deps--tests,--exclude-testssearch--rebuildcontext--rebuildimpact--rebuildshow<TARGET>..., several targetspathis a single stringdigestis the widest gap: #37 mademapanddigestone command with three orthogonal axes, and the MCP schema kept the two-flag form the change replaced. An agent asking fordigestover MCP cannot request--detail signatures, which is the middle ground between a bare-name digest and a full map.showmay be a decision rather than an omission — a singlepathstill accepts a glob, so the multi-target grammar is reachable in spirit. Worth confirming either way, since #49 established that a schema which under-promises costs an agent a capability it has no way to discover.Drift runs the other way too, and #49 already fixed one instance:
rebuildwas declared oncallers/callees/tracebut absent from their args structs, so serde dropped the key and the tools advertised a lever they did not have.Why nothing catches this
The two surfaces are declared in different languages in different files: clap derives in
src/lib.rs, a hand-written JSON literal insrc/mcp/tools.rs(plussrc/impact.rs,src/context.rs, andsrc/search/mcp.rs, which declare their own args). #49's guards check that an argument which exists describes itself. Neither one asks whether it exists on both sides.#57 closes the value half — defaults now come from
src/defaults.rs, so a retuned number cannot land on one surface alone, and three guards fail the build on a default declared anywhere else. Argument existence is untouched by that.Proposed guard
A
cli_mcp_paritytest comparing the argument names ofCli::command()againsttools::list(), with an explicit allowlist of intended differences:install,uninstall,status,hook,prompt,mcp), which have no MCP tool by design;--compact, which shapes text output that MCP does not render;--help;showtarget grammar turns out to be.The allowlist is the deliverable as much as the test is: it turns "these two happen to differ" into "these two differ on purpose, and here is the list", which is what a reviewer needs to judge the next change.
Related: #56 records the convention in
AGENTS.md, #57 shares the defaults.