Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions src/git/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,29 @@ Please note that mcp-server-git is currently in early development. The functiona
- `message` (string): Commit message
- Returns: Confirmation with new commit hash

6. `git_add`
6. `git_commit_signed`
- Records changes to the repository and signs the commit with GPG
- Inputs:
- `repo_path` (string): Path to Git repository
- `message` (string): Commit message
- `key_id` (string, optional): GPG key ID to use for signing. If omitted, the default signing key configured in Git will be used.
- Behavior: Uses the Git CLI `-S`/`--gpg-sign` flag to create a GPG-signed commit. This passes through to the system's `git` and `gpg` configuration, so GPG must be available and configured on the host.
- Returns: Confirmation with new commit hash. If signing fails (for example, no GPG key is configured), the underlying Git command will raise an error.

7. `git_add`
- Adds file contents to the staging area
- Inputs:
- `repo_path` (string): Path to Git repository
- `files` (string[]): Array of file paths to stage
- Returns: Confirmation of staged files

7. `git_reset`
8. `git_reset`
- Unstages all staged changes
- Input:
- `repo_path` (string): Path to Git repository
- Returns: Confirmation of reset operation

8. `git_log`
9. `git_log`
- Shows the commit logs with optional date filtering
- Inputs:
- `repo_path` (string): Path to Git repository
Expand All @@ -65,34 +74,36 @@ Please note that mcp-server-git is currently in early development. The functiona
- `end_timestamp` (string, optional): End timestamp for filtering commits. Accepts ISO 8601 format (e.g., '2024-01-15T14:30:25'), relative dates (e.g., '2 weeks ago', 'yesterday'), or absolute dates (e.g., '2024-01-15', 'Jan 15 2024')
- Returns: Array of commit entries with hash, author, date, and message

9. `git_create_branch`
10. `git_create_branch`
- Creates a new branch
- Inputs:
- `repo_path` (string): Path to Git repository
- `branch_name` (string): Name of the new branch
- `base_branch` (string, optional): Base branch to create from (defaults to current branch)
- Returns: Confirmation of branch creation
10. `git_checkout`

11. `git_checkout`
- Switches branches
- Inputs:
- `repo_path` (string): Path to Git repository
- `branch_name` (string): Name of branch to checkout
- Returns: Confirmation of branch switch
11. `git_show`

12. `git_show`
- Shows the contents of a commit
- Inputs:
- `repo_path` (string): Path to Git repository
- `revision` (string): The revision (commit hash, branch name, tag) to show
- Returns: Contents of the specified commit

12. `git_branch`
- List Git branches
- Inputs:
- `repo_path` (string): Path to the Git repository.
- `branch_type` (string): Whether to list local branches ('local'), remote branches ('remote') or all branches('all').
- `contains` (string, optional): The commit sha that branch should contain. Do not pass anything to this param if no commit sha is specified
- `not_contains` (string, optional): The commit sha that branch should NOT contain. Do not pass anything to this param if no commit sha is specified
- Returns: List of branches
13. `git_branch`
- List Git branches
- Inputs:
- `repo_path` (string): Path to the Git repository.
- `branch_type` (string): Whether to list local branches ('local'), remote branches ('remote') or all branches('all').
- `contains` (string, optional): The commit sha that branch should contain. Do not pass anything to this param if no commit sha is specified
- `not_contains` (string, optional): The commit sha that branch should NOT contain. Do not pass anything to this param if no commit sha is specified
- Returns: List of branches

## Installation

Expand Down
47 changes: 41 additions & 6 deletions src/git/src/mcp_server_git/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ class GitCommit(BaseModel):
repo_path: str
message: str

class GitCommitSigned(BaseModel):
repo_path: str
message: str
key_id: Optional[str] = Field(
None,
description="Optional GPG key ID to use for signing. If not provided, uses the default configured GPG key."
)

class GitAdd(BaseModel):
repo_path: str
files: list[str]
Expand Down Expand Up @@ -97,6 +105,7 @@ class GitTools(str, Enum):
DIFF_STAGED = "git_diff_staged"
DIFF = "git_diff"
COMMIT = "git_commit"
COMMIT_SIGNED = "git_commit_signed"
ADD = "git_add"
RESET = "git_reset"
LOG = "git_log"
Expand All @@ -122,6 +131,16 @@ def git_commit(repo: git.Repo, message: str) -> str:
commit = repo.index.commit(message)
return f"Changes committed successfully with hash {commit.hexsha}"

