
5.5 Git MCP服务器
“src\git”目录实现了一个名为“git”的MCP服务器,该模块专为与Git版本控制系统交互而设计。它提供了一组工具,允许大型语言模型(LLMs)执行各种Git操作,例如克隆仓库、检出变更、提交更改以及推送更新等。这些工具使得LLMs能够直接与Git仓库进行交互,从而在需要版本控制和协作的场景下增强模型的能力。通过这种方式,MCP服务器扩展了语言模型的功能,使其能够更有效地处理软件开发和版本管理相关的任务。简而言之,“git”服务器充当了语言模型与Git仓库之间的桥梁,为模型提供了版本控制操作的能力。
5.5.1 实现Git MCP服务器
文件src\git\src\mcp_server_git\server.py实现了一个基于MCP协议的Git版本控制服务器,提供了一系列Git操作工具。它通过定义与Git相关的工具(如查看状态、比较差异、提交更改、分支管理等),封装了GitPython库的核心功能,允许客户端通过MC协议调用这些工具来操作Git仓库。服务器支持从命令行参数或客户端提供的根目录获取仓库路径,对每个工具调用进行参数验证和处理,并以文本形式返回操作结果,实现了与Git仓库的交互功能,同时兼容了MCP协议的通信规范。
(1)下面代码的功能是定义Git操作工具列表及其实例化处理。原理是通过MCP协议注册一系列Git相关工具(如状态查看、差异比较、提交、分支管理等),每个工具包含名称、描述和输入参数schema,客户端可调用这些工具执行对应的Git操作,所有工具均基于GitPython库实现核心功能并返回标准化结果。
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name=GitTools.STATUS,
description="显示工作区状态",
inputSchema=GitStatus.model_json_schema(),
),
Tool(
name=GitTools.DIFF_UNSTAGED,
description="显示工作区中尚未暂存的更改",
inputSchema=GitDiffUnstaged.model_json_schema(),
),
Tool(
name=GitTools.DIFF_STAGED,
description="显示已暂存待提交的更改",
inputSchema=GitDiffStaged.model_json_schema(),
),
Tool(
name=GitTools.DIFF,
description="显示分支或提交之间的差异",
inputSchema=GitDiff.model_json_schema(),
),
Tool(
name=GitTools.COMMIT,
description="将更改记录到仓库",
inputSchema=GitCommit.model_json_schema(),
),
Tool(
name=GitTools.ADD,
description="将文件内容添加到暂存区",
inputSchema=GitAdd.model_json_schema(),
),
Tool(
name=GitTools.RESET,
description="取消所有已暂存的更改",
inputSchema=GitReset.model_json_schema(),
),
Tool(
name=GitTools.LOG,
description="显示提交日志",
inputSchema=GitLog.model_json_schema(),
),
Tool(
name=GitTools.CREATE_BRANCH,
description="从可选的基础分支创建新分支",
inputSchema=GitCreateBranch.model_json_schema(),
),
Tool(
name=GitTools.CHECKOUT,
description="切换分支",
inputSchema=GitCheckout.model_json_schema(),
),
Tool(
name=GitTools.SHOW,
description="显示提交的内容",
inputSchema=GitShow.model_json_schema(),
),
Tool(
name=GitTools.INIT,
description="初始化新的Git仓库",
inputSchema=GitInit.model_json_schema(),
),
Tool(
name=GitTools.BRANCH,
description="列出Git分支",
inputSchema=GitBranch.model_json_schema(),
)
]
(2)下面代码的功能是处理Git日志查询工具的调用请求。原理是解析客户端传入的仓库路径、最大记录数、时间范围等参数,通过GitPython库执行日志查询操作,支持按时间范围筛选提交记录,将结果格式化(包含提交哈希、作者、日期、消息)后返回,帮助用户查看仓库的提交历史。
def git_log(repo: git.Repo, max_count: int = 10, start_timestamp: Optional[str] = None, end_timestamp: Optional[str] = None) -> list[str]:
if start_timestamp or end_timestamp:
# 使用git log命令进行日期筛选
args = []
if start_timestamp:
args.extend(['--since', start_timestamp]) # 起始时间筛选参数
if end_timestamp:
args.extend(['--until', end_timestamp]) # 结束时间筛选参数
# 格式化输出:哈希、作者、日期、消息,用换行分隔
args.extend(['--format=%H%n%an%n%ad%n%s%n'])
# 执行git log命令并拆分结果
log_output = repo.git.log(*args).split('\n')
log = []
# 按每组4条记录(哈希、作者、日期、消息)处理提交信息
for i in range(0, len(log_output), 4):
# 确保索引不越界且未超过最大记录数
if i + 3 < len(log_output) and len(log) < max_count:
log.append(
f"提交: {log_output[i]}\n"
f"作者: {log_output[i+1]}\n"
f"日期: {log_output[i+2]}\n"
f"消息: {log_output[i+3]}\n"
)
return log
else:
# 无日期筛选时的简单日志查询逻辑
commits = list(repo.iter_commits(max_count=max_count)) # 获取最近max_count次提交
log = []
for commit in commits:
log.append(
f"提交: {commit.hexsha!r}\n"
f"作者: {commit.author!r}\n"
f"日期: {commit.authored_datetime}\n"
f"消息: {commit.message!r}\n"
)
return log
# 在工具调用处理中对应的分支
case GitTools.LOG:
log = git_log(
repo,
arguments.get("max_count", 10), # 从参数获取最大记录数,默认10
arguments.get("start_timestamp"), # 起始时间参数
arguments.get("end_timestamp") # 结束时间参数
)
return [TextContent(
type="text",
text="提交历史:\n" + "\n".join(log) # 拼接日志列表为文本
)]
(3)下面代码的功能是处理分支相关工具的调用请求,包括创建分支、切换分支和列出分支。原理是通过解析客户端参数,利用GitPython库执行分支创建(基于指定基础分支)、切换分支(检出操作)和分支列表查询(支持按本地/远程/全部类型及提交包含关系筛选),并返回操作结果,实现Git分支的全生命周期管理。
def git_create_branch(repo: git.Repo, branch_name: str, base_branch: str | None = None) -> str:
# 确定新分支的基础分支,未指定则使用当前活跃分支
if base_branch:
base = repo.references[base_branch]
else:
base = repo.active_branch
# 创建新分支
repo.create_head(branch_name, base)
return f"已从'{base.name}'创建分支'{branch_name}'"
def git_checkout(repo: git.Repo, branch_name: str) -> str:
# 切换到指定分支
repo.git.checkout(branch_name)
return f"已切换到分支'{branch_name}'"
def git_branch(repo: git.Repo, branch_type: str, contains: str | None = None, not_contains: str | None = None) -> str:
# 处理包含指定提交的分支筛选参数
match contains:
case None:
contains_sha = (None,)
case _:
contains_sha = ("--contains", contains)
# 处理不包含指定提交的分支筛选参数
match not_contains:
case None:
not_contains_sha = (None,)
case _:
not_contains_sha = ("--no-contains", not_contains)
# 处理分支类型参数(本地、远程、全部)
match branch_type:
case 'local':
b_type = None # 本地分支无额外参数
case 'remote':
b_type = "-r" # 远程分支参数
case 'all':
b_type = "-a" # 所有分支参数
case _:
return f"无效的分支类型: {branch_type}"
# 执行git branch命令,None值会被GitPython自动忽略
branch_info = repo.git.branch(b_type, *contains_sha, *not_contains_sha)
return branch_info
# 在工具调用处理中对应的分支
case GitTools.CREATE_BRANCH:
result = git_create_branch(
repo,
arguments["branch_name"], # 新分支名称
arguments.get("base_branch") # 基础分支,可选
)
return [TextContent(
type="text",
text=result
)]
case GitTools.CHECKOUT:
result = git_checkout(repo, arguments["branch_name"]) # 要切换的分支名称
return [TextContent(
type="text",
text=result
)]
case GitTools.BRANCH:
result = git_branch(
repo,
arguments.get("branch_type", 'local'), # 分支类型,默认本地
arguments.get("contains", None), # 包含的提交,可选
arguments.get("not_contains", None), # 不包含的提交,可选
)
return [TextContent(
type="text",
text=result
)]
5.5.2 MCP服务测试
文件src\git\tests\test_server.py的功能是使用pytest框架来测试mcp-server-git服务中的Git操作函数。测试覆盖了检出分支、切换分支、列出分支、添加文件等Git命令。每个测试函数都创建了一个临时的Git仓库,执行特定的Git操作,然后验证结果是否符合预期。测试完成后,临时仓库会被删除。这些测试确保mcp-server-git模块中的函数能够正确地与Git仓库交互,并返回正确的结果。
import pytest
from pathlib import Path
import git
from mcp_server_git.server import git_checkout, git_branch, git_add
import shutil
@pytest.fixture
def test_repository(tmp_path: Path):
"""创建一个临时测试仓库并初始化"""
repo_path = tmp_path / "temp_test_repo"
test_repo = git.Repo.init(repo_path)
# 创建测试文件并提交
Path(repo_path / "test.txt").write_text("test")
test_repo.index.add(["test.txt"])
test_repo.index.commit("初始提交")
yield test_repo
# 测试结束后清理临时仓库
shutil.rmtree(repo_path)
def test_git_checkout_existing_branch(test_repository):
"""测试切换到已存在的分支"""
test_repository.git.branch("test-branch")
result = git_checkout(test_repository, "test-branch")
assert "已切换到分支 'test-branch'" in result
assert test_repository.active_branch.name == "test-branch"
def test_git_checkout_nonexistent_branch(test_repository):
"""测试切换到不存在的分支(应抛出异常)"""
with pytest.raises(git.GitCommandError):
git_checkout(test_repository, "nonexistent-branch")
def test_git_branch_local(test_repository):
"""测试列出本地分支"""
test_repository.git.branch("new-branch-local")
result = git_branch(test_repository, "local")
assert "new-branch-local" in result
def test_git_branch_remote(test_repository):
"""测试列出远程分支(无远程仓库时)"""
# GitPython在没有远程仓库的情况下很难创建远程分支
# 本测试将检查指定'remote'但无实际远程仓库时的行为
result = git_branch(test_repository, "remote")
assert "" == result.strip() # 无远程分支时应为空
def test_git_branch_all(test_repository):
"""测试列出所有分支"""
test_repository.git.branch("new-branch-all")
result = git_branch(test_repository, "all")
assert "new-branch-all" in result
def test_git_branch_contains(test_repository):
"""测试列出包含指定提交的分支"""
# 创建新分支并提交内容
test_repository.git.checkout("-b", "feature-branch")
Path(test_repository.working_dir / Path("feature.txt")).write_text("feature content")
test_repository.index.add(["feature.txt"])
commit = test_repository.index.commit("feature commit")
test_repository.git.checkout("master")
result = git_branch(test_repository, "local", contains=commit.hexsha)
assert "feature-branch" in result
assert "master" not in result
def test_git_branch_not_contains(test_repository):
"""测试列出不包含指定提交的分支"""
# 创建新分支并提交内容
test_repository.git.checkout("-b", "another-feature-branch")
Path(test_repository.working_dir / Path("another_feature.txt")).write_text("another feature content")
test_repository.index.add(["another_feature.txt"])
commit = test_repository.index.commit("another feature commit")
test_repository.git.checkout("master")
result = git_branch(test_repository, "local", not_contains=commit.hexsha)
assert "another-feature-branch" not in result
assert "master" in result
def test_git_add_all_files(test_repository):
"""测试暂存所有文件"""
file_path = Path(test_repository.working_dir) / "all_file.txt"
file_path.write_text("adding all")
result = git_add(test_repository, ["."])
# 检查文件是否被暂存
staged_files = [item.a_path for item in test_repository.index.diff("HEAD")]
assert "all_file.txt" in staged_files
assert result == "Files staged successfully"
def test_git_add_specific_files(test_repository):
"""测试暂存指定文件"""
file1 = Path(test_repository.working_dir) / "file1.txt"
file2 = Path(test_repository.working_dir) / "file2.txt"
file1.write_text("file 1 content")
file2.write_text("file 2 content")
result = git_add(test_repository, ["file1.txt"])
# 检查只有指定文件被暂存
staged_files = [item.a_path for item in test_repository.index.diff("HEAD")]
assert "file1.txt" in staged_files
assert "file2.txt" not in staged_files
assert result == "Files staged successfully"
448

被折叠的 条评论
为什么被折叠?



