Repository navigation
feat: add a before-tool hook and a private directory per tool call - #32
Merged
Martin Bens (SpiGAndromeda) merged 1 commit intoOct 1, 2026
Merged
Conversation
A server whose tools share a per-call step, such as an authorization check or an audit line, had to repeat it in every `tool_*` function, and a tool added later could skip it (#28). A tool that allocated a file for one call had no place for it that the SDK would clean up, and no stated contract for its own `trap … EXIT` (#27). `mcp_before_tool_call`, when the server defines it, runs before every dispatched tool with the tool name and the validated arguments, in the tool's own shell, so variables, the working directory, functions and traps it sets reach the tool. `_mcp_run_tool` saves and restores `set` and `shopt` options around it, and runs its own steps after the hook through `builtin` and bare redirections only, so a hook-defined function or a changed `PATH` cannot break them. A non-zero return refuses the call with an `isError` result carrying the hook's output. The markers `hook-running`, `hook-returned` and `hook-refused` let `handle_tools_call` tell a refusal, an `exit` and a signal apart, and each is logged as such. Each call now creates one root with `mktemp -d` (mode 700) holding `output`, `sentinel`, `hook-output`, the markers, and `tmp/`, which is exported as `MCP_CALL_TMPDIR` to the hook, the tool and the cancel hook. The in-flight record keeps its format. `_mcp_call_root_of` checks a path before any root is derived from it, and `_mcp_remove_call_root` removes only `mcp-call.*` directories after the call's group has ended, restoring write and search permission on directories the tool locked. A root that cannot be created answers `isError`, which also stops a failed `mktemp` from ending a server under `set -o posix`. In-process test captures that spawn a tool keep stderr out of the parsed JSON, because bash can print a `child setpgid` line there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Martin Bens (SpiGAndromeda)
deleted the
feat/before-tool-hook-and-call-tmpdir
branch
October 1, 2026 18:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28. Closes #27.
Problem
A server whose tools share a per-call step, such as an authorization check, a rebinding of where the tool works, or an audit line, had to repeat that step in every
tool_*function. Nothing noticed a tool that skipped it (#28).A tool that allocated a file for one call had no place for it that the SDK would clean up. The README did not say whether the tool's own
trap … EXITwas supported (#27).Behavior
Before-tool hook
A server may define
mcp_before_tool_call. Every dispatchedtools/callruns it after the declaration check and argument validation, with the tool name and the validated arguments JSON. It runs in the tool's wrapper subshell, so the hook and the tool share one shell.setorshoptare restored before the tool runs.isErrorcarrying the hook's output. This is logged as a refusal, not as a tool failure.exitinstead ofreturn, or killed by a signal: the tool does not run, and theisErrorresult names which of the two happened.declare -Fonly. The SDK's own steps after the hook use onlybuiltin-prefixed builtins and plain redirections, so a function the hook defines or aPATHit changes does not affect them. A hook must not define a function namedbuiltin.handle_tools_callinside a tool runs the hook again.Per-call directory
Each call gets one root from
mktemp -d(mode 700). It holds the call's output, sentinel, hook output and markers, and atmp/directory exported asMCP_CALL_TMPDIRto the hook, the tool and the call'stool_<name>_cancelhook.mcp-call.*directory. Every path read back from the in-flight record is checked before use.isError, and the tool does not run. Previously the output file'smktempwas unchecked, so a failure ended a server running underset -o posixwithout a response.TMPDIR. a call's root directory leaks when the dispatch subshell dies before the in-flight record is written #31 covers the window betweenmktempand the in-flight record write.EXITtrap contractA tool may install
trap … EXIT. It runs when the tool returns and on the SIGTERM step of a cancellation or shutdown, and not after SIGKILL. The hook and the tool share oneEXITtrap.