Skip to content

Commit 1d6eeea

Browse files
committed
feat(git): add signed commit tool with GPG support
- Add git_commit_signed tool that supports GPG signing of commits - Accepts optional key_id parameter to sign with specific GPG key - Uses git commit -S flag for signing (works with default or specified key) - Add comprehensive tests for signed commits - Update GitTools enum and tool registration
1 parent decb360 commit 1d6eeea

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

src/git/src/mcp_server_git/server.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ class GitCommit(BaseModel):
3838
repo_path: str
3939
message: str
4040

41+
class GitCommitSigned(BaseModel):
42+
repo_path: str
43+
message: str
44+
key_id: Optional[str] = Field(
45+
None,
46+
description="Optional GPG key ID to use for signing. If not provided, uses the default configured GPG key."
47+
)
48+
4149
class GitAdd(BaseModel):
4250
repo_path: str
4351
files: list[str]
@@ -97,6 +105,7 @@ class GitTools(str, Enum):
97105
DIFF_STAGED = "git_diff_staged"
98106
DIFF = "git_diff"
99107
COMMIT = "git_commit"
108+
COMMIT_SIGNED = "git_commit_signed"
100109
ADD = "git_add"
101110
RESET = "git_reset"
102111
LOG = "git_log"
@@ -122,6 +131,17 @@ def git_commit(repo: git.Repo, message: str) -> str:
122131
commit = repo.index.commit(message)
123132
return f"Changes committed successfully with hash {commit.hexsha}"
124133

134+
def git_commit_signed(repo: git.Repo, message: str, key_id: str | None = None) -> str:
135+
# Use the git command directly for signing support
136+
if key_id:
137+
repo.git.commit("-S" + key_id, "-m", message)
138+
else:
139+
repo.git.commit("-S", "-m", message)
140+
141+
# Get the commit hash of HEAD
142+
commit_hash = repo.head.commit.hexsha
143+
return f"Changes committed and signed successfully with hash {commit_hash}"
144+
125145
def git_add(repo: git.Repo, files: list[str]) -> str:
126146
if files == ["."]:
127147
repo.git.add(".")
@@ -277,6 +297,11 @@ async def list_tools() -> list[Tool]:
277297
description="Records changes to the repository",
278298
inputSchema=GitCommit.model_json_schema(),
279299
),
300+
Tool(
301+
name=GitTools.COMMIT_SIGNED,
302+
description="Records changes to the repository with GPG signature",
303+
inputSchema=GitCommitSigned.model_json_schema(),
304+
),
280305
Tool(
281306
name=GitTools.ADD,
282307
description="Adds file contents to the staging area",
@@ -388,6 +413,17 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
388413
text=result
389414
)]
390415

416+
case GitTools.COMMIT_SIGNED:
417+
result = git_commit_signed(
418+
repo,
419+
arguments["message"],
420+
arguments.get("key_id")
421+
)
422+
return [TextContent(
423+
type="text",
424+
text=result
425+
)]
426+
391427
case GitTools.ADD:
392428
result = git_add(repo, arguments["files"])
393429
return [TextContent(

src/git/tests/test_server.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
git_reset,
1414
git_log,
1515
git_create_branch,
16-
git_show
16+
git_show,
17+
git_commit_signed,
1718
)
1819
import shutil
1920

@@ -246,3 +247,42 @@ def test_git_show_initial_commit(test_repository):
246247
assert "Commit:" in result
247248
assert "initial commit" in result
248249
assert "test.txt" in result
250+
251+
def test_git_commit_signed_without_key_id(test_repository):
252+
# Create and stage a new file
253+
file_path = Path(test_repository.working_dir) / "signed_test.txt"
254+
file_path.write_text("testing signed commit")
255+
test_repository.index.add(["signed_test.txt"])
256+
257+
# Note: This test may fail if GPG is not configured on the system
258+
# In that case, it should raise a GitCommandError
259+
try:
260+
result = git_commit_signed(test_repository, "Test signed commit")
261+
assert "Changes committed and signed successfully" in result
262+
assert "with hash" in result
263+
264+
# Verify the commit was actually created
265+
latest_commit = test_repository.head.commit
266+
assert latest_commit.message.strip() == "Test signed commit"
267+
except git.GitCommandError as e:
268+
# GPG not configured or signing failed - this is expected in CI/test environments
269+
pytest.skip(f"GPG signing not available: {str(e)}")
270+
271+
def test_git_commit_signed_with_key_id(test_repository):
272+
# Create and stage a new file
273+
file_path = Path(test_repository.working_dir) / "signed_test_with_key.txt"
274+
file_path.write_text("testing signed commit with key")
275+
test_repository.index.add(["signed_test_with_key.txt"])
276+
277+
# Note: This test may fail if GPG is not configured or key doesn't exist
278+
try:
279+
result = git_commit_signed(test_repository, "Test signed commit with key", "TESTKEY123")
280+
assert "Changes committed and signed successfully" in result
281+
assert "with hash" in result
282+
283+
# Verify the commit was actually created
284+
latest_commit = test_repository.head.commit
285+
assert latest_commit.message.strip() == "Test signed commit with key"
286+
except git.GitCommandError as e:
287+
# GPG not configured, key not found, or signing failed - expected in CI/test environments
288+
pytest.skip(f"GPG signing with specific key not available: {str(e)}")

0 commit comments

Comments
 (0)