SSD Nodes Learn 🎉 VPS החל מ־$5.50/חודש
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-13

מה השתנה בשרתי MCP חסרי מצב (Stateless)?

עדכון MCP מיום 2026-07-28 הסיר את ה-initialize handshake ואת ה-sessions. גלו כיצד השינוי משפיע על הגדרות ה-reverse proxy, בדיקות תקינות, ניהול timeouts ומנגנוני אימות.

מהו שרת MCP חסר מצב (stateless)

שרת MCP חסר מצב אינו שומר מידע על לקוחות בין בקשות. כל בקשה נושאת את גרסת הפרוטוקול, את יכולות הלקוח ואת פרטי האימות שהשרת צריך כדי להשיב לה; לכן, כל תהליך בכל מכונה יכול לענות על כל בקשה. MCP (או Model Context Protocol, הפורמט שבו סוכנים משתמשים כדי לגשת לכלים) הפך זאת לכלל בגרסה 2026-07-28, אשר הסירה את ה-handshake של initialize ואת ה-HTTP session שהיה מבוסס עליו.

זוהי כל המהות התפעולית. שרת שאינו שומר דבר עבור לקוח יכול לשבת מאחורי load balancer רגיל ללא session affinity, לעבור אתחול במהלך deploy מבלי לנתק לקוחות, ולרוץ כארבעה תהליכים זהים במקום כאחד. שרת מבוסס session אינו מסוגל לבצע אף אחד מאלה ללא מנגנונים נוספים.

ה-Model Context Protocol הוא פרוטוקול חסר מצב: כל המידע הדרוש לעיבוד בקשה כלול בבקשה עצמה. השרת מעבד כל בקשה באופן עצמאי; אין להסיק מצב מבקשות קודמות, גם אם הן הגיעו באותו חיבור או stream.

"חסר מצב" לא אומר שהשרת שלך לא שומר כלום. מסד הנתונים, התור וה-cache שלך עדיין קיימים. המשמעות היא שה-פרוטוקול אינו נושא מצב על גבי החיבור, ולכן אסור לשרת להתייחס לחיבור, לתהליך או ל-socket פתוח כאל תחליף ל"לקוח זה, באמצע שיחה".

מה הוסר בגרסה 2026-07-28

2026-07-28 היא הגרסה הנוכחית של המפרט נכון לאוגוסט 2026. בהשוואה ל-2025-11-25, היא מסירה חמישה רכיבים שהיו קיימים כדי לתמוך ב-sessions.

  • הבקשה initialize וההתראה notifications/initialized. אין יותר handshake כלל (SEP-2575).
  • ה-header‏ Mcp-Session-Id, וסיום session באמצעות HTTP DELETE (SEP-2567).
  • ה-stream העצמאי של HTTP GET שדרכו שרתים דחפו התראות. הוא מוחלף על ידי subscriptions/listen, בקשת POST רגילה שהתגובה שלה היא stream ארוך-טווח.
  • יכולת חידוש (resumability) של SSE (server-sent events) stream. ה-header‏ Last-Event-ID ומזהי ה-per-event הוסרו, כך ש-stream שבור גורם לאובדן הבקשה שהייתה בתהליך, והלקוח חייב להוציא אותה מחדש כבקשה חדשה עם מזהה בקשה חדש.
  • ping, logging/setLevel ו-notifications/roots/list_changed. רמת ה-log היא כעת שדה ברמת הבקשה, io.modelcontextprotocol/logLevel בתוך _meta.

שיטה אחת נוספה, וכל שרת חייב לממש אותה. server/discover מחזירה את גרסאות הפרוטוקול הנתמכות, היכולות והזהות של השרת בקריאה אחת. זהו הדבר הקרוב ביותר ל-handshake שנותר, וקריאה אליו היא אופציונלית עבור לקוחות.

מדוע היה קשה להריץ את ה־session transport בסביבת production

