SSD Nodes Learn
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-07-24

להרצת שרתי MCP על VPS עבור סוכני AI

מדריך להקמת שרתי MCP באמצעות stdio ו-remote HTTP על VPS. למידה על שימוש ב-systemd, הגדרת TLS, אימות ופתרון תקלות בזרם JSON-RPC עבור סוכני AI.

מה אתם בונים

שתי הגדרות MCP פעילות על VPS אחד. הראשונה היא שרת stdio — כלי מערכת קבצים או מסד נתונים ש-Claude Code מפעיל כתהליך בן (child process) ומתקשר איתו דרך pipe. השני הוא שרת remote HTTP הפועל כשירות רשת קבוע תחת systemd ו-nginx reverse proxy עם TLS, הנגיש לכל MCP client שמפנה אליו. ההתקנה של כל אחד מהם קצרה. רוב המדריך עוסק בשני הנושאים המורכבים: שמירה על זרם JSON-RPC נקי, ומניעת פרסום של endpoint של כלי ללא אימות (unauthenticated) באינטרנט הציבורי.

מהו MCP בפועל

Model Context Protocol הוא תקן המאפשר ללקוח AI — כגון Claude Code, Claude Desktop, ה- Gemini CLI on a VPS או סקריפט משלך — לקרוא לכלי עזר חיצוניים ולקרוא משאבים חיצוניים. המודל עצמו אינו מריץ שום דבר. הוא פונה ללקוח, הלקוח מתקשר בפורמט JSON-RPC 2.0 אל server של MCP, והשרת מריץ את הכלי ומחזיר את התוצאה. פרוטוקול אחד מאפשר לשרת שנכתב פעם אחת לעבוד עם כל לקוח התומך ב-MCP.

קיימים שני סוגי transport, ויתר המדריך מחולק לפיהם:

  • stdio. הלקוח מפעיל את השרת כתהליך בן (child process) ומחליף הודעות JSON-RPC המופרדות בשורות חדשות (newline-delimited) דרך ה-standard input וה-standard output שלו. ללא רשת, ללא פורט וללא אימות — גבול האמון הוא התהליך עצמו. כמעט כל כלי מקומי פועל בשיטה זו.
  • Streamable HTTP (והגרסה הישנה שלו, HTTP+SSE). השרת הוא שירות אינטרנט (web service) הפועל לאורך זמן. הלקוח מתחבר דרך HTTP והשרת יכול לשלוח תשובות בשיטת Server-Sent Events. כך ניתן לחלוק שרת אחד עם לקוחות רבים, או להריץ כלי שחייב לפעול על המחשב באופן קבוע.

בחר ב-stdio כאשר הכלי שייך למכונה אחת ולמשתמש אחד. בחר ב-HTTP כאשר מדובר בשירות משותף.

דרישות קדם ומכשולים נפוצים

הנח שקיימת שרת Ubuntu 24.04 KVM חדש עם הרשאות root או sudo. מעבר לכך:

  • סביבת ריצה (Runtime) עבור השרת. רוב שרתי הייחוס מבוססים על Node או Python. בגרסת Ubuntu 24.04 מותקן Node 18, אך חבילות MCP רבות דורשות Node 20 ומעלה. לכן, מומלץ להתקין גרסת LTS עדכנית מ-NodeSource או nvm במקום להסתמך על apt. 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 pin) מול הערות השחרור של השרת ולא כעובדה מוחלטת.

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 בשורש הפרויקט:

