bug(hooks,mcp): hand-written madt_ call strings drift from the registered tool surface #706
Labels
No labels
breaking
bug
documentation
enhancement
epic
good first issue
help wanted
refactoring
resolution/duplicate
resolution/invalid
resolution/wontfix
security
severity/critical
severity/high
severity/low
severity/medium
status/abandoned
status/blocked
status/needs-decision
status/needs-info
status/needs-verification
testing
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sixfold-space/madtea#706
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happens
My hook deny text and parts of my MCP text print an agent-facing call in the bare form
madt_<tool>(action="..."). The worktree guard is the clearest case. Its list branch prints:madt_worktrees(action="list") # MCP / agent session(
hooks/scripts/check-git-worktree.sh:192; the same bare form at:152,:165,:166,:179, and the header at:23-26.)An MCP client does not register the tool under that bare name. A client that namespaces a server's tools (the common case) exposes it under a prefixed id, for example
mcp__madtea__madt_worktrees. The baremadt_worktreesis not that id, so an agent that copies the printed call gets "no such tool available". A fixed bare string cannot name the callable id for every client, because the hook and the text cannot know the client's prefix.Against the decision record
ADR 0008 (the operation registry is the source of record) already removes this drift for one layer. The
internal/opspecregistry is the single source of record for each operation's description, and both the CLI and the MCP schema render from it, so their descriptions cannot drift. Its drift guards run against that registry.This issue does not fork ADR 0008. It extends the same principle to three surfaces the registry does not cover, and that have no drift guard:
internal/opspec, and they hand-writemadt_*(action=...)call strings.internal/mcp/server.go:306,312hand-writes "Call these madt_* tools directly" and "Use the madtea tools (madt_finish, madt_clone, etc.)".internal/mcp/tools_help.go:780andinternal/mcp/prompts.go:67repeat the bare form.internal/mcp/foreignrepo.go:259,271,291,371printmadt_worktrees(action="add", ...),madt_commit(...), andmadt_finish(...)as the call to run.The surfaces that drift
Plugin hooks (each hand-writes
madt_*(action=...)):hooks/scripts/check-git-worktree.shhooks/scripts/check-git-cli.shhooks/scripts/check-git-local.shhooks/scripts/check-git-remote.sh(:408)hooks/scripts/check-foreign-primary.sh(:53)hooks/scripts/check-git-commit.shhooks/scripts/surface-tool-error.shMCP text outside the rendered descriptions:
internal/mcp/server.go:306,312,internal/mcp/tools_help.go:780,internal/mcp/prompts.go:67,internal/mcp/foreignrepo.go:259,271,291,371.Why it drifts
The hand-written call strings and the registered tool surface share no source. Three ways they fall out of step, none caught today:
Two open issues are the same family inside the rendered layer: #686 (the
madt_finishschema advertisessummary=@file, which the MCP server never implements) and #690 (two tool names differ by one letter).Fix (options)
I have not picked the approach.
internal/parity/andinternal/mcp/description_drift.go, that scans the hook scripts and the hand-written MCP text formadt_<tool>(action="...")strings, and fails the gate when a string names a tool or an action thatinternal/opspecdoes not define.internal/opspec, so no hand-written surface can name a tool or an action that does not exist.Acceptance criteria
madt_tool or action thatinternal/opspecdoes not define.