בגרסאות 2025-11-25 ומטה, שרת היה יכול להנפיק מזהה session בעת האתחול ולהחזיר אותו בתוך ה־header‏ Mcp-Session-Id בתגובה ל־InitializeResult. לאחר מכן, הלקוח נדרש לשלוח את ה־header הזה בכל בקשה עוקבת. גרסת הפרוטוקול שסוכמה והיכולות של הלקוח נשמרו בזיכרון השרת, תחת מפתח של אותו מזהה. לכל אחת מהבחירות הללו יש עלות תפעולית.

  • אתחול של השרת מחק את טבלת ה־sessions. המפרט חייב את השרת להשיב לכל בקשה הנושאת מזהה session שאינו קיים ב־404 Not Found, וחייב את הלקוח להתחיל מחדש עם InitializeRequest חדש. כל פריסת גרסה (deploy) הפכה לאירוע של התחברות מחדש עבור כל לקוח מחובר.
  • רפליקה שנייה לא הכירה את ה־sessions של הרפליקה הראשונה. הרחבת המערכת (scaling out) דרשה ניתוב דביק (sticky routing) ב־load balancer, או שימוש ב־session store משותף שכל רפליקה קראה ממנו בכל בקשה.
  • טבלת ה־sessions צרכה זיכרון שגדל ככל שהיו יותר לקוחות לא פעילים. ה־DELETE היה אופציונלי, ולקוחות שנסגרו מבלי לשלוח אותו הותירו רשומות יתומות בזיכרון.
  • תוצאות של רשימות יכלו להשתנות בין חיבור לחיבור, ולכן ביצוע caching לפני השרת היה לא בטוח.

הסרת ה־sessions פותרת את כל ארבע הבעיות הללו בבת אחת. זהו השינוי שחשוב להבין לפני שמשנים הגדרות כלשהן.

מה כל בקשה נושאת כעת

כל בקשת POST לנקודת הקצה של MCP עומדת בפני עצמה. גרסת הפרוטוקול ויכולות הלקוח עוברות בגוף הבקשה תחת _meta, ושדות נבחרים משוכפלים לכותרות HTTP כדי שגורם מתווך יוכל לנתב לפיהם מבלי לנתח JSON.

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {"location": "Seattle, WA"},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

השדות io.modelcontextprotocol/protocolVersion ו-io.modelcontextprotocol/clientCapabilities נדרשים בכל בקשה. השדה clientInfo אינו חובה, אך מומלץ שהלקוחות ישלחו אותו. בקשה שחסר בה שדה נדרש נחשבת למעוותת (malformed), ולכן על השרת לדחות אותה עם שגיאת JSON-RPC מסוג -32602 וקוד HTTP 400 Bad Request.

הכותרת Mcp-Method נדרשת בכל בקשה. הכותרת Mcp-Name נדרשת ב-tools/call, resources/read ו-prompts/get. ערך הכותרת חייב להתאים לערך בגוף הבקשה; שרת המעבד את גוף הבקשה חייב לדחות אי-התאמה עם 400 Bad Request וקוד שגיאה -32020, HeaderMismatch. כלל זה קיים מכיוון שמאזן עומסים המנתב לפי כותרות ושרת המבצע פעולות לפי גוף הבקשה הם שני מקורות אמת שונים. אם אתם מנתבים או מבצעים הגבלת קצב (rate-limiting) לפי כותרות אלו, בדקו תחילה את MCP-Protocol-Version: גרסאות מוקדמות מעולם לא אימתו את הכותרת מול גוף הבקשה, ולכן בגרסאות אלו ערך הכותרת אינו מהימן.

אי-הסכמה על גרסה היא כעת שגיאה רגילה ברמת הבקשה, במקום כשל בביצוע לחיצת יד (handshake). שרת שאינו תומך בגרסה המבוקשת משיב ב-400 Bad Request עם שגיאה -32022, UnsupportedProtocolVersion, ומפרט את הגרסאות הנתמכות ב-data.supported. הלקוח בוחר גרסה אחת מהרשימה ומנסה שוב.

לאן נעלם ה-state: אסימונים (tokens), סמנים (cursors) ומנויים (subscriptions)

