Kebanyakan aplikasi LLM mentok di tembok. Modelnya bisa baca teks dan nulis teks, tapi nggak bisa nyentuh database kamu, query API kamu, atau baca file yang kamu buka dua detik lalu. Tiap tim nyelesain ini dengan cara sendiri, dan tiap integrasi jadi one-off yang kustom.
Model Context Protocol (MCP) adalah tebakan bahwa ini harusnya jadi standar. Anthropic open-source di akhir 2024; SDK Python dan TypeScript nyusul. Pertengahan 2026 ini, MCP server udah ada buat Postgres, GitHub, Sentry, Linear, Cloudflare, dan beberapa ratus tool lain. Client-nya termasuk Claude Code, Claude Desktop, Cursor, Cline, dan OpenAI Agents SDK.
Artikel ini bikin MCP server beneran dari nol. Dia expose tool yang query database SQLite, resource yang baca file lokal, dan template prompt. Terus kita konekin ke Claude Code dan liat modelnya manggil.
Prasyarat
- Python 3.10 atau lebih baru (
python3 --version) - uv buat manage dependency
- Claude Code terinstall (
npm install -g @anthropic-ai/claude-code) atau Claude Desktop - Database SQLite, atau kesabaran buat bikin satu di langkah 2
MCP Itu Sebenernya Apa
MCP itu protokol JSON-RPC dengan tiga peran:
- Host: aplikasi yang dipake user (Claude Code, Cursor, agent kamu sendiri)
- Client: hidup di dalam host, ngomong MCP ke satu server
- Server: expose tool, resource, dan prompt ke client
Host jalanin banyak client. Tiap client konek ke satu server. Server nggak bisa liat server lain. Server juga nggak bisa liat seluruh percakapan kamu. Spek protokolnya ada di modelcontextprotocol.io/specification dan revisi stabil yang sekarang adalah 2025-06-18.
Tiga primitif yang bisa di-expose server:
- Tools: fungsi yang bisa dipanggil model (setara function calling OpenAI)
- Resources: data read-only yang bisa di-fetch model, dialamat pakai URI
- Prompts: template prompt yang bisa ditrigger ulang sama user
Langkah 1: Scaffold Project
```bash uv init mcp-server-demo cd mcp-server-demo uv add "mcp[cli]" ```
Perintah ini bikin virtualenv, install MCP Python SDK dengan CLI extras, dan setup pyproject.toml. SDK-nya yang resmi dari github.com/modelcontextprotocol/python-sdk. Line v1.x itu rilis stabil yang sekarang; v2 masih alpha pas artikel ini ditulis, jadi kamu harus pin mcp>=1.27,<2 kalau publish ini sebagai library.
Verifikasi installnya:
```bash uv run python -c "import mcp; print(mcp.version)"
1.27.x atau lebih baru
```
Langkah 2: Bikin Database Contoh
Skip langkah ini kalau kamu udah punya database. Kalau belum:
```bash sqlite3 data.db <<'SQL' CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT, lifetime_value REAL DEFAULT 0 ); INSERT INTO customers (name, email, lifetime_value) VALUES ('Anya Kusuma', '[email protected]', 1240.50), ('Budi Santoso', '[email protected]', 89.00), ('Citra Wijaya', '[email protected]', 4320.75), ('Dimas Pratama', '[email protected]', 0); SQL ```
Empat baris. Cukup buat ngetes tool-nya nanti.
Langkah 3: Nulis Server-nya
Replace isi hello.py (atau bikin server.py) dengan ini:
```python """MCP server minimal dengan satu tool, satu resource, dan satu prompt.""" import json import sqlite3 from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("customer-tools")
DB_PATH = Path(file).parent / "data.db"
@mcp.tool() def search_customers( min_ltv: float = 0, limit: int = 10, ) -> str: """Search customers by minimum lifetime value.
Args:
min_ltv: only return customers with lifetime_value >= this
limit: maximum number of rows to return (1-100)
"""
if not 1 <= limit <= 100:
return json.dumps({"error": "limit must be between 1 and 100"})
conn = sqlite3.connect(DB_PATH)
try:
rows = conn.execute(
"SELECT id, name, email, lifetime_value "
"FROM customers WHERE lifetime_value >= ? "
"ORDER BY lifetime_value DESC LIMIT ?",
(min_ltv, limit),
).fetchall()
finally:
conn.close()
return json.dumps(
[
{
"id": r[0],
"name": r[1],
"email": r[2],
"lifetime_value": r[3],
}
for r in rows
],
indent=2,
)
@mcp.resource("customer://{customer_id}") def get_customer(customer_id: int) -> str: """Fetch a single customer record by ID.""" conn = sqlite3.connect(DB_PATH) try: row = conn.execute( "SELECT id, name, email, lifetime_value FROM customers WHERE id = ?", (customer_id,), ).fetchone() finally: conn.close()
if row is None:
return json.dumps({"error": f"no customer with id {customer_id}"})
return json.dumps(
{
"id": row[0],
"name": row[1],
"email": row[2],
"lifetime_value": row[3],
},
indent=2,
)
@mcp.prompt() def customer_summary(style: str = "concise") -> str: """Generate a prompt for summarising the customer base.""" styles = { "concise": "Summarise the top 5 customers by lifetime value in 3 bullets.", "detailed": "Analyse the customer base, flag accounts with LTV under 100, " "and suggest retention actions for high-value accounts.", } return styles.get(style, styles["concise"])
if name == "main": mcp.run(transport="stdio") ```
Beberapa hal yang perlu dicatet:
Decorator-nya datang dari FastMCP, wrapper level tinggi di atas class Server level rendah. API level rendah ngasih kamu kontrol lebih besar atas protokolnya; FastMCP itu yang kamu butuhin buat 90% server. Type hint di tool jadi JSON schema yang diliat model. Docstring-nya jadi deskripsi tool yang dipake model buat nentuin kapan tool itu harus dipanggil. Dua-duanya penting: tool dengan nama search_customers tapi nggak ada docstring adalah tool yang nggak bakal dicari model.
Kita pake transport="stdio" buat development lokal. Host spawn server-nya sebagai subprocess dan ngomong lewat stdin/stdout. Buat akses lewat network, ganti ke transport="streamable-http" (ditambahin di revisi spek 2025-06-18; SSE lagi di-deprecate).
Langkah 4: Konekin ke Claude Code
CLI MCP ngehandle registrasi. Dari direktori project:
```bash claude mcp add --transport stdio customer-tools -- uv run python server.py ```
Verifikasi registrasinya:
```bash claude mcp list ```
Kamu harusnya liat customer-tools di list dengan status hijau. Kalau merah, jalanin uv run python server.py yang sama sendiri buat liat errornya; masalah yang umum itu dependency yang kurang atau typo di path DB.
Langkah 5: Pake
Buka Claude Code di direktori mana aja dan coba:
Tampilkan 3 customer teratas berdasarkan lifetime value.
Claude bakal manggil search_customers dengan limit=3, dapet baris-barisnya, dan format. Transcript lengkap ada di log session. Buat liat persis tool mana yang dipanggil, cek log pesan di ~/.claude/projects/<project>/<session>.jsonl.
Coba resource-nya:
Email customer dengan ID 2 apa?
Claude bakal fetch customer://2. Sama juga buat prompt: dia muncul sebagai slash command (/customer-summary) yang bisa kamu invoke manual atau biar agent yang pake kalau butuh prompt pembuka.
Kapan MCP Itu Pilihan yang Salah
MCP itu berlebihan kalau:
- Kamu cuma punya satu client. Function calling di SDK OpenAI atau Anthropic lebih simpel dan kamu skip protokolnya.
- Datanya udah di-expose lewat HTTP dengan OpenAPI spec yang bersih. MCP server yang cuma bungkus
requests.getadalah versi yang lebih buruk dari API-nya. - Kamu butuh streaming real-time. MCP support notifikasi tapi model round-trip-nya asumsi request/response. Buat data push-based, pake webhook + queue.
- Kamu bikin aplikasi chat, bukan agent yang make tool. Primitif prompt-nya ada, tapi overlap sama system prompt dan nambah layer tanpa banyak nilai.
Tes jujurnya: kalau tool kamu bisa diekspresin dalam 50 baris Python dan cuma Claude Code yang bakal manggil, tinggal tulis fungsinya. MCP mulai kebayar kalau tool yang sama perlu jalan di Claude Code, Cursor, dan agent kustom yang lagi kamu bangun.
Tips Debugging
Server crash diam-diam pas Claude manggil. Tambah logging handler. FastMCP pake mcp.server.fastmcp.utilities.logging.get_logger(). Apa pun yang kamu log muncul di stderr host kalau host nampilinnya; Claude Code nampilin di pane metadata server.
Tool nggak dipanggil. Model mutusin buat manggil tool berdasarkan nama dan deskripsinya. Kalau deskripsinya vague, model nggak bakal nyari. Jadi spesifik: "Search customers by minimum lifetime value" lebih baik daripada "Search."
Error schema mismatch. SDK generate JSON schema dari type hint kamu. int jadi integer, Optional[str] jadi string tanpa requirement, list[dict] jadi array of object. Kalau kamu butuh sesuatu yang SDK nggak bisa infer (enum, default yang ke-skip SDK), pake Annotated[int, Field(description="...")] dari Pydantic.
Banyak client, satu DB. SQLite lock pas nulis. Kalau tool kamu mutasi state dan host jalanin beberapa session paralel, ganti ke queue atau database yang handle concurrency. Layer MCP nggak bantu ini; dia cuma expose apa yang kamu kasih.
Mau Lanjut ke Mana
Begitu dasarnya jalan:
- Tambah autentikasi. Spek 2025-06-18 nambahin support OAuth 2.1; Python SDK punya helper
mcp.client.auth. Buat server stdio, usernya adalah siapa pun yang bisa jalanin command-nya, yang biasanya udah cukup. - Ganti ke transport HTTP kalau kamu mau server-nya bisa dijangkau client remote. Streamable HTTP transport adalah rekomendasi yang sekarang; SSE lagi di-phase out.
- Publish ke registry. Registry server MCP resmi ada di registry.modelcontextprotocol.io. Listing server di sana adalah cara orang lain nemuinnya.
- Tambah sampling. Sampling biarin server minta model host-nya buat ngerjain sesuatu (misal, ngerangkum dokumen yang di-fetch). Ini fitur MCP yang paling jarang dipake, dan ini yang bikin MCP server jadi setara agent, bukan cuma tool.
Seluruh demo-nya sekitar 90 baris Python. Sebagian besar nilai-nya ada di desain: pilih tool yang tight-set, nulis deskripsi yang bisa diaksiin sama model, dan jaga server-nya stateless. Sisanya cuma noise protokol.