modelcontextprotocol / modelcontextprotocol/servers

git_checkout reports "Switched to branch 'X'" when the checkout actually detached HEAD

Open Beginner friendly
#4,804 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
90.5k
Forks
11.7k
Avg merge
2d 2h
Merged PRs (30d)
5

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/git/src/mcp_server_git/server.py around lines 202-204 and inspect the existing git_checkout tests in src/git/tests/test_server.py. Verify branch checkouts retain the existing message while revisions that detach HEAD report the detached state, then run the test file and confirm the reported behavior matches the checkout outcome.

Written by the indexing model from the issue text.

Assessment

Tech stack
git, python
Domain
api, devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
86/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.