ה-state לא נעלם. הוא עבר למקומות שניתן לראות ולתעד בלוגים.

פרטי הזיהוי עוברים בכל בקשה. אין session שאליו ניתן להצמיד זהות, לכן ה-access token עובר בכל קריאת HTTP ומאומת בכל פעם מחדש. פרטים נוספים נמצאים בסעיף האימות להלן.

סמנים (cursors) חייבים לשאת את המיקום שלהם. דפדוף (pagination) ב-tools/list, ב-resources/list, ב-prompts/list וב-resources/templates/list משתמש במחרוזת סמן אטומה, ואסור ללקוחות לנתח או לשנות אותה. בשרת בעל תהליך יחיד היה נהוג לשמור את ה-offset בזיכרון, תחת מפתח של ה-session. ללא session, הסמן חייב להכיל מספיק מידע כדי שכל replica תוכל להמשיך את רשימת הפריטים; לכן יש לקודד את המיקום בתוך הסמן ולחתום עליו, או לשמור אותו באחסון שכל ה-replicas משתפות. סמן לא תקין צריך להחזיר -32602. חתמו על הסמן, כיוון שסמן אטום הוא עדיין קלט שמגיע מהלקוח, שהקוד שלכם מפענח ונותן בו אמון.

מנויים (subscriptions) שייכים לבקשה, לא לחיבור. לקוח שמעוניין בהתראות על שינויים שולח subscriptions/listen עם מסנן המציין את הסוגים המבוקשים: toolsListChanged, promptsListChanged, resourcesListChanged ו-resourceSubscriptions. השרת משיב ב-notifications/subscriptions/acknowledged ומחזיק את זרם התגובה פתוח. אם הזרם מתנתק, השרת לא שומר דבר, והלקוח שולח מחדש את subscriptions/listen כדי לקבל את המידע שוב.

state של יישום בין קריאות הופך ל-handle מפורש. כאשר שרת באמת חייב לזכור משהו בין קריאות, התשובה לפי המפרט היא מזהה שנוצר על ידי השרת ומועבר בחזרה כארגומנט רגיל של כלי (tool). הוא מופיע ב-schema של הכלי, ניתן לתעד אותו בלוגים, והוא לעולם לא משתמע מעצם החיבור. שרת עם נתונים אישיים אמיתיים מאחוריו, כמו שרת דואר MCP באירוח עצמי, משתמש בתבנית זו במקום ב-session: מזהה תיבת הדואר או הטיוטה הוא ארגומנט של הכלי, כך שכל replica יכולה לטפל בקריאה הבאה. כלים רבים כלל אינם זקוקים ל-handle: כלי חיפוש המבוסס על מופע SearXNG אישי מקבל שאילתה ומחזיר תוצאות, ללא צורך להמשיך דבר בקריאה הבאה וללא חשיבות לשאלה איזו replica ענתה.

פריסה: reverse proxy, פסקי זמן ובדיקות תקינות

נקודת הקצה של MCP היא נתיב יחיד המקבל בקשות POST. רוב התעבורה מורכבת מבקשה קצרה ותגובת JSON, שכל proxy מטפל בהן ללא קושי. היוצא מן הכלל הוא תגובות streaming, שבהן הגדרות ברירת המחדל של ה-proxy עלולות להפריע. זהו החלק שמשתנה במעבר מהדגמה על מחשב נייד ל-שרת MCP שרץ על גבי VPS.

location /mcp {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 1h;
    proxy_send_timeout 1h;
}

ההגדרה proxy_buffering off חשובה כיוון ש-nginx מבצע buffering לתגובות proxied כברירת מחדל, מה שגורם לעיכוב באירועי SSE עד למילוי ה-buffer או לסיום התגובה. המפרט דורש מהשרתים לשלוח X-Accel-Buffering: no בתגובות SSE, ו-nginx מכבד כותרת זו; לכן, שרת תקין ינחה את ה-proxy שלכם כראוי. הגדירו את ה-directive בכל זאת, שכן זהו החלק שנמצא בשליטתכם.

