From 88320daffa627575f3365505f29b56aab05a5cd0 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 18 Dec 2025 13:22:52 +0000 Subject: [PATCH 1/3] fix(git): import BadName directly to fix pyright errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import `BadName` from `git.exc` directly instead of accessing it via `git.exc.BadName`, which pyright doesn't recognize as a valid attribute access on the `git` module. This fixes the pyright CI failures introduced by the recent security patches (GHSA merges). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/git/src/mcp_server_git/server.py | 5 +++-- src/git/tests/test_server.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index 23e9b53f..58d8178d 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -13,6 +13,7 @@ from mcp.types import ( ) from enum import Enum import git +from git.exc import BadName from pydantic import BaseModel, Field # Default number of context lines to show in diff output @@ -119,7 +120,7 @@ def git_diff(repo: git.Repo, target: str, context_lines: int = DEFAULT_CONTEXT_L # Defense in depth: reject targets starting with '-' to prevent flag injection, # even if a malicious ref with that name exists (e.g. via filesystem manipulation) if target.startswith("-"): - raise git.exc.BadName(f"Invalid target: '{target}' - cannot start with '-'") + raise BadName(f"Invalid target: '{target}' - cannot start with '-'") repo.rev_parse(target) # Validates target is a real git ref, throws BadName if not return repo.git.diff(f"--unified={context_lines}", target) @@ -187,7 +188,7 @@ def git_checkout(repo: git.Repo, branch_name: str) -> str: # Defense in depth: reject branch names starting with '-' to prevent flag injection, # even if a malicious ref with that name exists (e.g. via filesystem manipulation) if branch_name.startswith("-"): - raise git.exc.BadName(f"Invalid branch name: '{branch_name}' - cannot start with '-'") + raise BadName(f"Invalid branch name: '{branch_name}' - cannot start with '-'") 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}'" diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 3dba7387..054bf8c7 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -1,6 +1,7 @@ import pytest from pathlib import Path import git +from git.exc import BadName from mcp_server_git.server import ( git_checkout, git_branch, @@ -40,7 +41,7 @@ def test_git_checkout_existing_branch(test_repository): def test_git_checkout_nonexistent_branch(test_repository): - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "nonexistent-branch") def test_git_branch_local(test_repository): @@ -316,25 +317,25 @@ def test_validate_repo_path_symlink_escape(tmp_path: Path): def test_git_diff_rejects_flag_injection(test_repository): """git_diff should reject flags that could be used for argument injection.""" - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--output=/tmp/evil") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--help") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "-p") def test_git_checkout_rejects_flag_injection(test_repository): """git_checkout should reject flags that could be used for argument injection.""" - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--help") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--orphan=evil") - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "-f") @@ -398,7 +399,7 @@ def test_git_diff_rejects_malicious_refs(test_repository): malicious_ref_path.write_text(sha) # Even though the ref exists, it should be rejected - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_diff(test_repository, "--output=evil.txt") # Verify no file was created (the attack was blocked) @@ -417,7 +418,7 @@ def test_git_checkout_rejects_malicious_refs(test_repository): malicious_ref_path.write_text(sha) # Even though the ref exists, it should be rejected - with pytest.raises(git.exc.BadName): + with pytest.raises(BadName): git_checkout(test_repository, "--orphan=evil") # Cleanup From bd858d6745c32e9c6e2a7f45b383a9f4e6600623 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 18 Dec 2025 14:17:16 +0000 Subject: [PATCH 2/3] fix: add contents write permission to update-packages job (#3138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update-packages job needs to push tags to the repository but was missing the required `permissions: contents: write`. This caused the workflow to fail with a 403 error when trying to push the version tag. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2da6ee94..20c9f83e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,6 +69,8 @@ jobs: if: ${{ needs.create-metadata.outputs.npm_packages != '[]' || needs.create-metadata.outputs.pypi_packages != '[]' }} runs-on: ubuntu-latest environment: release + permissions: + contents: write outputs: changes_made: ${{ steps.commit.outputs.changes_made }} steps: From c7d60d635abcad61fc0cf0bb3f0ca8d83bcd2eec Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 18 Dec 2025 16:25:49 +0000 Subject: [PATCH 3/3] fix: use --frozen instead of --locked in release workflow (#3140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: regenerate uv.lock after version bump in release script When the release script bumps the version in pyproject.toml, it needs to also regenerate the uv.lock file. Otherwise the lockfile becomes out of sync and `uv sync --locked` fails in CI with: "The lockfile at uv.lock needs to be updated" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: use --frozen instead of --locked in release workflow The release script bumps the version in pyproject.toml, which causes the lockfile to be out of sync (uv includes the package's own version in the lockfile). Using --frozen skips the lockfile freshness check while still using pinned dependency versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .github/workflows/release.yml | 2 +- scripts/release.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20c9f83e..ba42d7b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,7 +132,7 @@ jobs: - name: Install dependencies working-directory: src/${{ matrix.package }} - run: uv sync --locked --all-extras --dev + run: uv sync --frozen --all-extras --dev - name: Run pyright working-directory: src/${{ matrix.package }} diff --git a/scripts/release.py b/scripts/release.py index 05d76c0a..e4ce1274 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -97,6 +97,9 @@ class PyPiPackage: with open(self.path / "pyproject.toml", "w") as f: f.write(tomlkit.dumps(data)) + # Regenerate uv.lock to match the updated pyproject.toml + subprocess.run(["uv", "lock"], cwd=self.path, check=True) + def has_changes(path: Path, git_hash: GitHash) -> bool: """Check if any files changed between current state and git hash"""