Cara menjalankan MCP server di VPS
Pelajari cara menjalankan server stdio dan remote HTTP di VPS menggunakan systemd dan nginx. Panduan ini membahas aspek keamanan TLS dan autentikasi JSON-RPC.
Apa yang Anda bangun
Dua pengaturan MCP yang berfungsi pada satu VPS. Pertama, server stdio — sebuah alat filesystem atau database yang dijalankan Claude Code sebagai proses anak dan berkomunikasi melalui pipe. Kedua, server remote HTTP yang berjalan sebagai layanan jaringan jangka panjang di bawah systemd dan nginx reverse proxy dengan TLS, yang dapat diakses oleh klien MCP mana pun yang diarahkan ke sana. Proses instalasi untuk keduanya sangat singkat. Sebagian besar panduan ini membahas dua hal yang krusial: menjaga aliran JSON-RPC tetap bersih, dan tidak pernah menempatkan endpoint alat tanpa autentikasi di internet publik.
Apa itu MCP sebenarnya
Model Context Protocol adalah standar agar klien AI — Claude Code, Claude Desktop, Gemini CLI pada VPS, atau skrip Anda sendiri — dapat memanggil alat eksternal dan membaca sumber daya eksternal. Model itu sendiri tidak menjalankan apa pun. Model meminta klien, klien mengirimkan pesan JSON-RPC 2.0 ke server MCP, lalu server menjalankan alat tersebut dan mengembalikan hasilnya. Satu protokol memungkinkan server yang Anda buat satu kali dapat bekerja dengan setiap klien yang mendukung MCP.
Terdapat dua jenis transport, dan seluruh panduan ini dibagi berdasarkan keduanya:
- stdio. Klien menjalankan server sebagai proses anak (child process) dan bertukar pesan JSON-RPC yang dipisahkan oleh baris baru melalui standard input dan standard output. Tanpa jaringan, tanpa port, tanpa autentikasi — batas kepercayaan terletak pada proses itu sendiri. Hampir semua alat lokal menggunakan cara ini.
- Streamable HTTP (dan pendahulunya, HTTP+SSE). Server adalah layanan web yang berjalan terus-menerus. Klien terhubung melalui HTTP dan server dapat mengirimkan respons secara streaming sebagai Server-Sent Events. Cara ini digunakan untuk berbagi satu server dengan banyak klien, atau menjalankan alat yang harus tetap berjalan secara permanen pada mesin tersebut.
Pilih stdio jika alat tersebut hanya digunakan oleh satu mesin dan satu pengguna. Pilih HTTP jika alat tersebut adalah layanan bersama.
Prasyarat dan kendala yang perlu diperhatikan
Asumsikan Anda memiliki VPS Ubuntu 24.04 KVM baru dengan akses root atau sudo. Selain itu:
- Runtime bahasa pemrograman server. Sebagian besar server referensi menggunakan Node atau Python. Ubuntu 24.04 menyertakan Node 18, sedangkan beberapa paket MCP saat ini memerlukan Node 20 atau yang lebih baru. Oleh karena itu, instal versi LTS terbaru dari NodeSource atau nvm daripada mengandalkan
apt. Python 3.12 sudah tersedia. - Domain dan DNS record A, tetapi hanya untuk server HTTP jarak jauh — TLS memerlukan nama yang mengarah ke VPS ini. Contoh stdio tidak memerlukan DNS sama sekali.
- RAM 512 MB sudah cukup. Server MCP adalah proses JSON-RPC yang ringan; penggunaan memori bergantung pada alat yang Anda gunakan (seperti driver database atau cache file), bukan pada protokolnya.
- Spesifikasi masih baru dan terus berkembang. Revisi 2025-03-26 mengganti HTTP+SSE dengan Streamable HTTP dan menandai SSE sebagai deprecated. SSE masih berfungsi dan banyak server masih menggunakannya, jadi anggaplah batasan transport apa pun sebagai hal yang perlu diperiksa kembali terhadap catatan rilis server, bukan sebagai aturan mutlak.
Step 1: hubungkan stdio server ke Claude Code
Mulai dengan filesystem server — server ini resmi, dipelihara secara aktif, dan hanya membutuhkan Node. Perintah di bawah ini akan mendaftarkannya ke Claude Code dan membatasi cakupannya pada proyek saat ini agar tersimpan dalam file yang dapat dikomit:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiPemisah -- sangat penting: semua teks setelah pemisah tersebut adalah perintah yang akan dijalankan oleh Claude Code, bukan flag untuk Claude Code. Hal ini akan menulis .mcp.json di root proyek:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Belum ada yang berjalan. Saat Anda menjalankan Claude Code berikutnya di direktori ini, agent akan membaca .mcp.json, menjalankan npx -y @modelcontextprotocol/server-filesystem ... sebagai child process, dan melakukan handshake MCP melalui stdin/stdout proses tersebut. Konfirmasi keberhasilannya:
claude mcp listServer yang berfungsi dengan baik akan mencetak perintahnya dan tanda centang hijau — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Di dalam sesi, slash command /mcp akan menampilkan daftar tool yang disediakan server (read_file, write_file, list_directory), dan agent kini dapat memanggilnya pada path yang telah Anda izinkan. Tool database memiliki struktur yang sama — ganti paketnya dan masukkan connection string sebagai argumen terakhir — tetapi periksa repositori server tersebut untuk nama paket terbaru, karena server Postgres referensi telah berganti pengelola lebih dari satu kali.
Inilah tujuan utama menjalankan agent di mesin tersebut: sesi Claude Code berjalan di VPS di dalam tmux, dan stdio server miliknya berjalan tepat di sampingnya dengan akses langsung ke file proyek dan layanan lokal, tanpa perlu round-trip jaringan.
Langkah 2: membangun server HTTP jarak jauh
Server stdio akan berhenti saat proses induknya berakhir. Jika Anda membutuhkan alat yang tetap berjalan untuk setiap klien — seperti alat operasi bersama, gateway database, atau sesuatu yang dipanggil oleh laptop dan CI Anda — Anda memerlukan transport HTTP dan layanan yang nyata. Berikut adalah server Python minimal menggunakan SDK resmi yang mengekspos satu alat:
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")Perhatikan host="127.0.0.1". Server ini hanya terikat ke localhost — tidak ada perangkat luar yang dapat menjangkaunya secara langsung, yang merupakan hal yang diinginkan sebelum sistem autentikasi tersedia. Instal server di dalam virtualenv sendiri agar systemd memiliki jalur interpreter yang stabil:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Step 3: keep it alive with systemd
A tool that is down when the agent reaches for it is worse than no tool. Write /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetThe absolute path to the venv Python in ExecStart is not optional — point it at /usr/bin/python3 and the process starts with ModuleNotFoundError: No module named 'mcp', because the system interpreter never saw your pip install. Enable and check:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus should read active (running). The curl comes back HTTP/1.1 400 Bad Request with a JSON-RPC error in the body — the request carried no session and no valid JSON payload — and that is exactly what you want: it proves the port answers and speaks the protocol. Connection refused or an empty reply means the process is not bound where you think; read journalctl -u mcp-ops -n 50.
Step 4: Pasang TLS dan reverse proxy di depannya
Server mendengarkan pada localhost. Untuk mengaksesnya dari mana saja, Anda harus mengakhiri TLS di nginx dan melakukan proxy ke dalam. Instal nginx, dapatkan sertifikat menggunakan Certbot dan Let's Encrypt pada nginx, lalu buat blok location. Bagian krusial adalah menonaktifkan buffering, karena perilaku default nginx adalah menahan respons hingga selesai, yang akan menghentikan aliran SSE selamanya:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Muat ulang dengan sudo nginx -t && sudo systemctl reload nginx. Jika Anda sudah menjalankan sekumpulan container, tugas yang sama dapat dilakukan oleh reverse proxy Traefik dengan TLS otomatis — fitur ini menerbitkan sertifikat dan melakukan routing berdasarkan hostname, Anda hanya perlu menambahkan label pada container MCP. Dalam kondisi apa pun, reverse proxy sekarang adalah satu-satunya layanan pada port publik, dan ia mengarah ke layanan yang belum Anda amankan. Perbaiki hal tersebut sebelum Anda mendaftarkan URL di mana pun.
Step 5: aturan keamanan utama dalam topik ini
Jangan pernah mengekspos endpoint MCP tanpa autentikasi. Server MCP bukan sekadar API read-only. Server ini memberikan akses ke tool — ke file Anda, database Anda, dan terkadang shell. /mcp yang terbuka di internet publik adalah ancaman dengan jangkauan yang sama dengan agen AI Anda: mereka dapat melihat daftar tool Anda, lalu menjalankannya. Perlakukan ini sama seperti socket admin tanpa autentikasi, karena memang itulah fungsinya.
Tiga metode pertahanan, berdasarkan urutan prioritas:
- Jangan publikasikan server tersebut. Simpan server di
127.0.0.1dan akses melalui laptop Anda menggunakan SSH tunnel:ssh -L 8000:127.0.0.1:8000 matt@vps, lalu arahkan client kehttp://127.0.0.1:8000/mcp. Tidak ada data yang terekspos ke publik. - Gunakan jaringan privat. Gunakan alamat tunnel dari self-hosted WireGuard VPN dan hanya izinkan peer VPN untuk mengaksesnya. Internet publik hanya akan melihat port yang tertutup.
- Jika harus publik, wajibkan penggunaan token. Solusi yang tepat adalah alur MCP OAuth yang didukung secara asli oleh transport HTTP. Solusi praktis minimal adalah menggunakan shared bearer token yang diperiksa pada proxy — cara ini efisien dan dapat menghentikan serangan secara total:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Buat token menggunakan openssl rand -hex 32, dan jangan pernah menghubungkan server langsung ke 0.0.0.0 tanpa salah satu metode di atas. Client kemudian mengirimkan token sebagai header. Pada Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Atur MCP_TOKEN di shell Anda agar rahasia tidak tersimpan dalam bentuk plaintext di .mcp.json — Claude Code akan mengambil nilai ${MCP_TOKEN} dari environment saat waktu pembacaan.
Step 6: debug dengan MCP Inspector
Jika server mengalami malfungsi, jangan menebak dari dalam agent — gunakan Inspector, klien pengujian berbasis web resmi, untuk mengontrolnya secara langsung. Untuk server stdio, gunakan perintah yang sama dengan yang dijalankan oleh agent:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpProses ini akan menjalankan UI pada http://localhost:6274 (versi terbaru akan menampilkan URL dengan query string MCP_PROXY_AUTH_TOKEN — gunakan tautan tersebut agar UI tidak menolak koneksi) dan proxy pada port 6277. Klik Connect, lalu List Tools, kemudian Call Tool dengan argumen yang sebenarnya. Jika berhasil di Inspector tetapi gagal di agent, kesalahan terletak pada konfigurasi client Anda, bukan pada server. Untuk server HTTP jarak jauh, pilih transport Streamable HTTP, masukkan https://mcp.example.com/mcp, tambahkan header Authorization, lalu hubungkan — ini adalah cara tercepat untuk memastikan autentikasi dan proxy sudah benar sebelum menggunakan agent.
Menjaga server tetap mutakhir
MCP berkembang dengan cepat, jadi lakukan pembaruan sesuai jadwal. Server Node yang dijalankan dengan npx -y akan mengambil versi terbaru setiap kali dijalankan; hal ini praktis tetapi tidak dapat direproduksi. Tetapkan versi eksak yang telah Anda uji — baca versi tersebut dari npm view @modelcontextprotocol/server-filesystem version dan tambahkan ke nama paket di .mcp.json (@modelcontextprotocol/server-filesystem@<version>) — ini penting setelah server berjalan, dan lakukan pembaruan secara sengaja. Server Python di bawah systemd diperbarui dengan sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" diikuti oleh sudo systemctl restart mcp-ops. Perhatikan revisi spesifikasi yang ditargetkan SDK Anda saat melakukan upgrade — perubahan melintasi batas SSE-ke-Streamable-HTTP dapat mengubah transport yang harus diminta oleh klien Anda.
Mode kegagalan, dengan string yang akan Anda lihat
Agent menunjukkan server gagal. claude mcp list mencetak ✗ Failed to connect, dan TUI melaporkan MCP server 'filesystem' failed to start. Jalankan claude --debug dan Anda biasanya akan melihat Error: spawn npx ENOENT — perintah tersebut tidak ada dalam PATH agent. Runtime hilang atau tidak berada di lokasi yang dicari agent: Node tidak terinstal, npx absen, atau virtualenv Python dirujuk hanya dengan nama saja. Perbaiki perintah ke path absolut atau instal runtime, lalu hubungkan kembali.
Server stdio terhubung, lalu langsung terputus. Client mencatat error parse JSON — seperti Unexpected token 'S', "Server sta"... is not valid JSON atau Failed to parse message. Penyebabnya selalu sama: server menulis baris log ke stdout. Pada mode stdio, stdout adalah saluran JSON-RPC, sehingga teks tambahan apa pun akan merusak stream dan proses handshake gagal. Pada Node, console.log masuk ke stdout — gunakan console.error. Pada Python, print() biasa masuk ke stdout — tulis log dengan logging yang dikonfigurasi ke sys.stderr, atau gunakan file=sys.stderr. Aturannya mutlak: pada mode stdio, hanya JSON-RPC yang boleh ada di stdout, semua teks untuk manusia harus di stderr.
Server jarak jauh mengalami timeout atau tertutup saat mid-handshake. Client gagal dengan MCP error -32000: Connection closed, atau Inspector tertahan pada Connect dan tidak pernah menampilkan daftar tools. Di balik nginx, ini disebabkan oleh buffering: proxy menahan stream SSE alih-alih melakukan flush, sehingga client menunggu respons yang tidak pernah datang. Tambahkan proxy_buffering off; (dan sisa blok pada Langkah 4) ke location. Konfirmasi dengan curl -N terhadap URL publik — Anda seharusnya melihat data event datang secara bertahap, bukan sekaligus di akhir.
Autentikasi ditolak. Client melaporkan Error POSTing to endpoint (HTTP 401) atau secara jelas 401 Unauthorized. Header hilang, token salah, atau variabel shell kosong saat client membaca konfigurasi — ini jebakan umum, karena ${MCP_TOKEN} akan menjadi kosong jika variabel tidak diatur dan nginx kemudian melihat Bearer tanpa nilai. Lakukan echo pada variabel tersebut, tambahkan kembali header, dan verifikasi apakah byte yang tepat sesuai dengan token di nginx if.
Service tidak dapat berjalan di bawah systemd. journalctl -u mcp-ops menunjukkan ModuleNotFoundError: No module named 'mcp' — ExecStart merujuk ke Python sistem alih-alih interpreter venv. Atau Address already in use — proses lain menggunakan port 8000; cari proses tersebut dengan sudo ss -ltnp | grep 8000.
FAQ
Apa itu server MCP?
MCP adalah program yang menyediakan alat dan sumber daya ke klien AI melalui Model Context Protocol menggunakan JSON-RPC 2.0. Model AI tidak menjalankan alat tersebut secara langsung — model meminta klien, klien memanggil server MCP, lalu server mengeksekusi dan mengembalikan hasil. Karena protokol ini bersifat standar, satu server dapat bekerja dengan klien mana pun yang patuh, baik itu Claude Code, Claude Desktop, maupun Gemini CLI.
Apa perbedaan antara transport stdio dan HTTP?
Server stdio dijalankan oleh klien sebagai proses anak (child process) dan berkomunikasi melalui stdin/stdout. Server ini hanya aktif selama klien berjalan pada satu mesin dan tidak memerlukan jaringan atau autentikasi. Server HTTP adalah layanan jaringan yang berjalan terus-menerus yang dapat diakses oleh banyak klien secara bersamaan, sehingga memerlukan TLS dan autentikasi. Gunakan stdio untuk alat lokal pengguna tunggal; gunakan HTTP (Streamable HTTP pada server saat ini) untuk segala sesuatu yang bersifat bersama atau persisten.
Bagaimana cara mengamankan server MCP jarak jauh?
Asumsikan server tersebut memberikan akses alat ke file, database, atau shell Anda, maka jangan pernah mengeksposnya tanpa autentikasi. Cara terbaik adalah dengan membatasi server pada localhost dan mengaksesnya melalui terowongan SSH atau VPN pribadi; jika server harus bersifat publik, letakkan di belakang reverse proxy yang menerapkan bearer token atau alur MCP OAuth. Buat token dengan openssl rand -hex 32 dan jangan pernah mengikat server ke 0.0.0.0 tanpa salah satu metode tersebut di depannya.
Bagaimana cara melakukan debug pada server yang tidak mau berjalan?
Pertama, periksa claude mcp list — ✗ Failed to connect dengan spawn ... ENOENT berarti perintah atau runtime tidak ditemukan, maka perbaiki path atau instal runtime tersebut. Jika server terhubung lalu terputus dengan error JSON parse, server tersebut mencatat log ke stdout dan merusak aliran JSON-RPC; pindahkan semua log ke stderr. Untuk masalah lainnya, jalankan perintah yang sama persis di bawah MCP Inspector, yang menjalankan server secara terisolasi agar Anda dapat membedakan bug server dari bug konfigurasi klien.