VPS üzerinde MCP server kurulumu nasıl yapılır?
AI ajanları için stdio ve remote HTTP transport yöntemleriyle MCP server kurulumu yapın. systemd, TLS ve auth yapılandırmaları hakkında teknik rehber.
İnşa edilen yapı
Tek bir VPS üzerinde iki adet çalışan MCP kurulumu. İlk olarak bir stdio sunucusu — Claude Code tarafından bir child process olarak başlatılan ve bir pipe üzerinden iletişim kurulan bir dosya sistemi veya veritabanı aracı. Ardından systemd arkasında uzun süreli bir ağ servisi olarak çalışan, TLS ile yapılandırılmış bir nginx reverse proxy ile desteklenen ve herhangi bir MCP istemcisi tarafından erişilebilen bir remote HTTP sunucusu. Her iki kurulumun da boyutu küçüktür. Bu kılavuzun büyük bir kısmı, asıl kritik olan iki konuya odaklanmaktadır: JSON-RPC akışının temiz tutulması ve kimlik doğrulaması olmayan bir araç uç noktasının (endpoint) doğrudan internete açılmaması.
MCP'nin gerçekte ne olduğu
Model Context Protocol; Claude Code, Claude Desktop, VPS üzerindeki Gemini CLI veya kendi betiğiniz gibi bir AI istemcisinin harici araçları çağırması ve harici kaynakları okuması için standart bir yöntemdir. Modelin kendisi herhangi bir işlem yürütmez. İstemciye talep gönderir, istemci MCP sunucusuna JSON-RPC 2.0 protokolü ile iletir, sunucu aracı çalıştırır ve sonucu geri döndürür. Tek bir protokol sayesinde, bir kez yazdığınız sunucu MCP destekleyen her istemci ile çalışır.
İki taşıma katmanı (transport) mevcuttur ve bu kılavuzun geri kalanı bu katmanlara göre ayrılmıştır:
- stdio. İstemci, sunucuyu bir alt süreç (child process) olarak başlatır; standart giriş (stdin) ve standart çıkış (stdout) üzerinden satır sonu ile ayrılmış JSON-RPC mesajları alışverişi yapar. Ağ, port veya kimlik doğrulama gerektirmez; güven sınırı sürecin kendisidir. Neredeyse tüm yerel araçlar bu şekilde sunulur.
- Streamable HTTP (ve eski versiyonu HTTP+SSE). Sunucu, sürekli çalışan bir web servisidir. İstemci HTTP üzerinden bağlanır ve sunucu yanıtları Server-Sent Events olarak akış halinde iletebilir. Bu yöntem, bir sunucuyu birçok istemci ile paylaşmak veya kalıcı olarak bir makinede çalışması gereken bir araç çalıştırmak için kullanılır.
Araç tek bir makineye ve tek bir kullanıcıya aitse stdio seçilmelidir. Paylaşılan bir servis söz konusuysa HTTP seçilmelidir.
Ön Koşullar ve dikkat edilmesi gerekenler
root veya sudo yetkisine sahip, yeni kurulmuş bir Ubuntu 24.04 KVM VPS varsayılmaktadır. Bunun haricinde:
- Sunucunun yazıldığı bir çalışma zamanı (runtime). Referans sunucuların çoğu Node veya Python kullanmaktadır. Ubuntu 24.04, Node 18 ile birlikte gelir; ancak güncel MCP paketlerinin çoğu Node 20 veya daha yeni bir sürüm gerektirmektedir. Bu nedenle
aptyerine NodeSource veya nvm üzerinden güncel bir LTS sürümü kurulmalıdır. Python 3.12 halihazırda mevcuttur. - Bir alan adı ve DNS A kaydı. Bu kayıt sadece uzak HTTP sunucusu için gereklidir; TLS, bu VPS'e yönlenen bir isme ihtiyaç duyar. stdio örneği için DNS gerekmemektedir.
- 512 MB RAM yeterlidir. MCP sunucuları hafif JSON-RPC süreçleridir; bellek maliyeti protokolden değil, kullandığınız araçtan (bir veritabanı sürücüsü, bir dosya önbelleği vb.) kaynaklanır.
- Spesifikasyon yenidir ve güncellenmektedir. 2025-03-26 revizyonu, HTTP+SSE yerine Streamable HTTP'yi getirmiş ve SSE'yi kullanımdan kaldırılmış (deprecated) olarak işaretlemiştir. SSE hala çalışmaktadır ve birçok sunucu bunu desteklemeye devam etmektedir; bu nedenle herhangi bir taşıma katmanı (transport) kuralını mutlak gerçek olarak kabul etmek yerine, sunucunun sürüm notlarına göre tekrar kontrol edilmelidir.
Adım 1: stdio sunucusunu Claude Code içine entegre edin
Dosya sistemi sunucusu ile başlayın; bu sunucu resmi bir sunucudur, aktif olarak güncellenmektedir ve Node dışında hiçbir gereksinimi yoktur. Aşağıdaki tek komut, sunucuyu Claude Code'a kaydeder ve mevcut projeye sınırlandırarak commit edilebilir bir dosyaya yazar:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/api-- ayırıcısı önemlidir: ondan sonra gelen her şey Claude Code'un çalıştıracağı komuttur, Claude Code için bir flag değildir. Bu işlem, proje kök dizinine bir .mcp.json dosyası yazar:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Henüz çalışan bir süreç yoktur. Bu dizinde Claude Code'u bir sonraki başlatışınızda, agent .mcp.json dosyasını okur, npx -y @modelcontextprotocol/server-filesystem ... sürecini bir child process olarak başlatır ve o sürecin stdin/stdout kanalları üzerinden MCP el sıkışmasını gerçekleştirir. İşlemin başarılı olduğunu şu şekilde doğrulayın:
claude mcp listSağlıklı bir sunucu, komutunu ve yeşil bir onay işareti olan filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected simgesini yazdırır. Oturum içerisinde, /mcp slash komutu sunucunun sunduğu araçları (read_file, write_file, list_directory) listeler; agent artık bu araçları izin verdiğiniz yollar üzerinde çağırabilir. Veritabanı aracı da aynı yapıdadır; paketi değiştirin ve son argüman olarak bir bağlantı dizesi (connection string) iletin. Ancak, referans Postgres sunucusu birden fazla kez el değiştirdiği için güncel paket adı için sunucunun kendi deposunu kontrol edin.
Agent'ı makine üzerinde çalıştırmanın temel amacı budur: Claude Code oturumu tmux içindeki VPS üzerinde yaşar ve stdio sunucuları, ağ gecikmesi olmadan proje dosyalarına ve yerel servislere doğrudan erişim sağlayarak hemen yanında çalışır.
Adım 2: uzak bir HTTP sunucusu oluşturun
Bir stdio sunucusu, ana süreciyle birlikte kapanır. Her istemci için sürekli açık kalan bir araca ihtiyaç duyulduğunda —paylaşılan bir operasyon aracı, bir veritabanı ağ geçidi veya hem dizüstü bilgisayarınızın hem de CI sisteminizin çağırdığı bir araç gibi— HTTP transport ve gerçek bir servis gereklidir. Aşağıda, resmi SDK kullanılarak oluşturulmuş ve tek bir aracı dışarıya açan minimal bir Python sunucusu bulunmaktadır:
# /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")host="127.0.0.1" notuna dikkat edin. Sunucu yalnızca localhost'a bağlanır; sistem dışındaki hiçbir yapı ona doğrudan erişemez. Bu, kimlik doğrulama mekanizmaları kurulmadan önce istenen bir durumdur. systemd'nin kararlı bir interpreter yoluna sahip olması için sunucuyu kendi virtualenv ortamına yükleyin:
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: systemd ile çalışmaya devam etmesini sağlayın
Agent bir araca erişmeye çalıştığında araç kapalıysa, bu durum aracın hiç olmamasından daha kötüdür. /etc/systemd/system/mcp-ops.service komutunu yazın:
[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.targetExecStart içindeki venv Python yolu opsiyonel değildir; yolu /usr/bin/python3 olarak belirtin. İşlem ModuleNotFoundError: No module named 'mcp' ile başlar, çünkü sistem yorumlayıcısı pip install içeriğini görmez. Etkinleştirin ve kontrol edin:
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 çıktısı active (running) olmalıdır. curl, gövdesinde bir JSON-RPC hatası ile HTTP/1.1 400 Bad Request döner; istek bir oturum ve geçerli bir JSON yükü içermiyordu. Bu istenen bir durumdur: portun yanıt verdiğini ve protokolü kullandığını kanıtlar. Connection refused veya boş bir yanıt, işlemin beklediğiniz yere bağlanmadığı anlamına gelir; journalctl -u mcp-ops -n 50 içeriğini okuyın.
Adım 4: TLS ve ters proxy kurulumu yapın
Sunucu localhost üzerinde dinleme yapmaktadır. Herhangi bir yerden erişim sağlamak için TLS sonlandırması nginx üzerinde yapılmalı ve istek içeriye yönlendirilmelidir. nginx kurulumunu gerçekleştirin, Certbot ve Let's Encrypt ile nginx üzerinde sertifika alın ve ardından location bloğunu yazın. Kritik nokta buffering özelliğinin devre dışı bırakılmasıdır; nginx varsayılan olarak yanıtın tamamlanmasını bekler, bu durum SSE akışının sonsuza kadar durmasına neden olur:
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;
}
}sudo nginx -t && sudo systemctl reload nginx ile yeniden yükleyin. Eğer halihazırda bir konteyner kümesi çalıştırıyorsanız, aynı işlem otomatik TLS özellikli Traefik reverse proxy tarafından sizin yerinize yapılabilir; bu araç sertifikayı oluşturur ve hostname üzerinden yönlendirme yapar, size sadece MCP konteynerine label eklemek kalır. Her iki durumda da, ters proxy şu an halka açık port üzerindeki tek bileşendir ve henüz güvenliğini sağlamadığınız bir servise yönlenmektedir. URL'yi herhangi bir yere kaydetmeden önce bu sorunu giderin.
Step 5: Bu konunun temelini oluşturan güvenlik kuralı
Kimlik doğrulaması yapılmamış bir MCP uç noktasını asla dış dünyaya açmayın. Bir MCP sunucusu sadece salt okunur bir API değildir. Dosyalarınıza, veritabanınıza ve bazen bir shell terminaline araç erişimi sağlar. İnternet üzerinde açık bir /mcp, yapay zeka ajanınızla aynı yetkilere sahip bir yabancıdır: araçlarınızı listeler ve ardından bunları çağırır. Bunu, kimlik doğrulaması yapılmamış bir admin soketi gibi değerlendirin; çünkü gerçekte öyledir.
Tercih sırasına göre üç savunma yöntemi:
- Yayınlamayın. Sunucuyu
127.0.0.1üzerinde tutun ve dizüstü bilgisayarınızdan bir SSH tüneli ile erişin:ssh -L 8000:127.0.0.1:8000 matt@vpskomutunu çalıştırın ve istemciyihttp://127.0.0.1:8000/mcpadresine yönlendirin. Hiçbir veri dış dünyaya açılmaz. - Özel bir ağa yerleştirin. Bir self-hosted WireGuard VPN tünel adresine bağlayın ve yalnızca VPN eşlerinin erişmesine izin verin. Genel internet sadece kapalı bir port görür.
- Eğer halka açık olması gerekiyorsa, bir token gerektirin. Doğru çözüm, HTTP transport'un yerel olarak desteklediği MCP OAuth akışıdır. Pratik olan minimum çözüm ise proxy üzerinde kontrol edilen paylaşımlı bir bearer token kullanımıdır; bu yöntem ucuzdur ve rastgele saldırıları tamamen durdurur:
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...
}Token'ı openssl rand -hex 32 ile oluşturun ve sunucuyu bu yöntemlerden biri olmadan doğrudan 0.0.0.0 adresine bağlamayın. İstemci, token'ı bir header olarak gönderir. Claude Code kullanımında:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Sır (secret) verisinin .mcp.json içinde düz metin olarak kalmaması için kabuğunuzda (shell) MCP_TOKEN değişkenini ayarlayın — Claude Code, okuma sırasında ${MCP_TOKEN} değişkenini ortamdan (environment) genişletir.
Step 6: MCP Inspector ile hata ayıklama
Sunucu hatalı çalıştığında, tahminde bulunmak yerine resmi web tabanlı test istemcisi olan Inspector ile doğrudan test edin. stdio sunucuları için, ajanın çalıştırdığı komutun aynısını kullanın:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpBu işlem http://localhost:6274 üzerinde bir kullanıcı arayüzü ve 6277 portunda bir proxy başlatır (güncel sürümler MCP_PROXY_AUTH_TOKEN sorgu dizinine sahip bir URL yazdırır; arayüzün reddedilmemesi için bu bağlantıyı kullanın). Connect butonuna tıklayın, ardından List Tools ve son olarak gerçek argümanlarla Call Tool seçeneklerini kullanın. Eğer Inspector üzerinde işlem başarılı oluyor ancak ajanda hata veriyorsa, sorun sunucuda değil, istemci yapılandırmasındadır. Uzak HTTP sunucuları için Streamable HTTP transport seçeneğini seçin, https://mcp.example.com/mcp adresini girin, Authorization header bilgisini ekleyin ve bağlanın; bu yöntem, herhangi bir ajan devreye girmeden kimlik doğrulama ve proxy ayarlarının doğruluğunu kanıtlamanın en hızlı yoludur.
Sunucuları güncel tutmak
MCP hızlı gelişmektedir, bu nedenle yamaları belirli bir takvime göre uygulayın. npx -y ile başlatılan Node sunucuları her başlatmada en son sürümü çeker; bu durum kullanışlıdır ancak tekrarlanabilir değildir. Test ettiğiniz tam sürümü sabitleyin — sürümü npm view @modelcontextprotocol/server-filesystem version üzerinden okuyun ve .mcp.json (@modelcontextprotocol/server-filesystem@<version>) içindeki paket adına ekleyin. Sunucu kararlılığı kritik hale geldiğinde, sürümleri bilinçli olarak yükseltin. Systemd altındaki Python sunucuları, sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" ve ardından sudo systemctl restart mcp-ops ile güncellenir. Yükseltme yaparken SDK'nın hedeflediği spesifikasyon revizyonunu kontrol edin — SSE'den Streamable-HTTP sınırına geçiş, istemcilerin talep etmesi gereken taşıma protokolünü değiştirebilir.
Hata modları ve göreceğiniz dizeler
Agent, sunucunun başarısız olduğunu gösterir. claude mcp list, ✗ Failed to connect yazdırır ve TUI, MCP server 'filesystem' failed to start rapor eder. claude --debug komutunu çalıştırdığınızda genellikle Error: spawn npx ENOENT ile karşılaşırsınız — komut agent'ın PATH değişkeninde bulunmamaktadır. Çalışma zamanı (runtime) eksiktir veya agent'ın aradığı konumda değildir: Node yüklü değildir, npx eksiktir veya bir Python sanal ortamı (virtualenv) sadece ismiyle referans verilmiştir. Komutu tam yol (absolute path) olarak düzeltin veya çalışma zamanını yükleyip tekrar bağlanın.
Bir stdio sunucusu bağlanır ve ardından anında bağlantıyı keser. İstemci, Unexpected token 'S', "Server sta"... is not valid JSON veya Failed to parse message gibi bir JSON ayrıştırma hatası günlüğe kaydeder. Sebep her zaman aynıdır: sunucu stdout üzerine bir log satırı yazmıştır. stdio modunda stdout, JSON-RPC kanalıdır; bu nedenle araya giren herhangi bir metin akışı bozar ve el sıkışma (handshake) başarısız olur. Node kullanımında, console.log stdout'a gider — console.error kullanın. Python kullanımında, çıplak bir print() stdout'a gider — logları sys.stderr olarak yapılandırılmış logging ile yazın veya file=sys.stderr parametresini iletin. Kural kesindir: stdio modunda stdout üzerinde sadece JSON-RPC bulunmalıdır, insan tarafından okunabilir tüm çıktılar stderr üzerinde olmalıdır.
Uzak sunucu zaman aşımına uğrar veya el sıkışma sırasında bağlantıyı kapatır. İstemci MCP error -32000: Connection closed hatası verir veya Inspector Connect aşamasında takılı kalır ve araçları listelemez. nginx arkasındaki bu durum tamponlamadan (buffering) kaynaklanır: proxy, SSE akışını boşaltmak (flush) yerine tutar, bu nedenle istemci asla gelmeyecek bir yanıtı bekler. location içerisine proxy_buffering off; (ve Adım 4'teki bloğun geri kalanını) ekleyin. Genel URL üzerinden curl -N ile doğrulayın — etkinlik verilerinin tek seferde değil, kademeli olarak geldiğini görmelisiniz.
Kimlik doğrulama (Auth) reddedilir. İstemci Error POSTing to endpoint (HTTP 401) veya doğrudan 401 Unauthorized rapor eder. Ya başlık (header) eksiktir, ya token yanlıştır ya da istemci konfigürasyonu okurken kabuk değişkeni (shell variable) boştur — bu yaygın bir hatadır, çünkü değişken tanımlı değilse ${MCP_TOKEN} boşluğa genişler ve nginx bu durumda değer içermeyen Bearer verisini görür. Değişkeni echo ile yazdırın, başlığı tekrar ekleyin ve baytların nginx if içindeki token ile tam olarak eşleştiğini doğrulayın.
Servis systemd altında başlatılamıyor. journalctl -u mcp-ops, ModuleNotFoundError: No module named 'mcp' gösterir — ExecStart venv yorumlayıcısı yerine sistem Python'ını işaret etmektedir. Veya Address already in use — başka bir süreç 8000 portunu işgal ediyor; bunu sudo ss -ltnp | grep 8000 ile bulun.
FAQ
MCP sunucusu tam olarak nedir?
MCP sunucusu, JSON-RPC 2.0 kullanarak araçları ve kaynakları bir AI istemcisine sunan bir programdır. AI modeli aracı doğrudan çalıştırmaz; istemciye talep gönderir, istemci MCP sunucusunu çağırır, sunucu ise işlemi gerçekleştirip sonucu döndürür. Protokol standart olduğu için, bir sunucu Claude Code, Claude Desktop veya Gemini CLI gibi uyumlu tüm istemcilerle çalışabilir.
stdio ve HTTP taşıma katmanı arasındaki fark nedir?
stdio sunucusu, istemci tarafından bir alt süreç (child process) olarak başlatılır ve stdin/stdout üzerinden iletişim kurar. Bu nedenle, tek bir makinedeki tek bir istemciyle sınırlıdır; ağ veya kimlik doğrulama gerektirmez. HTTP sunucusu ise birçok istemcinin aynı anda erişebileceği, sürekli çalışan bir ağ servisidir; bu yüzden TLS ve kimlik doğrulama gerektirir. Yerel ve tek kullanıcılı araçlar için stdio; paylaşılan veya kalıcı yapılar için HTTP (mevcut sunucularda Streamable HTTP) kullanın.
Uzak bir MCP sunucusu nasıl güvenli hale getirilir?
Sunucunun dosyalarınıza, veritabanınıza veya kabuğunuza (shell) araç erişimi sağladığını varsayarak, sunucuyu asla kimlik doğrulaması olmadan dışarıya açmayın. En iyi yöntem, sunucuyu localhost'a bağlamak ve bir SSH tüneli veya özel bir VPN üzerinden erişmektir. Eğer sunucu halka açık olmak zorundaysa, bir bearer token veya MCP OAuth akışını zorunlu kılan bir ters proxy (reverse proxy) arkasına yerleştirin. Token'ı openssl rand -hex 32 ile oluşturun ve sunucuyu bu yöntemlerden biri olmadan 0.0.0.0 adresine bağlamayın.
Başlamayan bir sunucu nasıl hata ayıklanır (debug)?
Öncelikle claude mcp list kontrol edilmelidir — ✗ Failed to connect ile spawn ... ENOENT hatası, komutun veya çalışma zamanının (runtime) eksik olduğu anlamına gelir; bu durumda yolu (path) düzeltin veya ilgili yazılımı yükleyin. Eğer bağlantı kuruluyor ancak bir JSON ayrıştırma (parse) hatası ile bağlantı kopuyorsa, sunucu stdout üzerinden log yazıyor ve JSON-RPC akışını bozuyor demektir; tüm logları stderr'a yönlendirin. Diğer tüm durumlar için, komutu MCP Inspector altında çalıştırın. MCP Inspector sunucuyu izole bir şekilde çalıştırarak, hatanın sunucudan mı yoksa istemci yapılandırmasından mı kaynaklandığını anlamanızı sağlar.