FastMCP: Membangun MCP Server Sendiri dengan Python dalam Hitungan Menit
Halo temen-temen, kali ini kita bahas cara membangun MCP server sendiri pakai FastMCP. Kalau kalian sudah pernah memakai MCP server buatan orang lain di Claude Desktop atau di editor kalian, dan sekarang penasaran gimana caranya bikin sendiri supaya AI bisa mengakses database internal, API perusahaan, atau tool khusus tim kalian, tutorial ini jawabannya.
Singkatnya, MCP (Model Context Protocol) adalah protokol standar yang memungkinkan aplikasi AI terhubung ke sumber data dan tool eksternal. Analoginya sering disebut seperti USB-C untuk AI: satu standar colokan, banyak perangkat. Sebelum ada MCP, setiap integrasi harus dibuat khusus untuk setiap aplikasi. Dengan MCP, kalian menulis server sekali dan dia bisa dipakai oleh Claude Desktop, Claude Code, dan aplikasi lain yang mendukung protokol yang sama.
FastMCP adalah framework Python yang membuat pembuatan MCP server jadi sangat sederhana. Kalau kalian pernah pakai FastAPI, kalian akan langsung merasa familiar, karena gayanya mirip: dekorator di atas fungsi Python biasa.
Introduction
Tiga Konsep Dasar MCP
Sebelum ngoding, kalian perlu paham tiga hal yang bisa disediakan sebuah MCP server.
Tools adalah fungsi yang bisa dipanggil model untuk melakukan sesuatu. Ini yang paling sering dipakai. Contohnya mencari data di database, mengirim email, membuat tiket, atau menghitung sesuatu. Analoginya seperti endpoint POST di REST API, karena bisa punya efek samping.
Resources adalah data yang bisa dibaca model. Contohnya isi file, hasil query yang sudah disiapkan, atau konfigurasi. Analoginya seperti endpoint GET, seharusnya tanpa efek samping. Resources biasanya dipilih pengguna atau aplikasi, bukan diputuskan model sendiri.
Prompts adalah template percakapan yang bisa dipanggil pengguna. Contohnya template review kode atau template analisis laporan. Ini muncul sebagai perintah siap pakai di aplikasi klien.
Untuk kebanyakan kasus praktis, kalian akan paling banyak memakai Tools. Jadi di tutorial ini kita fokus ke sana, tapi tetap akan aku tunjukkan dua lainnya.
Transport: STDIO atau HTTP
Ada dua cara MCP server berkomunikasi dengan klien.
STDIO artinya klien menjalankan server kalian sebagai proses anak dan berkomunikasi lewat standard input dan output. Ini yang dipakai Claude Desktop untuk server lokal. Sederhana, aman, tidak perlu jaringan.
HTTP artinya server kalian berjalan sebagai layanan web dan klien terhubung lewat jaringan. Ini yang kalian pakai kalau servernya di-deploy terpusat dan diakses banyak orang.
Aturan praktisnya: pakai STDIO untuk tool pribadi atau tool yang mengakses file lokal, pakai HTTP untuk tool yang dipakai bersama satu tim.
Instalasi
python -m venv venv
source venv/bin/activate # Linux / Mac
venv\Scripts\activate # Windows
pip install fastmcp
Cek versinya:
fastmcp version
Server Pertama
Mari mulai dari yang paling sederhana.
# server.py
from fastmcp import FastMCP
mcp = FastMCP("Server Utilitas Tim")
@mcp.tool
def tambah(a: float, b: float) -> float:
"""Menjumlahkan dua angka."""
return a + b
@mcp.tool
def hitungharikerja(tanggalmulai: str, tanggalselesai: str) -> int:
"""Hitung jumlah hari kerja (Senin sampai Jumat) antara dua tanggal.
Args:
tanggalmulai: format YYYY-MM-DD
tanggalselesai: format YYYY-MM-DD
"""
from datetime import date, timedelta
mulai = date.fromisoformat(tanggalmulai)
selesai = date.fromisoformat(tanggalselesai)
jumlah = 0
kursor = mulai
while kursor <= selesai:
if kursor.weekday() < 5:
jumlah += 1
kursor += timedelta(days=1)
return jumlah
if name == "main":
mcp.run()
Selesai. Itu MCP server yang berfungsi penuh.
Ada dua hal yang perlu kalian perhatikan baik-baik di kode itu, karena inilah yang menentukan apakah tool kalian dipakai dengan benar oleh model.
Pertama, type hints. FastMCP memakai anotasi tipe Python untuk menghasilkan skema JSON yang dikirim ke model. Kalau kalian menulis a: float, model tahu parameter itu harus angka. Kalau kalian tidak menulis tipe, model harus menebak, dan tebakannya sering salah.
Kedua, docstring. Docstring adalah deskripsi yang dibaca model untuk memutuskan kapan memanggil tool ini. Tulis dengan jelas dan spesifik. "Menjumlahkan dua angka" jauh lebih berguna daripada "fungsi tambah". Dan sebutkan format yang diharapkan seperti contoh di atas, karena model tidak bisa menebak kalian mau format tanggal yang mana.
Menjalankan dan Menguji
Untuk menguji sebelum menghubungkan ke aplikasi apa pun:
fastmcp dev server.py
Perintah ini membuka MCP Inspector di browser, di mana kalian bisa melihat daftar tool, memanggilnya manual, dan melihat responsnya. Ini cara debugging paling cepat, dan aku sarankan kalian selalu menguji di sini dulu sebelum menghubungkan ke Claude.
Untuk menghubungkan ke Claude Desktop:
fastmcp install claude-desktop server.py
Atau tambahkan manual ke file konfigurasi Claude Desktop:
{
"mcpServers": {
"utilitas-tim": {
"command": "python",
"args": ["/path/lengkap/ke/server.py"]
}
}
}
Setelah itu restart Claude Desktop, dan tool kalian akan muncul.
Tool yang Lebih Nyata: Akses Database
Contoh penjumlahan itu terlalu sederhana. Ini contoh yang lebih mirip pekerjaan nyata.
import sqlite3
from typing import Literal
from fastmcp import FastMCP
mcp = FastMCP("Database Penjualan")
DB = "penjualan.db"
def koneksi():
conn = sqlite3.connect(DB)
conn.rowfactory = sqlite3.Row
return conn
@mcp.tool
def caripelanggan(katakunci: str, batas: int = 10) -> list[dict]:
"""Cari pelanggan berdasarkan nama atau email.
Args:
katakunci: teks yang dicari di kolom nama dan email
batas: jumlah maksimal hasil, default 10
"""
with koneksi() as conn:
rows = conn.execute(
"""
SELECT id, nama, email, kota, totaltransaksi
FROM pelanggan
WHERE nama LIKE ? OR email LIKE ?
ORDER BY totaltransaksi DESC
LIMIT ?
""",
(f"%{katakunci}%", f"%{katakunci}%", batas),
).fetchall()
return [dict(r) for r in rows]
@mcp.tool
def ringkasanpenjualan(
periode: Literal["harian", "mingguan", "bulanan"] = "bulanan",
jumlahperiode: int = 6,
) -> list[dict]:
"""Ambil ringkasan total penjualan per periode.
Args:
periode: granularitas agregasi
jumlahperiode: berapa periode terakhir yang diambil
"""
formatwaktu = {
"harian": "%Y-%m-%d",
"mingguan": "%Y-%W",
"bulanan": "%Y-%m",
}[periode]
with koneksi() as conn:
rows = conn.execute(
f"""
SELECT strftime('{formatwaktu}', tanggal) AS periode,
COUNT() AS jumlahtransaksi,
SUM(total) AS totalpenjualan
FROM transaksi
GROUP BY periode
ORDER BY periode DESC
LIMIT ?
""",
(jumlahperiode,),
).fetchall()
return [dict(r) for r in rows]
if name == "main":
mcp.run()
Perhatikan pemakaian Literal untuk parameter periode. Ini memberi tahu model bahwa hanya ada tiga nilai yang valid, sehingga dia tidak mengarang nilai lain. Untuk parameter yang punya pilihan terbatas, selalu pakai Literal atau Enum.
Perhatikan juga bahwa aku memakai query berparameter, bukan f-string untuk nilai. Nilai yang datang dari model harus diperlakukan seperti input pengguna: jangan pernah dimasukkan langsung ke SQL. Ini penting sekali, karena model bisa mengarang parameter apa saja.
Tool yang Memanggil API Eksternal
import httpx
from fastmcp import FastMCP
mcp = FastMCP("Integrasi CRM")
BASEURL = "https://api.crm-internal.com/v1"
APIKEY = os.environ["CRMAPIKEY"]
@mcp.tool
async def buattiket(judul: str, deskripsi: str, prioritas: str = "normal") -> dict:
"""Buat tiket support baru di CRM.
Args:
judul: judul singkat tiket
deskripsi: penjelasan lengkap masalah
prioritas: rendah, normal, tinggi, atau kritis
"""
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(
f"{BASEURL}/tickets",
headers={"Authorization": f"Bearer {APIKEY}"},
json={"title": judul, "description": deskripsi, "priority": prioritas},
)
resp.raiseforstatus()
data = resp.json()
return {
"idtiket": data["id"],
"url": data["weburl"],
"status": data["status"],
}
FastMCP mendukung fungsi async secara native, dan untuk tool yang melakukan panggilan jaringan, async adalah pilihan yang benar supaya server kalian tidak memblokir.
Perhatikan bahwa aku mengembalikan dictionary yang sudah dirapikan, bukan seluruh respons API mentah. Ini praktik yang baik. Respons API sering berisi puluhan field yang tidak relevan, dan semuanya akan memakan token di konteks model. Kembalikan hanya yang berguna.
Resources: Data yang Bisa Dibaca
from fastmcp import FastMCP
mcp = FastMCP("Dokumentasi Internal")
@mcp.resource("docs://kebijakan-cuti")
def kebijakancuti() -> str:
"""Kebijakan cuti karyawan yang berlaku saat ini."""
with open("dokumen/kebijakancuti.md", encoding="utf-8") as f:
return f.read()
@mcp.resource("data://pelanggan/{idpelanggan}")
def profilpelanggan(idpelanggan: str) -> dict:
"""Profil lengkap seorang pelanggan berdasarkan ID."""
with koneksi() as conn:
row = conn.execute(
"SELECT FROM pelanggan WHERE id = ?", (idpelanggan,)
).fetchone()
if row is None:
raise ValueError(f"pelanggan {idpelanggan} tidak ditemukan")
return dict(row)
Resource kedua di atas memakai template URI dengan parameter. Ini memungkinkan klien mengambil data spesifik dengan pola alamat yang konsisten.
Prompts: Template Siap Pakai
@mcp.prompt
def analisiskeluhan(tekskeluhan: str) -> str:
"""Template untuk menganalisis keluhan pelanggan secara terstruktur."""
return f"""Analisis keluhan pelanggan berikut dan berikan:
Kategori masalah
Tingkat urgensi dengan alasannya
Departemen yang harus menangani
Saran respons pertama untuk pelanggan
Keluhan:
{tekskeluhan}
"""
Prompt ini akan muncul di aplikasi klien sebagai perintah yang bisa dipilih pengguna, biasanya lewat menu slash command.
Menangani Error dengan Benar
Ini bagian yang sering diabaikan padahal sangat berpengaruh pada kualitas interaksi.
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
mcp = FastMCP("Server Aman")
@mcp.tool
def ambillaporan(idlaporan: str) -> dict:
"""Ambil laporan berdasarkan ID."""
if not idlaporan.startswith("RPT-"):
raise ToolError("ID laporan harus diawali 'RPT-', contoh: RPT-2026-001")
laporan = database.cari(idlaporan)
if laporan is None:
raise ToolError(f"Laporan {idlaporan} tidak ditemukan. "
f"Gunakan tool carilaporan untuk melihat daftar yang tersedia.")
return laporan
ToolError adalah pesan yang sengaja kalian tunjukkan ke model. Perhatikan bahwa pesannya tidak cuma bilang "error", tapi memberi tahu model apa yang salah dan apa yang harus dilakukan selanjutnya. Model membaca pesan ini dan bisa memperbaiki sendiri panggilannya. Ini membuat perbedaan besar antara agent yang macet dan agent yang bisa pulih dari kesalahan.
Sebaliknya, exception biasa yang tidak tertangkap akan menghasilkan pesan error generik yang tidak membantu siapa pun.
Menjalankan sebagai HTTP Server
Untuk deployment terpusat:
if name == "main":
mcp.run(transport="http", host="0.0.0.0", port=8000)
Klien kemudian terhubung ke http://server-kalian:8000/mcp.
Kalau kalian menjalankannya di jaringan yang bisa diakses banyak orang, pikirkan autentikasi. FastMCP mendukung middleware autentikasi, dan minimal kalian harus menaruhnya di belakang reverse proxy dengan otentikasi. Jangan pernah mengekspos MCP server yang bisa mengubah data ke internet terbuka tanpa perlindungan.
Menguji Server Kalian
Testing itu penting karena tool kalian akan dipanggil oleh model dengan parameter yang tidak selalu kalian bayangkan.
import pytest
from fastmcp import Client
from server import mcp
@pytest.mark.asyncio
async def testhitungharikerja():
async with Client(mcp) as client:
hasil = await client.calltool(
"hitungharikerja",
{"tanggalmulai": "2026-09-01", "tanggalselesai": "2026-09-30"},
)
assert hasil.data == 22
@pytest.mark.asyncio
async def testdaftartool():
async with Client(mcp) as client:
tools = await client.listtools()
nama = [t.name for t in tools]
assert "hitungharikerja" in nama
Client bawaan FastMCP bisa terhubung langsung ke objek server di memori, jadi test kalian berjalan cepat tanpa perlu menjalankan proses terpisah.
Tips dan Best Practice
Tulis docstring seolah-olah kalian sedang menjelaskan ke rekan kerja baru. Model membaca itu untuk memutuskan kapan memakai tool kalian. Docstring yang buruk berarti tool yang jarang dipakai atau dipakai salah.
Selalu pakai type hints yang spesifik. Literal["harian", "mingguan"] jauh lebih baik daripada str.
Kembalikan data yang ringkas dan sudah dirapikan. Setiap field yang tidak perlu adalah token yang terbuang dan potensi kebingungan.
Jangan bikin terlalu banyak tool dalam satu server. Kalau kalian punya 40 tool, model akan kesulitan memilih. Kelompokkan jadi beberapa server sesuai domain, atau gabungkan tool yang mirip dengan parameter mode.
Perlakukan semua parameter dari model sebagai input tidak tepercaya. Validasi, batasi, dan pakai query berparameter. Model bisa mengarang nilai apa pun.
Beri batas pada tool yang berpotensi mengembalikan banyak data. Selalu ada parameter batas dengan nilai default yang wajar.
Untuk tool yang mengubah data atau punya efek samping serius, pertimbangkan pola konfirmasi: buat tool terpisah untuk melihat pratinjau perubahan sebelum tool yang benar-benar mengeksekusinya.
Catat log setiap pemanggilan tool beserta parameternya. Waktu ada yang aneh, log ini yang menyelamatkan kalian.
Kesimpulan
FastMCP membuat pembuatan MCP server jadi pekerjaan beberapa menit, bukan beberapa hari. Ringkasannya:
MCP menstandarkan cara aplikasi AI terhubung ke tool dan data, sehingga kalian menulis server sekali dan bisa dipakai banyak aplikasi.
Tools untuk aksi, Resources untuk data yang dibaca, Prompts untuk template. Sebagian besar kebutuhan praktis terpenuhi dengan Tools.
Type hints dan docstring bukan dokumentasi opsional, keduanya adalah antarmuka yang dibaca model untuk memakai tool kalian dengan benar.
ToolError dengan pesan yang informatif membuat model bisa memperbaiki sendiri kesalahannya.
Pakai STDIO untuk tool lokal dan HTTP untuk tool yang dipakai bersama, dan jangan lupa autentikasi kalau servernya bisa diakses jaringan.
Uji dengan fastmcp dev sebelum menghubungkan ke aplikasi apa pun, dan tulis test otomatis dengan Client bawaan.
Coba mulai dari satu tugas berulang di tim kalian, misalnya mencari status pesanan atau mengambil ringkasan metrik mingguan. Bungkus jadi satu tool MCP, hubungkan ke Claude, lalu rasakan bedanya ketika AI bisa mengakses data nyata perusahaan kalian alih-alih menebak. Selamat mencoba.