<!-- title: 造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 工具的开发者。

## 前置条件

- 已安装 Claude Code（CLI 可调用），并完成首轮登录。
- Python 3.11+，能用 `python --version` 看到 3.11 以上版本。
- 系统装有 `git`（本教程的封装对象）。
- 有任意一个 Git 仓库用来测试（本教程用 `~/projects/demo` 演示，你换成自己的仓库即可）。
- 了解 Python 装饰器与类型注解的基础语法。

## 为什么要把 Git 封装成 MCP？

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

- "这个函数最近一次被谁、在哪次提交里改过？"
- "`orderTotal` 这个变量名是哪次提交引入的？当时提交说明写了什么？"
- "v1.4.0 到 v1.5.0 之间，`src/checkout/` 目录下有哪些文件改动？"
- "`main` 和 `release` 分支当前差了几个提交？"

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

## 第一步：搭建项目与依赖

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

```bash
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 ...
```

创建服务器文件：

```bash
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`：

```python
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 黑名单**，三重护栏确保只读。

## 第三步：定义核心考古工具

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

```python
@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 一键添加（推荐）**

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

**方式 B：手写项目级 `.mcp.json`**

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

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

> 用 `which python` 拿到绝对路径，把上面的 `python` 换成绝对路径更稳妥（避免 Claude Code 找不到虚拟环境里的 Python）。

验证注册是否成功：

```bash
claude mcp list
```

预期输出：

```
git-archaeology  ✓ connected  (5 tools)
```

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

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

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

```bash
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 时可以顺手读取。续写脚本：

```python
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` 里的历史决策挖出来，变成模型可直接推理的上下文。

## 架构与调用流程

```mermaid
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
```

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

```mermaid
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 结论
```

## 延伸阅读

- **MCP 官方规范（Tools）**：`modelcontextprotocol.io/specification/2025-06-18/server/tools`——工具生命周期、`notifications/tools/list_changed`、结果格式定义。
- **FastMCP 文档**：`fastmcp.wiki`——`@mcp.tool` / `@mcp.resource` / `@mcp.prompt` 三大原语，以及 Context 对象（上报进度、日志）。
- **官方 Python SDK**：`github.com/modelcontextprotocol/python-sdk`——比 FastMCP 更底层，适合需要完全控制协议细节的场景。
- **MCP Inspector**：`github.com/modelcontextprotocol/inspector`——本文第五步用的调试器，支持消息回放与断言。
- **Claude Code MCP 配置**：`docs.claude.com/en/docs/claude-code/mcp`——`claude mcp add/list/remove` 与 `.mcp.json` 的权威说明。

## 训练营适用性评估

- **适合训练营类型**：MCP 开发实战营、AI 工程师进阶营、企业内部"AI 辅助研发"培训（2–3 天沉浸式）。
- **预计教学时长**：讲师演示 45 分钟 + 学员动手 90 分钟 + 调试答疑 30 分钟，合计约 2.5–3 小时。
- **难度定位**：中等。前置需掌握 Python 基础与 Git 常用命令；MCP 协议本身不要求深入，FastMCP 把细节封装得很薄。
- **可改造的练习方向**：把只读护栏去掉、加上 `git cherry-pick` 做成"半自动修 bug"工具；或把 `git` 换成 `kubectl`/`docker`，做运维诊断 MCP——同一套"白名单+列表传参"骨架完全通用。
- **验收标准**：学员能在自己的真实仓库里，让 Claude Code 用至少 3 个自制工具回答一个考古问题，且 Inspector 中工具全部返回 200/正常文本。

## 参考来源

1. [FastMCP — The fast, Pythonic way to build MCP servers (GitHub)](https://github.com/PrefectHQ/fastmcp) — 本教程使用的 `@mcp.tool` / `@mcp.resource` 装饰器与三原语架构来源。
2. [Tools — Model Context Protocol 官方规范](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) — 工具定义、调用与结果格式的权威规范。
3. [MCP Python SDK (官方)](https://github.com/modelcontextprotocol/python-sdk) — FastMCP 之下的底层 SDK，stdio 传输与协议实现参考。
4. [FastMCP in Python: Build a Real MCP Server (2026 Guide)](https://www.danilchenko.dev/posts/fastmcp-mcp-server/) — 2026 年 FastMCP 3.x 生产级服务器的实战写法。
5. [Building Your First MCP Server with Python in 2026](https://dev-faizan.medium.com/building-your-first-mcp-server-with-python-in-2026-complete-tutorial-with-3-real-examples-49700e902656) — 含 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
