首页 › tutorials › 造Git考古MCP:让模型秒懂提交历史

Claude Code MCP 服务器开发实战:造一个 Git 考古工具,让模型秒懂提交历史

概述

本教程带你用 Python + FastMCP 从零构建一个 Git 考古 MCP 服务器,把 git log、git show、git diff、git blame、git branch 等只读命令封装成模型可调用的工具。学完你将掌握:MCP 工具的定义与参数 Schema、子进程安全调用、只读护栏与注入防护、stdio 传输、Claude Code 的 MCP 注册与调试,以及 Resources 原语的扩展用法。适合已读完入门篇、想造"能真正在项目里用"的 MCP 工具的开发者。

前置条件

为什么要把 Git 封装成 MCP?

模型本身能读文件,但读不出代码的历史。git log、git blame 这些信息不在工作区文件里,而在 .git 对象库中,必须通过 git 命令解析。把这套能力做成 MCP 工具后,模型可以回答这类"考古"问题:

这些正是排查 bug、做代码审查、写 release notes 时最耗时的环节。

第一步:搭建项目与依赖

新建一个目录,安装 FastMCP(截至 2026 年,社区主流的 MCP 服务器构建框架,对应 PrefectHQ/fastmcp,当前 3.x 系列):

mkdir ~/mcp-git-archaeology && cd ~/mcp-git-archaeology
python -m venv .venv && source .venv/bin/activate
pip install "fastmcp>=3.0"

预期结果:

Successfully installed fastmcp-3.x.x pydantic-2.x.x ...

创建服务器文件:

touch git_archaeology_mcp.py

常见问题:pip install 报网络超时——加国内镜像 pip install fastmcp -i https://pypi.tuna.tsinghua.edu.cn/simple。若你用的是官方 SDK 而非 FastMCP,包名是 mcp,导入方式略有不同,本教程统一用 FastMCP(API 更简洁)。

第二步:服务器骨架与安全执行器

先把"执行 git 命令"这件事做到安全可控。核心原则:子命令白名单 + 强制只读参数 + 仓库校验 + 超时 + 输出截断。把下面代码写入 git_archaeology_mcp.py:

import subprocess
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("git-archaeology")

# 只放行只读/查询类子命令,写操作(add/commit/push/reset 等)一律拒绝
ALLOWED_SUBCOMMANDS = {
    "log", "show", "diff", "blame", "branch", "name-rev",
    "rev-parse", "ls-files", "describe", "shortlog", "merge-base",
}
# 这些参数一旦出现就拒绝,防止 git 通过 -c / ext.* 执行外部命令
FORBIDDEN_TOKENS = ("--no-pager", "-c", "core.", "ext.", "GIT_")

MAX_OUTPUT = 20_000  # 单次工具返回上限,避免撑爆模型上下文


def _run_git(repo: str, args: list[str], timeout: float = 15.0) -> str:
    """安全地执行一条只读 git 命令并返回文本。"""
    repo_path = Path(repo).expanduser().resolve()

    # 1) 仓库存在性校验
    if not repo_path.exists():
        raise ValueError(f"路径不存在: {repo_path}")

    # 2) 子命令白名单
    if not args or args[0] not in ALLOWED_SUBCOMMANDS:
        cmd = args[0] if args else "(空)"
        raise ValueError(f"不允许的子命令: {cmd}(仅允许只读查询)")

    # 3) 注入防护:扫描每个参数,拦截危险 token
    for a in args:
        if any(tok in a for tok in FORBIDDEN_TOKENS):
            raise ValueError(f"参数含被禁止的 token: {a!r}")

    # 4) 真正执行(用列表传参,不经 shell,杜绝 shell 注入)
    proc = subprocess.run(
        ["git", "-C", str(repo_path), "--no-pager"] + args,
        capture_output=True, text=True, timeout=timeout,
    )
    if proc.returncode != 0:
        # 把 git 的 stderr 透传给模型,便于它自我修正
        raise RuntimeError(proc.stderr.strip() or f"git {args[0]} 执行失败")

    out = proc.stdout
    if len(out) > MAX_OUTPUT:
        out = out[:MAX_OUTPUT] + f"\n\n…(输出已截断,原始长度 {len(proc.stdout)} 字符,请缩小查询范围)"
    return out

为什么这样写:模型可能"聪明"地构造参数。如果直接 subprocess.run("git " + user_input, shell=True),一句 log; rm -rf . 就能把仓库删了。这里用列表传参(不经 shell)+ 子命令白名单 + 危险 token 黑名单,三重护栏确保只读。

第三步:定义核心考古工具

接着把五个最常用的考古场景做成工具。续写同一文件:

@mcp.tool
def git_log(
    repo: str,
    max_count: int = 20,
    path: str | None = None,
    author: str | None = None,
    since: str | None = None,
) -> str:
    """查看仓库提交历史,可按文件路径、作者、时间过滤。

    Args:
        repo: 本地 Git 仓库的绝对路径或相对路径。
        max_count: 返回的提交数量上限,默认 20。
        path: 只看某个文件的提交历史,例如 src/api/bill.py。
        author: 按作者名模糊匹配过滤。
        since: 起始时间,例如 2.weeks 或 2026-06-01。
    返回: 每行一条提交,格式 `短哈希|作者|日期|提交说明`。
    """
    args = ["log", f"-n{max_count}",
            "--pretty=format:%h|%an|%ad|%s", "--date=short"]
    if author:
        args += ["--author", author]
    if since:
        args += ["--since", since]
    if path:
        args += ["--", path]  # -- 之后是路径,避免和选项混淆
    return _run_git(repo, args)


@mcp.tool
def git_show(repo: str, ref: str) -> str:
    """查看某次提交的完整内容:提交说明 + 改动 diff。

    Args:
        repo: 仓库路径。
        ref: 提交哈希、标签或引用,例如 a1b2c3d、v1.5.0、HEAD~3。
    返回: 该提交的元数据与 unified diff。
    """
    return _run_git(repo, ["show", "--stat", "--patch", ref])


@mcp.tool
def git_blame(repo: str, file: str, start: int | None = None,
              end: int | None = None) -> str:
    """逐行追溯某个文件每一行最后由谁、哪次提交修改。

    Args:
        repo: 仓库路径。
        file: 相对仓库根的文件路径,例如 src/api/bill.py。
        start: 起始行号(可选)。
        end: 结束行号(可选)。
    返回: blame 文本,每行前缀是提交短哈希与作者。
    """
    args = ["blame", "-w", "-M"]  # -w 忽略空白, -M 检测行内移动
    if start and end:
        args += ["-L", f"{start},{end}"]
    args += ["--", file]
    return _run_git(repo, args)


@mcp.tool
def git_diff_refs(repo: str, base: str, head: str) -> str:
    """对比两个引用(分支/标签/哈希)之间的差异。

    Args:
        repo: 仓库路径。
        base: 基线引用,例如 main。
        head: 目标引用,例如 feature/checkout。
    返回: base...head 的 unified diff 与文件清单。
    """
    return _run_git(repo, ["diff", "--stat", "--patch", f"{base}...{head}"])


@mcp.tool
def git_branches(repo: str, remote: bool = True) -> str:
    """列出仓库所有分支及其最新提交。

    Args:
        repo: 仓库路径。
        remote: 是否包含远程分支,默认 True。
    返回: 每行一个分支:`分支名 最新短哈希 提交说明`。
    """
    args = ["branch", "--list", "-v",
            "--format=%(refname:short)|%(objectname:short)|%(contents:subject)"]
    if remote:
        args = ["branch", "-a", "--format=%(refname:short)|%(objectname:short)|%(contents:subject)"]
    return _run_git(repo, args)


if __name__ == "__main__":
    mcp.run()  # 默认 stdio 传输

预期工具清单(启动后模型能看到 5 个工具):

git_log        查看提交历史
git_show       查看某次提交内容
git_blame      逐行追溯修改者
git_diff_refs  对比两个引用差异
git_branches   列出所有分支

常见问题 1:git_log 返回空——多半是 path 参数写错或文件在仓库根外,让模型先用 git_branches 确认仓库正常。 常见问题 2:git_show 报 ambiguous argument——ref 拼错。模型通常会自动重试,先调 git_log 拿到正确哈希。 常见问题 3:大仓库 git_log 超时——把 max_count 调小,或加 since 缩小范围。

第四步:注册到 Claude Code

FastMCP 服务器通过 stdio 与 Claude Code 通信,需要把它登记进 Claude Code 的 MCP 配置。两种等价方式任选其一。

方式 A:用 CLI 一键添加(推荐)

claude mcp add git-archaeology -- python /home/你/mcp-git-archaeology/git_archaeology_mcp.py

方式 B:手写项目级 .mcp.json

在你想用它的工作目录里创建 .mcp.json:

{
  "mcpServers": {
    "git-archaeology": {
      "command": "python",
      "args": ["/home/你/mcp-git-archaeology/git_archaeology_mcp.py"]
    }
  }
}

用 which python 拿到绝对路径,把上面的 python 换成绝对路径更稳妥(避免 Claude Code 找不到虚拟环境里的 Python)。

验证注册是否成功:

claude mcp list

预期输出:

git-archaeology  ✓ connected  (5 tools)

常见问题:状态显示 ✗ failed to connect。九成原因是:① Python 路径不对;② 虚拟环境没装 FastMCP;③ 脚本有语法错误。手动跑一次 python git_archaeology_mcp.py,若立刻报错退出,就是脚本问题;若 hang 住等输入(stdio 模式正常表现),说明脚本身没问题。

第五步:调试——用 MCP Inspector 看工具实际行为