{
  "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. בתוך הסשן, פקודת ה-slash /mcp תציג את הכלים שהשרת מספק (read_file, write_file, list_directory), וה-agent יוכל לקרוא להם בנתיבים שאישרת. כלי database עובד באותו אופן — החלף את ה-package והעבר מחרוזת חיבור (connection string) כארגומנט האחרון — אך בדוק במאגר (repository) של השרת את שם ה-package העדכני, מכיוון ששרת ה-Postgres הרפרנציאלי עבר מספר שינויים.

זהו המטרה המרכזית של הרצת ה-agent על המכונה: סשן ה-Claude Code רץ בתוך ה-VPS בתוך tmux, ושרתי ה-stdio שלו רצים לצדו עם גישה ישירה לקובצי הפרויקט ולשירותים המקומיים, ללא השהיית רשת (network round-trip).

Step 2: build a remote HTTP server

שרת stdio נסגר יחד עם תהליך האב שלו. כאשר נדרש כלי שיישאר פעיל עבור כל לקוח — כלי ניהול משותף, שער לבסיס נתונים, או כלי שגם המחשב האישי וגם ה-CI קוראים לו — יש צורך בתעבורת HTTP ובשירות (service) ממשי. להלן שרת 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 בלבד — שום גורם חיצוני לא יכול להגיע אליו ישירות, וזה בדיוק המצב הרצוי לפני שמוגדר אימות (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

הנתיב המלא ל-Python בתוך ה-venv ב-ExecStart הוא חובה — הפנו אותו אל /usr/bin/python3 והתהליך יתחיל עם ModuleNotFoundError: No module named 'mcp', מכיוון שה-interpreter של המערכת לא מכיר את ה-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 בגוף ההודעה — הבקשה לא כללה session ולא payload JSON תקין — וזה בדיוק מה שצריך: זה מוכיח שה-port מגיב ומדבר בبرוטוקול. Connection refused או מענה ריק פירושם שהתהליך אינו מאזין בכתובת שחשבתם; קראו את journalctl -u mcp-ops -n 50.

Step 4: הוספת TLS ו-reverse proxy

השרת מאזין ב-localhost. כדי להגיע אליו מכל מקום, יש לבצע TLS termination ב-nginx ולהעביר את הבקשה (proxy) פנימה. התקן את nginx, השג תעודה באמצעות Certbot and Let's Encrypt on nginx, ולאחר מכן כתוב את ה-location block. החלק הקריטי הוא ביטול ה-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;
    }
}

בצע reload באמצעות sudo nginx -t && sudo systemctl reload nginx. אם אתה מריץ כבר מערך של containers, אותה עבודה תתבצע עבורך על ידי Traefik reverse proxy with automatic TLS — הוא מנפיק את התעודה ומנתב לפי hostname, ואתה רק צריך להוסיף labels ל-MCP container. בכל מקרה, ה-reverse proxy הוא כעת היחיד הפתוח בפורט ציבורי, והוא מפנה לשירות שעדיין לא אבטח. תקן זאת לפני שתירשם ה-URL במקום כלשהו.

Step 5: כלל האבטחה המרכזי בנושא זה

לעולם אל תחשוף MCP endpoint ללא אימות. שרת MCP אינו API לקריאה בלבד. הוא מעניק גישה לכלים (tools) — לקבצים שלך, למסד הנתונים שלך, ולעיתים גם ל-shell. /mcp פתוח באינטרנט הציבורי הוא גורם זר בעל הרשאות זהות עם סוכן ה-AI שלך: הוא יכול לרשום את הכלים שלך ואז להפעיל אותם. התייחס אליו בדיוק כמו ל-admin socket ללא אימות, כי זה בדיוק מה שהוא.

שלוש דרכי הגנה, לפי סדר עדיפות:

  1. אל תפרסם אותו. השאר את השרת ב-127.0.0.1 וגש אליו מהמחשב האישי שלך באמצעות SSH tunnel: ssh -L 8000:127.0.0.1:8000 matt@vps, ולאחר מכן הפנה את ה-client אל http://127.0.0.1:8000/mcp. שום דבר לא נחשף לאינטרנט.
  2. הצב אותו ברשת פרטית. בצע binding לכתובת ה-tunnel של self-hosted WireGuard VPN ואפשר רק ל-VPN peers לגשת אליו. האינטרנט הציבורי יראה פורט סגור.
  3. אם הוא חייב להיות ציבורי, דרוש token. הפתרון הנכון הוא MCP OAuth flow הנתמך באופן טבעי בתעבורת 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...
}

צור את ה-token באמצעות openssl rand -hex 32, ולעולם אל תבצע binding לשרת עצמו ל-0.0.0.0 ללא אחד הפתרונות הללו לפניו. ה-client ישלח את ה-token כ-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 שלך כדי שהסוד (secret) לעולם לא יישמר ב-.mcp.json כטקסט גלוי — Claude Code מרחיב את ${MCP_TOKEN} מהסביבה (environment) בזמן הקריאה.

