نصب و اجرای MCP servers روی VPS
آموزش راهاندازی سرورهای Model Context Protocol با استفاده از stdio و remote HTTP در VPS به همراه تنظیمات systemd، TLS و روشهای احراز هویت امن.
آنچه در حال ساخت آن هستید
دو پیکربندی MCP فعال روی یک VPS. ابتدا یک سرور stdio؛ ابزاری برای سیستم فایل یا پایگاه داده که Claude Code آن را به عنوان یک child process اجرا میکند و از طریق یک pipe با آن ارتباط برقرار میکند. سپس یک سرور remote HTTP که به عنوان یک سرویس شبکه با عمر طولانی در پشت systemd و یک nginx reverse proxy با TLS اجرا میشود و توسط هر MCP client که به آن اشاره کنید، قابل دسترسی است. فرآیند نصب برای هر دو کوچک است. بخش اصلی این راهنما مربوط به دو مورد چالشبرانگیز است: حفظ سلامت جریان JSON-RPC و عدم قرار دادن یک endpoint ابزار بدون احراز هویت در اینترنت عمومی.
ماهیت واقعی MCP
Model Context Protocol یک استاندارد است که به کلاینتهای هوش مصنوعی — مانند Claude Code، Claude Desktop، Gemini CLI on a VPS یا اسکریپت شخصی شما — اجازه میدهد تا ابزارهای خارجی را فراخوانی کرده و منابع خارجی را بخوانند. خودِ مدل هیچ عملیاتی را اجرا نمیکند. مدل از کلاینت درخواست میکند، کلاینت از طریق پروتکل JSON-RPC 2.0 با یک server MCP صحبت میکند، و سپس سرور ابزار را اجرا کرده و نتیجه را بازمیگرداند. با وجود یک پروتکل واحد، سروری که یک بار مینویسید با تمام کلاینتهایی که از MCP پشتیبانی میکنند، کار میکند.
دو نوع Transport وجود دارد که باقی این راهنما بر اساس آنها تقسیمبندی شده است:
- stdio. کلاینت، سرور را به عنوان یک child process اجرا میکند و پیامهای JSON-RPC را که با خط جدید (newline) جدا شدهاند، از طریق standard input و standard output مبادله میکند. بدون نیاز به شبکه، پورت یا احراز هویت؛ مرز اعتماد، خودِ پروسس است. تقریباً تمام ابزارهای محلی به این روش عرضه میشوند.
- Streamable HTTP (و نسخه قدیمیتر آن، HTTP+SSE). سرور یک وبسرویس با زمان فعالیت طولانی است. کلاینت از طریق HTTP متصل میشود و سرور میتواند پاسخها را به صورت Server-Sent Events ارسال کند. این روش برای اشتراکگذاری یک سرور با کلاینتهای متعدد، یا اجرای ابزاری که باید به صورت دائمی روی سیستم باقی بماند، استفاده میشود.
زمانی که ابزار متعلق به یک ماشین و یک کاربر است، از stdio استفاده کنید. زمانی که ابزار یک سرویس مشترک است، از HTTP استفاده کنید.
پیشنیازها و نکات مهم
فرض کنید یک VPS تازه با سیستمعامل Ubuntu 24.04 و دسترسی root یا sudo دارید. علاوه بر آن:
- محیط اجرای کد (Runtime) مورد نیاز. اکثر سرورهای مرجع با Node یا Python نوشته شدهاند. نسخه Ubuntu 24.04 دارای Node 18 است، اما بسیاری از بستههای فعلی MCP به Node 20 یا بالاتر نیاز دارند؛ بنابراین به جای اعتماد به
apt، یک نسخه LTS جدید را از NodeSource یا nvm نصب کنید. Python 3.12 از قبل نصب شده است. - یک دامنه و رکورد DNS A، اما فقط برای سرور HTTP از راه دور؛ برای TLS به نامی نیاز دارید که به این VPS اشاره کند. مثال stdio به هیچ DNS ای نیاز ندارد.
- مقدار 512 MB RAM کافی است. سرورهای MCP فرآیندهای سبک JSON-RPC هستند؛ هزینه حافظه مربوط به ابزاری است که استفاده میکنید (مانند یک درایور پایگاه داده یا حافظه پنهان فایل)، نه خودِ پروتکل.
- مشخصات فنی (Spec) در حال تغییر است. در نسخه 2025-03-26، پروتکل HTTP+SSE با Streamable HTTP جایگزین شد و SSE منسوخ (deprecated) اعلام شد. SSE همچنان کار میکند و سرورهای زیادی از آن استفاده میکنند، بنابراین هر محدودیت در لایه انتقال (transport) را به عنوان یک اصل قطعی ندانید و حتماً با یادداشتهای انتشار (release notes) سرور تطبیق دهید.
Step 1: متصل کردن یک stdio server به Claude Code
کار را با filesystem server شروع کنید؛ این سرور رسمی است، بهطور فعال نگهداری میشود و تنها به Node نیاز دارد. دستور زیر آن را در Claude Code ثبت کرده و محدود به پروژه فعلی میکند تا در یک فایل قابل commit قرار بگیرد:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiجداکننده -- اهمیت دارد: هر آنچه بعد از آن میآید، دستوری است که Claude Code اجرا میکند، نه یک flag برای Claude Code. این کار یک .mcp.json در ریشه (root) پروژه ایجاد میکند:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}هنوز هیچ فرآیندی در حال اجرا نیست. دفعه بعد که Claude Code را در این دایرکتوری اجرا کنید، agent فایل .mcp.json را میخواند، npx -y @modelcontextprotocol/server-filesystem ... را به عنوان یک child process اجرا میکند و عملیات MCP handshake را از طریق stdin/stdout آن فرآیند انجام میدهد. صحت اتصال را با دستور زیر تایید کنید:
claude mcp listیک سرور سالم، دستور خود و یک تیک سبز — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected — را چاپ میکند. در داخل session، دستور slash /mcp ابزارهای ارائه شده توسط سرور (read_file, write_file, list_directory) را لیست میکند و agent اکنون میتواند آنها را در مسیرهایی که اجازه دادهاید، فراخوانی کند. ابزار دیتابیس نیز ساختار مشابهی دارد — فقط package را تغییر دهید و یک connection string را به عنوان آرگومان نهایی ارسال کنید — اما نام package فعلی را در مخزن (repository) خودِ سرور چک کنید، زیرا سرور مرجع Postgres چندین بار تغییر مالک شده است.
هدف اصلی اجرای agent روی سیستم (box) همین است: session Claude Code در داخل tmux روی VPS اجرا میشود، و stdio serverهای آن دقیقاً در کنار آن با دسترسی مستقیم به فایلهای پروژه و سرویسهای محلی اجرا میشوند، بدون نیاز به رفت و برگشت شبکه (network round-trip).
Step 2: ساخت یک HTTP server از راه دور
یک stdio server همراه با پروسهی والد خود بسته میشود. زمانی که به ابزاری نیاز دارید که برای تمام کلاینتها فعال بماند — مانند یک ابزار عملیاتی مشترک، یک gateway برای دیتابیس، یا چیزی که هم لپتاپ شما و هم CI شما آن را فراخوانی میکنند — به پروتکل HTTP و یک سرویس واقعی نیاز دارید. در اینجا یک Python server ساده با استفاده از SDK رسمی آورده شده است که یک tool را ارائه میدهد:
# /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" توجه کنید. این server فقط به localhost bind میشود — هیچ موجودیت خارجی نمیتواند مستقیماً به آن دسترسی داشته باشد، که این دقیقاً همان چیزی است که قبل از پیادهسازی auth نیاز دارید. آن را در یک virtualenv مجزا نصب کنید تا systemd مسیر مفسر (interpreter path) پایداری داشته باشد:
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
ابزاری که هنگام درخواست agent از سوی آن از دسترس خارج شده باشد، بدتر از نبودن ابزار است. این دستور را بنویسید: /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.targetاستفاده از مسیر مطلق (absolute path) برای Python در venv در ExecStart اجباری است — آن را به /usr/bin/python3 اشاره دهید و فرآیند با ModuleNotFoundError: No module named 'mcp' شروع میشود، زیرا مفسر سیستم هرگز pip install شما را مشاهده نکرده است. فعالسازی و بررسی:
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/mcpخروجی status باید active (running) باشد. بخش curl با یک خطای JSON-RPC در بدنه، مقدار HTTP/1.1 400 Bad Request را برمیگرداند — درخواست فاقد session و payload معتبر JSON بوده است — و این دقیقاً همان چیزی است که میخواهید: این موضوع ثابت میکند که port پاسخگو است و پروتکل را درک میکند. اگر Connection refused یا یک پاسخ خالی دریافت کردید، یعنی فرآیند در مکانی که فکر میکنید bind نشده است؛ journalctl -u mcp-ops -n 50 را بخوانید.
Step 4: قرار دادن TLS و یک reverse proxy در مقابل آن
سرور روی localhost گوش میدهد. برای دسترسی به آن از هر جایی، باید TLS را در nginx پایان دهید و درخواستها را به داخل پروکسی کنید. nginx را نصب کنید، با استفاده از Certbot and Let's Encrypt on nginx گواهی دریافت کنید و سپس بلوک location را بنویسید. بخش حیاتی، غیرفعال کردن buffering است؛ زیرا رفتار پیشفرض nginx نگه داشتن پاسخ تا تکمیل شدن آن است که باعث توقف همیشگی یک SSE stream میشود:
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 تنظیمات را reload کنید. اگر از مجموعهای از containerها استفاده میکنید، Traefik reverse proxy with automatic TLS همین کار را برای شما انجام میدهد؛ این ابزار گواهی را صادر کرده و بر اساس hostname مسیریابی میکند و شما فقط کافی است labelها را به container مربوط به MCP اضافه کنید. در هر دو حالت، اکنون reverse proxy تنها سرویسی است که روی یک پورت عمومی باز است و به سرویسی اشاره میکند که هنوز آن را ایمن نکردهاید. قبل از اینکه URL را در جایی ثبت کنید، این مورد را برطرف کنید.
Step 5: قانون امنیتی اصلی در این مبحث
هرگز یک MCP endpoint بدون احراز هویت را در معرض دسترسی قرار ندهید. یک MCP server صرفاً یک API با دسترسی فقط خواندنی نیست. این سرور دسترسی به ابزارها (tools) را فراهم میکند — به فایلها، پایگاه داده و گاهی یک shell. یک /mcp باز در اینترنت عمومی، مانند غریبهای است که همان سطح دسترسی عامل هوش مصنوعی شما را دارد: آنها لیست ابزارهای شما را میخوانند و سپس آنها را فراخوانی میکنند. با آن دقیقاً مانند یک admin socket بدون احراز هویت برخورد کنید، زیرا واقعیت همین است.
سه روش دفاعی، به ترتیب اولویت:
- آن را منتشر نکنید. سرور را روی
127.0.0.1نگه دارید و از طریق یک SSH tunnel از لپتاپ خود به آن متصل شوید: ابتداssh -L 8000:127.0.0.1:8000 matt@vpsرا اجرا کنید و سپس کلاینت را بهhttp://127.0.0.1:8000/mcpمتصل کنید. در این حالت هیچ چیزی در معرض دسترسی قرار نمیگیرد. - آن را در یک شبکه خصوصی قرار دهید. آدرس tunnel یک self-hosted WireGuard VPN را bind کنید و اجازه دهید فقط کاربران VPN به آن دسترسی داشته باشند. اینترنت عمومی تنها یک پورت بسته را مشاهده میکند.
- اگر حتماً باید عمومی باشد، از توکن استفاده کنید. راه حل استاندارد، جریان MCP OAuth است که پروتکل HTTP به صورت بومی از آن پشتیبانی میکند. حداقل راه حل کاربردی، استفاده از یک shared bearer token است که در سطح proxy بررسی میشود؛ این روش کمهزینه است و جلوی حملات تصادفي را میگیرد:
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...
}توکن را با استفاده از openssl rand -hex 32 تولید کنید و هرگز سرور را بدون داشتن یکی از این روشها به 0.0.0.0 bind نکنید. کلاینت سپس توکن را به عنوان یک header ارسال میکند. در Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'متغیر MCP_TOKEN را در shell خود تنظیم کنید تا رمز عبور هرگز به صورت متن ساده (plaintext) در .mcp.json ذخیره نشود — Claude Code مقدار ${MCP_TOKEN} را در زمان اجرا از محیط سیستم (environment) فراخوانی میکند.
Step 6: debug with the MCP Inspector
وقتی سرور دچار مشکل میشود، به جای حدس زدن از داخل agent، مستقیماً از Inspector استفاده کنید؛ این ابزار، کلاینت تست رسمی و مبتنی بر وب است. برای یک stdio server، همان دستوری را که agent اجرا میکند، به آن بدهید:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpاین دستور یک UI در http://localhost:6274 اجرا میکند (در نسخههای جدید، یک URL با یک query string از نوع MCP_PROXY_AUTH_TOKEN نمایش داده میشود — حتماً از همان لینک دقیق استفاده کنید، در غیر این صورت UI اجازه اتصال نمیدهد) و یک proxy روی پورت 6277 ایجاد میکند. روی Connect کلیک کنید، سپس List Tools را انتخاب کنید و در نهایت Call Tool را با آرگومانهای واقعی اجرا کنید. اگر دستور در Inspector با موفقیت اجرا شد اما در agent با خطا مواجه شد، مشکل از تنظیمات client است، نه سرور. برای remote HTTP server، نوع transport را روی Streamable HTTP قرار دهید، https://mcp.example.com/mcp را وارد کنید، header مربوط به Authorization را اضافه کنید و متصل شوید — این سریعترین راه برای تایید صحت احراز هویت و proxy، پیش از درگیر کردن agent است.
Keeping servers updated
MCP با سرعت زیادی پیشرفت میکند، بنابراین طبق یک برنامه مشخص، پچها را اعمال کنید. Node serverهایی که با npx -y اجرا میشوند، در هر بار اجرا آخرین نسخه را دریافت میکنند؛ این کار راحت است اما قابلیت بازتولید (reproducible) ندارد. نسخه دقیق مورد آزمایش خود را ثابت نگه دارید — آن را از npm view @modelcontextprotocol/server-filesystem version بخوانید و به نام package در .mcp.json (@modelcontextprotocol/server-filesystem@<version>) اضافه کنید. وقتی پایداری سرور اهمیت پیدا کرد، نسخه را آگاهانه ارتقا دهید. Python serverهایی که تحت مدیریت systemd هستند، با اجرای sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" و سپس sudo systemctl restart mcp-ops بهروزرسانی میشوند. هنگام ارتقا، به نسخه مشخص شده در spec که SDK شما هدف قرار داده است، توجه کنید — تغییر از SSE به Streamable-HTTP میتواند پروتکل انتقال (transport) مورد نیاز کلاینتها را تغییر دهد.
حالتهای شکست و رشتههای نمایش داده شده
ایجنت خطای شکست سرور را نشان میدهد. claude mcp list عبارت ✗ Failed to connect را چاپ میکند و TUI خطای MCP server 'filesystem' failed to start را گزارش میدهد. دستور claude --debug را اجرا کنید؛ معمولاً با Error: spawn npx ENOENT مواجه میشوید — یعنی دستور در PATH اِجنت وجود ندارد. محیط زماناجرا (runtime) نصب نشده یا در مسیر مورد نظر اِجنت نیست: Node نصب نیست، npx وجود ندارد، یا یک virtualenv Python با نام ساده ارجاع داده شده است. مسیر دستور را به یک مسیر مطلق (absolute path) تغییر دهید یا runtime را نصب کنید، سپس دوباره متصل شوید.
یک سرور stdio متصل شده و بلافاصله قطع میشود. کلاینت خطای JSON parse را ثبت میکند — چیزی شبیه به Unexpected token 'S', "Server sta"... is not valid JSON یا Failed to parse message. علت همیشه یکسان است: سرور یک خط لاگ را در stdout نوشته است. در حالت stdio، خروجی stdout همان کانال JSON-RPC است؛ بنابراین هر متن اضافی جریان داده را خراب کرده و فرآیند handshake متوقف میشود. در Node، دستور console.log به stdout میرود — از console.error استفاده کنید. در Python، دستور bare print() به stdout میرود — لاگها را با تنظیم logging روی sys.stderr بنویسید، یا از file=sys.stderr استفاده کنید. این یک قانون قطعی است: در حالت stdio، فقط JSON-RPC باید در stdout باشد و تمام خروجیهای قابل خواندن برای انسان باید در stderr باشند.
یک سرور از راه دور در حین handshake با timeout مواجه شده یا اتصال را قطع میکند. کلاینت با خطای MCP error -32000: Connection closed شکست میخورد، یا Inspector در مرحله Connect متوقف شده و ابزارها را لیست نمیکند. در پشت nginx، علت این مشکل buffering است: پروکسی به جای تخلیه (flush) جریان SSE، آن را نگه میدارد، بنابراین کلاینت منتظر پاسخی میماند که هرگز نمیرسد. عبارت proxy_buffering off; (و بقیه بلوک در Step 4) را به location اضافه کنید. با استفاده از curl -N روی URL عمومی بررسی کنید — باید ببینید که دادههای event به صورت تدریجی میرسند، نه اینکه همه آنها یکجا در انتها برسند.
احراز هویت (Auth) رد میشود. کلاینت خطای Error POSTing to endpoint (HTTP 401) یا مستقیماً 401 Unauthorized را گزارش میدهد. یا هدر (header) وجود ندارد، یا توکن اشتباه است، یا متغیر shell هنگام خواندن تنظیمات توسط کلاینت خالی بوده است — این یک تله رایج است، زیرا اگر متغیر تعریف نشده باشد، ${MCP_TOKEN} به هیچ تبدیل میشود و سپس nginx با Bearer بدون مقدار مواجه میشود. مقدار متغیر را echo کنید، هدر را مجدداً اضافه کنید و بررسی کنید که بایتهای دقیق با توکن در nginx if مطابقت داشته باشند.
سرویس تحت systemd اجرا نمیشود. journalctl -u mcp-ops عبارت ModuleNotFoundError: No module named 'mcp' را نشان میدهد — ExecStart به جای مفسر venv، به Python سیستم اشاره میکند. یا Address already in use — یک فرآیند دیگر پورت 8000 را اشغال کرده است؛ آن را با sudo ss -ltnp | grep 8000 پیدا کنید.
FAQ
سرور MCP دقیقاً چیست؟
این یک برنامه است که ابزارها و منابع را از طریق Model Context Protocol و با استفاده از JSON-RPC 2.0 در اختیار یک AI client قرار میدهد. مدل AI هرگز ابزار را مستقیماً اجرا نمیکند؛ بلکه از client درخواست میکند، client سرور MCP را فراخوانی میکند، و سپس سرور دستور را اجرا کرده و نتیجه را بازمیگرداند. به دلیل استاندارد بودن این پروتکل، یک سرور با هر client سازگاری کار میکند، خواه Claude Code باشد، خواه Claude Desktop یا Gemini CLI.
تفاوت بین transport از نوع stdio و HTTP چیست؟
یک سرور stdio توسط client به عنوان یک child process اجرا میشود و از طریق stdin/stdout ارتباط برقرار میکند؛ بنابراین این سرور فقط با یک client روی یک ماشین در دسترس است و به شبکه یا احراز هویت نیاز ندارد. یک سرور HTTP یک سرویس شبکهای با قابلیت فعالیت مداوم است که چندین client میتوانند همزمان به آن متصل شوند، به همین دلیل به TLS و احراز هویت نیاز دارد. برای ابزارهای محلی و تککاربره از stdio استفاده کنید؛ برای موارد اشتراکی یا دائمی از HTTP (در سرورهای فعلی از Streamable HTTP) استفاده کنید.
چگونه یک سرور MCP از راه دور را ایمن کنم؟
فرض کنید این سرور به فایلها، پایگاه داده یا shell شما دسترسی دارد؛ بنابراین هرگز آن را بدون احراز هویت در معرض دسترسی قرار ندهید. بهترین روش این است که سرور را فقط به localhost محدود کنید و از طریق یک SSH tunnel یا VPN خصوصی به آن متصل شوید؛ اگر سرور باید عمومی باشد، آن را پشت یک reverse proxy قرار دهید که bearer token یا جریان MCP OAuth را اعمال کند. توکن را با openssl rand -hex 32 تولید کنید و هرگز سرور را بدون یکی از این روشها به 0.0.0.0 متصل نکنید.
چگونه سروری را که اجرا نمیشود، عیبیابی کنم؟
ابتدا claude mcp list را بررسی کنید — خطای ✗ Failed to connect با spawn ... ENOENT به این معناست که دستور یا runtime وجود ندارد، پس مسیر (path) را اصلاح یا آن را نصب کنید. اگر سرور متصل شد اما با خطای JSON parse قطع شد، یعنی سرور در حال ارسال لاگ به stdout است و جریان JSON-RPC را خراب میکند؛ تمام لاگها را به stderr منتقل کنید. برای سایر موارد، دستور دقیق را در MCP Inspector اجرا کنید؛ این ابزار سرور را در حالت ایزوله اجرا میکند تا بتوانید خطای سرور را از خطای پیکربندی client تشخیص دهید.