开发期最怕"工具调用没反应却不知原因"。MCP Inspector 是官方调试器,能独立启动你的服务器并手动调用每个工具、查看请求/响应原文:

npx -y @modelcontextprotocol/inspector python git_archaeology_mcp.py

浏览器打开它给出的本地地址(默认 http://localhost:5173),在 GUI 里:

  1. 点 Connect,左侧应列出 5 个工具。
  2. 选 git_log,填 repo=/home/你/projects/demo、max_count=5,点 Run Tool。
  3. 右侧返回区会显示 git log 的文本。

预期结果(以演示仓库为例):

a1b2c3d|张三|2026-07-01|fix: 修复订单总额四舍五入错误
e4f5g6h|李四|2026-06-28|feat: 新增优惠券叠加逻辑
...

如果返回的是一串 JSON 错误,对照错误信息修脚本。Inspector 是"先把工具调通再交给模型"的关键一环,能省下大量在 Claude Code 里反复试错的时间。

第六步:进阶——加上 Resources,让模型主动读项目文档

工具是"模型主动调用",Resources 是"模型按需读取的资源"。给考古 MCP 暴露仓库的 README.md、CHANGELOG.md,模型在写 release notes 时可以顺手读取。续写脚本:

DOCS = ["README.md", "CHANGELOG.md", "CONTRIBUTING.md"]


@mcp.resource("git://{repo}/docs/{name}")
def read_doc(repo: str, name: str) -> str:
    """读取仓库根目录下的指定文档文件。

    URI 模板中的 {repo} 和 {name} 由客户端填充。
    """
    if name not in DOCS:
        raise ValueError(f"只允许读取: {DOCS}")
    target = (Path(repo).expanduser().resolve()) / name
    if not target.exists():
        return f"({name} 不存在)"
    return target.read_text(encoding="utf-8")

URI 模板 git://{repo}/docs/{name} 是 FastMCP 的写法,客户端可枚举或按需展开。重启服务器后,Inspector 的 Resources 标签页会出现对应条目。

常见问题:Resource 在 Claude Code 里不自动展开 {repo}。Resources 更适合"固定路径"场景;想让模型自动找仓库,优先用工具(模型会自己传 repo 参数)。

实战案例:让模型当"代码侦探"

在演示仓库 ~/projects/demo 里启动 Claude Code,问它:

"src/api/bill.py 里的 orderTotal 函数,最近一次改动是谁做的?当时提交说明怎么写的?给我看那次改动的 diff。"

模型会自动编排三个工具调用:

  1. git_blame(repo=..., file="src/api/bill.py") → 定位 orderTotal 行对应的提交哈希 c9d8e7f。
  2. git_show(repo=..., ref="c9d8e7f") → 拿到提交说明与 diff。
  3. 综合两者给出结论。

模型最终回答(示意):

orderTotal 由 张三 在 c9d8e7f(2026-07-01)最后一次修改,
提交说明:"fix: 修复订单总额四舍五入错误"。
那次改动把第 42 行的 round(total, 1) 改成了 round(total, 2),
原因是金额按角舍入会少收 0.1 元。

这正是"考古"的价值——把埋在 .git 里的历史决策挖出来,变成模型可直接推理的上下文。

架构与调用流程

flowchart LR
    U[用户提问] --> CC[Claude Code]
    CC -->|JSON-RPC over stdio| Srv[git-archaeology MCP]
    Srv --> WH[子命令白名单/注入护栏]
    WH -->|列表传参, no shell| GIT[git CLI]
    GIT --> R[(本地 .git 仓库)]
    R --> Srv -->|截断后文本| CC --> U

一次"考古"的多工具编排时序:

sequenceDiagram
    participant U as 用户
    participant C as Claude Code
    participant M as MCP 服务器
    participant G as git
    U->>C: orderTotal 谁改的?
    C->>M: tools/call git_blame(bill.py)
    M->>G: git -C repo blame bill.py
    G-->>M: 每行 -> 哈希 c9d8e7f
    M-->>C: blame 文本
    C->>M: tools/call git_show(c9d8e7f)
    M->>G: git show c9d8e7f
    G-->>M: 提交说明 + diff
    M-->>C: show 文本
    C-->>U: 综合 blame+show 结论

延伸阅读

训练营适用性评估

参考来源

  1. FastMCP — The fast, Pythonic way to build MCP servers (GitHub) — 本教程使用的 @mcp.tool / @mcp.resource 装饰器与三原语架构来源。
  2. Tools — Model Context Protocol 官方规范 — 工具定义、调用与结果格式的权威规范。
  3. MCP Python SDK (官方) — FastMCP 之下的底层 SDK,stdio 传输与协议实现参考。
  4. FastMCP in Python: Build a Real MCP Server (2026 Guide) — 2026 年 FastMCP 3.x 生产级服务器的实战写法。
  5. Building Your First MCP Server with Python in 2026 — 含 Resources/Prompts 与多真实案例的入门到进阶教程。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入