Kamu punya aplikasi LLM, dan modelnya perlu cek cuaca, cari data customer, atau manggil API internal. Jadi kamu nulis glue yang sama untuk ketiga kalinya: JSON schema buat tiap fungsi, validasi argumen, formatter biar modelnya bisa baca hasilnya. Setiap aplikasi ngelakuin ini. Nggak ada yang ngelakuinnya dengan cara yang kompatibel.
MCP (Model Context Protocol) ngilangin glue itu dengan standar. Protokol open source berbasis JSON-RPC 2.0 yang bikin aplikasi LLM mana pun bisa ngobrol sama server mana pun yang nyediain tools, resources, atau prompts. Claude Desktop, Claude Code, Cursor, dan VS Code Copilot semua ngerti protokol ini secara native. Kamu nulis satu server, semua host itu bisa pake.
Satu peringatan sebelum kamu copy-paste kode dari internet: Python SDK-nya dirilis ulang besar-besaran ke v2 di 2026, dan kebanyakan tutorial lama pake API v1 yang dibangun di sekitar class bernama FastMCP. Kode itu nggak bakal jalan di SDK sekarang. Semua isi artikel ini udah dites pake mcp 2.0.0 di Python 3.12, dan snippet di bawah ini kode yang beneran saya jalanin.
Prasyarat
- Python 3.10 ke atas
- uv atau pip
- npx (Node.js) cuma kalau mau pake web UI Inspector
- Koneksi internet, karena contohnya manggil API cuaca Open-Meteo (gratis, tanpa API key)
Langkah 1: Install SDK
pip install "mcp[cli]"
Extra [cli] nambahin command-line tool mcp (dev, run, install) di atas SDK. Kalau pake uv, padanannya uv add "mcp[cli]". Cek hasil installnya:
mcp version
MCP version 2.0.0
Kalau yang keluar 1.x, berarti pip kamu ke-resolve ke rilis lama. Install ulang di environment yang bersih.
Langkah 2: Nulis server-nya
Bikin server.py dengan isi ini. Ini server MCP lengkap yang langsung jalan:
import json
import urllib.parse
import urllib.request
from mcp.server import MCPServer
from pydantic import BaseModel, Field
mcp = MCPServer("Weather")
class WeatherReport(BaseModel):
"""Current conditions at a location."""
temperature_c: float = Field(description="Temperature in Celsius")
wind_speed_kmh: float = Field(description="Wind speed in km/h")
weather_code: int = Field(
description="WMO weather code: 0 = clear, 3 = overcast, 51 = light drizzle, 61 = rain"
)
@mcp.tool()
def get_weather(
latitude: float = Field(description="Latitude, e.g. -6.2088 for Jakarta"),
longitude: float = Field(description="Longitude, e.g. 106.8456 for Jakarta"),
) -> WeatherReport:
"""Get current weather for any location. Uses the free Open-Meteo API, no API key needed."""
params = urllib.parse.urlencode(
{
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m,wind_speed_10m,weather_code",
}
)
url = f"https://api.open-meteo.com/v1/forecast?{params}"
with urllib.request.urlopen(url, timeout=10) as resp:
current = json.load(resp)["current"]
return WeatherReport(
temperature_c=current["temperature_2m"],
wind_speed_kmh=current["wind_speed_10m"],
weather_code=current["weather_code"],
)
@mcp.resource("weather://location/{city}")
def location_info(city: str) -> str:
"""Coordinates for a few Indonesian cities. Stand-in for a real geocoding database."""
cities = {
"jakarta": (-6.2088, 106.8456),
"bandung": (-6.9175, 107.6191),
"surabaya": (-7.2575, 112.7521),
"yogyakarta": (-7.7956, 110.3695),
}
lat, lon = cities.get(city.lower())
if lat is None:
return f"Unknown city: {city}. Known: {', '.join(sorted(cities))}"
return f"{city}: latitude {lat}, longitude {lon}"
if __name__ == "__main__":
mcp.run()
Ada tiga hal yang terjadi di sini, dan ini bagian di mana SDK-nya beneran berguna.
@mcp.tool() ngubah get_weather jadi tool. Modelnya lihat nama fungsi, docstring sebagai deskripsi, dan type hints sebagai schema argumen. Kamu nggak perlu nulis JSON Schema manual, dan schema-nya otomatis dikirim ke client pas tools/list.
get_weather itu plain def, bukan async def. SDK ngejalanin tool sync di thread sendiri biar nggak ngeblok server. Kalau tool kamu butuh I/O pake library async, deklarasiin async def dan await di dalemnya; SDK yang nge-await. Dua-duanya jalan, nggak ada yang perlu di-config.
Return type-nya pydantic model, jadi hasilnya structured output. Modelnya dapet blok teks JSON, aplikasi host-nya dapet dict ber-typed, dua-duanya dari satu return statement. Pemisahan ini intinya: LLM baca prosa, kode kamu baca data.
Tool-nya sendiri manggil Open-Meteo, API cuaca gratis tanpa key. urllib.parse.urlencode ngerakit query string, json.load nge-parse responsnya, terus kamu return WeatherReport. Kalau request-nya gagal, urllib lempar exception, SDK nangkep dan ngubah jadi tool error, dan modelnya lihat isError dengan pesan yang bisa dia tanggepin. Saya tes jalur ini pake koordinat nggak valid dan dapet Error executing tool get_weather: HTTP Error 400: Bad Request balik. Kamu nggak nulis satu baris pun plumbing itu.
@mcp.resource("weather://location/{city}") ngedaftarin resource template. Resource itu data yang di-load aplikasi ke context, bukan sesuatu yang diputuskan model buat dipanggil. Placeholder {city} jadi parameter fungsi, jadi weather://location/bandung itu URI beneran yang bisa dibaca.
Guard if __name__ == "__main__": itu penting. mcp dev, mcp run, mcp install, dan test kamu semua nge-import file ini. Kalau mcp.run() nggak di-guard, server bakal nyala tiap kali modulnya di-load.
Langkah 3: Tes in-memory
Client dari SDK bisa nyambung langsung ke objek server. Nggak ada subprocess, nggak ada port, tapi panggilannya tetap lewat layer protokol lengkap. Ini test harness yang dipake dokumentasi SDK sendiri, sekaligus bisa jadi embedding API:
import asyncio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
tools = await client.list_tools()
print("TOOLS:")
for t in tools.tools:
print(f" - {t.name}: {t.description}")
result = await client.call_tool(
"get_weather", {"latitude": -6.2088, "longitude": 106.8456}
)
print("\nCALL get_weather(jakarta):")
print(" content: ", result.content)
print(" structured_content:", result.structured_content)
templates = await client.list_resource_templates()
print(
"\nRESOURCE TEMPLATES:",
[t.uri_template for t in templates.resource_templates],
)
resource = await client.read_resource("weather://location/bandung")
print("READ weather://location/bandung:", resource.contents)
asyncio.run(main())
python test_client.py
TOOLS:
- get_weather: Get current weather for any location. Uses the free Open-Meteo API, no API key needed.
CALL get_weather(jakarta):
content: [TextContent(text='{\n "temperature_c": 33.5,\n "wind_speed_kmh": 7.4,\n "weather_code": 51\n}', ...)]
structured_content: {'temperature_c': 33.5, 'wind_speed_kmh': 7.4, 'weather_code': 51}
RESOURCE TEMPLATES: ['weather://location/{city}']
READ weather://location/bandung: bandung: latitude -6.9175, longitude 107.6191
Itu panggilan API beneran. Pas saya jalanin, Jakarta lapor 33.5 derajat, angin 7.4 km/h, weather code 51 (gerimis ringan). content itu yang dibaca model. structured_content itu data ber-typed buat aplikasi kamu.
Langkah 4: Tes lewat stdio, cara host nyambung
Host desktop nge-launch server kamu sebagai subprocess dan ngobrol lewat stdin/stdout-nya. Transport ini namanya stdio, dan kamu bisa reproduksi persis:
import asyncio
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
server = StdioServerParameters(
command="uv",
args=["run", "--with", "mcp[cli]", "server.py"],
)
async def main() -> None:
async with Client(stdio_client(server)) as client:
tools = await client.list_tools()
print("Tools over stdio:", [t.name for t in tools.tools])
result = await client.call_tool(
"get_weather", {"latitude": -6.9175, "longitude": 107.6191}
)
print("Bandung weather:", result.structured_content)
asyncio.run(main())
python test_stdio.py
Tools over stdio: ['get_weather']
Bandung weather: {'temperature_c': 30.4, 'wind_speed_kmh': 8.4, 'weather_code': 0}
Script ini nge-spawn server.py, negosiasi versi protokol, nge-list tools, dan manggil get_weather lewat pipe beneran. Bandung balik 30.4 derajat, langit cerah. Kalau ini jalan, tinggal config host yang bisa bikin salah.
Langkah 5: Coba-coba di MCP Inspector
uv run mcp dev server.py
mcp dev nge-start server kamu dan buka Inspector di browser. Dia butuh npx di PATH. Ada satu tab per primitif. Tab Tools ngerender form dengan field latitude dan longitude, dibangun dari type hints kamu, sama persis kayak yang bakal dibangun client lain. Tab Resources nge-list template-nya; buka weather://location/bandung dan kamu lihat koordinatnya.
Langkah 6: Nyambungin ke host beneran
Semua host di-config dengan satu launch command:
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
Command ini jalan dari direktori mana pun tanpa perlu activate virtualenv. Ini penting, karena host nge-launch server kamu dari working directory dia sendiri dengan environment yang nyaris kosong.
Claude Desktop satu-satunya host yang bisa di-config langsung dari CLI:
uv run mcp install server.py
Perintah ini nulis launch command ke claude_desktop_config.json, ngubah path kamu jadi absolut, dan nge-pin versi SDK. Quit total Claude Desktop, jangan cuma nutup window-nya, terus buka lagi.
Claude Code cuma butuh satu baris:
claude mcp add weather -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
Cursor baca .cursor/mcp.json di root project:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}
VS Code baca .vscode/mcp.json. Idepun sama, dua bedanya: key pembungkusnya servers bukan mcpServers, dan tiap entry deklarasiin "type": "stdio". Dokumentasi resmi punya dua-duanya.
Dua kegagalan yang nutupin kebanyakan pertanyaan support. Path relatif, karena host nggak launch dari direktori shell kamu, jadi server.py wajib absolut. Dan host yang nggak di-restart total, jadi dia masih jalan pake config lama.
Langkah 7: Sajikan lewat HTTP
stdio buat pemakaian lokal. Kalau mau ngasih server kamu ke orang yang nggak punya file kamu, kasih mereka URL. Ganti baris terakhir:
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=3001)
Client-nya Client yang sama, cuma pake URL sebagai ganti objek server:
client = Client("http://127.0.0.1:3001/mcp")
Opsi transport masuk ke run(), jangan pernah ke MCPServer(...). Constructor itu ngejelasin server kamu itu apa: nama, versi, instruksi. run() ngejelasin gimana dia disajikan. Kebalik urutannya, Python langsung lempar TypeError sebelum MCP sempet kebagian. Transport SSE yang lama masih ada buat client legacy, tapi Streamable HTTP udah ngantiin dia sejak revisi spesifikasi 2025-03-26. Jangan bangun apa-apa baru di atas SSE.
Kapan pake MCP vs alternatifnya
| Pendekatan | Pake kapan |
|---|---|
| Plain function calling di aplikasi sendiri | Kamu cuma punya satu client dan satu server dalam satu codebase. MCP cuma seremoni. |
| Tool glue manual di atas REST | Kamu kontrol satu aplikasi dan satu API, dan nggak ada pihak lain yang bakal pake. |
| Server MCP | Lebih dari satu aplikasi AI harus bisa pake tools kamu, atau kamu mau dapet tool discovery, schema generation, dan validasi tanpa nulis sendiri. |
Tradeoff yang jujur: MCP butuh satu dependency dan satu protokol buat dipelajari, dan buat satu aplikasi doang itu murni overhead. Biaya itu lunas begitu host kedua muncul, karena kamu nggak ngubah satu baris pun kode server.
Gotcha
- Di stdio, stdout itu wire-nya. Jangan
print()dari server; pake modullogging. SDK mindahin output stray yang ke-flush ke stderr selama serving, tapi apa pun yang ditulis ke stdout sebelum serving mulai bakal ngerusak aliran protokol. - Taruh
run()di bawahif __name__ == "__main__":. Semua tool yang nge-load server kamu nge-import file-nya duluan. - Host ngasih server kamu environment minimal, bukan environment shell kamu. Kalau server butuh API key, kasih lewat
mcp install -v KEY=valueatau parameterenv=diStdioServerParameters. mcp installbutuh direktori config Claude Desktop udah ada, artinya Claude Desktop minimal pernah dijalanin sekali.
Langkah selanjutnya
- Server nge-expose tiga primitif; artikel ini baru bikin tools dan satu resource template. Halaman prompts langkah berikutnya, terus structured output buat return type yang lebih kaya.
- Kalau deploy lewat HTTP, baca dokumentasi authorization sebelum naruh di belakang hostname beneran.
- Cek MCP registry sebelum nulis server sendiri. Banyak tool yang tinggal satu baris config.