خوادم MCP على VPS لوكلاء برمجة بالذكاء الاصطناعي
شغّل خوادم MCP على VPS الخاص بك ليحصل وكلاء الذكاء الاصطناعي على أدوات حقيقية: نقل stdio ونقل HTTP البعيد، وsystemd، وTLS، والمصادقة، وكل نمط فشل.
ما الذي تبنيه
إعدادان يعملان لـ MCP على VPS واحد. الأول خادم stdio — أداة نظام ملفات أو قاعدة بيانات يشغّلها Claude Code كعملية فرعية ويتحادث معها عبر أنبوب (pipe). والثاني خادم HTTP بعيد يعمل كخدمة شبكية دائمة خلف systemd ووكيل nginx العكسي (reverse proxy) المزوَّد بـ TLS، يستطيع الوصول إليه أي عميل MCP توجّهه نحوه. تثبيت أيٍّ منهما بسيط. معظم هذا الدليل يدور حول الأمرين اللذين يسبّبان المتاعب الحقيقية: إبقاء تدفق JSON-RPC نظيفًا، وعدم وضع نقطة نهاية (endpoint) لأداة غير مصادَق عليها على الإنترنت العام أبدًا.
ما هو MCP فعلًا
Model Context Protocol (بروتوكول سياق النموذج، MCP) هو طريقة معيارية لعميل ذكاء اصطناعي — Claude Code، أو Claude Desktop، أو Gemini CLI على VPS، أو سكربت من تأليفك — لاستدعاء أدوات خارجية وقراءة موارد خارجية. النموذج نفسه لا ينفّذ شيئًا. إنه يطلب من العميل، والعميل يتحدث بـ JSON-RPC 2.0 إلى خادم MCP، والخادم ينفّذ الأداة ويعيد النتيجة. بروتوكول واحد، فالخادم الذي تكتبه مرة واحدة يعمل مع كل عميل يتحدث MCP.
هناك طريقتا نقل (transport)، وباقي هذا الدليل بأكمله ينقسم بحسبهما:
- stdio. يُنشئ العميل الخادم كعملية فرعية، ويتبادل معه رسائل JSON-RPC مفصولة بأسطر جديدة عبر مدخله القياسي (stdin) ومخرجه القياسي (stdout). لا شبكة، ولا منفذ، ولا مصادقة — حدود الثقة هي العملية نفسها. بهذه الطريقة تُقدَّم تقريبًا كل أداة محلية.
- Streamable HTTP (وسليفه الأقدم، HTTP+SSE). الخادم خدمة ويب دائمة التشغيل. يتصل العميل عبر HTTP، ويستطيع الخادم بث الاستجابات في شكل Server-Sent Events. بهذه الطريقة تشارك خادمًا واحدًا بين عملاء كثيرين، أو تشغّل أداة يجب أن تبقى على الجهاز بشكل دائم.
اختر stdio حين تنتمي الأداة إلى جهاز واحد ومستخدم واحد. واختر HTTP حين تكون خدمة مشتركة.
المتطلبات المسبقة والمزالق الحقيقية
افترض خادم VPS جديدًا يعمل بنظام Ubuntu 24.04 من نوع KVM مع صلاحيات root أو sudo. وفيما عدا ذلك:
- بيئة تشغيل (runtime) يُكتب بها الخادم. معظم الخوادم المرجعية مكتوبة بـ Node أو Python. يأتي Ubuntu 24.04 بالإصدار Node 18، وعدة حزم MCP الحالية تتطلب Node 20 أو أحدث، لذا ثبّت إصدار LTS حاليًا من NodeSource أو nvm بدلًا من الاتكال على
apt. أما Python 3.12 فموجود مسبقًا. - نطاق (domain) وسجل DNS من نوع A، لكن فقط لخادم HTTP البعيد — فـ TLS يحتاج إلى اسم يُحلَّل إلى عنوان هذا الـ VPS. أما مثال stdio فلا يحتاج إلى أي DNS.
- 512 ميغابايت من RAM تكفي بسهولة. خوادم MCP عمليات JSON-RPC خفيفة؛ وتكلفة الذاكرة هي ما تلمسه أداتك (مشغّل قاعدة بيانات، ذاكرة تخزين مؤقت للملفات)، لا تكلفة البروتوكول نفسه.
- المواصفة (spec) حديثة ومتغيّرة. استبدلت مراجعة 2025-03-26 بروتوكول HTTP+SSE بـ Streamable HTTP، ووسمت SSE بأنه مهجور (deprecated). ومع ذلك ما زال SSE يعمل وما زالت خوادم كثيرة تتحدث به، فعامل أي تثبيت لطريقة نقل بعينها على أنه شيء يستوجب إعادة التحقق منه في ملاحظات إصدار الخادم، لا حقيقة ثابتة.
الخطوة 1: وصل خادم stdio بـ Claude Code
ابدأ بخادم نظام الملفات (filesystem) — فهو رسمي، ويُصان بفعالية، ولا يحتاج إلى شيء سوى Node. الأمر الوحيد أدناه يسجّله لدى Claude Code ويحصره في نطاق المشروع الحالي بحيث ينتهي به الحال في ملف يمكن رفعه (commit) إلى Git:
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 عند جذر المشروع:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}لا شيء يعمل بعد. وفي المرة التالية التي تشغّل فيها Claude Code داخل هذا المجلد، يقرأ الوكيل (agent) الملف .mcp.json، وينشئ npx -y @modelcontextprotocol/server-filesystem ... كعملية فرعية، وينفّذ المصافحة (handshake) الخاصة بـ MCP عبر stdin/stdout الخاصين بتلك العملية. تأكد من أن الأمر نجح:
claude mcp listالخادم السليم يطبع أمره وعلامة صح خضراء — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. وداخل الجلسة، يسرد أمر الشرطة المائلة /mcp الأدوات التي يعرضها الخادم (read_file، وwrite_file، وlist_directory)، ويستطيع الوكيل الآن استدعاءها على المسارات التي سمحت بها. أداة قاعدة البيانات لها الشكل نفسه — بدّل الحزمة ومرّر سلسلة اتصال كوسيطها الأخير — لكن تحقق من مستودع الخادم نفسه لمعرفة اسم الحزمة الحالي، لأن الجهة القائمة على صيانة خادم Postgres المرجعي تغيّرت أكثر من مرة.
هذه هي الفكرة الأساسية وراء تشغيل الوكيل على الجهاز: جلسة Claude Code تعيش على الـ VPS داخل tmux، وخوادم stdio التابعة لها تعمل مباشرة بجانبها بوصول مباشر إلى ملفات المشروع والخدمات المحلية، من دون أي رحلة ذهاب وإياب عبر الشبكة.
الخطوة 2: ابنِ خادم HTTP بعيدًا
ينتهي خادم stdio بانتهاء العملية الأم (parent process) التي أطلقته. وحين تريد أداة تبقى متاحة لكل عميل — أداة تشغيل مشتركة، أو بوابة قاعدة بيانات، أو شيء يستدعيه حاسوبك المحمول ونظام CI لديك معًا — تحتاج إلى نقل HTTP وخدمة حقيقية. إليك خادم Python بسيط يستخدم الـ SDK الرسمي، ويعرض أداة واحدة:
# /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". الخادم يرتبط بـ localhost فقط — لا شيء خارج الجهاز يستطيع الوصول إليه مباشرة، وهذا بالضبط ما تريده قبل وجود المصادقة. ثبّته في بيئة افتراضية (virtualenv) خاصة به حتى يملك systemd مسارًا ثابتًا للمفسِّر (interpreter):
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]"الخطوة 3: أبقِه يعمل بواسطة systemd
أداة معطّلة حين يحتاجها الوكيل أسوأ من عدم وجود أداة أصلًا. اكتب الملف /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المسار المطلق إلى مفسِّر 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 فيعيد HTTP/1.1 400 Bad Request مع خطأ JSON-RPC في المتن — لأن الطلب لم يحمل جلسة ولا حمولة JSON صالحة — وهذا بالضبط ما تريده: فهو يثبت أن المنفذ يستجيب ويتحدث بالبروتوكول. أما Connection refused أو ردّ فارغ فيعني أن العملية غير مرتبطة بالمكان الذي تظنه؛ اقرأ journalctl -u mcp-ops -n 50.
الخطوة 4: ضع TLS ووكيلًا عكسيًا في الأمام
الخادم يستمع على localhost. وللوصول إليه من أي مكان، تُنهي TLS عند nginx وتُمرّر الطلب إلى الداخل. ثبّت nginx، واحصل على شهادة بواسطة Certbot وLet's Encrypt على nginx، ثم اكتب كتلة location. الجزء الحاسم هو تعطيل التخزين المؤقت (buffering)، لأن سلوك nginx الافتراضي يحتفظ بالاستجابة حتى تكتمل، وهذا يجمّد تدفق SSE إلى الأبد:
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. وإذا كنت تدير أسطولًا من الحاويات بالفعل، فإن وكيل Traefik العكسي بشهادات TLS تلقائية ينجز عنك المهمة نفسها — فهو يصدر الشهادة ويوجّه بحسب اسم المضيف، ولا يبقى عليك سوى إضافة تسميات (labels) إلى حاوية MCP. في الحالتين، الوكيل العكسي هو الآن الشيء الوحيد على منفذ عام، وهو يشير إلى خدمة لم تؤمّنها بعد. أصلح ذلك قبل أن تسجّل الرابط في أي مكان.
الخطوة 5: قاعدة الأمان التي تهيمن على هذا الموضوع
لا تكشف أبدًا نقطة نهاية MCP غير مصادَق عليها. خادم MCP ليس واجهة API للقراءة فقط. إنه يمنح وصولًا إلى الأدوات — إلى ملفاتك، وقاعدة بياناتك، وأحيانًا إلى shell. مسار /mcp مفتوح على الإنترنت العام هو غريب يملك المدى نفسه الذي يملكه وكيلك الذكي: يسرد أدواتك، ثم يستدعيها. عامله تمامًا كما تعامل مقبس إدارة (admin socket) غير مصادَق عليه، لأن هذا بالضبط ما هو عليه.
ثلاثة خطوط دفاع، مرتبة بحسب الأفضلية:
- لا تنشره. أبقِ الخادم على
127.0.0.1وصله من حاسوبك المحمول عبر نفق SSH:ssh -L 8000:127.0.0.1:8000 matt@vps، ثم وجّه العميل إلىhttp://127.0.0.1:8000/mcp. لا شيء يُكشَف أبدًا بهذه الطريقة. - ضعه على شبكة خاصة. اربطه بعنوان النفق الخاص بشبكة VPN من نوع WireGuard مستضافة ذاتيًا، ولا تسمح بالوصول إليه إلا لأقران VPN. الإنترنت العام لا يرى سوى منفذ مغلق.
- وإن كان لا بد أن يكون عامًا، فاشترط رمزًا (token). الحل الصحيح هو تدفق OAuth الخاص بـ MCP الذي يدعمه نقل HTTP أصلًا. أما الحد الأدنى العملي فهو رمز حامل (bearer token) مشترك يُتحقَّق منه عند الوكيل العكسي — رخيص، ويوقف الهجمات العابرة كليًا:
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 أبدًا من دون أحد هذه الحواجز أمامه. يرسل العميل عندها الرمز كترويسة (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 الخاص بك حتى لا ينتهي السر في .mcp.json نصًا صريحًا أبدًا — إذ يوسّع Claude Code القيمة ${MCP_TOKEN} من البيئة وقت القراءة.
الخطوة 6: صحّح الأخطاء باستخدام MCP Inspector
حين يسيء خادم التصرف، لا تخمّن من داخل الوكيل — بل شغّله مباشرة بواسطة Inspector، عميل الاختبار الرسمي المستند إلى الويب. لخادم stdio، مرّر إليه الأمر نفسه الذي يشغّله الوكيل:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpيبدأ واجهة على http://localhost:6274 (الإصدارات الحديثة تطبع رابطًا يحمل سلسلة استعلام MCP_PROXY_AUTH_TOKEN — استخدم ذلك الرابط بعينه وإلا رفضتك الواجهة) ووكيلًا على المنفذ 6277. انقر Connect، ثم List Tools، ثم Call Tool بوسائط حقيقية. وإذا نجح الأمر في Inspector لكنه فشل في الوكيل، فالخلل في إعدادات عميلك، لا في الخادم. أما لخادم HTTP البعيد، فاختر نقل Streamable HTTP، وأدخل https://mcp.example.com/mcp، وأضف ترويسة Authorization، واتصل — هذه أسرع طريقة لإثبات أن المصادقة والوكيل العكسي صحيحان قبل إشراك أي وكيل ذكاء اصطناعي.
الحفاظ على تحديث الخوادم
MCP يتطور بسرعة، فطبّق التحديثات وفق جدول منتظم. خوادم Node المُشغَّلة بـ npx -y تجلب أحدث إصدار مع كل تشغيل، وهذا مريح لكنه غير قابل لإعادة الإنتاج؛ ثبّت الإصدار الدقيق الذي اختبرته — اقرأه من npm view @modelcontextprotocol/server-filesystem version وألحقه باسم الحزمة في .mcp.json (@modelcontextprotocol/server-filesystem@<version>) — بمجرد أن يصبح خادم ما مهمًّا، وحدّثه عمدًا حين تقرر ذلك. أما خوادم Python تحت systemd فتُحدَّث بالأمر sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" يتبعه sudo systemctl restart mcp-ops. وراقب مراجعة المواصفة التي يستهدفها الـ SDK لديك عند الترقية — فالقفز عبر الحد الفاصل بين SSE وStreamable HTTP قد يغيّر طريقة النقل التي يجب أن تطلبها عملاؤك.
أنماط الفشل، مع النصوص التي ستراها
الوكيل يُظهر أن الخادم فشل. يطبع claude mcp list الرسالة ✗ Failed to connect، وتبلغ الواجهة النصية (TUI) بـ MCP server 'filesystem' failed to start. شغّل claude --debug وسترى غالبًا Error: spawn npx ENOENT — أي أن الأمر ليس موجودًا في PATH الخاص بالوكيل. بيئة التشغيل إما مفقودة أو ليست حيث يبحث عنها الوكيل: Node غير مثبَّت، أو npx غائب، أو مفسِّر Python الخاص بـ virtualenv مُشار إليه باسمه المجرد. أصلح الأمر إلى مسار مطلق أو ثبّت بيئة التشغيل، ثم أعد الاتصال.
خادم stdio يتصل، ثم ينقطع فورًا. يسجّل العميل خطأ تحليل JSON — شيئًا يشبه Unexpected token 'S', "Server sta"... is not valid JSON أو Failed to parse message. والسبب واحد دائمًا: كتب الخادم سطر سجلّ إلى stdout. وفي stdio، stdout هو قناة JSON-RPC نفسها، فأي نص شارد يُفسد التدفق وتموت المصافحة. في Node، يذهب console.log إلى stdout — استخدم console.error بدلًا منه. وفي Python، يذهب print() المجرد إلى stdout — اكتب السجلّات بواسطة logging مضبوطة على sys.stderr، أو مرّر file=sys.stderr. القاعدة مطلقة: في stdio، لا شيء على stdout سوى JSON-RPC، وكل ما هو موجَّه للبشر يذهب إلى stderr.
خادم بعيد تنتهي مهلته أو يُغلق في منتصف المصافحة. يفشل العميل برسالة MCP error -32000: Connection closed، أو يتجمد Inspector عند Connect ولا يسرد أي أدوات أبدًا. خلف nginx يكون السبب هو التخزين المؤقت (buffering): يحتفظ الوكيل العكسي بتدفق SSE بدلًا من تفريغه، فينتظر العميل استجابة لن تصل أبدًا. أضف proxy_buffering off; (وبقية الكتلة من الخطوة 4) إلى location. تأكد بالأمر curl -N على الرابط العام — يجب أن ترى بيانات الأحداث تصل تدريجيًا، لا دفعة واحدة في النهاية.
المصادقة مرفوضة. يبلغ العميل بـ Error POSTing to endpoint (HTTP 401) أو ببساطة 401 Unauthorized. إما أن الترويسة مفقودة، أو الرمز خاطئ، أو أن متغيّر الـ shell كان فارغًا حين قرأ العميل الإعداد — وهذا فخ شائع، لأن ${MCP_TOKEN} يتوسّع إلى لا شيء إن كان المتغيّر غير مضبوط، فيرى nginx عندئذٍ القيمة Bearer من دون أي قيمة بعدها. اطبع المتغيّر، وأعد إضافة الترويسة، وتحقق من أن البايتات مطابقة تمامًا للرمز الموجود في شرط if الخاص بـ nginx.
الخدمة ترفض البدء تحت systemd. يُظهر journalctl -u mcp-ops الرسالة ModuleNotFoundError: No module named 'mcp' — أي أن ExecStart يشير إلى مفسِّر Python الخاص بالنظام بدلًا من مفسِّر الـ venv. أو Address already in use — عملية أخرى تشغل المنفذ 8000؛ اعثر عليها بالأمر sudo ss -ltnp | grep 8000.
FAQ
ما هو خادم MCP بالضبط؟
هو برنامج يعرض أدوات وموارد لعميل ذكاء اصطناعي عبر Model Context Protocol، مستخدمًا JSON-RPC 2.0. نموذج الذكاء الاصطناعي لا ينفّذ الأداة بنفسه أبدًا — إنه يطلب من عميله، والعميل يستدعي خادم MCP، والخادم ينفّذ الأمر ويعيد نتيجة. ولأن البروتوكول معياري، فإن خادمًا واحدًا يعمل مع أي عميل متوافق، سواء أكان Claude Code أم Claude Desktop أم Gemini CLI.
ما الفرق بين نقل stdio ونقل HTTP؟
خادم stdio يشغّله العميل كعملية فرعية ويتواصل معه عبر stdin/stdout، فيحيا ويموت مع عميل واحد على جهاز واحد ولا يحتاج إلى شبكة ولا مصادقة. أما خادم HTTP فهو خدمة شبكية دائمة يستطيع الوصول إليها عملاء كثيرون في وقت واحد، ولهذا يتطلب TLS ومصادقة. استخدم stdio للأدوات المحلية أحادية المستخدم؛ واستخدم HTTP (أي Streamable HTTP في الخوادم الحالية) لكل ما هو مشترك أو دائم.
كيف أؤمّن خادم MCP بعيدًا؟
افترض أنه يمنح وصولًا على مستوى الأدوات إلى ملفاتك أو قاعدة بياناتك أو shell، ولا تكشفه أبدًا من دون مصادقة. الأفضل أن تُبقيه مرتبطًا بـ localhost وتصله عبر نفق SSH أو VPN خاص؛ وإن كان لا بد أن يكون عامًا، فضعه خلف وكيل عكسي يفرض رمز حامل (bearer token) أو تدفق OAuth الخاص بـ MCP. ولّد الرمز بالأمر openssl rand -hex 32 ولا تربط الخادم بـ 0.0.0.0 أبدًا من دون أحد هذين أمامه.
كيف أصحّح خادمًا يرفض البدء؟
تحقق أولًا من claude mcp list — فرسالة ✗ Failed to connect مصحوبة بـ spawn ... ENOENT تعني أن الأمر أو بيئة التشغيل مفقودة، فأصلح المسار أو ثبّتها. وإذا اتصل الخادم ثم انقطع بخطأ تحليل JSON، فهو يكتب سجلّاته إلى stdout ويُفسد تدفق JSON-RPC؛ فانقل كل السجلّات إلى stderr. أما في أي حالة أخرى، فشغّل الأمر بعينه تحت MCP Inspector، الذي يشغّل الخادم بمعزل عن غيره فتستطيع تمييز خلل في الخادم من خلل في إعداد العميل.