Step 6: debug with the MCP Inspector

כאשר שרת פועל בצורה לא תקינה, אל תנסו לנחש את הסיבה מתוך ה-agent — השתמשו ב-Inspector, לקוח הבדיקה הרשמי מבוסס ה-web, כדי לשלוט בו ישירות. עבור שרת stdio, הריצו את אותה פקודה שה-agent מריץ:

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

התהליך מפעיל ממשק משתמש (UI) ב-http://localhost:6274 (גרסאות אחרונות מדפיסות URL עם מחרוזת שאילתה של MCP_PROXY_AUTH_TOKEN — השתמשו בקישור המדויק הזה, אחרת ה-UI יסרב להתחבר) ו-proxy בפורט 6277. לחצו על Connect, לאחר מכן על List Tools, ואז על Call Tool עם ארгуמנטים אמיתיים. אם הפעולה מצליחה ב-Inspector אך נכשלת ב-agent, הבאג נמצא בהגדרות ה-client ולא בשרת. עבור שרת HTTP מרחוק, בחרו בטרנספורט Streamable HTTP, הזינו את https://mcp.example.com/mcp, הוסיפו את ה-header של Authorization, והתחברו — זו הדרך המהירה ביותר לוודא שהאימות (auth) וה-proxy תקינים לפני בדיקת ה-agent.

Keeping servers updated

MCP מתפתח במהירות, לכן יש להחיל עדכונים על פי לוח זמנים. שרתי Node שהופעלו עם npx -y מורידים את הגרסה האחרונה בכל הפעלה; זה נוח אך אינו מאפשר שחזור (non-reproducible). יש לקבע את הגרסה המדויקת שנבדקה — קרא את הגרסה מתוך 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. יש לעקוב אחר גרסת המפרט (spec revision) שאליה מכוון ה-SDK שלך בעת שדרוג — מעבר בין SSE ל-Streamable-HTTP עלול לשנות את סוג ה-transport שהלקוחות (clients) נדרשים לבקש.

Failure modes, with the strings you will see

The agent shows the server failed. claude mcp list מדפיס את ✗ Failed to connect, וה-TUI מדווח על MCP server 'filesystem' failed to start. הרצת claude --debug תראה בדרך כלל את Error: spawn npx ENOENT — הפקודה אינה נמצאת ב-PATH של ה-agent. סביבת ההרצה חסרה או אינה נמצאת בנתיב שבו ה-agent מחפש: Node לא מותקן, npx חסר, או שנעשה שימוש בשם פקודה עבור virtualenv Python. תקן את הפקודה לנתיב מוחלט (absolute path) או התקן את סביבת ההרצה, ולאחר מכן התחבר מחדש.

A stdio server connects, then instantly drops. הלקוח (client) רושם שגיאת JSON parse — כמו Unexpected token 'S', "Server sta"... is not valid JSON או Failed to parse message. הסיבה היא תמיד זהה: השרת כתב שורת לוג ל-stdout. בשיטת stdio, ה-stdout הוא ערוץ ה-JSON-RPC, לכן כל טקסט זר משבש את הזרם (stream) ומנתק את ה-handshake. ב-Node, console.log נשלח ל-stdout — השתמש ב-console.error. ב-Python, פקודת print() פשוטה נשלחת ל-stdout — כתוב לוגים באמצעות logging המוגדר ל-sys.stderr, או העבר את file=sys.stderr. הכלל הוא מוחלט: ב-stdio, רק JSON-RPC נשלח ל-stdout, וכל תוכן המיועד לבני אדם נשלח ל-stderr.

A remote server times out or closes mid-handshake. הלקוח נכשל עם MCP error -32000: Connection closed, או שה-Inspector נתקע על Connect ולא מציג רשימת כלים. מאחורי nginx מדובר בבעיית buffering: ה-proxy מחזיק את זרם ה-SSE במקום לפלוט אותו (flush), ולכן הלקוח ממתין לתגובה שמעולם לא מגיעה. הוסף את proxy_buffering off; (ואת שאר הבלוק מ-Step 4) ל-location. ודא זאת באמצעות curl -N מול ה-URL הציבורי — אתה אמור לראות את נתוני האירועים מגיעים בהדרגה, ולא כולם בבת אחת בסוף.

