本教程带你用 Python + FastMCP 从零构建一个 Git 考古 MCP 服务器,把 git log、git show、git diff、git blame、git branch 等只读命令封装成模型可调用的工具。学完你将掌握:MCP 工具的定义与参数 Schema、子进程安全调用、只读护栏与注入防护、stdio 传输、Claude Code 的 MCP 注册与调试,以及 Resources 原语的扩展用法。适合已读完入门篇、想造"能真正在项目里用"的 MCP 工具的开发者。
python --version 看到 3.11 以上版本。git(本教程的封装对象)。~/projects/demo 演示,你换成自己的仓库即可)。模型本身能读文件,但读不出代码的历史。git log、git blame 这些信息不在工作区文件里,而在 .git 对象库中,必须通过 git 命令解析。把这套能力做成 MCP 工具后,模型可以回答这类"考古"问题:
orderTotal 这个变量名是哪次提交引入的?当时提交说明写了什么?"src/checkout/ 目录下有哪些文件改动?"main 和 release 分支当前差了几个提交?"这些正是排查 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缩小范围。
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 是官方调试器,能独立启动你的服务器并手动调用每个工具、查看请求/响应原文:
npx -y @modelcontextprotocol/inspector python git_archaeology_mcp.py
浏览器打开它给出的本地地址(默认 http://localhost:5173),在 GUI 里:
git_log,填 repo=/home/你/projects/demo、max_count=5,点 Run Tool。预期结果(以演示仓库为例):
a1b2c3d|张三|2026-07-01|fix: 修复订单总额四舍五入错误
e4f5g6h|李四|2026-06-28|feat: 新增优惠券叠加逻辑
...
如果返回的是一串 JSON 错误,对照错误信息修脚本。Inspector 是"先把工具调通再交给模型"的关键一环,能省下大量在 Claude Code 里反复试错的时间。
工具是"模型主动调用",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。"
模型会自动编排三个工具调用:
git_blame(repo=..., file="src/api/bill.py") → 定位 orderTotal 行对应的提交哈希 c9d8e7f。git_show(repo=..., ref="c9d8e7f") → 拿到提交说明与 diff。模型最终回答(示意):
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 结论
modelcontextprotocol.io/specification/2025-06-18/server/tools——工具生命周期、notifications/tools/list_changed、结果格式定义。fastmcp.wiki——@mcp.tool / @mcp.resource / @mcp.prompt 三大原语,以及 Context 对象(上报进度、日志)。github.com/modelcontextprotocol/python-sdk——比 FastMCP 更底层,适合需要完全控制协议细节的场景。github.com/modelcontextprotocol/inspector——本文第五步用的调试器,支持消息回放与断言。docs.claude.com/en/docs/claude-code/mcp——claude mcp add/list/remove 与 .mcp.json 的权威说明。git cherry-pick 做成"半自动修 bug"工具;或把 git 换成 kubectl/docker,做运维诊断 MCP——同一套"白名单+列表传参"骨架完全通用。@mcp.tool / @mcp.resource 装饰器与三原语架构来源。[!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 等场景全覆盖。
