Blocked on shopwareLabs/bash-mcp-sdk#28. Nothing to do here until that ships and the lock is bumped.
#186 landed per-call project root resolution. Every tool function opens with worktree_enter "${args}" || return 1, and worktree_conformance.bats exists to prove none was forgotten. The SDK issue proposes an optional mcp_before_tool_call the dispatcher calls before the tool function, inside the same subshell. If it lands, that call moves out of the tools.
What changes
One function in plugins/dev-tooling/shared/worktree.sh, next to worktree_enter, shared by all three servers:
mcp_before_tool_call() {
local tool_name="$1"
local args="$2"
case "${tool_name}" in
cwd|set_project_root) return 0 ;;
esac
worktree_enter "${args}"
}
cwd and set_project_root stay exempt for the reason they do not call worktree_enter today: they manage the resolution state rather than running against it, so cwd has to report a sticky root that no longer resolves and set_project_root has to be able to clear one.
worktree_enter itself does not change.
What does not move
worktree_assert_dependencies (31 sites) has to run after resolve_scope, which is per-tool, and is not uniform — mcp-server-js-storefront/lib/ludtwig.sh:33 passes the kind php from a JS server, and the three worktree_prepare tools skip it deliberately.
worktree_assert_paths_within_root (11 sites) takes the tool's own parsed paths_json.
Folding scope resolution and the dependency assertion in as well is possible and not proposed here: it trades 31 call sites for a per-tool kind-and-exemption table in the hook, which someone then has to keep correct.
Three behaviors to check, not assume
Each depends on the hook running inside the dispatch subshell rather than before it.
worktree_resolve_root installs trap 'worktree_release_owned_temp' EXIT for the merged-config temp file, relying on that trap firing when the dispatch subshell ends. A hook outside the subshell leaks that file once per call, silently.
worktree_root_banner writes to stdout and currently leads the tool output because worktree_enter is the first line. From the hook it still leads only if the hook's stdout is the same capture.
worktree_enter prints its refusal and returns 1. The refusal has to reach the caller as an isError with the same text, via the dispatcher's existing non-zero-exit path.
Pin all three in the replacement suite.
Scope
|
Lines |
| Tool libraries (32 call sites; 2 of them carry a placement comment that goes with the call) |
−69 measured |
shared/worktree.sh |
+13 |
plugin-tests/dev-tooling/worktree_conformance.bats (262 today) |
−140 to −180 estimated |
AGENTS.md, plugin README |
~+3 |
The conformance suite's two properties — every non-exempt tool resolves its root, and none runs a command against a refused root — hold by construction afterwards. What is worth keeping from it is the reconciliation of tool_* functions against tools.json, which catches an undeclared tool and has nothing to do with worktrees. Keeping that lands the saving at the low end.
worktree_resolution.bats, worktree_state.bats, worktree_php_tools.bats and worktree_js_tools.bats are untouched; they cover the module and tool behavior, not the wiring.
worktree.sh does not shrink. The reason to do this is that "every tool resolves its root before running" stops being a property a suite re-proves against an enumeration and becomes a property of the dispatcher, so a tool added later gets it without anyone remembering.
Not addressed
project_root still has to be declared in each tool's inputSchema — 24 occurrences in the PHP server, 30 in admin, 24 in storefront — because additionalProperties: false rejects an undeclared parameter. Removing that needs a different SDK feature (a schema fragment merged into every tool) and is a separate, larger proposal.
Prerequisite
A .mcp-sdk.lock bump (version= and sha256=) plus .github/scripts/vendor-mcp-sdk.sh, per .claude/rules/template-sync.md. The vendored copies are CI-gated as byte-identical, so the hook cannot be tried by patching shared/mcpserver_core.sh locally.
Blocked on shopwareLabs/bash-mcp-sdk#28. Nothing to do here until that ships and the lock is bumped.
#186 landed per-call project root resolution. Every tool function opens with
worktree_enter "${args}" || return 1, andworktree_conformance.batsexists to prove none was forgotten. The SDK issue proposes an optionalmcp_before_tool_callthe dispatcher calls before the tool function, inside the same subshell. If it lands, that call moves out of the tools.What changes
One function in
plugins/dev-tooling/shared/worktree.sh, next toworktree_enter, shared by all three servers:cwdandset_project_rootstay exempt for the reason they do not callworktree_entertoday: they manage the resolution state rather than running against it, socwdhas to report a sticky root that no longer resolves andset_project_roothas to be able to clear one.worktree_enteritself does not change.What does not move
worktree_assert_dependencies(31 sites) has to run afterresolve_scope, which is per-tool, and is not uniform —mcp-server-js-storefront/lib/ludtwig.sh:33passes the kindphpfrom a JS server, and the threeworktree_preparetools skip it deliberately.worktree_assert_paths_within_root(11 sites) takes the tool's own parsedpaths_json.Folding scope resolution and the dependency assertion in as well is possible and not proposed here: it trades 31 call sites for a per-tool kind-and-exemption table in the hook, which someone then has to keep correct.
Three behaviors to check, not assume
Each depends on the hook running inside the dispatch subshell rather than before it.
worktree_resolve_rootinstallstrap 'worktree_release_owned_temp' EXITfor the merged-config temp file, relying on that trap firing when the dispatch subshell ends. A hook outside the subshell leaks that file once per call, silently.worktree_root_bannerwrites to stdout and currently leads the tool output becauseworktree_enteris the first line. From the hook it still leads only if the hook's stdout is the same capture.worktree_enterprints its refusal and returns 1. The refusal has to reach the caller as anisErrorwith the same text, via the dispatcher's existing non-zero-exit path.Pin all three in the replacement suite.
Scope
shared/worktree.shplugin-tests/dev-tooling/worktree_conformance.bats(262 today)AGENTS.md, plugin READMEThe conformance suite's two properties — every non-exempt tool resolves its root, and none runs a command against a refused root — hold by construction afterwards. What is worth keeping from it is the reconciliation of
tool_*functions againsttools.json, which catches an undeclared tool and has nothing to do with worktrees. Keeping that lands the saving at the low end.worktree_resolution.bats,worktree_state.bats,worktree_php_tools.batsandworktree_js_tools.batsare untouched; they cover the module and tool behavior, not the wiring.worktree.shdoes not shrink. The reason to do this is that "every tool resolves its root before running" stops being a property a suite re-proves against an enumeration and becomes a property of the dispatcher, so a tool added later gets it without anyone remembering.Not addressed
project_rootstill has to be declared in each tool'sinputSchema— 24 occurrences in the PHP server, 30 in admin, 24 in storefront — becauseadditionalProperties: falserejects an undeclared parameter. Removing that needs a different SDK feature (a schema fragment merged into every tool) and is a separate, larger proposal.Prerequisite
A
.mcp-sdk.lockbump (version=andsha256=) plus.github/scripts/vendor-mcp-sdk.sh, per.claude/rules/template-sync.md. The vendored copies are CI-gated as byte-identical, so the hook cannot be tried by patchingshared/mcpserver_core.shlocally.