Agent kamu bisa baca file. Yang biasanya nggak bisa dia lakukan adalah menjawab "apa yang masuk ke branch ini minggu ini, dan apa yang belum di-commit" tanpa jalanin lima command shell lalu menebak-nebak format outputnya.
MCP server kecil bisa menutup celah itu. Ganti prompt shell dengan tiga panggilan bertipe: commit terakhir, file yang berubah, dan README. Di versi stabil SDK Python resmi sekarang (mcp 2.3.0), masing-masing cuma fungsi biasa dengan docstring. Schema argumennya diambil dari type hint, jadi nggak ada JSON Schema terpisah yang harus dijaga tetap sinkron.
Yang perlu disiapkan
- Python 3.11 atau lebih baru
uv, yang dipakai di seluruh dokumentasi SDK (pipjuga bisa)gitdan satu repo buat dites- Host MCP: Claude Code, Cursor, atau VS Code 1.99 ke atas dengan ekstensi GitHub Copilot di mode agent
Tool, resource, prompt: pilih berdasarkan siapa yang memutuskan
Server mengekspos tiga jenis benda, dan bedanya ada di kontrol:
- Tool adalah fungsi yang dipanggil model atas inisiatifnya sendiri.
- Resource adalah data yang dimuat host ke context.
- Prompt adalah template yang dipanggil user dengan nama.
Buat inspeksi repo, commit dan status working tree masuk kategori tool, karena model yang harus memutuskan kapan melihat. README masuk resource, karena host bisa melampirkannya di awal dan menghemat satu round trip. Nggak ada prompt di sini; prompt baru masuk akal kalau manusia butuh slash command.
Servernya
Bikin server.py:
import subprocess
from pathlib import Path
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer(
"Repo Lens",
instructions="Inspect the repo before proposing changes. Call recent_commits first.",
)
class Commit(BaseModel):
sha: str
author: str
subject: str
date: str
def _git(repo: str, *args: str) -> str:
path = Path(repo).expanduser().resolve()
if not (path / ".git").exists():
raise ToolError(f"{path} is not a git repository")
result = subprocess.run(
["git", "-C", str(path), *args],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
raise ToolError(result.stderr.strip() or "git failed")
return result.stdout
@mcp.tool()
def recent_commits(repo: str, limit: int = 5) -> list[Commit]:
"""List recent commits on the current branch of a local repo.
Use this before proposing a diff so you know what changed lately.
"""
out = _git(
repo,
"log",
f"-n{limit}",
"--pretty=format:%h\x1f%an\x1f%s\x1f%ad",
"--date=short",
)
commits = []
for line in out.splitlines():
sha, author, subject, date = line.split("\x1f")
commits.append(Commit(sha=sha, author=author, subject=subject, date=date))
return commits
@mcp.tool()
def changed_files(repo: str) -> list[str]:
"""List files with uncommitted changes, including untracked ones."""
out = _git(repo, "status", "--porcelain")
return [line[3:] for line in out.splitlines() if line.strip()]
@mcp.resource("repo://{repo}/readme")
def readme(repo: str) -> str:
"""The repository README, for project context."""
path = Path(repo).expanduser().resolve()
for name in ("README.md", "readme.md", "README"):
candidate = path / name
if candidate.exists():
return candidate.read_text(encoding="utf-8")
return f"(no README at {path})"
if __name__ == "__main__":
mcp.run()
Tiga hal ini menjelaskan sebagian besar perilaku yang kamu lihat di client:
- Decorator adalah keseluruhan registrasinya. Nama, deskripsi, dan schema dibaca dari fungsinya.
- Docstring jadi deskripsi tool yang dibaca model. Baris pertama harus pendek dan konkret; agent yang memilih di antara delapan tool membaca baris itu, bukan source code kamu.
- Nilai default bikin parameter jadi opsional.
limit: int = 5muncul di schema sebagai integer opsional dengan default 5.
mcp.run() tanpa argumen jalan lewat stdio, transport yang diharapkan host lokal. Bungkus dengan if __name__ == "__main__", karena host meng-import modul itu buat membaca objek servernya.
Jalankan di Inspector
uv run mcp dev server.py
Inspector mencetak URL lokal. Tiap primitif dapat satu tab, dan form argumennya dibuat dari type hint. Anotasi yang salah langsung kelihatan sebagai form yang salah sebelum ada model yang terlibat, dan itu debugging paling murah yang kamu dapat hari ini.
Test tanpa host
Nggak perlu subprocess, nggak perlu port. Kasih objek servernya langsung ke Client:
import anyio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
tools = await client.list_tools()
print([tool.name for tool in tools.tools])
result = await client.call_tool("recent_commits", {"repo": "/home/me/code/api", "limit": 3})
print(result.is_error)
print(result.structured_content)
if __name__ == "__main__":
anyio.run(main)
Ini konek in-process dan mengekspos method yang sama dengan sesi beneran: list_tools, call_tool, read_resource. Arahkan repo ke path nyata waktu dijalankan, karena tool yang raise nggak melempar exception di sini. Hasilnya balik sebagai result normal dengan is_error=True dan structured_content berisi None.
Poin terakhir itu yang paling sering bikin masalah. Kode client yang membaca structured_content tanpa cek is_error duluan bakal menganggap panggilan gagal sebagai sukses kosong.
Sambungkan ke host
Claude Code cukup satu command, semua setelah -- adalah launch command-nya:
claude mcp add repo-lens -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
Jalankan /mcp di dalam sesi buat memastikan servernya konek dan tool-nya terdaftar.
Cursor membaca .cursor/mcp.json di root project:
{
"mcpServers": {
"repo-lens": {
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}
VS Code membaca .vscode/mcp.json dengan key pembungkus yang beda:
{
"servers": {
"repo-lens": {
"type": "stdio",
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}
Setelah edit config mana pun, restart host-nya. Claude Desktop harus ditutup total, bukan cuma jendelanya, karena config dibaca sekali waktu start.
Empat hal yang bikin gagal
- Ada yang nulis ke stdout. Di stdio, stdout adalah protokolnya. Satu
print()nyasar waktu import, atau wrapper script yang mencetak banner, ngasih pesan rusak ke host dan koneksinya putus. Pakailogging, handler default-nya nulis ke stderr. - Path-nya relatif. Host menjalankan server dari working directory miliknya, bukan dari tempat kamu mengetik config. Semua path di config harus absolut, termasuk path ke
uvkalau host nggak menemukannya. - Exception biasa, bukan
ToolError. PesanToolErrormasuk ke content hasil, jadi model bisa membacanya dan mencoba lagi. Exception lain cuma jadiError executing tooltanpa detail, dan agent nggak punya apa pun buat dikoreksi. - Servernya sehat tapi daftar tool-nya basi. Tambah tool, lupa restart, host masih nampilin daftar lama dari launch terakhir.
Kapan ini lebih baik dari shell, kapan nggak
Kalau agent kamu sudah punya akses shell dan izin menjalankan git, server ini berfungsi sebagai pagar, bukan kemampuan baru. Yang kamu beli adalah set command tetap dengan argumen bertipe, jadi model nggak bisa mengarang flag atau nyangkut di pager interaktif. Yang kamu bayar adalah satu proses dan satu file config tambahan.
Dia berguna di tiga situasi. Repo-nya ada di mesin lain, jadi objek yang sama kamu sajikan lewat Streamable HTTP dan agent dapat URL, bukan akses filesystem. Kamu mau membatasi apa yang bisa dijalankan, misalnya tool read-only tanpa commit atau push. Atau host-nya memang nggak punya shell, dan MCP satu-satunya jalur.
Alternatif buat repo remote adalah MCP server resmi GitHub, yang sudah mencakup issue, pull request, dan pencarian kode se-organisasi. Bikin sendiri masuk akal kalau yang perlu diekspos nggak ada di GitHub, misalnya dashboard deploy internal atau katalog service.
Langkah berikutnya
Sengaja biarkan versi pertama read-only. Tool yang bisa commit itu berbahaya sampai kamu punya jalur konfirmasi, dan model bakal memanggilnya dengan percaya diri di working tree yang kotor.
Dua tambahan yang layak dikerjakan: tool search_code yang membungkus git grep -n, jadi agent dapat nomor baris persis bukan tebakan, dan allowlist root. Sekarang repo menerima path apa pun di disk. Itu masih wajar buat server stdio lokal yang kamu jalankan sendiri, dan nggak wajar begitu kamu mengeksposnya lewat HTTP, karena pemanggilnya yang memilih path.
Total file-nya sekitar 60 baris. Itu intinya: daripada mengajari agent git lewat paragraf di system prompt, kamu kasih tiga fungsi dengan schema yang nggak bisa dia salah tafsirkan.