def git_commit_signed(repo: git.Repo, message: str, key_id: str | None = None) -> str:
# Use the git command directly for signing support
if key_id:
repo.git.commit("-S" + key_id, "-m", message)
else:
repo.git.commit("-S", "-m", message)
# Get the commit hash of HEAD
commit_hash = repo.head.commit.hexsha
return f"Changes committed and signed successfully with hash {commit_hash}"

def git_add(repo: git.Repo, files: list[str]) -> str:
if files == ["."]:
repo.git.add(".")
Expand All @@ -142,9 +161,9 @@ def git_log(repo: git.Repo, max_count: int = 10, start_timestamp: Optional[str]
if end_timestamp:
args.extend(['--until', end_timestamp])
args.extend(['--format=%H%n%an%n%ad%n%s%n'])

log_output = repo.git.log(*args).split('\n')

log = []
# Process commits in groups of 4 (hash, author, date, message)
for i in range(0, len(log_output), 4):
Expand Down Expand Up @@ -272,6 +291,11 @@ async def list_tools() -> list[Tool]:
description="Records changes to the repository",
inputSchema=GitCommit.model_json_schema(),
),
Tool(
name=GitTools.COMMIT_SIGNED,
description="Records changes to the repository with GPG signature",
inputSchema=GitCommitSigned.model_json_schema(),
),
Tool(
name=GitTools.ADD,
description="Adds file contents to the staging area",
Expand Down Expand Up @@ -343,7 +367,7 @@ def by_commandline() -> Sequence[str]:
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
repo_path = Path(arguments["repo_path"])

# For all commands, we need an existing repo
repo = git.Repo(repo_path)

Expand Down Expand Up @@ -383,6 +407,17 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
text=result
)]

case GitTools.COMMIT_SIGNED:
result = git_commit_signed(
repo,
arguments["message"],
arguments.get("key_id")
)
return [TextContent(
type="text",
text=result
)]

case GitTools.ADD:
result = git_add(repo, arguments["files"])
return [TextContent(
Expand All @@ -400,7 +435,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
# Update the LOG case:
case GitTools.LOG:
log = git_log(
repo,
repo,
arguments.get("max_count", 10),
arguments.get("start_timestamp"),
arguments.get("end_timestamp")
Expand All @@ -409,7 +444,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
type="text",
text="Commit history:\n" + "\n".join(log)
)]

case GitTools.CREATE_BRANCH:
result = git_create_branch(
repo,
Expand Down Expand Up @@ -446,7 +481,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
type="text",
text=result
)]

case _:
raise ValueError(f"Unknown tool: {name}")

Expand Down
41 changes: 40 additions & 1 deletion src/git/tests/test_server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from pathlib import Path
import git
from mcp_server_git.server import git_checkout, git_branch, git_add, git_status
from mcp_server_git.server import git_checkout, git_branch, git_add, git_status, git_commit_signed
import shutil

@pytest.fixture
Expand Down Expand Up @@ -97,3 +97,42 @@ def test_git_status(test_repository):

assert result is not None
assert "On branch" in result or "branch" in result.lower()

def test_git_commit_signed_without_key_id(test_repository):
# Create and stage a new file
file_path = Path(test_repository.working_dir) / "signed_test.txt"
file_path.write_text("testing signed commit")
test_repository.index.add(["signed_test.txt"])

# Note: This test may fail if GPG is not configured on the system
# In that case, it should raise a GitCommandError
try:
result = git_commit_signed(test_repository, "Test signed commit")
assert "Changes committed and signed successfully" in result
assert "with hash" in result

# Verify the commit was actually created
latest_commit = test_repository.head.commit
assert latest_commit.message.strip() == "Test signed commit"
except git.GitCommandError as e:
# GPG not configured or signing failed - this is expected in CI/test environments
pytest.skip(f"GPG signing not available: {str(e)}")

def test_git_commit_signed_with_key_id(test_repository):
# Create and stage a new file
file_path = Path(test_repository.working_dir) / "signed_test_with_key.txt"
file_path.write_text("testing signed commit with key")
test_repository.index.add(["signed_test_with_key.txt"])

# Note: This test may fail if GPG is not configured or key doesn't exist
try:
result = git_commit_signed(test_repository, "Test signed commit with key", "TESTKEY123")
assert "Changes committed and signed successfully" in result
assert "with hash" in result

# Verify the commit was actually created
latest_commit = test_repository.head.commit
assert latest_commit.message.strip() == "Test signed commit with key"
except git.GitCommandError as e:
# GPG not configured, key not found, or signing failed - expected in CI/test environments
pytest.skip(f"GPG signing with specific key not available: {str(e)}")