ערך ברירת המחדל של proxy_read_timeout הוא 60 שניות. זרם subscriptions/listen שנשאר שקט לזמן ארוך מזה ייסגר על ידי nginx, ולא על ידי השרת שלכם, כך שהלוגים יראו תהליך תקין בעוד שהלקוח יציג זרם שנותק. העלו את הערך במיקום ה-MCP בלבד, ולא עבור כל השרת. מומלץ ששרתים ישלחו שורת הערה ב-SSE (שורה המתחילה בנקודתיים) כ-keep-alive בתקופות של שקט, מה שמונע מגורמי ביניים לנתק את הזרם עקב פקיעת זמן.

Caddy דורש פחות הגדרות. הוא מבצע buffering חלקי כברירת מחדל ליעילות תעבורה ומבצע flush מיידי כאשר התגובה נושאת Content-Type: text/event-stream, כך ש-streaming עובד ללא directives נוספים.

mcp.example.com {
	reverse_proxy 127.0.0.1:8080 {
		health_uri /healthz
		health_interval 10s
	}
}

שימו לב לנקודה אליה מצביעה בדיקת התקינות. אל תכוונו בדיקה פעילה לנקודת הקצה של MCP עם GET, כיוון ששרת המממש רק גרסה זו עונה 405 Method Not Allowed ל-GET ו-DELETE, בעוד ששיטת הבדיקה כברירת מחדל של Caddy היא GET. ה-proxy יסמן אז backend תקין לחלוטין כלא זמין. הגישו נתיב פשוט כמו /healthz עבור ה-proxy, ובדקו את הפרוטוקול בנפרד באמצעות POST.

curl -sS https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

200 הנושא רשימת supportedVersions מעיד על כך שהתהליך פעיל ומדבר את הפרוטוקול. 404 עם שגיאת JSON-RPC מסוג -32601 מעיד על כך שהתהליך פעיל אך אינו משרת את server/discover, שכל שרת 2026-07-28 חייב לממש. 400 עם -32022 מעיד על כך שהבודק שלכם ביקש גרסה שגרסה זו אינה תומכת בה, וזה בדיוק מה שתרצו לזהות לאחר שדרוג תלויות. ל-nginx בקוד פתוח אין בדיקות תקינות פעילות, לכן השתמשו ב-max_fails ו-fail_timeout פסיביים ב-upstream ובצעו את בדיקת הפרוטוקול מתוך מערכת הניטור שלכם.

אתחול מדורג (rolling restart) כעת עולה לכם רק בבקשות שנמצאות בעיבוד. בצעו ניקוז (drain), תנו לבקשות POST פתוחות להסתיים, הפעילו את התהליך החדש, והלקוחות יבצעו מחדש את מה שנכשל. הדבר היחיד שעדיין יתנתק הוא כל זרם subscriptions/listen פתוח, כיוון שזרם זה הוא חיבור חי לתהליך ספציפי. חוסר מצב (statelessness) הסיר את ה-session affinity, אך לא את ה-connection affinity עבור זרם שפתוח כרגע, ושום כלל ניתוב לא פותר זאת. לקוח יכול להבחין בהבדל: זרם שמסתיים בתוצאת subscriptions/listen ריקה נסגר בצורה תקינה, וזרם שמסתיים ללא תוצאה כזו נקטע, מה שהלקוח עשוי לפרש כסיבה להתחבר מחדש.

Caching הופך לאפשרי לראשונה. תוצאות משיטות ה-list נושאות כעת ttlMs ו-cacheScope, ו-cacheScope: "public" מורה לגורמי ביניים משותפים שהם רשאים לשמור את התגובה ב-cache. זה בטוח רק כיוון שתוצאות ה-list אינן משתנות עוד לפי חיבור, וזו תוצאה ישירה של הסרת ה-sessions.

מדוע אימות משתנה כאשר אין session

