You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
git: git_create_branch is marked non-destructive but silently resets an existing packed branch #5059
git_create_branch is annotated destructiveHint: false, but if a branch with the requested name already exists as a packed ref (as after git gc, git pack-refs, or in a fresh clone), the call silently moves that branch to the new base. The previous tip becomes unreachable from every ref, and no reflog entry records the reset. git branch <name> on the CLI would refuse.
Handler calls repo.create_head(branch_name, base) without checking whether the branch exists: server.py#L183-L195.
GitPython only refuses to overwrite an existing loose ref file, so a packed ref is overwritten.
Reproduce
In a scratch repo: create main, then a branch feature with one extra commit, and run git pack-refs --all.
Start the git server on that repo.
Call git_create_branch with {"repo_path": "<repo>", "branch_name": "feature", "base_branch": "main"}.
The call reports Created branch 'feature' from 'main'; feature now points at main, its previous commit is unreachable, and git reflog show feature has no entry for the move.
Why it matters
MCP clients relax confirmation for tools that declare destructiveHint: false, so this call can be auto-approved, and it can discard commits.
Possible fix
Check for an existing branch (loose or packed) and return an error, or require an explicit force argument, matching the git CLI. While a force path exists, destructiveHint: true would be accurate.
Found during an audit of MCP tool annotations (AI-assisted source review, then reproduced locally). Happy to send a PR.
Confirmed the mechanism end to end on GitPython 3.1.50, and there's a wrinkle worth recording: the report is accurate against main, but the active line already fixes it — and the fix there has no test for this case.
The mechanism, reproduced. With a repo whose branches are packed (.git/refs/heads/ empty, both entries only in .git/packed-refs):
heads: ['feature', 'main'] # packed refs are visible here
create_head('feature', heads['main'])
-> SUCCEEDED, feature now at 8ade8bb4 # the old tip is gone, no reflog entry
So two things are true at once: repo.heads does see packed refs, and create_head still overwrites one silently. That combination is what makes this easy to fix incorrectly — a check against .git/refs/heads/<name> passes every loose-ref test and still loses commits after pack-refs.
Where the code stands.main has no existence check in git_create_branch at all, so the report is right for that line. v2/main does — server.py raises Cannot create branch '<name>': refs/heads/<name> already exists, refined by ffb7a76d to compare head names. Because it goes through repo.heads, it covers the packed case. What's missing there is coverage: the tests only exercise creating a new branch, so nothing pins the rejection or the packed variant.
Why the packed variant is the one to pin. I ran the two candidate tests against a deliberately incomplete implementation — replacing the check with a loose-ref-only one — and the loose test passes while the packed test fails. That is the whole difference between the two cases, and it is the case this issue is about.
Since the policy here is issues-and-prompts rather than diffs, here is the prompt I'd use to reproduce the coverage on v2/main:
In src/git/tests/test_server.py, add two tests after test_git_create_branch_from_base. Each creates a feature branch with a commit of its own, records its tip, returns to the default branch (read it from active_branch, don't assume master), and asserts that git_create_branch(repo, "feature") raises ValueError matching already exists and that feature's tip is unchanged. The second test additionally runs git pack_refs --all and asserts the loose ref file for feature does not exist before the call, so it cannot silently stop covering the packed path. Verify both tests fail with the existence check removed, and that only the packed one fails if the check is narrowed to the filesystem.
Thanks for confirming this end to end, and for pointing out the v2/main fix. Agreed that the packed-ref test is the one to pin, since a loose-ref check passes everything else.
A question for the maintainers: will the currently published mcp-server-git on main get the existence check, or is v2 the release that fixes it? Once creating over an existing name is rejected, destructiveHint: false becomes accurate, since the call can only add a ref.
Thank you for reporting this, and apologies for the long wait for a response.
Since this was filed, the reference servers have been rebuilt for the 2026-07-28 MCP specification (#4857) and released as v2 (2.0.0 on npm for the TypeScript servers, 2026.10.19 on PyPI for the Python servers). Each server now runs on v2 of its SDK, and the behavior described here has either been fixed or no longer exists in the same form. On v2/main git_create_branch refuses a name that already exists, packed or loose, so it can no longer move an existing branch.
This is not a judgment on whether the problem was real; it's a consequence of the code it describes having been replaced. This is happening as part of the backlog triage in #4875.
If this still happens on the v2 release, we'd genuinely like to know. Please open a fresh issue with the version you're running and steps to reproduce, and we'll look at it. Note that this repository accepts contributions as issues rather than pull requests; see CONTRIBUTING.md.
Activity
CryoThrust commentedon Oct 7, 2026
Confirmed the mechanism end to end on GitPython 3.1.50, and there's a wrinkle worth recording: the report is accurate against
main, but the active line already fixes it — and the fix there has no test for this case.The mechanism, reproduced. With a repo whose branches are packed (
.git/refs/heads/empty, both entries only in.git/packed-refs):So two things are true at once:
repo.headsdoes see packed refs, andcreate_headstill overwrites one silently. That combination is what makes this easy to fix incorrectly — a check against.git/refs/heads/<name>passes every loose-ref test and still loses commits afterpack-refs.Where the code stands.
mainhas no existence check ingit_create_branchat all, so the report is right for that line.v2/maindoes —server.pyraisesCannot create branch '<name>': refs/heads/<name> already exists, refined byffb7a76dto compare head names. Because it goes throughrepo.heads, it covers the packed case. What's missing there is coverage: the tests only exercise creating a new branch, so nothing pins the rejection or the packed variant.Why the packed variant is the one to pin. I ran the two candidate tests against a deliberately incomplete implementation — replacing the check with a loose-ref-only one — and the loose test passes while the packed test fails. That is the whole difference between the two cases, and it is the case this issue is about.
Since the policy here is issues-and-prompts rather than diffs, here is the prompt I'd use to reproduce the coverage on
v2/main:ranausmanai commentedon Oct 8, 2026
Thanks for confirming this end to end, and for pointing out the
v2/mainfix. Agreed that the packed-ref test is the one to pin, since a loose-ref check passes everything else.A question for the maintainers: will the currently published
mcp-server-gitonmainget the existence check, or is v2 the release that fixes it? Once creating over an existing name is rejected,destructiveHint: falsebecomes accurate, since the call can only add a ref.cliffhall commentedon Oct 19, 2026
Closing: superseded by the v2 release.
Thank you for reporting this, and apologies for the long wait for a response.
Since this was filed, the reference servers have been rebuilt for the 2026-07-28 MCP specification (#4857) and released as v2 (2.0.0 on npm for the TypeScript servers, 2026.10.19 on PyPI for the Python servers). Each server now runs on v2 of its SDK, and the behavior described here has either been fixed or no longer exists in the same form. On v2/main git_create_branch refuses a name that already exists, packed or loose, so it can no longer move an existing branch.
This is not a judgment on whether the problem was real; it's a consequence of the code it describes having been replaced. This is happening as part of the backlog triage in #4875.
If this still happens on the v2 release, we'd genuinely like to know. Please open a fresh issue with the version you're running and steps to reproduce, and we'll look at it. Note that this repository accepts contributions as issues rather than pull requests; see
CONTRIBUTING.md.Thanks for taking the time to report it.
ranausmanai commentedon Oct 20, 2026
Thanks for the fix in v2 and the clear explanation.