JSON-RPC 2.0 — שכבת הבסיס של MCP
למה JSON-RPC 2.0
כל הודעה ב-MCP היא הודעת JSON-RPC 2.0. זה פרוטוקול RPC קטן מאוד: אובייקט JSON עם שם method, פרמטרים ומזהה, ותשובה שמותאמת לבקשה לפי המזהה. אין בו הגדרה של transport, של אימות או של סכמות, רק מבנה ההודעה והכללים להתאמה בין בקשה לתשובה.
שלוש תכונות הפכו אותו לבחירה טבעית. הוא דו-כיווני: כל צד יכול לשלוח הודעות, ואין הנחה שרק הלקוח שואל. הוא לא תלוי ב-transport: אותה הודעה עוברת כשורה ב-stdin/stdout או כגוף של HTTP POST. והוא כבר מוכר מ-LSP, עם ספריות בכל שפה.
JSON-RPC מגדיר ארבעה סוגי הודעות: request, result response, error response ו-notification. בסעיפים הבאים נראה כל אחד מהם כפי שהוא מופיע ב-MCP, כולל ההגבלות ש-MCP מוסיף מעל JSON-RPC הבסיסי.

Request — בקשה שמצפה לתשובה
בקשה כוללת ארבעה שדות. jsonrpc הוא תמיד המחרוזת 2.0. method הוא שם הפעולה, למשל tools/list או tools/call. params הוא אובייקט הפרמטרים (אופציונלי ב-JSON-RPC). ו-id מזהה את הבקשה.
MCP מחמיר את הכללים של id. הוא חייב להיות מחרוזת או מספר שלם, ובניגוד ל-JSON-RPC הבסיסי הוא לא יכול להיות null. בנוסף, id לא יכול להיות זהה ל-id של בקשה אחרת שאותו שולח שלח ועוד לא קיבל עליה תשובה.
ה-id הוא מה שמחבר תשובה לבקשה. ה-client יכול לשלוח כמה בקשות בלי לחכות, והתשובות עלולות לחזור בסדר אחר, ולכן הוא מחזיק טבלה של בקשות פתוחות לפי id. מספר עולה הוא הבחירה הנפוצה; UUID כמחרוזת מתאים כשכמה רכיבים שולחים בקשות באותו ערוץ.
ב-MCP כל בקשה של client חייבת לכלול בתוך params את השדה _meta עם גרסת הפרוטוקול והיכולות של ה-client. הדוגמה למטה היא בקשת tools/list מלאה. בדוגמאות בפרקים הבאים נשמיט לפעמים את ה-_meta כדי לקצר, כמו שה-spec עצמו עושה, אבל בהודעה אמיתית הוא חובה.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {}
},
"io.modelcontextprotocol/clientInfo": {
"name": "my-host",
"version": "1.4.0"
}
}
}
}Result response ו-resultType
תשובה מוצלחת מכילה את אותו id ואת השדה result. ה-result הוא אובייקט שהמבנה שלו תלוי ב-method: ל-tools/list יש שדה tools, ל-tools/call יש content, וכן הלאה.
חדש ב-2026-07-28: כל result חייב לכלול שדה resultType. הערך complete אומר שהבקשה הסתיימה וזו התשובה הסופית. הערך input_required אומר שהשרת צריך מידע נוסף כדי להשלים את הבקשה. זו תשובת ביניים של דפוס MRTR, ונפרט עליה בפרק 7. extensions יכולים להוסיף ערכים נוספים, וערך שה-client לא מכיר חייב להיחשב לא תקין.
תאימות לאחור: שרתים בגרסאות קודמות לא שולחים resultType בכלל. לכן ה-spec מחייב את ה-client להתייחס ל-result בלי resultType כאל complete.
גם ל-result יכול להיות _meta. השרת אמור (SHOULD) לשים שם את io.modelcontextprotocol/serverInfo, שם וגרסה, כדי להזדהות בכל תשובה בלי להסתמך על מצב של חיבור. השדות ttlMs ו-cacheScope בדוגמה שייכים לתשובות של רשימות: הם אומרים ל-client כמה זמן מותר לשמור את הרשימה ב-cache, ונרחיב עליהם בפרק 5.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [
{
"name": "search_issues",
"description": "חיפוש issues לפי טקסט חופשי",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
],
"ttlMs": 300000,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "issues-server",
"version": "0.9.2"
}
}
}
}Error response — כשהבקשה נכשלת
כשבקשה נכשלת ברמת הפרוטוקול, השרת מחזיר error במקום result, ואף פעם לא את שניהם. ל-error יש code (מספר שלם, חובה), message (תיאור קצר, רצוי משפט אחד) ו-data אופציונלי מכל סוג, לפרטים נוספים כמו שגיאות מקוננות.
ה-id של תשובת שגיאה זהה ל-id של הבקשה. החריג היחיד הוא בקשה פגומה כל כך שאי אפשר לקרוא ממנה id, למשל JSON שבור. במקרה כזה מותר להשמיט את ה-id.
הדוגמה מראה את התשובה ל-tools/call עם שם של כלי שלא קיים. ב-MCP כלי לא מוכר מדווח בקוד -32602 (Invalid params): הבקשה עצמה תקינה מבחינת JSON-RPC, אבל הפרמטר name לא מתאים לשום כלי.
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32602,
"message": "Unknown tool: delete_everything"
}
}Notification — הודעה חד-כיוונית
notification היא הודעה בלי id. בלי id אין גם תשובה: אסור לצד המקבל להשיב (MUST NOT), אפילו לא בשגיאה. זה מתאים לאירועים: משהו השתנה, הנה עדכון התקדמות, בטל את הבקשה ההיא.
השמות של notifications ב-MCP מתחילים ב-notifications/. בדוגמה, notifications/cancelled: ה-client מודיע שהוא כבר לא צריך את התשובה לבקשה 42. השרת אמור להפסיק לעבד אותה ולא לשלוח תשובה, ואם העיבוד כבר הסתיים מותר לו להתעלם מההודעה.
ב-stdio זו הדרך היחידה לבטל בקשה. ב-Streamable HTTP, סגירת ה-stream של התשובה היא עצמה אות הביטול, ואין צורך בהודעה. נחזור לזה בפרק 4, יחד עם notifications/progress ו-subscriptions.
חלק מה-notifications קשורות לבקשה מסוימת (התקדמות, הודעות לוג) ויוצאות על אותו stream שלה. אחרות מגיעות מ-subscription ארוך טווח. לכולן אותו מבנה: jsonrpc, method ו-params אופציונלי.
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 42,
"reason": "User requested cancellation"
}
}_meta — מטא-דאטה ברמת הפרוטוקול
_meta הוא שדה שמור שמופיע ב-params של בקשות וב-result של תשובות. הוא נושא מטא-דאטה שאינה חלק מהפרמטרים העסקיים של ה-method. במקום להוסיף שדות חדשים לכל method, הפרוטוקול שם כאן את כל מה שחוצה את כל ה-methods: גרסה, זהות, יכולות ומעקב.
מפתח ב-_meta בנוי מ-prefix אופציונלי ומשם. ה-prefix הוא רצף labels מופרדים בנקודות שמסתיים ב-/, ומומלץ לכתוב אותו ב-reverse DNS, למשל com.example/. כל prefix שה-label השני שלו הוא modelcontextprotocol או mcp שמור ל-MCP: io.modelcontextprotocol/ ו-dev.mcp/ שמורים, אבל com.example.mcp/ לא, כי ה-label השני שלו הוא example.
המפתחות השמורים ב-2026-07-28: io.modelcontextprotocol/protocolVersion (גרסת הפרוטוקול של הבקשה, חובה), io.modelcontextprotocol/clientCapabilities (היכולות של ה-client, חובה), io.modelcontextprotocol/clientInfo (שם וגרסה של ה-client, מומלץ), io.modelcontextprotocol/logLevel (רמת לוג מינימלית לבקשה), io.modelcontextprotocol/serverInfo (בתשובות), io.modelcontextprotocol/subscriptionId (ב-notifications של subscription), progressToken (בקשה לקבל הודעות התקדמות), ו-traceparent, tracestate ו-baggage להעברת trace context של OpenTelemetry.
בקשה שחסר בה שדה חובה היא malformed, והשרת חייב לדחות אותה בקוד -32602, וב-HTTP בסטטוס 400. איך בוחרים גרסה ומה עושים כשאין התאמה, נראה בפרק 3.
clientInfo ו-serverInfo הם הצהרה עצמית של השולח, והפרוטוקול לא מאמת אותם. הם נועדו לתצוגה, ללוגים ולדיבאג. אסור לבסס עליהם החלטות אבטחה, ולא כדאי לשנות התנהגות לפיהם.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_issues",
"arguments": { "query": "login bug" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"progressToken": "call-2",
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01"
}
}
}קודי שגיאה
MCP משתמש בקודים הסטנדרטיים של JSON-RPC לכשלים כלליים: -32700 Parse error (ה-JSON לא תקין), -32600 Invalid Request (JSON תקין שאינו הודעת JSON-RPC תקינה), -32601 Method not found, -32602 Invalid params ו--32603 Internal error.
JSON-RPC שומר את הטווח -32000 עד -32099 לשגיאות שהמימוש מגדיר, ו-MCP מחלק אותו לשניים. הטווח -32000 עד -32019 הוא legacy: קודים שמימושים הקצו לפני שהיה כלל. אסור להקצות בו קודים חדשים, ומלבד -32002 אסור להניח משמעות כלשהי לקוד מתוכו. הטווח -32020 עד -32099 שמור בלעדית ל-spec: מותר לשלוח רק קודים שה-spec הגדיר, ורק במשמעות שלהם.
הקודים ש-MCP מגדיר כיום: -32020 HeaderMismatch, כשכותרות ה-HTTP לא תואמות לגוף הבקשה (פרק 4). -32021 MissingRequiredClientCapability, כשהשרת צריך יכולת שה-client לא הצהיר עליה; השדה data.requiredCapabilities מפרט מה חסר (פרק 3). -32022 UnsupportedProtocolVersion, כשהשרת לא תומך בגרסה שבבקשה; ב-data מופיעות הגרסאות שהוא כן תומך בהן, כמו בדוגמה. בשלושתם, ב-HTTP, הסטטוס חייב להיות 400.
בגרסה הקודמת, 2025-11-25, שימש הקוד -32002 ל-resource not found, והוא הוחלף ב--32602. הקוד -32042 (URL elicitation required) היה קיים רק ב-2025-11-25. שניהם נשארים שמורים ולא ימוחזרו. מימוש של 2026-07-28 לא שולח אותם, אבל client צריך (SHOULD) עדיין לקבל -32002 משרתים ישנים.
לשגיאות אפליקטיביות משלכם, ה-spec ממליץ להקצות קודים מחוץ לטווח השמור של JSON-RPC (-32768 עד -32000). ושגיאות מקומיות, כמו timeout שה-SDK זורק, לא צריכות להיראות כאילו הגיעו מהצד השני.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}שגיאת פרוטוקול מול שגיאת ביצוע
ב-MCP יש שני ערוצים לדווח על כישלון, וההבדל ביניהם הוא למי השגיאה מיועדת. שגיאת פרוטוקול (JSON-RPC error) אומרת שהבקשה עצמה שבורה: method לא קיים, פרמטרים לא תקינים, כלי לא מוכר, תקלה פנימית. שגיאות כאלה מיועדות בעיקר לקוד של ה-client, והמודל כנראה לא יוכל לתקן אותן.
שגיאת ביצוע של כלי (tool execution error) היא תשובה מוצלחת מבחינת JSON-RPC: result עם resultType של complete, שבו isError הוא true וב-content יש הסבר. זה המקרה של כלי שרץ ונכשל: API חיצוני החזיר 500, תאריך בפורמט שגוי, ערך מחוץ לטווח.
למה זה משנה: ה-spec ממליץ (SHOULD) ל-client להעביר שגיאות ביצוע למודל, כדי שיוכל לתקן את עצמו ולנסות שוב עם ארגומנטים אחרים. ראינו את זה בנושא AI Agents, בפרק תכנון כלים טובים: הודעת שגיאה שמסבירה מה לא תקין ומה כן מצופה היא חלק מהממשק של הכלי. שגיאות פרוטוקול מותר להעביר למודל, אבל הסיכוי שיעזרו לו קטן.
כלל אצבע למי שכותב שרת: אם המודל יכול לתקן את הבעיה על ידי שינוי הארגומנטים, החזירו isError. אם הבעיה במבנה הבקשה או בשרת עצמו, החזירו JSON-RPC error. נחזור לזה עם דוגמאות נוספות בפרק 5.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "Invalid date '2026-13-01': month must be 01-12. Use format YYYY-MM-DD."
}
],
"isError": true
}
}
מה היה שונה בגרסאות קודמות
בגרסה הקודמת של הפרוטוקול, 2025-11-25, מעטפת ה-JSON-RPC הייתה כמעט זהה, עם שלושה הבדלים עיקריים. הראשון: ל-result לא היה שדה resultType. מכאן הכלל שתשובה בלי resultType נחשבת complete.
השני: בקשות רגילות לא נשאו גרסה ויכולות ב-_meta. הצדדים החליפו אותן פעם אחת, בבקשת initialize בתחילת החיבור, ומאותו רגע השרת זכר אותן לאורך ה-session. ב-HTTP הגרסה שהוסכמה עברה בכל בקשה רק בכותרת MCP-Protocol-Version. הכותרת הזו קיימת גם ב-2026-07-28, אבל שם היא רק שיקוף של השדה ב-_meta, והערכים חייבים להיות זהים (פרק 4). בפרק 3 נראה את ה-initialize המלא.
השלישי: ב-2025-11-25 גם השרת שלח בקשות ל-client על אותו חיבור, למשל elicitation/create, sampling/createMessage ו-roots/list, ושני הצדדים יכלו לשלוח ping. ב-2026-07-28 שרת לא יוזם בקשות: הוא מבקש מידע דרך תשובת input_required (פרק 7), ו-ping הוסר.
ועוד הערה היסטורית: JSON-RPC 2.0 מאפשר batching, מערך של כמה הודעות בגוף אחד. גרסה 2025-03-26 של MCP תמכה בזה, גרסה 2025-06-18 הסירה את התמיכה, והיא לא חזרה מאז. ב-MCP כל הודעה נשלחת לבד.
מעטפה ב-TypeScript: סיווג וניתוב הודעות
כדי לסגור את הפרק, הנה הלב של כל מימוש MCP: קבלת הודעה גולמית, זיהוי הסוג שלה וניתוב. בקשות הולכות ל-handler לפי method, תשובות הולכות לבקשה הפתוחה שמחכה להן לפי id, ו-notifications מטופלות בלי להחזיר דבר.
שימו לב לסדר הבדיקות. מה שמבדיל בין request ל-notification הוא רק נוכחות של id, ומה שמבדיל בין result ל-error הוא איזה מהשדות קיים. ה-dispatcher מחזיר את קודי השגיאה שראינו: -32600 להודעה לא תקינה, -32601 ל-method לא מוכר, ו--32603 לחריגה לא צפויה ב-handler. (את -32700 מחזירים עוד לפני כן, כש-JSON.parse נכשל.)
הקוד לא תלוי ב-transport: הוא מקבל אובייקט שכבר עבר JSON.parse ומחזיר אובייקט לשליחה, או null כשאין מה לשלוח. שרת אמיתי יבדוק גם את שדות החובה ב-_meta לפני הפעלת ה-handler; את זה נוסיף בפרק 3, ובפרק 4 נחבר את ה-dispatcher ל-stdio. ה-SDKs הרשמיים כוללים שכבה מקבילה ומלאה יותר.
type RequestId = string | number;
type Params = Record<string, unknown>;
interface JsonRpcRequest { jsonrpc: "2.0"; id: RequestId; method: string; params?: Params }
interface JsonRpcNotification { jsonrpc: "2.0"; method: string; params?: Params }
interface JsonRpcResult { jsonrpc: "2.0"; id: RequestId; result: { resultType?: string; [key: string]: unknown } }
interface JsonRpcError { jsonrpc: "2.0"; id?: RequestId; error: { code: number; message: string; data?: unknown } }
type Classified =
| { kind: "request"; msg: JsonRpcRequest }
| { kind: "notification"; msg: JsonRpcNotification }
| { kind: "result"; msg: JsonRpcResult }
| { kind: "error"; msg: JsonRpcError }
| { kind: "invalid"; reason: string };
function classifyMessage(raw: unknown): Classified {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
return { kind: "invalid", reason: "message must be a single JSON object (no batching)" };
}
const m = raw as Record<string, unknown>;
if (m.jsonrpc !== "2.0") return { kind: "invalid", reason: 'jsonrpc must be "2.0"' };
const idOk = typeof m.id === "string" || Number.isInteger(m.id);
if (typeof m.method === "string") {
if (!("id" in m)) return { kind: "notification", msg: m as unknown as JsonRpcNotification };
if (!idOk) return { kind: "invalid", reason: "request id must be a string or integer (not null)" };
return { kind: "request", msg: m as unknown as JsonRpcRequest };
}
if ("result" in m && idOk) return { kind: "result", msg: m as unknown as JsonRpcResult };
if ("error" in m) return { kind: "error", msg: m as unknown as JsonRpcError };
return { kind: "invalid", reason: "not a request, notification or response" };
}
type Handler = (params: Params | undefined) => Promise<JsonRpcResult["result"]>;
const handlers: Record<string, Handler> = {
"tools/list": async () => ({ resultType: "complete", tools: [], ttlMs: 60000, cacheScope: "public" }),
};
// תשובות לבקשות שאנחנו שלחנו, לפי id
const pending = new Map<RequestId, (msg: JsonRpcResult | JsonRpcError) => void>();
async function dispatch(raw: unknown): Promise<JsonRpcResult | JsonRpcError | null> {
const c = classifyMessage(raw);
switch (c.kind) {
case "invalid":
return { jsonrpc: "2.0", error: { code: -32600, message: c.reason } };
case "notification":
return null; // אף פעם לא עונים ל-notification
case "result":
case "error": {
const id = c.msg.id;
if (id !== undefined) {
pending.get(id)?.(c.msg);
pending.delete(id);
}
return null;
}
case "request": {
const handler = handlers[c.msg.method];
if (!handler) {
return { jsonrpc: "2.0", id: c.msg.id, error: { code: -32601, message: `Method not found: ${c.msg.method}` } };
}
try {
return { jsonrpc: "2.0", id: c.msg.id, result: await handler(c.msg.params) };
} catch {
return { jsonrpc: "2.0", id: c.msg.id, error: { code: -32603, message: "Internal error" } };
}
}
}
}