עם session, היה מפתה לבצע אימות פעם אחת ב-initialize ואז להתייחס ל-session ID כהוכחה לכל מה שבא לאחר מכן. session ID שמשמש בדרך זו הוא bearer credential ללא קהל יעד (audience), ללא תאריך תפוגה וללא מנגנון ביטול, המונפק על ידי השרת שלך. הסרת ה-sessions מסירה את קיצור הדרך הזה, והחלופה מחמירה יותר.

שרת MCP מאובטח מתפקד כ-resource server מסוג OAuth 2.1. כל בקשת HTTP מהלקוח חייבת לשאת Authorization: Bearer <access token>, והשרת מאמת את ה-token בכל בקשה. האימות כולל את ה-audience: השרת חייב לוודא שה-token הונפק עבורו באופן ספציפי, בהתאם ל-RFC 8707 (מחווני משאבים עבור OAuth 2.0), ואסור לו לקבל או להעביר tokens המיועדים לכל דבר אחר. לקוחות מבקשים את ה-audience הנכון על ידי שליחת הפרמטר resource עם ה-URI הקנוני של השרת.

גילוי (discovery) מתבצע על בסיס אתגר. כאשר מגיעה בקשה ללא token שמיש, השרת משיב ב-401 Unauthorized.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

הלקוח קורא את resource_metadata, מושך את המסמך הזה (RFC 9728, מטא-נתונים של משאב מוגן ב-OAuth 2.0, ששרתי MCP חייבים לממש), מוצא את שרת ההרשאות ומריץ את התהליך. token תקף עם הרשאות מועטות מדי מקבל 403 Forbidden עם error="insufficient_scope" וה-scopes הנדרשים עבור אותה פעולה.

יש לכך שתי השלכות על אופן ההפעלה. אימות ה-token מתבצע כעת בכל בקשה ולא פעם אחת לכל session, לכן נסיעה הלוך-חזור ברשת לנקודת קצה של introspection בכל קריאה תבוא לידי ביטוי ב-latency שלך: העדף tokens שניתן לאמת מקומית מול חתימה, קהל יעד ותאריך תפוגה, או בצע caching לתוצאת האימות לחלון זמן קצר לפי ה-token. ומכיוון שאין session שמחזיק זהות, יש לחשב את ההרשאות מתוך ה-token בכל קריאה. זהו מודל אמין יותר ממודל ה-session, והוא משתלב עם הפרקטיקה הרחבה של הרחקת אישורים מתהליך ה-agent, נושא המכוסה ב-שמירה על סודות מחוץ ל-AI agent.

מה נכון לגבי גרסה זו ומה לא

כל האמור לעיל מתאר את גרסה 2026-07-28. הוא אינו מתאר את MCP לנצח, ואינו מתאר את השרת שפרסת בשנה שעברה.

לקוחות ושרתים בגרסה 2025-11-25 ומטה עדיין משתמשים במודל ה-handshake. המפרט מגדיר גרסאות אלו כ-legacy, ואת הגרסאות המבוססות על metadata לכל בקשה כ-modern. שרת התומך רק בגרסה זו, בעת פגישה עם לקוח ישן, צריך להשיב 405 Method Not Allowed ל-GET או ל-DELETE בנקודת הקצה של MCP, להתעלם מכל header מסוג Mcp-Session-Id מבלי ליצור או להדהד אחד כזה, ולהתעלם מ-Last-Event-ID כיוון שזרמים (streams) אינם ניתנים לחידוש. שרת התומך בשתי התקופות יכול לשרת את שתיהן בנקודת קצה אחת: בקשה הנושאת _meta מודרני תטופל ללא מצב (statelessly), ובקשת initialize תבחר בסמנטיקת ה-session הישנה.

