Kamu pengen AI coding assistant kamu bisa baca database, search dokumentasi, atau manggil API internal. Sekarang kamu harus paste context manual ke chat, atau bikin integrasi custom yang gampang rusak kalau vendor model ganti sesuatu.
Model Context Protocol (MCP) bantu masalah ini. MCP adalah standar terbuka yang bikin client MCP-compatible apa pun (Claude Desktop, Cursor, VS Code dengan Continue, dll) bisa ngobrol dengan server MCP apa pun lewat protokol yang sama. Kamu bikin servernya sekali. Semua client yang compatible bisa pake.
Tutorial ini bikin MCP server yang berfungsi pake Python SDK resmi. Bukan demo mainan. Server dengan tools asli, resources, prompts, error handling, dan dua mode transport. Kamu bisa langsung jalanin dan tes di akhir.
Prerequisites
- Python 3.10 atau lebih baru (cek pake
python3 --version) uvterinstall (rekomendasi untuk development MCP SDK). Install pakecurl -LsSf https://astral.sh/uv/install.sh | sh- Familiar dasar Python. Kamu harus nyaman sama fungsi, type hints, dan dasar async.
- Client MCP-compatible buat testing. MCP Inspector (dibahas di bawah) jalan tanpa setup client apa pun.
Apa itu MCP sebenernya
MCP pake arsitektur client-server. MCP host (aplikasi AI kayak Claude Desktop atau Cursor) bikin satu MCP client per server yang dia connect. Tiap client jaga koneksi dedicated ke servernya.
Protokolnya jalan di dua layer:
- Data layer: pesan JSON-RPC 2.0. Di sini tools, resources, dan prompts berada.
- Transport layer: gimana bytes berpindah antara client dan server. Dua pilihan: stdio (buat server lokal, cepat, tanpa network) dan Streamable HTTP (buat server remote, support multiple clients).
Server ngekspos tiga primitives:
- Tools: fungsi yang LLM bisa panggil. Bayangin POST endpoints. Baca database, manggil API, jalanin komputasi.
- Resources: data yang LLM bisa baca ke context. Bayangin GET endpoints. Isi file, baris database, dokumentasi.
- Prompts: template pesan yang reusable. Instruksi pre-built buat task spesifik.
SDK abstraksiin plumbing JSON-RPC. Kamu nulis fungsi Python dengan type hints dan docstrings. SDK generate schema, handle protokol, dan manage connection lifecycle.
Langkah 1: Setup project
Bikin project baru pake uv:
uv init mcp-server-tutorial
cd mcp-server-tutorial
uv add "mcp[cli]"
Ini install MCP Python SDK dengan CLI tools. Stable v1.x yang kamu dapet secara default, dan itu yang kamu mau buat production. SDK ada di versi 1.28.1 di PyPI per tulisan ini.
Verifikasi install:
uv run mcp --help
Kamu harus liat CLI help output yang nge-list command yang available kayak dev dan install.
Langkah 2: Bikin tool pertama
Bikin server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Tutorial")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run(transport="stdio")
Itu adalah MCP server lengkap. Decorator @mcp.tool() daftarin fungsi sebagai tool. Type hints jadi JSON Schema. Docstring jadi deskripsi tool yang LLM liat. Nggak ada schema manual, nggak ada request parsing.
Baris transport="stdio" kasih tau server buat komunikasi lewat standard input dan output. Ini transport buat server lokal. Client spawn server process dan ngobrol lewat stdin/stdout.
Langkah 3: Tes pake MCP Inspector
MCP Inspector adalah web UI buat tes MCP server tanpa butuh client AI lengkap. Jalanin pake:
npx @modelcontextprotocol/inspector uv run server.py
Ini launch Inspector dan connect ke server kamu otomatis. Buka URL yang di-print (biasanya http://localhost:6274). Kamu akan liat:
- Tab Tools yang nge-list tool
addkamu dengan schemanya - Form input yang pre-filled dari type hints
- Tombol buat call tool dan liat hasilnya
Ketik a=5 dan b=3, klik Call, dan kamu dapet 8. Ini cara tercepat buat verifikasi server kamu jalan sebelum dicolok ke client asli.
Langkah 4: Tambah tools asli dengan error handling
Kalkulator oke buat tes. Sekarang bikin sesuatu yang lebih dekat ke use case asli: server yang baca file dan search direktori. Ini yang official filesystem server lakuin, tapi disederhanain.
import os
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("FileTools")
@mcp.tool()
def read_file(path: str) -> str:
"""Read the contents of a file at the given path.
Args:
path: Absolute or relative path to the file.
"""
p = Path(path)
if not p.exists():
raise ValueError(f"File not found: {path}")
if not p.is_file():
raise ValueError(f"Not a file: {path}")
if p.stat().st_size > 1_000_000:
raise ValueError(f"File too large (max 1MB): {path}")
return p.read_text(encoding="utf-8")
@mcp.tool()
def list_directory(path: str) -> str:
"""List files and subdirectories in a directory.
Args:
path: Path to the directory to list.
"""
p = Path(path)
if not p.exists():
raise ValueError(f"Directory not found: {path}")
if not p.is_dir():
raise ValueError(f"Not a directory: {path}")
entries = []
for entry in sorted(p.iterdir()):
entry_type = "dir" if entry.is_dir() else "file"
size = entry.stat().st_size if entry.is_file() else 0
entries.append(f"[{entry_type}] {entry.name} ({size} bytes)")
return "\n".join(entries) if entries else "Directory is empty"
@mcp.tool()
def grep_files(directory: str, pattern: str) -> str:
"""Search for a text pattern in all files under a directory.
Args:
directory: Root directory to search in.
pattern: Text to search for (case-insensitive).
"""
root = Path(directory)
if not root.is_dir():
raise ValueError(f"Directory not found: {directory}")
matches = []
pattern_lower = pattern.lower()
for filepath in root.rglob("*"):
if not filepath.is_file():
continue
if filepath.suffix in (".pyc", ".so", ".png", ".jpg", ".pdf"):
continue
try:
content = filepath.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
for i, line in enumerate(content.splitlines(), 1):
if pattern_lower in line.lower():
matches.append(f"{filepath}:{i}: {line.strip()}")
if not matches:
return f"No matches found for '{pattern}' in {directory}"
return "\n".join(matches[:50])
if __name__ == "__main__":
mcp.run(transport="stdio")
Beberapa hal buat diperhatiin:
- Raise
ValueErrordi dalam tool ngirim pesan error balik ke client sebagai error result. LLM liat dan bisa adjust. Ini lebih baik dari balikin string error generic karena protokol bedain hasil sukses dari error. - Guard ukuran file 1MB mencegah file gede ke-load tanpa sengaja ke context window LLM.
- Tool grep skip ekstensi file biner dan batasi hasil ke 50 match biar response tetap manageable.
Langkah 5: Tambah resources
Resources adalah data read-only yang client bisa load ke context. Beda dari tools, LLM nggak "call" resources. Client application yang tentuin kapan load, sering berdasarkan aksi user atau saran LLM.
@mcp.resource("config://app")
def get_config() -> str:
"""Return the application configuration as JSON."""
return json.dumps({
"version": "1.0.0",
"max_file_size": 1048576,
"allowed_extensions": [".txt", ".md", ".py", ".json"],
}, indent=2)
@mcp.resource("file://{path}")
def get_file_resource(path: str) -> str:
"""Return the contents of a file as a resource."""
p = Path(path)
if not p.is_file():
raise ValueError(f"File not found: {path}")
return p.read_text(encoding="utf-8")
URI template file://{path} berarti client bisa request file:///home/user/notes.txt dan SDK ekstrak /home/user/notes.txt sebagai parameter path. Gitu cara dynamic resources kerja.
Static resources kayak config://app punya URI tetap. Client fetch pake URI persis itu.
Langkah 6: Tambah prompts
Prompts adalah template pesan yang reusable. Waktu user pilih prompt di clientnya, prompt itu generate pesan berformat yang masuk ke percakapan.
@mcp.prompt()
def review_code(filepath: str) -> str:
"""Generate a prompt for reviewing a file's code."""
return f"""Please review the code in {filepath}.
Focus on:
- Potential bugs or edge cases
- Code clarity and naming
- Missing error handling
Read the file first, then provide your review."""
@mcp.prompt()
def explain_error(error_message: str) -> str:
"""Generate a prompt for explaining an error message."""
return f"I got this error and I'm not sure what it means. Can you explain it and suggest how to fix it?\n\n{error_message}"
Di client kayak Claude Desktop, prompt-prompt ini muncul di prompt picker. User isi argumen filepath atau error_message, dan pesan yang di-generate masuk ke chat.
Langkah 7: Jalanin sebagai remote server dengan Streamable HTTP
Transport stdio cocok buat tools lokal. Tapi kalau kamu mau multiple client connect ke server yang sama, atau mau deploy di server, kamu butuh transport Streamable HTTP.
Ganti baris run:
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Jalanin:
uv run server.py
Server mulai di http://localhost:8000/mcp. Multiple client bisa connect ke endpoint ini bersamaan. Ini transport yang kamu pake buat MCP server yang di-deploy.
Buat tes pake Inspector, start server di satu terminal, terus di terminal lain:
npx @modelcontextprotocol/inspector
Di UI Inspector, pilih "Streamable HTTP" sebagai transport type dan masukin http://localhost:8000/mcp sebagai URL.
Langkah 8: Connect ke Claude Desktop
Buat pake server kamu di Claude Desktop, edit file konfigurasi. Di macOS ada di ~/Library/Application Support/Claude/claude_desktop_config.json. Di Windows ada di %APPDATA%\Claude\claude_desktop_config.json.
Buat server stdio:
{
"mcpServers": {
"filetools": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp-server-tutorial", "server.py"]
}
}
}
Buat server HTTP:
{
"mcpServers": {
"filetools": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
}
Restart Claude Desktop. Kamu harus liat tools dan prompts dari server kamu available di interface.
Pitfall yang sering muncul
Server stdio nggak boleh print ke stdout. Panggilan print() apa pun di kode server kamu rusakin stream JSON-RPC. Pake logging sebagai gantinya, yang ke stderr. MCP SDK udah konfigurasi logging buat kamu.
Return value yang gede. Tools yang balikin string panjang bakal habisin context window LLM. Batasi hasil kamu. Tool grep di atas batasi ke 50 match. Lakukan sama buat tool apa pun yang bisa balikin output nggak terbatas.
Operasi blocking di async tools. Kalau kamu tandai tool sebagai async def, jangan panggil blocking I/O langsung. Pake asyncio.to_thread() atau anyio.to_thread.run_sync() buat offload blocking call. SDK support sync dan async tools, jadi kalau tool kamu naturally blocking, pake def aja bukan async def.
Path traversal. File tools di atas nggak batasi path. Buat apa pun yang mendekati production, validasi bahwa resolved path tetap di dalam direktori yang diizinkan:
ALLOWED_ROOT = Path("/home/user/projects").resolve()
def safe_path(path: str) -> Path:
resolved = Path(path).resolve()
if not str(resolved).startswith(str(ALLOWED_ROOT)):
raise ValueError(f"Access denied: {path} is outside allowed root")
return resolved
Kapan pake MCP vs. alternatif
MCP bukan satu-satunya cara buat kasih LLM akses ke tools. Kamu bisa pake function calling langsung lewat API OpenAI atau Anthropic. Kamu bisa pake framework kayak LangChain atau CrewAI. Ini kapan MCP masuk akal:
- Multiple clients: Kamu mau server yang sama kerja di Claude Desktop, Cursor, VS Code, dan client MCP-compatible masa depan apa pun tanpa rewrite. Ini selling point utama. Function calling ngikat kamu ke API satu vendor.
- Separation of concerns: Logic tool ada di server. Interaksi LLM ada di client. Server kamu nggak tau dan nggak peduli model mana yang jalan.
- Local-first: transport stdio berarti server jalan di mesin user dengan akses ke file dan tools lokal. Nggak perlu ekspos web API.
Kapan MCP berlebihan:
- Single client, single model, nggak ada reuse. Kalau kamu bikin satu bot buat satu API, function calling lebih simpel.
- Kamu butuh coupling ketat antara hasil tool dan konstruksi prompt. MCP pisahin ini by design.
Ringkasan
Kamu sekarang punya MCP server yang berfungsi dengan tools, resources, prompts, dan dua mode transport. Kode server lengkap:
import os
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("FileTools")
# --- Tools ---
@mcp.tool()
def read_file(path: str) -> str:
"""Read the contents of a file."""
p = Path(path)
if not p.exists() or not p.is_file():
raise ValueError(f"File not found: {path}")
if p.stat().st_size > 1_000_000:
raise ValueError("File too large (max 1MB)")
return p.read_text(encoding="utf-8")
@mcp.tool()
def list_directory(path: str) -> str:
"""List files in a directory."""
p = Path(path)
if not p.is_dir():
raise ValueError(f"Not a directory: {path}")
entries = []
for entry in sorted(p.iterdir()):
t = "dir" if entry.is_dir() else "file"
entries.append(f"[{t}] {entry.name}")
return "\n".join(entries) if entries else "Empty"
# --- Resources ---
@mcp.resource("config://app")
def get_config() -> str:
"""Return app configuration."""
return json.dumps({"version": "1.0.0"}, indent=2)
# --- Prompts ---
@mcp.prompt()
def review_code(filepath: str) -> str:
"""Generate a code review prompt."""
return f"Please review the code in {filepath}."
if __name__ == "__main__":
mcp.run(transport="stdio")
Next steps: package server kamu pake uv build dan publish ke PyPI biar orang lain bisa install pake uvx your-server-name. Atau connect ke workflow coding tim kamu dengan nambahin ke config MCP di Cursor atau VS Code.
Referensi
- MCP Architecture Overview - dokumentasi arsitektur dan konsep resmi
- MCP Python SDK (v1.x) - SDK yang dipake di tutorial ini, stable release line
- MCP Inspector - tool testing dan debugging buat MCP server