Auth is rejected. הלקוח מדווח על Error POSTing to endpoint (HTTP 401) או פשוט על 401 Unauthorized. ה-header חסר, ה-token שגוי, או שמשתנה ה-shell היה ריק כאשר הלקוח קרא את הקונפיגורציה — מלכוד נפוץ, כיוון ש-${MCP_TOKEN} מתפרס (expands) לכלום אם המשתנה לא מוגדר, ואז nginx רואה את Bearer ללא ערך. בצע echo למשתנה, הוסף מחדש את ה-header, וודא שהבייטים המדויקים תואמים ל-token ב-nginx if.

The service will not start under systemd. journalctl -u mcp-ops מציג את ModuleNotFoundError: No module named 'mcp'ExecStart מצביע על ה-Python של המערכת במקום על ה-interpreter של ה-venv. או Address already in use — תהליך אחר תופס את פורט 8000; מצא אותו באמצעות sudo ss -ltnp | grep 8000.

FAQ

מהו בדיוק שרת MCP?

זוהי תוכנה החושפת כלים ומשאבים ללקוח AI באמצעות ה-Model Context Protocol, תוך שימוש ב-JSON-RPC 2.0. מודל ה-AI אינו מריץ את הכלי בעצמו — הוא מבקש זאת מהלקוח שלו, הלקוח קורא לשרת ה-MCP, והשרת מבצע את הפעולה ומחזיר תוצאה. מכיוון שהפרוטוקול הוא סטנדרטי, שרת אחד יכול לעבוד עם כל לקוח תואם, בין אם זה Claude Code, Claude Desktop, או Gemini CLI.

מה ההבדל בין stdio לבין HTTP transport?

שרת stdio מופעל על ידי הלקוח כתהליך בן (child process) ומתקשר דרך stdin/stdout. לכן, הוא פועל ונסגר יחד עם לקוח אחד במכונה אחת, ואינו זקוק לרשת או לאימות. שרת HTTP הוא שירות רשת הפועל לאורך זמן שניתן להגיע אליו מכלל לקוחות בו-זמנית, ולכן הוא מחייב TLS ואימות. השתמש ב-stdio עבור כלים מקומיים למשתמש יחיד; השתמש ב-HTTP (או Streamable HTTP בשרתים נוכחיים) עבור כל דבר משותף או קבוע.

איך מאבטחים שרת MCP מרחוק?

בהנחה שהשרת מעניק גישה לכלים עבור הקבצים, מסד הנתונים או ה-shell שלך, לעולם אין לחשוף אותו ללא אימות. הדרך הטובה ביותר היא להגביל אותו ל-localhost ולהגיע אליו דרך מנהרת SSH או VPN פרטי; אם הוא חייב להיות ציבורי, יש להציב אותו מאחורי reverse proxy המפעיל bearer token או את זרימת ה-MCP OAuth. צור את ה-token באמצעות openssl rand -hex 32 ולעולם אל תקשר את השרת ל-0.0.0.0 ללא אחד מהאמצעים הללו לפניו.

איך מבצעים debug לשרת שלא מצליח לעלות?

ראשית בדוק את claude mcp list — שגיאת ✗ Failed to connect עם spawn ... ENOENT מעידה על כך שהפקודה או סביבת ההרצה (runtime) חסרות, לכן יש לתקן את ה-path או להתקין אותן. אם השרת מתחבר ואז מתנתק עם שגיאת JSON parse, השרת כותב לוגים ל-stdout ומ corrupt את זרם ה-JSON-RPC; יש להעביר את כל ה-logging ל-stderr. עבור כל בעיה אחרת, הרץ את הפקודה המדויקת תחת ה-MCP Inspector, המריץ את השרת בבידוד כדי שתוכל להבחין בין באג בשרת לבאג בהגדרות הלקוח.