Kenapa MCP Jadi Standar Baru Integrasi AI di 2026?
Kalau kamu pernah membangun aplikasi berbasis LLM — entah itu chatbot, AI assistant, atau agen otonom — pasti pernah frustrasi dengan satu hal: menghubungkan model AI ke data dan tools eksternal. Setiap layanan punya API berbeda, format respons berbeda, dan cara autentikasi berbeda. Rasanya kayak harus belajar bahasa baru setiap kali mau integrasi satu service.
Capek, kan?
Nah, di sinilah Model Context Protocol (MCP) masuk sebagai game-changer. Diperkenalkan oleh Anthropic pada November 2024, MCP adalah standar terbuka yang menstandarkan bagaimana model AI berkomunikasi dengan tools dan sumber data eksternal. Analoginya sederhana: MCP itu seperti USB-C untuk aplikasi AI — satu protokol universal yang menghubungkan LLM ke mana saja.
Seberapa besar dampaknya? Per Maret 2026, MCP sudah mencapai 97 juta download SDK bulanan dengan lebih dari 13.000 server publik di GitHub. OpenAI, Google, Microsoft, dan AWS semuanya sudah mengadopsi protokol ini. Sebagai perbandingan, React butuh sekitar tiga tahun untuk mencapai 100 juta download bulanan — MCP melakukannya hanya dalam enam belas bulan. Gila juga, ya.
Di artikel ini, kamu akan belajar cara membangun MCP server sendiri menggunakan Python dan FastMCP 3.0 — dari instalasi, membuat tools dan resources, integrasi database dan API eksternal, menghubungkan ke Claude Desktop dan Claude Code, hingga deployment ke produksi. Semua disertai kode yang bisa langsung kamu jalankan.
Jadi, langsung saja kita mulai.
Apa Itu MCP dan Bagaimana Arsitekturnya?
Sebelum mulai coding, penting untuk memahami arsitektur MCP dulu. Biar kamu nggak cuma copy-paste tapi benar-benar tahu apa yang sedang kamu bangun.
Komponen Utama MCP
MCP menggunakan arsitektur client-server dengan komunikasi stateful melalui format JSON-RPC 2.0. Ada tiga komponen utama:
- MCP Host — Aplikasi AI tempat pengguna berinteraksi, misalnya Claude Desktop, Cursor, VS Code Copilot, atau ChatGPT. Host ini menjalankan LLM yang akan menggunakan tools dari server.
- MCP Client — Komponen di dalam host yang mengelola komunikasi antara LLM dan MCP server. Client menerjemahkan permintaan LLM ke format MCP dan mengonversi balasan MCP agar bisa dipahami LLM.
- MCP Server — Layanan yang menyediakan konteks, data, dan kemampuan kepada LLM. Ini yang akan kita bangun. Server terhubung ke sistem eksternal seperti database, API, dan file system.
Tiga Primitif MCP Server
Setiap MCP server bisa menyediakan tiga jenis kemampuan:
- Tools — Fungsi yang dapat dipanggil oleh LLM (mirip endpoint POST). Contoh:
search_database,send_email,create_ticket. Tools adalah primitif yang paling sering dipakai. - Resources — Data yang dapat dibaca oleh LLM (mirip endpoint GET). Contoh: isi file, konfigurasi sistem, data metrik real-time. Resources dimuat ke context window model.
- Prompts — Template prompt yang sudah disiapkan dan bisa dipakai ulang. Contoh: template untuk analisis kode, summarization, atau debugging.
Transport Layer
MCP mendukung dua mode transport utama:
- STDIO (Standard I/O) — Server berjalan sebagai proses lokal. Cocok untuk pengembangan dan integrasi desktop. Ini mode default untuk Claude Desktop.
- Streamable HTTP — Server berjalan sebagai web service. Ini standar produksi per spesifikasi MCP 2026, mendukung koneksi remote dengan TLS.
Setup Lingkungan Pengembangan
Oke, sekarang kita siapkan environment-nya. Bagian ini cukup straightforward.
Prasyarat
- Python 3.10 atau lebih tinggi
- Familiar dasar dengan Python async/await
uv— package manager Python yang cepat (opsional tapi sangat direkomendasikan)- Claude Desktop atau Claude Code sebagai MCP client untuk testing
Instalasi
# Install uv (jika belum ada)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Buat project baru
mkdir mcp-server-demo
cd mcp-server-demo
uv init
uv venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install FastMCP 3.0 dan dependencies
uv add "fastmcp"
uv add httpx # untuk HTTP requests ke API eksternal
# Opsional: install MCP CLI untuk debugging
uv add "mcp[cli]"
FastMCP 3.0, dirilis 19 Januari 2026, adalah framework paling populer untuk membangun MCP server di Python — digunakan oleh lebih dari 70% server MCP Python. Yang bikin menarik, kamu nggak perlu ngurusin detail protokolnya sama sekali. Cukup tulis fungsi Python biasa dengan dekorator, dan FastMCP yang mengurus sisanya.
Membangun MCP Server Pertama: Tools Dasar
Mari mulai dengan server sederhana yang menyediakan tools kalkulator dan text processing. Saya sengaja pakai contoh yang simpel dulu biar konsepnya jelas.
# server.py
from fastmcp import FastMCP
# Inisialisasi server dengan nama deskriptif
mcp = FastMCP("demo-tools")
@mcp.tool()
def hitung_kata(teks: str) -> dict:
"""Menghitung jumlah kata, karakter, dan kalimat dalam sebuah teks.
Args:
teks: Teks yang akan dianalisis
"""
kata = teks.split()
kalimat = [s.strip() for s in teks.replace("!", ".").replace("?", ".").split(".") if s.strip()]
return {
"jumlah_kata": len(kata),
"jumlah_karakter": len(teks),
"jumlah_kalimat": len(kalimat),
"rata_rata_kata_per_kalimat": round(len(kata) / max(len(kalimat), 1), 1),
}
@mcp.tool()
def konversi_mata_uang(jumlah: float, dari: str, ke: str) -> dict:
"""Konversi mata uang sederhana menggunakan kurs tetap (untuk demo).
Args:
jumlah: Jumlah uang yang akan dikonversi
dari: Kode mata uang asal (IDR, USD, EUR, JPY)
ke: Kode mata uang tujuan (IDR, USD, EUR, JPY)
"""
# Kurs terhadap USD (contoh statis — di produksi, gunakan API real-time)
kurs_ke_usd = {
"IDR": 0.0000625,
"USD": 1.0,
"EUR": 1.08,
"JPY": 0.0067,
}
dari_upper = dari.upper()
ke_upper = ke.upper()
if dari_upper not in kurs_ke_usd or ke_upper not in kurs_ke_usd:
return {"error": f"Mata uang tidak didukung. Pilihan: {list(kurs_ke_usd.keys())}"}
usd = jumlah * kurs_ke_usd[dari_upper]
hasil = usd / kurs_ke_usd[ke_upper]
return {
"dari": f"{jumlah} {dari_upper}",
"ke": f"{round(hasil, 2)} {ke_upper}",
"kurs": f"1 {dari_upper} = {round(kurs_ke_usd[dari_upper] / kurs_ke_usd[ke_upper], 6)} {ke_upper}",
}
if __name__ == "__main__":
mcp.run()
Ada beberapa hal penting yang perlu kamu perhatikan di sini:
- Type hints itu wajib — SDK menggunakan type hints untuk men-generate input schema secara otomatis. Tanpa type hints, model nggak bisa memanggil tool dengan benar. Serius, ini sering jadi sumber error buat pemula.
- Docstring menjadi deskripsi tool — Tulis docstring untuk model AI, bukan hanya untuk manusia. Semakin jelas deskripsinya, semakin akurat model menggunakan tool-mu.
- Return type harus serializable — Kembalikan
str,int,float,list, ataudict. Tipe lain perlu di-cast kestr().
Menguji Server dengan MCP Inspector
Sebelum menghubungkan ke Claude, selalu uji server-mu dulu pakai MCP Inspector. Jujur, ini tool debugging yang sering di-skip padahal sangat berguna.
# Jalankan inspector (membuka UI web di browser)
mcp inspector server.py
Inspector memberikan antarmuka web di mana kamu bisa melihat semua tools yang terdaftar, memanggilnya dengan parameter kustom, dan memeriksa respons. Ini sangat berharga untuk menguji logika tool sebelum menghubungkan ke client AI sungguhan.
Satu peringatan penting untuk server STDIO: jangan pernah menulis ke stdout menggunakan print(). Ini akan merusak pesan JSON-RPC dan bikin server-mu crash. Gunakan print(..., file=sys.stderr) kalau perlu logging, atau lebih baik gunakan modul logging Python dengan handler stderr. Saya pernah menghabiskan hampir satu jam debugging gara-gara satu baris print() yang terselip — jangan ulangi kesalahan yang sama.
Integrasi Database: MCP Server untuk SQLite
Salah satu use case paling populer MCP adalah akses database. Dan jujur, ini juga yang paling satisfying — bayangkan bisa ngobrol sama AI dan dia langsung query database-mu.
Mari kita bangun server yang memungkinkan AI membaca dan menganalisis data dari SQLite.
# db_server.py
import sqlite3
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP("database-tools")
# Konfigurasi path database
DB_PATH = Path("./data/app.db")
def get_connection() -> sqlite3.Connection:
"""Buat koneksi database dengan row_factory untuk hasil dict-like."""
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
return conn
@mcp.tool()
def list_tables() -> list[str]:
"""Tampilkan daftar semua tabel dalam database."""
conn = get_connection()
try:
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
return [row["name"] for row in cursor.fetchall()]
finally:
conn.close()
@mcp.tool()
def describe_table(table_name: str) -> list[dict]:
"""Tampilkan skema kolom dari sebuah tabel.
Args:
table_name: Nama tabel yang ingin dilihat skemanya
"""
conn = get_connection()
try:
# Validasi nama tabel untuk mencegah SQL injection
tables = [
row["name"]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
).fetchall()
]
if table_name not in tables:
return [{"error": f"Tabel '{table_name}' tidak ditemukan. Tabel yang tersedia: {tables}"}]
cursor = conn.execute(f"PRAGMA table_info([{table_name}])")
return [
{
"nama_kolom": row["name"],
"tipe": row["type"],
"nullable": not row["notnull"],
"primary_key": bool(row["pk"]),
"default": row["dflt_value"],
}
for row in cursor.fetchall()
]
finally:
conn.close()
@mcp.tool()
def query_database(sql: str, limit: int = 100) -> dict:
"""Jalankan query SELECT read-only pada database.
Args:
sql: Query SQL SELECT yang akan dijalankan (hanya SELECT yang diizinkan)
limit: Batas jumlah baris yang dikembalikan (default: 100, maks: 500)
"""
# Validasi: hanya izinkan SELECT
sql_stripped = sql.strip().upper()
if not sql_stripped.startswith("SELECT"):
return {"error": "Hanya query SELECT yang diizinkan demi keamanan."}
# Batasi jumlah baris
limit = min(limit, 500)
conn = get_connection()
try:
cursor = conn.execute(sql)
columns = [description[0] for description in cursor.description] if cursor.description else []
rows = cursor.fetchmany(limit)
return {
"columns": columns,
"rows": [dict(row) for row in rows],
"row_count": len(rows),
"limited": len(rows) == limit,
}
except sqlite3.Error as e:
return {"error": f"SQL error: {str(e)}"}
finally:
conn.close()
@mcp.resource("schema://database/overview")
def database_overview() -> str:
"""Ringkasan keseluruhan struktur database."""
conn = get_connection()
try:
tables = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
).fetchall()
overview_parts = ["# Database Overview\n"]
for table in tables:
name = table["name"]
count = conn.execute(f"SELECT COUNT(*) as c FROM [{name}]").fetchone()["c"]
cols = conn.execute(f"PRAGMA table_info([{name}])").fetchall()
col_names = ", ".join(c["name"] for c in cols)
overview_parts.append(f"## {name} ({count} rows)\nColumns: {col_names}\n")
return "\n".join(overview_parts)
finally:
conn.close()
if __name__ == "__main__":
mcp.run()
Ada beberapa keputusan desain yang worth mentioning di sini:
- Read-only enforcement — Tool
query_databasehanya mengizinkan query SELECT. Ini penting banget — kamu nggak mau model AI tiba-tiba menjalankanDROP TABLEdi database produksimu. - Row limit — Batas 500 baris mencegah query yang mengembalikan dataset besar yang bisa memenuhi context window LLM (dan bikin tagihan API membengkak).
- SQL injection prevention — Nama tabel divalidasi terhadap daftar tabel yang benar-benar ada di database.
- Resource untuk overview — Resource
schema://database/overviewmemungkinkan AI memuat ringkasan database ke context-nya sebelum mulai bertanya, sehingga query-nya jauh lebih akurat.
Integrasi API Eksternal: MCP Server Async
Untuk integrasi dengan API eksternal, kamu perlu pakai tools async supaya server tetap responsif saat menunggu respons HTTP. Ini penting — kalau pakai fungsi sinkron, server-mu bakal nge-block setiap kali ada request ke API luar.
# api_server.py
import os
import httpx
from fastmcp import FastMCP
mcp = FastMCP("api-tools")
@mcp.tool()
async def get_weather(city: str) -> dict:
"""Ambil data cuaca terkini untuk sebuah kota menggunakan Open-Meteo API (gratis, tanpa API key).
Args:
city: Nama kota (contoh: Jakarta, Surabaya, Bandung)
"""
async with httpx.AsyncClient() as client:
# Langkah 1: Geocoding — cari koordinat kota
geo_resp = await client.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1, "language": "id"},
)
geo_data = geo_resp.json()
if not geo_data.get("results"):
return {"error": f"Kota '{city}' tidak ditemukan."}
loc = geo_data["results"][0]
lat, lon = loc["latitude"], loc["longitude"]
# Langkah 2: Ambil data cuaca
weather_resp = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"timezone": "auto",
},
)
weather = weather_resp.json()["current"]
return {
"kota": loc["name"],
"negara": loc.get("country", "N/A"),
"suhu_celsius": weather["temperature_2m"],
"kelembapan_persen": weather["relative_humidity_2m"],
"kecepatan_angin_kmh": weather["wind_speed_10m"],
"kode_cuaca": weather["weather_code"],
}
@mcp.tool()
async def get_github_issues(owner: str, repo: str, limit: int = 5) -> list[dict]:
"""Ambil daftar issue terbaru dari repository GitHub.
Args:
owner: Pemilik repository (contoh: anthropics)
repo: Nama repository (contoh: claude-code)
limit: Jumlah issue yang ditampilkan (default: 5, maks: 20)
"""
limit = min(limit, 20)
headers = {"Accept": "application/vnd.github.v3+json"}
token = os.environ.get("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/issues",
headers=headers,
params={"state": "open", "per_page": limit, "sort": "created", "direction": "desc"},
)
if resp.status_code != 200:
return [{"error": f"GitHub API error: {resp.status_code}"}]
return [
{
"nomor": issue["number"],
"judul": issue["title"],
"pembuat": issue["user"]["login"],
"label": [l["name"] for l in issue["labels"]],
"dibuat": issue["created_at"],
"komentar": issue["comments"],
}
for issue in resp.json()
]
if __name__ == "__main__":
mcp.run()
Perhatikan penggunaan async def pada kedua tools. FastMCP otomatis mengenali fungsi async dan menanganinya secara asinkron. Dengan httpx.AsyncClient, server bisa menangani request lain saat menunggu respons API — ini krusial kalau server-mu punya banyak tools yang memanggil API eksternal secara bersamaan.
Menghubungkan ke Claude Desktop
Setelah server siap dan teruji via Inspector, saatnya menghubungkan ke Claude Desktop. Ini bagian yang paling seru — kamu akhirnya bisa lihat tools-mu beraksi di client AI sungguhan.
Konfigurasi Claude Desktop
Buka file konfigurasi Claude Desktop:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Tambahkan konfigurasi server-mu:
{
"mcpServers": {
"demo-tools": {
"command": "uv",
"args": ["run", "--directory", "/path/ke/mcp-server-demo", "server.py"]
},
"database-tools": {
"command": "uv",
"args": ["run", "--directory", "/path/ke/mcp-server-demo", "db_server.py"]
},
"api-tools": {
"command": "uv",
"args": ["run", "--directory", "/path/ke/mcp-server-demo", "api_server.py"],
"env": {
"GITHUB_TOKEN": "ghp_xxxxxxxxxxxx"
}
}
}
}
Simpan file, lalu restart Claude Desktop sepenuhnya — bukan hanya tutup window, pastikan prosesnya benar-benar berhenti. Setelah restart, kamu akan melihat ikon tools baru di antarmuka chat yang menandakan MCP server terhubung.
Menghubungkan ke Claude Code
Kalau kamu lebih suka CLI, Claude Code bikin prosesnya lebih simpel lagi:
# Tambahkan server MCP ke Claude Code
claude mcp add demo-tools -- uv run --directory /path/ke/mcp-server-demo server.py
claude mcp add database-tools -- uv run --directory /path/ke/mcp-server-demo db_server.py
# Dengan environment variable
claude mcp add api-tools -e GITHUB_TOKEN=ghp_xxx -- uv run --directory /path/ke/mcp-server-demo api_server.py
# Lihat daftar server yang terhubung
claude mcp list
Sekarang kamu bisa meminta Claude Code untuk menganalisis database-mu, mengecek cuaca, atau melihat GitHub issues — semuanya pakai natural language. Cukup keren, kan?
Menggabungkan Semuanya: Multi-Tool Server
Di produksi, biasanya kamu nggak mau menjalankan tiga server terpisah. Lebih praktis menggabungkan beberapa kemampuan dalam satu server. Berikut contoh yang menggabungkan tools, resources, dan prompts sekaligus:
# production_server.py
import sqlite3
import logging
import sys
from datetime import datetime
from pathlib import Path
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
# Konfigurasi logging ke stderr (BUKAN stdout!)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(sys.stderr)],
)
logger = logging.getLogger("production-server")
mcp = FastMCP(
"production-analytics",
description="Server analitik produksi dengan akses database dan tools pelaporan",
)
DB_PATH = Path("./data/analytics.db")
def get_db() -> sqlite3.Connection:
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
return conn
# --- TOOLS ---
@mcp.tool()
def get_sales_summary(start_date: str, end_date: str) -> dict:
"""Ambil ringkasan penjualan dalam rentang tanggal tertentu.
Args:
start_date: Tanggal awal format YYYY-MM-DD
end_date: Tanggal akhir format YYYY-MM-DD
"""
try:
# Validasi format tanggal
datetime.strptime(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d")
except ValueError:
raise ToolError("Format tanggal tidak valid. Gunakan YYYY-MM-DD.")
conn = get_db()
try:
result = conn.execute(
"""
SELECT
COUNT(*) as total_transaksi,
SUM(amount) as total_pendapatan,
AVG(amount) as rata_rata,
MIN(amount) as minimum,
MAX(amount) as maksimum
FROM sales
WHERE date BETWEEN ? AND ?
""",
(start_date, end_date),
).fetchone()
return dict(result)
except sqlite3.Error as e:
logger.error(f"Database error: {e}")
raise ToolError(f"Gagal mengambil data: {str(e)}")
finally:
conn.close()
@mcp.tool()
def get_top_products(limit: int = 10) -> list[dict]:
"""Ambil daftar produk terlaris berdasarkan jumlah penjualan.
Args:
limit: Jumlah produk yang ditampilkan (default: 10)
"""
limit = min(limit, 50)
conn = get_db()
try:
rows = conn.execute(
"""
SELECT product_name, SUM(quantity) as total_terjual, SUM(amount) as total_pendapatan
FROM sales
GROUP BY product_name
ORDER BY total_terjual DESC
LIMIT ?
""",
(limit,),
).fetchall()
return [dict(row) for row in rows]
finally:
conn.close()
# --- RESOURCES ---
@mcp.resource("analytics://dashboard/kpi")
def get_kpi_dashboard() -> str:
"""KPI dashboard terkini — dimuat ke context AI saat dibutuhkan."""
conn = get_db()
try:
today = datetime.now().strftime("%Y-%m-%d")
month_start = datetime.now().strftime("%Y-%m-01")
monthly = conn.execute(
"SELECT COUNT(*) as trx, COALESCE(SUM(amount), 0) as rev FROM sales WHERE date >= ?",
(month_start,),
).fetchone()
return f"""# KPI Dashboard — {today}
## Bulan Ini (sejak {month_start})
- Total Transaksi: {monthly['trx']}
- Total Pendapatan: Rp {monthly['rev']:,.0f}
"""
finally:
conn.close()
# --- PROMPTS ---
@mcp.prompt()
def analyze_sales_trend(period: str = "bulan ini") -> str:
"""Template prompt untuk analisis tren penjualan.
Args:
period: Periode analisis (contoh: bulan ini, Q1 2026, minggu lalu)
"""
return f"""Tolong analisis tren penjualan untuk periode: {period}
Langkah-langkah:
1. Gunakan tool get_sales_summary untuk mengambil data penjualan periode tersebut
2. Gunakan tool get_top_products untuk melihat produk terlaris
3. Baca resource analytics://dashboard/kpi untuk konteks KPI terkini
4. Berikan analisis yang mencakup:
- Ringkasan performa keseluruhan
- Produk yang perlu diperhatikan (naik/turun signifikan)
- Rekomendasi aksi berdasarkan data
"""
if __name__ == "__main__":
logger.info("Starting production analytics MCP server...")
mcp.run()
Server ini mendemonstrasikan beberapa pola produksi yang penting:
- ToolError untuk error yang visible ke client — Gunakan
ToolErrordari FastMCP untuk error yang harus dilihat AI (misalnya input tidak valid). Error internal cukup di-log, nggak perlu di-expose ke user. - Logging yang benar — Semua logging ke stderr, bukan stdout. Ini detail kecil tapi kalau salah, server-mu bakal crash misterius.
- Resource untuk context loading — KPI dashboard sebagai resource memungkinkan AI memuat konteks bisnis sebelum menjawab pertanyaan.
- Prompt template — Template analisis memberikan workflow terstruktur agar AI menggunakan tools secara sistematis, bukan asal tebak.
Deployment ke Produksi dengan Streamable HTTP
Sampai sini kita sudah pakai STDIO yang berjalan lokal. Tapi untuk deployment produksi yang bisa diakses remote, kamu perlu switch ke transport Streamable HTTP.
Server dengan Streamable HTTP
# http_server.py
import os
from fastmcp import FastMCP
mcp = FastMCP(
"production-server",
host="0.0.0.0",
port=8000,
)
# ... definisi tools sama seperti sebelumnya ...
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Menambahkan Autentikasi
FastMCP nggak menyertakan autentikasi secara default — ini aman untuk jaringan internal, tapi kalau server bisa diakses dari internet, kamu wajib menambahkan validasi. Jangan di-skip bagian ini.
# auth_middleware.py
import os
from functools import wraps
def require_api_key(func):
"""Middleware sederhana untuk validasi API key."""
@wraps(func)
async def wrapper(*args, **kwargs):
# Dalam FastMCP 3.0, gunakan BearerAuthProvider untuk auth yang lebih robust
api_key = os.environ.get("MCP_API_KEY")
if not api_key:
raise ValueError("MCP_API_KEY environment variable not set")
return await func(*args, **kwargs)
return wrapper
Docker Deployment
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install uv
RUN pip install uv
# Copy project files
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
COPY *.py ./
COPY data/ ./data/
# Expose port
EXPOSE 8000
# Jangan jalankan sebagai root
RUN useradd -m appuser
USER appuser
CMD ["uv", "run", "http_server.py"]
# Build dan jalankan
docker build -t mcp-server .
docker run -p 8000:8000 -e MCP_API_KEY=your-secret-key mcp-server
Checklist Keamanan Produksi
Sebelum deploy, pastikan kamu sudah cek semua item ini:
- Jangan pass user input langsung ke system commands atau raw SQL — Gunakan parameterized queries dan validasi input. Selalu.
- Simpan secrets di environment variables — Jangan hardcode API keys atau credentials di kode. Ini basic tapi masih sering dilanggar.
- Gunakan TLS — Untuk server Streamable HTTP di internet, selalu gunakan HTTPS. Letakkan reverse proxy (nginx, Cloudflare) di depan server-mu.
- Terapkan rate limiting — Cegah penyalahgunaan dengan membatasi jumlah request per menit.
- Allowlist tools — Expose hanya tools yang benar-benar dibutuhkan. Prinsip least privilege.
Observability dengan OpenTelemetry
Ini bagian yang menurut saya sering diabaikan tapi sebenarnya sangat penting. FastMCP 3.0 mendukung integrasi OpenTelemetry secara built-in, memberikan trace untuk setiap tool call, akses resource, dan pesan JSON-RPC. Di produksi, kamu nggak bisa pakai Inspector — jadi observability jadi satu-satunya cara untuk debug masalah.
# Contoh konfigurasi OpenTelemetry dasar
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
# Setup tracer
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
# FastMCP 3.0 otomatis mengirim trace jika OpenTelemetry terdeteksi
mcp = FastMCP("observable-server")
@mcp.tool()
def my_tool(param: str) -> str:
"""Tool ini otomatis di-trace oleh OpenTelemetry."""
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("custom-operation"):
# Logika bisnis di sini
return f"Hasil: {param}"
Dengan OpenTelemetry, kamu bisa melacak setiap tool call dari request masuk hingga respons keluar — termasuk latency, error rate, dan parameter yang digunakan. Integrasikan dengan Jaeger, Grafana Tempo, atau Datadog untuk visualisasi trace yang lengkap.
Tips Desain Tool yang Efektif
Dari pengalaman dan best practice komunitas MCP, berikut beberapa tips yang sudah terbukti bikin tools-mu lebih efektif digunakan AI:
- Satu tool, satu tanggung jawab — Jangan membuat satu tool raksasa yang melakukan segalanya. Pecah jadi tools yang fokus. AI jauh lebih baik dalam memilih tool yang tepat daripada menentukan mode operasi yang kompleks di dalam satu tool.
- Docstring adalah segalanya — Anggap kamu sedang menulis dokumentasi API untuk developer lain. Jelaskan apa yang tool lakukan, kapan sebaiknya digunakan, dan berikan contoh parameter yang valid.
- Error message yang informatif — Kembalikan pesan error yang bisa dipahami AI agar ia bisa memperbaiki approach-nya.
"Query failed"itu nggak berguna; tapi"Tabel 'users' tidak ditemukan. Tabel yang tersedia: customers, orders, products"membantu AI mencoba lagi dengan benar. - Batasi output — Jangan kembalikan 10.000 baris data. Context window LLM itu terbatas dan mahal. Berikan summary atau batasi jumlah hasil.
- Nama tool yang deskriptif —
get_sales_summaryjauh lebih baik dariget_data. Nama tool membantu AI memilih tool yang tepat dari daftar panjang.
Ekosistem MCP: Server Siap Pakai
Sebelum repot-repot membangun server kustom dari nol, cek dulu apakah sudah ada server yang sesuai kebutuhanmu. Per April 2026, ekosistemnya sudah sangat kaya.
Server Resmi (Tier 1)
- Database: PostgreSQL, SQLite, MongoDB
- Development: GitHub, GitLab, Jira
- Komunikasi: Slack, Discord, Notion
- Cloud: AWS, GCP, Azure
- File System: Local files, Google Drive
- Browser: Puppeteer, Playwright
Platform Integrasi
- Zapier MCP — Hubungkan AI ke 8.000+ aplikasi melalui satu koneksi. Praktis banget kalau butuh integrasi cepat.
- n8n MCP — Bangun workflow otomatis yang bisa dipanggil dari AI.
Dengan lebih dari 200 paket npm dan ribuan server komunitas, kemungkinan besar sudah ada server yang bisa kamu pakai langsung — atau setidaknya modifikasi sedikit sesuai kebutuhan.
Pertanyaan yang Sering Diajukan (FAQ)
Apakah saya perlu pengalaman coding untuk menggunakan MCP?
Tidak untuk menggunakan, ya untuk membangun server kustom. Menggunakan server MCP yang sudah ada melalui Claude Desktop atau Cursor nggak memerlukan coding — cukup edit file konfigurasi JSON. Tapi untuk membangun server sendiri, kamu butuh pengetahuan Python atau TypeScript dasar. Kabar baiknya, dengan FastMCP 3.0, server fungsional bisa dibuat hanya dalam 15-30 baris kode.
Apa bedanya MCP dengan API biasa?
API biasa didesain untuk komunikasi antar-aplikasi dengan format request/response yang kaku. MCP didesain khusus untuk komunikasi antara model AI dan tools. Kelebihannya: schema otomatis dari type hints, deskripsi tool yang dipahami AI, discovery mechanism (AI bisa "melihat" tools yang tersedia), dan protocol lifecycle management. Singkatnya, MCP membuat tools-mu AI-native — bukan sekadar wrapper API.
Apakah MCP aman untuk digunakan di produksi?
MCP sendiri adalah protokol — keamanannya tergantung pada implementasimu. Protokol nggak otomatis meng-sandbox eksekusi tools. Kalau server-mu bisa mengakses database produksi, AI juga bisa. Terapkan prinsip least privilege: gunakan read-only database connections, batasi scope tools, terapkan autentikasi, dan validasi semua input. Dengan langkah-langkah ini, MCP aman digunakan di produksi.
Bisa tidak satu MCP server digunakan oleh Claude, ChatGPT, dan Copilot sekaligus?
Bisa, dan inilah kekuatan utama MCP sebagai standar terbuka. Satu MCP server yang kamu bangun bisa terhubung ke semua client yang mendukung MCP — Claude Desktop, ChatGPT, VS Code Copilot, Cursor, Windsurf, dan lainnya — tanpa perubahan kode apa pun di sisi server. Build once, connect everywhere.
Bagaimana cara men-debug ketika MCP server tidak muncul di Claude Desktop?
Ini masalah yang cukup umum, dan biasanya solusinya simpel. (1) Pastikan kamu sudah restart Claude Desktop sepenuhnya, bukan hanya menutup window — cek di Activity Monitor atau Task Manager. (2) Periksa path di claude_desktop_config.json — gunakan path absolut dan pada Windows gunakan double backslash. (3) Jalankan server secara manual di terminal untuk memastikan nggak ada error import atau dependency. (4) Gunakan mcp inspector untuk memverifikasi server bekerja sebelum menghubungkan ke client. (5) Cek log Claude Desktop untuk pesan error spesifik.