Skip to content

dev-tooling: move per-tool worktree_enter into the SDK pre-dispatch hook #187

Description

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions