← Kembali ke Blog

Bikin MCP Server Pertamamu di Python: Panduan Praktis

Setiap LLM host ngarang cara sendiri buat ngekspos tool. Claude Desktop punya config sendiri. Cursor punya yang lain. Cline punya yang ketiga. Kalau kamu bikin integrasi tool dua kali, kamu mulai ngerasa duplikasinya. Model Context Protocol (MCP) itu upaya buat nutuk itu. Standar berbasis JSON-RPC yang bikin LLM host bisa manggil tool, baca resource, dan pake prompt yang ada di proses terpisah.

Spesifikasi stabil saat ini adalah 2025-06-18. Python SDK di PyPI versi 1.28.1, butuh Python 3.10+. Artikel ini nunjukin cara bikin server beneran pake SDK itu, lalu konekin ke dua host beneran (MCP Inspector dan Claude Desktop).

Prerequisites

  • Python 3.10 ke atas (cek dengan python --version)
  • uv terinstall (package manager Python modern)
  • Sekitar 5 menit

Kalau belum punya uv, install dulu:

curl -LsSf https://astral.sh/uv/install.sh | sh

Step 1: Bikin Project

uv init notes-mcp
cd notes-mcp
uv venv
source .venv/bin/activate
uv add "mcp[cli]"

Extras [cli] nge-pull Inspector launcher dan dev server. Tanpa ini, kamu harus install mcp[cli] terpisah nanti.

Step 2: Tulis Server-nya

Bikin file server.py. Seluruh server muat di satu file.

from mcp.server.fastmcp import FastMCP
from pathlib import Path
import json

mcp = FastMCP("notes")

NOTES_DIR = Path.home() / "notes"

@mcp.tool()
def list_notes() -> str:
    """List all note files in the notes directory."""
    if not NOTES_DIR.exists():
        return "Notes directory does not exist yet."
    files = sorted([p.name for p in NOTES_DIR.glob("*.md")])
    return json.dumps(files)

@mcp.tool()
def read_note(filename: str) -> str:
    """Read the contents of a note by filename (e.g. "todo.md")."""
    path = NOTES_DIR / filename
    if not path.exists():
        return f"Error: {filename} not found"
    return path.read_text(encoding="utf-8")

@mcp.tool()
def write_note(filename: str, content: str) -> str:
    """Create or overwrite a note file with the given content."""
    NOTES_DIR.mkdir(parents=True, exist_ok=True)
    (NOTES_DIR / filename).write_text(content, encoding="utf-8")
    return f"Wrote {filename} ({len(content)} chars)"

@mcp.resource("notes://list")
def notes_resource() -> str:
    """A resource that returns the list of notes as plain text."""
    if not NOTES_DIR.exists():
        return "No notes directory."
    return "\n".join(sorted(p.name for p in NOTES_DIR.glob("*.md")))

@mcp.prompt()
def summarize_notes() -> str:
    """A prompt template for summarizing all notes."""
    return "Read every note in the notes directory and produce a 5-bullet summary."

if __name__ == "__main__":
    mcp.run(transport="stdio")

Tiga hal yang perlu diperhatiin.

Pertama, dekorator @mcp.tool() ngubah function Python jadi tool MCP. Nama function, type hints, dan docstring jadi schema tool-nya. Nggak perlu nulis JSON Schema manual.

Kedua, @mcp.resource() ngekspos konteks read-only. Host bisa fetch on demand atau subscribe ke perubahannya. @mcp.prompt() ngekspos template prompt yang bisa dipanggil user pake slash command.

Ketiga, mcp.run(transport="stdio") mulaiin server yang ngomong JSON-RPC lewat standard input dan output. Ini transport default buat server lokal. Streamable HTTP adalah opsi lain, dipake kalau server jalan di proses atau mesin yang beda.

Step 3: Tes Pake MCP Inspector

Inspector itu web UI buat nge-tes MCP server apapun. Dia ngomong protokol langsung, jadi kamu bisa verifikasi server kamu jalan sebelum dicolok ke host beneran.

uv run mcp dev server.py

Perintah itu nge-print URL. Buka di browser (default http://localhost:6274). Kamu liat tiga tab: Tools, Resources, Prompts. Klik list_notes, pencet Run Tool, kamu dapet array JSON nama file. Inspector juga nangkep setiap frame JSON-RPC di panel kanan, berguna banget kalau ada yang error.

Step 4: Konekin ke Claude Desktop

Claude Desktop baca config MCP dari file JSON. Di macOS filenya di ~/Library/Application Support/Claude/claude_desktop_config.json. Di Windows, %APPDATA%\Claude\claude_desktop_config.json. Di Linux belum ada — tutorial resmi arahin user Linux ke path bikin client sendiri.

{
  "mcpServers": {
    "notes": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/notes-mcp", "run", "server.py"]
    }
  }
}

Ganti /absolute/path/to/notes-mcp sama path yang sebenarnya. Restart Claude Desktop. Server baru muncul sebagai ikon 🔧. Tanya "What notes do I have?" dan Claude bakal manggil list_notes. Tanya "Read todo.md" dan dia manggil read_note. Minta dia bikin note baru dan dia manggil write_note setelah nunjukin konten yang diajukan buat di-approve.

Step 5: Konekin ke Claude Code atau Cursor

Claude Code dan Cursor pake shape mcpServers yang sama, tapi file-nya beda tempat. Claude Code baca ~/.claude.json (atau .mcp.json di root project). Cursor baca ~/.cursor/mcp.json. Taruh blok config yang sama di situ dan server langsung bisa dipake di editor.

Aturan Logging yang Bakal Nolongin Kamu

Gotcha kecil yang ngejebak semua orang di percobaan pertama. Buat server stdio, jangan pernah nulis ke stdout. Protokol pake stdout buat frame JSON-RPC, jadi print() yang nyasar bakal ngerusak stream dan host liat error parse. Logging harus ke stderr atau ke file.

import sys
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr,
)

Kalau server kamu jalan di Inspector tapi rusak pas dicolok ke Claude Desktop, ini yang pertama dicek. Inspector lebih pemaaf soal output nyasar.

Ekspos Server Lewat HTTP

stdio cukup buat development lokal, tapi deployment production biasanya mau HTTP biar server bisa jalan di mesin yang beda. Python SDK support Streamable HTTP out of the box.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes", host="127.0.0.1", port=8000)

# ... tools, resources, prompts didefinisiin sama kayak sebelumnya ...

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

Config client juga berubah: "type": "http" dan "url": "http://your-host:8000/mcp" sebagai ganti "command" dan "args".

Dua catatan keamanan dari spec yang gampang di-skip. Server harus validasi header Origin di setiap request HTTP yang masuk buat ngegagal serangan DNS rebinding. Dan pas jalan di lokal, server harus bind ke 127.0.0.1, bukan 0.0.0.0, kecuali kamu ada alasan beneran buat nge-ekspos di jaringan.

Kapan MCP Berlebihan

Kalau kamu cuma butuh kasih satu host akses ke satu tool, kamu bisa wire itu di config native host-nya dan skip MCP. Protokol ini worth it kalau tool yang sama harus jalan di banyak host (Claude Desktop, Cursor, Claude Code, Cline, custom agent yang lagi kamu bikin) atau kalau kamu mau server yang bisa di-share sama seluruh tim. Dia juga ngebantu kalau tool-nya harus di proses terpisah demi keamanan — read-only database access, sandboxed code execution, apa pun di mana kamu nggak mau proses LLM yang pegang kredensial.

Lanjut ke Mana

  • Tambahin autentikasi ke server Streamable HTTP pake helper BearerAuth dari SDK
  • Publish server kamu ke PyPI biar orang lain bisa install pake uvx your-server
  • Browse MCP Registry buat liat reference server apa aja yang udah ada (filesystem, git, sqlite, fetch) dan baca source-nya buat liat polanya
  • Kombinasikan MCP sama loop agent lokal dari artikel sebelumnya — tool yang sama jalan baik modelnya di lokal atau di cloud

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis