(5-5)MCP服务器实战参考: Git MCP服务器

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"

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值