最近在折腾 MCP(Model Context Protocol)时,我遇到一个很实际的需求:大模型在 IDE 里写代码,却不知道当前 Git 仓库到底处于什么状态。让它分析“当前分支改了什么”,它只能靠猜;让它查最近的提交记录,它要么让我手动复制输出,要么干脆编一个。与其等官方工具补上这个能力,不如自己开发一个 Git 信息查询 MCP 服务,把 git status、git log、git diff、git branch 这类查询能力以标准协议暴露给模型。这篇东西适合想自己写 MCP Server 的开发者,也适合项目里准备接入 AI 辅助编码、需要精细化控制 Git 数据访问的人。我会把从协议理解、环境搭建、核心实现到安全边界的完整过程都过一遍,最后还会分享几个调试时踩到的坑。
1. 为什么我决定自建 Git 信息查询 MCP 服务
1.1 大模型和 Git 仓库之间缺一个“标准插座”
先说清楚我为什么要做这件事。大语言模型本身没有 shell 执行能力,也不会主动去读 .git 目录。你可以把 Git 仓库里所有的信息都理解为躺在磁盘上的状态数据,但模型既不能用眼睛看,也不能用命令行查。它只能基于对话上下文和训练知识来回答。如果你问它“当前分支改了什么”,它最大的可能就是根据你贴出来的代码片段做个推断,一旦工作区里发生过文件增删,推断结果基本等于瞎猜。
IDE 里那些 Git 插件虽然能展示状态,但那是写死的 UI 逻辑,模型没法把这些信息当作可推理的依据。MCP 的场景正好补上这一环:把查询能力封装成工具,模型在需要时自己决定调用。相当于给模型配了一个只读终端,它想看仓库状态时就敲一条命令,拿到输出后再基于真实数据回答。刚开始你会觉得这有点绕,但跑通之后会发现,这是让 AI 具备“对环境感知”的通用做法。
1.2 为什么不用现成的 GitHub 示例服务
先别急着写代码。社区里其实已经有不少 MCP Server,比如官方示例里的 mcp-server-github,可以把 GitHub 上的 issue、PR、代码搜索拉给模型。但这个方案偏远程托管仓库,对本地开发场景帮助有限。你本地没推送的分支、工作区里未暂存的改动、某个 commit 实际改了哪些文件,这些信息在 GitHub API 那边根本不存在。也有一些社区维护的 Git MCP 服务,但我试过几个,大多绑死了特定客户端,或者安装方式依赖第三方工具,改起来反而更费劲。
所以我的判断是:自建一个只读的 Git 查询服务,用最少的代码暴露最需要的命令。一来可控,二来可以按团队内部仓库结构和权限模型做定制。后面聊到安全边界时你会发现,自建的意义不只是“能用”,而是“知道它在什么边界内工作”。像 Figma MCP、数据库 MCP 这类现成服务解决的是通用接入问题,而本地 Git 仓库这种场景,带有很强的工程上下文和个人工作流属性,自己写往往比适配别人的实现更快。
1.3 与 Agent Skill 的区别
搜索这个问题时经常看到“agent skill 和 mcp 有什么区别”。我理解两者的定位完全不同。Skill 更像是 Agent 内部的一种编排资产,本质是提示词、示例脚本和工作流的组合,它决定 Agent 在什么场景下用哪些策略。MCP 则是 Agent 与外部工具之间的通信协议,负责把“调用工具”这件事标准化。说得直白点,Skill 教模型怎么想,MCP 让模型能真正碰到外部世界。
在实际项目里,两者经常搭配使用。你可以在 Skill 里写清楚“当用户问仓库状态时,优先调用 git_status 工具”,MCP 那边就专心把 git_status 实现得又快又稳。我见过一些团队把大量业务逻辑塞进 Skill,结果 MCP 工具反而很单薄,这其实颠倒了职责。MCP 更接近基础设施,Skill 更接近工作方法。
| 维度 | Agent Skill | MCP |
|---|---|---|
| 本质 | 提示词/工作流/脚本的组合,Agent 内部的编排资产 | 标准协议,定义工具发现与调用的接口 |
| 作用位置 | Agent 内部逻辑层 | Agent 与外部服务之间 |
| 典型例子 | 一个带 Prompt 的代码评审 Skill | git-query-server 通过 JSON-RPC 暴露 git 查询工具 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先吃透 MCP 的请求链路:工具调用不是魔法
2.1 Server / Client / Host 三者的关系
在写代码之前,先理解 MCP 的参与者。Host 是用户直接面对的那一端,比如 Claude Desktop、IDE 插件;Client 是 Host 内部嵌着的 SDK,负责和 Server 建立连接;Server 就是我们马上要写的进程,负责注册工具、接收调用、返回结果。三者的关系很好记:Host 是前台,Client 是前台手里的话机,Server 是电话那头真正干活的人。
注意一点:Server 本身不参与模型推理。模型决定要不要调用某个工具、传什么参数,这些都是在 Host 侧发生的;Server 只需要保证面对 tools/list 和 tools/call 两种请求时能给出正确响应。这个边界想清楚,后面遇到“模型为什么不调用工具”的问题时才知道去哪查。很多人一上来就背概念,结果调不通时把错误全归到 Server 代码上,实际却是 Client 配置没加载,或者模型根本没接收到工具描述。
2.2 一次工具调用的完整时序
拆开来看,一次调用的完整链路大概是:模型根据上下文判断需要查仓库状态,于是生成一个工具调用意图,Client 把这个意图转成 JSON-RPC 请求发给 Server;Server 收到后找到对应工具函数,执行 subprocess 调起 git 命令,拿到 stdout;然后把结果包装成 JSON-RPC 响应返回给 Client;Client 再把响应消息追加进模型对话上下文,模型看到真实输出后组织语言回答你。
这里有个细节经常被忽略:工具返回的内容会直接变成模型上下文的一部分,所以输出长度、格式都非常重要。如果你让 server 返回几百行 git diff,模型可能被无关噪音淹没,反而抓不住重点。这也是为什么我建议把输出做严格裁剪,后面讲实现时会说到。
2.3 stdio 与 HTTP 传输层
MCP 的传输层在本地开发场景下基本不用纠结,直接用 stdio。Server 由客户端进程拉起,通过标准输入输出通信,不需要开放端口、不需要认证,也省去了进程生命周期管理。你可以把 stdio 模式理解成“给我一个命令,客户端负责启动它并喂数据”。缺点是 Server 没法同时服务多个客户端,但对于 IDE 里单用户场景完全够用。
如果你的 Server 要部署在远程、给多个客户端共享,那就得走 HTTP + SSE(现在 SDK 也支持 streamable HTTP)。这种情况下必须做鉴权和访问控制,因为一旦监听在网络上,任何人都可能调用你的 git 查询接口。我的建议是:能本地就跑本地,别为了“看起来高大上”去裸暴露 HTTP 端到公网。
2.4 三个核心协议方法
作为 Server 开发者,最常关心的协议方法只有三个。initialize:客户端启动后先和服务端握手,确认协议版本和能力。tools/list:客户端拉取工具清单,这个清单会经过 Host 侧处理后变成模型能看到的工具描述。tools/call:客户端带着工具名和参数调用服务端。理解了这三个方法,你再看 FastMCP 的源码基本不费劲,它把 JSON-RPC 的收发、连接生命周期都封装好了,我们只需要注册函数。
3. 服务端实现:Python + FastMCP 搭一个可运行的 Git 查询服务
3.1 环境准备与依赖
当前 MCP 官方 SDK 对 Python 支持已经很完善,Python 3.10 以上即可。我建议用 uv 管理环境,因为后面 mcp CLI、Inspector 都依赖它。初始化项目后安装依赖:uv add "mcp[cli]"。如果你更习惯 pip,直接 pip install "mcp[cli]" 也行,差别不大。项目里只需要一个 Python 文件就能跑起来。
前提是本机装了 git 命令行工具,git --version 能正常输出。这里别用 IDE 内置的 Git 能力偷懒,我们是直接调系统命令,所以确保 git 命令在 PATH 里。Windows 用户尤其要注意:如果通过某些 IDE 启动 Server,PATH 可能和终端不一样,建议配置里写绝对路径,后面踩坑章节会细说。
3.2 初始化 FastMCP 服务器
先创建一个 git_query_server.py 文件,我习惯把仓库目录放在环境变量里,而不是写死在代码中:
python复制import os
import subprocess
from mcp.server.fastmcp import FastMCP
REPO_ROOT = os.environ.get("GIT_REPO", os.getcwd())
mcp = FastMCP("git-query")
环境变量 GIT_REPO 用来指定要查询的仓库根目录。为什么不直接在代码里写死?因为一个 Server 进程通常只服务一个仓库,写成环境变量方便在不同项目间复用,也方便在客户端配置里切换。如果你只是临时用,把文件放到任意仓库目录下,什么都不配也能运行,默认取当前工作目录。
FastMCP 是 mcp SDK 提供的高层封装。它帮我处理了 JSON-RPC 和 stdio 的细节,我只需要用装饰器把普通 Python 函数变成工具。如果后面需要更细粒度控制,也可以直接用底层 Server API,但第一版尽量不要自找麻烦。
3.3 核心工具实现
先写一个统一的 git 命令执行函数,所有工具都复用它:
python复制def run_git(args: list[str], cwd: str | None = None) -> str:
target_dir = cwd or REPO_ROOT
result = subprocess.run(
["git", *args],
cwd=target_dir,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10,
)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or "git command failed")
return result.stdout.strip()
几个细节解释一下。参数用列表传而不是字符串拼,是为了避免 shell 注入,也让参数边界清晰。timeout=10 秒防卡死。encoding=utf-8 加 errors=replace 是为了兼容非 UTF-8 输出,Windows 环境下尤其管用。returncode 不为 0 时直接抛 RuntimeError,FastMCP 会把它转成工具调用失败信息返回给模型,模型才能知道自己调用错了。
然后注册四个核心工具:
python复制@mcp.tool()
def git_status() -> str:
"""查看当前仓库的变更状态,包括当前分支、已暂存改动、未暂存改动和未跟踪文件,适合回答仓库现在是否干净、改了什么这类问题。"""
return run_git(["status", "--short", "--branch"])
@mcp.tool()
def git_log(max_count: int = 10) -> str:
"""查看最近max_count条提交记录,返回简洁的单行提交记录,包含提交哈希、提交说明和分支指针。"""
max_count = max(1, min(int(max_count), 50))
return run_git(["log", f"-{max_count}", "--oneline", "--decorate"])
@mcp.tool()
def git_diff(target: str = "HEAD") -> str:
"""查看指定提交或工作区相对于HEAD的改动内容。target默认是HEAD,执行时相当于git diff HEAD,返回包含文件改动列表和具体行变更。"""
return run_git(["diff", target])
@mcp.tool()
def git_branch() -> str:
"""列出所有本地分支,并显示每个分支的跟踪关系和最新提交信息。"""
return run_git(["branch", "-vv"])
这里注意:FastMCP 会把 Python 的函数签名翻译成 JSON Schema 给模型看。max_count 的参数描述、默认值、边界约束都会成为模型选择参数的依据。我把 docstring 写得像使用说明,不是写给自己看的注释,而是写给模型看的提示。你写工具时千万别吝啬这几行文字,模型是否在正确场景调用工具,很大程度取决于描述够不够具体。
git_status 返回的是 --short 格式,因为短格式省 token,模型也能直接读到关键信息。git_log 限制最多 50 条,避免一次拉太多刷爆上下文。git_diff 默认查 HEAD 和工作区的差,这是“当前改了什么”的高频实现。git_branch 用 -vv 可以看到本地分支和上游跟踪关系,对模型理解分支状态很有用。这几个工具覆盖了日常开发中最常见的查询需求。
3.4 输出结构与上下文控制
有人会问,为什么不返回 JSON 而返回纯文本。MCP 工具本身可以返回结构化内容,但现阶段很多模型消费文本时更自然。Git 命令的输出本来就是给人看的文本,直接透传给模型,解析成本最低。但是要注意控制输出大小,我在实际使用时会加一个最大字符数限制:超过 20000 字符就截断,并在末尾提示“输出已截断”。否则一次大范围 git diff 可能直接把对话上下文撑爆,模型反而答非所问。
4. 安全边界:白名单、路径校验与只读约束一个都不能少
4.1 LLM 调用外部命令的威胁模型
先讲威胁模型,再讲怎么防。正常情况下工具参数来自模型生成,而模型生成的内容会受用户 Prompt 影响。假设用户说“帮我执行 git status --porcelain && git reset --hard”,模型如果被诱导拼接参数,你的查询服务就可能变成任意命令执行器。有人觉得这是杞人忧天,但在 AI Agent 场景里,Prompt 注入是真实存在的风险。尤其 Git 命令里藏着不少可以读写文件、修改状态的 flag,比如 --git-dir 可以改变仓库路径,--work-tree 可以指定工作树,还有 config 能改配置。
所以我们不能只做一个“把参数交给 git”的工具,必须加白名单。查询服务只允许白名单里的命令,并且对参数做校验。我见过有人把 commit、push 也放进查询服务里,理由是“模型偶尔需要帮忙提交”,但从安全角度看,查询和写操作一定要分开。混在一起,权限边界会越来越模糊。
4.2 仓库路径校验与目录逃逸
如果 Server 支持通过环境变量或参数指定仓库目录,一定要做路径校验。用 Path.resolve() 把传入路径转成绝对路径,再检查它是否位于 REPO_ROOT 之下,否则直接拒绝
