What happens
git_checkout validates branch_name with repo.rev_parse(...), which resolves any revision — a commit sha, a tag, HEAD~1, a remote-tracking ref — not only branch names. It then checks out and returns a fixed string:
repo.rev_parse(branch_name) # Validates branch_name is a real git ref, throws BadName if not
repo.git.checkout(branch_name)
return f"Switched to branch '{branch_name}'"
src/git/src/mcp_server_git/server.py lines 202-204 on main (the flag-injection guard above them is elided). The tool description is Switches branches, the parameter is branch_name, and the README says the same: "git_checkout … Returns: Confirmation of branch switch". For every revision that is not a branch, the checkout detaches HEAD and the reply still claims a branch switch.
Reproduction
mcp-server-git 2026.8.18, mcp 1.30.0, over stdio, on a fresh repo with three commits, a tag v1 and a branch feature:
branch_name "feature" -> Switched to branch 'feature' | On branch feature (correct)
branch_name "nonexistent" -> isError=true, repo unchanged (correct)
branch_name "49cea63" (a sha) -> Switched to branch '49cea63' | HEAD detached at 49cea63
branch_name "v1" (a tag) -> Switched to branch 'v1' | HEAD detached at v1
branch_name "HEAD~1" -> Switched to branch 'HEAD~1' | HEAD detached at f1de938
branch_name "origin/main" -> Switched to branch 'origin/main' | HEAD detached at origin/main
branch_name "refs/heads/feature" -> Switched to branch 'refs/heads/feature' | HEAD detached at refs/heads/feature
The last two are the realistic ones: an agent writes origin/main or a full ref name far more often than a raw sha.
Why this matters for the calling model
The reply is the only thing the model sees: content is a single TextContent, structuredContent is null, isError is false, the tool declares no outputSchema, and its annotations are static. Nothing in the response distinguishes an attached checkout from a detached one.
Git itself does distinguish them. git checkout <sha> writes the You are in 'detached HEAD' state advisory to stderr, and leaving a detached HEAD prints Warning: you are leaving 1 commit behind together with the git branch <new-branch-name> <sha> recipe. repo.git.checkout() returns '' and that advisory is discarded, so the server replaces git's own warning with a success sentence. Work committed from there is on no branch and reachable only through the reflog until gc expires it; the tool never says so, and nothing in its reply suggests calling git_status.
Suggested fix
Report the state rather than assume it:
repo.git.checkout(branch_name)
if repo.head.is_detached:
return f"HEAD is now detached at {repo.head.commit.hexsha[:7]}"
return f"Switched to branch '{repo.active_branch.name}'"
Verified against main: src/git/tests/test_server.py stays at 47 passed, and git_checkout on a branch name still returns Switched to branch '<name>'. Rejecting non-branch revisions would also close it, but that removes a capability some callers use deliberately.
Same root cause as #4762 and #4763 — no verification of the outcome after the git call — so this could fold into the same fix if that is easier to review.
What happens
git_checkoutvalidatesbranch_namewithrepo.rev_parse(...), which resolves any revision — a commit sha, a tag,HEAD~1, a remote-tracking ref — not only branch names. It then checks out and returns a fixed string:src/git/src/mcp_server_git/server.pylines 202-204 onmain(the flag-injection guard above them is elided). The tool description isSwitches branches, the parameter isbranch_name, and the README says the same: "git_checkout… Returns: Confirmation of branch switch". For every revision that is not a branch, the checkout detaches HEAD and the reply still claims a branch switch.Reproduction
mcp-server-git2026.8.18,mcp1.30.0, over stdio, on a fresh repo with three commits, a tagv1and a branchfeature:The last two are the realistic ones: an agent writes
origin/mainor a full ref name far more often than a raw sha.Why this matters for the calling model
The reply is the only thing the model sees:
contentis a single TextContent,structuredContentis null,isErroris false, the tool declares nooutputSchema, and its annotations are static. Nothing in the response distinguishes an attached checkout from a detached one.Git itself does distinguish them.
git checkout <sha>writes theYou are in 'detached HEAD' stateadvisory to stderr, and leaving a detached HEAD printsWarning: you are leaving 1 commit behindtogether with thegit branch <new-branch-name> <sha>recipe.repo.git.checkout()returns''and that advisory is discarded, so the server replaces git's own warning with a success sentence. Work committed from there is on no branch and reachable only through the reflog until gc expires it; the tool never says so, and nothing in its reply suggests callinggit_status.Suggested fix
Report the state rather than assume it:
Verified against
main:src/git/tests/test_server.pystays at 47 passed, andgit_checkouton a branch name still returnsSwitched to branch '<name>'. Rejecting non-branch revisions would also close it, but that removes a capability some callers use deliberately.Same root cause as #4762 and #4763 — no verification of the outcome after the git call — so this could fold into the same fix if that is easier to review.