לכן, בדוק את מחרוזת הגרסה לפני שתסתמך על מידע זה. אם ה-SDK שלך עדיין שולח initialize, ה-sessions עדיין רלוונטיים לפריסה שלך והבעיות הקשורות ל-session שתוארו לעיל עדיין דורשות את ניהולך. אותו הדבר תקף בצד הלקוח: תהליך סוכן (agent) על המכונה שלך, כגון ההגדרה ב-הרצת סוכן קידוד על VPS, הוא חסר מצב (stateless) במובן זה רק אם הספרייה שבה הוא משתמש מדברת בגרסה מודרנית. קרא את הגרסה שה-runtime שלך מנהל משא ומתן עליה, לאחר מכן קרא את גרסת המפרט התואמת, והתייחס לדף זה כתיאור של גרסה אחת בשם, ולא של הפרוטוקול בכללותו.

FAQ

האם שרת MCP ללא מצב (stateless) אומר שאני לא יכול לשמור שום דבר?

לא. המונח "ללא מצב" מתאר את הפרוטוקול, לא את היישום שלך. מסדי נתונים, תורים ומטמונים (caches) עובדים בדיוק כפי שעבדו קודם לכן. מה שמשתנה הוא שמצב המשתרע על פני כמה קריאות חייב להיות מיוחס למזהה מפורש שהלקוח מעביר בכל בקשה, כגון מזהה (handle) שהונפק על ידי השרת בתוך ארגומנט של כלי. מה שאסור לעשות הוא להסיק הקשר מתוך החיבור: המפרט קובע ששרת אינו רשאי להסתמך על בקשות קודמות באותו חיבור כדי לקבוע יכולות, גרסת פרוטוקול או זהות לקוח, כיוון שכל בקשה מספקת נתונים אלו בתוך _meta.

האם אני עדיין זקוק ל-sticky sessions ב-load balancer שלי?

לא עבור בקשות רגילות. תחת גרסה 2026-07-28, כל POST נושא עמו את גרסת הפרוטוקול, היכולות והאישורים שלו, כך שכל עותק (replica) יכול לענות על כל בקשה ושיטת round-robin היא תקינה. הדבר היחיד שנותר בעל אורך חיים ארוך הוא זרם התגובות subscriptions/listen, המהווה חיבור פתוח יחיד לתהליך יחיד. הוא מסתיים כאשר התהליך מסתיים, והלקוח שולח מחדש subscriptions/listen כדי לכונן אותו מחדש. זהו אורך חיים של חיבור ולא זיקת סשן (session affinity), ושום חוק ניתוב לא מונע זאת.

מה קרה ל-Mcp-Session-Id ולזרם ה-HTTP GET?

שניהם הוסרו בגרסה 2026-07-28, תחת SEP-2567 ו-SEP-2575. שרת המממש רק גרסה זו צריך להשיב 405 Method Not Allowed ל-GET ו-DELETE בנקודת הקצה של MCP, ועליו להתעלם מ-header מסוג Mcp-Session-Id במקום להדהד אותו בחזרה. התראות על שינויים ביוזמת השרת עוברות כעת בזרם התגובות של בקשת subscriptions/listen במקום בזרם GET עצמאי. שרתים שחייבים להמשיך לשרת לקוחות ישנים מממשים את התנהגות הגרסה הקודמת לצד גרסה זו.

כיצד עלי לבצע בדיקת תקינות (health check) לשרת MCP ללא לחיצת יד (handshake)?

השתמש בשתי רמות. כוון את הבדיקה הפעילה של ה-proxy לנתיב HTTP פשוט שהיישום שלך מגיש, כיוון ש-GET לנקודת הקצה של MCP מחזיר בצדק 405 ויסמן backend תקין כלא זמין. לאחר מכן, בדוק את הפרוטוקול עצמו על ידי שליחת POST ל-server/discover, שכל שרת 2026-07-28 חייב לממש, וודא שהתשובה היא HTTP 200 ושהיא מפרטת גרסת פרוטוקול שהלקוחות שלך משתמשים בה. תשובת 404 עם שגיאת JSON-RPC מסוג -32601 משמעותה שהתהליך רץ אך אינו מגיש את המתודה הזו, ותשובת 400 עם -32022 משמעותה שהגרסה שביקשת אינה נתמכת ב-build הזה.