דלג לתוכן
27/30פרק 27 מתוך 30

השקת שרת MCP: TypeScript ו-Python, במדידה

אותו שרת נכתב פעמיים — שלושה כלים, resource ו-prompt — ואז נמדד: 94 חבילות מול 28, ו-cold start של 145ms מול 709.

בעמוד הזה

הנה כל הוויכוח על השפה, במדידה, לפני שנאמרה עליו מילה.

spawn → tools/list answered, median of 25 launchesTEXT
node ./incidents.js         144.5 ms
python incidents.py         709.4 ms
npx incidents-mcp           712.6 ms

שתי השורות הראשונות הן ההשוואה שכולם רוצים. השורה השלישית היא אותו שרת TypeScript מהשורה הראשונה, מופעל כפי שבאמת היה מופץ — והיא נוחתת שלוש מילישניות מ-Python.

פרק 26 קרא את Model Context Protocol מול המפרט שלו עצמו עם JSON-RPC גולמי, כי ל-JSON-RPC גולמי אין שפה. בפרק הזה יש שתיים, ומשקל הטיעון נופל כאן: אותו שרת, כתוב פעמיים. שלושה כלים, resource אחד, prompt אחד, שני ה-SDKs, בלי קיצורי דרך באף צד. ואז ה-transports, ה-inspector, ה-401, והמספרים שאף אחד לא פרסם.

השרת, ולמה יש בו את חמשת הדברים האלה

קישור למקטע: השרת, ולמה יש בו את חמשת הדברים האלה

יומן אירועים. שלושה כלים, כי הפיצול של פרק 18 בין קריאות לכתיבות חייב להיות גלוי: search_incidents קורא, open_incident כותב ומחזיר handle, resolve_incident לוקח את ה-handle הזה וסוגר. resource אחד, incidents://open, כי קריאת הרשימה הנוכחית היא משהו שהאפליקציה מצמידה. prompt אחד, postmortem, כי ״כתוב את זה״ היא slash command של אדם. זו היררכיית השליטה של פרק 26 — מודל, אפליקציה, אדם — שהפכה לחמישה רישומים.

ה-handle חשוב יותר ממה שנדמה. פרק 26 שבר לוח שנה צעצוע בכך ששמר את המצב שלו במערך ברמת המודול: לפרוטוקול אין session, ולכן כלי יצירה מחזיר מזהה אטום וכל קריאה מאוחרת יותר מקבלת אותו כארגומנט רגיל. שום דבר באף אחד מהקבצים לא מניח שהקורא הוא התהליך שפתח אותו.

הנה אותו כלי בשתי השפות, רשום זה לצד זה:

incidents.tsTS
server.registerTool(
  "resolve_incident",
  {
    description:
      "Close an incident by handle and record its cause.",
    inputSchema: {
      id: z.string().describe(
        "The handle returned by open_incident, e.g. INC-3."),
      cause: z.string().describe(
        "One sentence. What actually broke."),
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: true,
      idempotentHint: true,
    },
  },
  async ({ id, cause }) => {
    const at = OPEN.findIndex((i) => i.id === id);
    if (at < 0) {
      return { isError: true, content: [{ type: "text",
        text: `No open incident ${id}. ` +
              `Call search_incidents first.` }] };
    }
    const [done] = OPEN.splice(at, 1);
    return { content: [{ type: "text",
      text: JSON.stringify({ ...done, cause }) }] };
  },
);
incidents.pyPYTHON
@server.tool(
    description=
      "Close an incident by handle and record its cause.",
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
        idempotentHint=True,
    ),
)
def resolve_incident(
    id: Annotated[str, Field(description=
        "The handle returned by open_incident, e.g. INC-3.")],
    cause: Annotated[str, Field(description=
        "One sentence. What actually broke.")],
) -> Incident:
    for at, i in enumerate(OPEN):
        if i["id"] == id:
            done = OPEN.pop(at)
            return {**done, "cause": cause}
    raise ValueError(
        f"No open incident {id}. Call search_incidents first.")

קראו קודם את מה שזהה, כי זה הממצא. שניהם מצהירים על שם, תיאור, שני ארגומנטים מסוג string עם תיאור, ושלוש annotations; שניהם פונקציה אחת; אף אחד מהם לא מזכיר JSON-RPC, framing, stdout או גרסת פרוטוקול. שני ה-SDKs התכנסו לאותה צורה, וזה מה ש-״Tier 1״ אמור לומר.1

שני הבדלים הם אמיתיים ושניהם יחזרו בהמשך. TypeScript מתאר ארגומנטים עם ספריית schema — כאן Zod — וה-schema הוא ערך שאתם כותבים. Python מתאר אותם עם ה-type hints של הפונקציה עצמה וקורא אותם בזמן import, ולכן הוא יודע דברים על הפונקציה שקובץ ה-TypeScript מעולם לא אמר לו. ומסלול השגיאה: TypeScript מחזיר תוצאת כלי עם isError, Python זורק חריגה. שמרו את זה בראש.

ארבעת הרישומים האחרים לא שונים במבנה. ה-resource הוא server.registerResource("open-incidents", "incidents://open", …) מול @server.resource("incidents://open", …); ה-prompt הוא registerPrompt מול @server.prompt. השורה האחרונה בכל קובץ היא ה-transport: await server.connect(new StdioServerTransport()) מול server.run().

קבצים מלאים: 81 שורות לא ריקות ו-3,060 בתים של TypeScript מול 63 ו-2,555. קחו את זה עם המלח הראוי — ספירת שורות מודדת formatter לא פחות משהיא מודדת שפה, ולכן אף אחד מהמספרים לא נמצא בטבלת הכותרת למטה.

ההוכחה שהשפה בלתי נראית היא הרצה אחת של client פעמיים, באחת-עשרה שורות:

client.tsTS
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client({ name: "incident-cli", version: "1.0.0" });
await client.connect(new StdioClientTransport({
  command: process.argv[2], args: process.argv.slice(3) }));

const { tools } = await client.listTools();
console.log("tools:", tools.map((t) => t.name).join(", "));

const opened = await client.callTool({ name: "open_incident",
  arguments: { title: "Queue backed up", severity: "sev2" } });
console.log("open_incident ->", JSON.stringify(opened.content));

כוונו אותו לכל שרת בתורו. פלט אמיתי, מקוצר:

TEXT
$ node client.ts node incidents.ts
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\"id\":\"INC-3\"}"}]

$ node client.ts ./py/.venv/bin/python ./py/incidents.py
tools: search_incidents, open_incident, resolve_incident
open_incident -> [{"type":"text","text":"{\n  \"id\": \"INC-3\"\n}"}]

אותם כלים, אותו סדר, אותו handle. client ב-TypeScript לא יכול לדעת במה השרת כתוב, והוא אף פעם לא שואל. זו כל ההבטחה של פרוטוקול, והיא מחזיקה.

עכשיו הסתכלו על הרווחים בתוצאה השנייה, כי זה לא קוסמטי: ה-SDK של Python עושה serialise ל-payloads עם pydantic_core.to_json(result, fallback=str, indent=2). בקריאת resource עם שני incidents ברשימה, גוף ה-TypeScript הוא 136 תווים ו-37 token של o200k_base; גוף ה-Python הוא 185 ו-62. שישים ושמונה אחוז יותר tokens עבור שורות זהות, בתשלום של מי שקורא את ה-resource לתוך prompt, בכל פעם.

לקטלוג יש אותו סיפור עם סיבה גדולה יותר. שני השרתים, אותם שלושה כלים, tools/list נשקל מפתח-מפתח:

keyTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
total342480

ה-input schemas של Python זולים יותר — גשר ה-Zod של TypeScript מטביע $schema ו-additionalProperties על כל אחד מהם. כל פער ה-138 token הוא output schema שאף אחד לא כתב. resolve_incident מסומן כ--> Incident, ולכן ה-SDK גזר JSON Schema עבור סוג ההחזרה ושלח אותו. זה שימושי באמת — זה מה שמאפשר ל-client לאמת structuredContent — וזה 187 tokens מה-context window שלכם שמגיעים בגלל type hint. הכלל של פרק 24 על definitions שדוחקות החוצה את החומר החשוב חל גם על schemas שלא ידעתם שיש לכם.

לשבור בכוונה: הודעת השגיאה שדלפה

קישור למקטע: לשבור בכוונה: הודעת השגיאה שדלפה

שני מסלולי השגיאה למעלה אינם בחירת סגנון. תנו לכל שרת כלי שנכשל כמו שאינטגרציה אמיתית נכשלת, וקראו מה מגיע למודל.

tools/call on a tool that raisesTEXT
TypeScript  {"content":[{"type":"text","text":
              "connect ECONNREFUSED 10.0.3.7:5432 (db-prod-eu, user=reporting)"}],
             "isError":true}

Python      {"content":[{"text":"Error executing tool boom","type":"text"}],
             "isError":true}

ה-SDK של TypeScript הכניס כתובת פנימית, פורט, שם database ו-service account לתוך ה-context של המודל. ה-SDK של Python לא הכניס לשם שום דבר מזה; ה-traceback הלך אל stderr ונשאר על השרת.

זה לא bug באף אחד מהם. שתיהן החלטות, וההחלטה של Python כתובה ב-docstring שלה: ToolError הוא ״כשל שציפיתם לו״ וההודעה שלו מוחזרת ״ב-content כדי שהמודל יקרא״; כל דבר אחר ״מטופל כקריסה: המודל רואה רק Error executing tool <name>, והשרת רושם את ה-traceback ב-ERROR״. המחלקה של מקרה הקריסה אומרת בקול את השאר — ״שום דבר מהמקור לא מגיע ל-client״.

שתי ההתנהגויות שגויות בחצי מהמקרים. פרק 18 טען ששגיאת validation צריכה לחזור כתוצאת כלי שהמודל יכול לקרוא ולתקן, כי זו השורה עם המינוף הגבוה ביותר ברוב האינטגרציות; בצד של Python זה דורש לזרוק ToolError במפורש, ו-ValueError חשוף זורק לפח את המשפט השימושי. הטיעון של פרק 30 הולך בכיוון ההפוך: כל מה שכלי מחזיר נוחת ב-context ש-prompt injection מאוחר יותר יכול לנסות לקרוא בחזרה, ומחרוזת חריגה שלא עברה ביקורת היא הטקסט הכי פחות מבוקר במערכת שלכם.

הכלל ששורד את שניהם: החליטו, לכל כלי, מה מותר לכשל לומר, וכתבו את המחרוזת הזו בעצמכם. לעולם אל תתנו לטקסט ברירת המחדל של חריגה להחליט, באף שפה.

לשבור בכוונה: שורה אחת אל standard output

קישור למקטע: לשבור בכוונה: שורה אחת אל standard output

המדריך הרשמי מציין את הכלל בלי הסתייגות: ״עבור שרתים מבוססי STDIO: לעולם אל תכתבו ל-stdout. כתיבה ל-stdout תשחית את הודעות ה-JSON-RPC ותשבור את השרת שלכם. הפונקציה print() כותבת ל-stdout כברירת מחדל, אז השאירו אותה מחוץ לשרת STDIO לחלוטין.״1 פרק 26 ציטט את הגרסה הנורמטיבית — שרת ״MUST NOT write anything to its stdout that is not a valid MCP message״.2

הוסיפו שורה אחת לכל שרת וקראו את ה-stream הגולמי:

raw stdout, first two linesTEXT
TypeScript  incidents server starting
            {"result":{"protocolVersion":"2025-11-25", … },"jsonrpc":"2.0","id":1}

Python      {"jsonrpc":"2.0","id":1,"result":{ … }}
            incidents server starting

זה של Python גרוע יותר, והסיבה אינה MCP. תהליך שה-stdout שלו הוא pipe ולא terminal מקבל stream עם block buffering, ולכן השורה התועה מתרוקנת כשה-buffer מחליט — כאן, ביציאה, אחרי תגובה שלפניה היא נכתבה. ההשחתה לא מופיעה במקום שבו ה-bug נמצא. הוסיפו flush=True, או ספרייה שעושה flush, והיא זזה.

ואז החלק שמסביר למה זה מגיע לייצור. הזינו את השרת השבור לשלושה clients:

TEXT
naive parser, dirty server   SyntaxError: Unexpected token 'i',
                             "incidents "... is not valid JSON
SDK client, dirty server     tools: search_incidents, open_incident, resolve_incident
MCP Inspector, dirty server  full catalogue, no warning

ה-parser בן שבע השורות מת מיד. ה-client הרשמי וה-Inspector מושכים בכתפיים — הם מדלגים על השורה וממשיכים. כלל ששובר רק את ה-clients שאף אחד לא משתמש בהם הוא כלל שמגיע ל-production שלם, ולכן שווה לשבור אותו בכוונה כאן ולא בלוג של לקוח.

מצב ה-CLI של ה-Inspector הוא החצי שנשכח: npx @modelcontextprotocol/inspector --cli <command> --method tools/list מדפיס קטלוג ויוצא, מה שהופך אותו ל-scriptable באופן שה-UI בדפדפן אינו.3

שני ה-SDKs הותקנו נקי, כל אחד לתיקייה שלו, בלי שום דבר משותף:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
גרסת הפרוטוקול האחרונה שמיושמת2025-11-252026-07-28
חבילות transitive שהותקנו9428
גודל מותקן13.9 MiB44.3 MiB
קבצים בדיסק3,3862,018
חבילות צד שלישי שנטענו כדי לשרת stdio8 מתוך 9418 מתוך 28
הפעלת interpreter חשוף, חציון19.4 ms11.1 ms
spawn → tools/list נענה, חציון מתוך 25144.5 ms709.4 ms
קטלוג tools/list, tokens של o200k_base342480

כל שורה מפתיעה בכיוון אחר, ולכן כדאי להריץ את ההשוואה ולא להניח.

TypeScript מתקין יותר מפי שלוש חבילות ופחות משליש מהבתים. 94 dependencies הם אקוסיסטם npm שמתנהג כמו עצמו — fast-deep-equal, es-errors, dunder-proto. ה-28 של Python הן פחותות במספר ועצומות: cryptography, pydantic-core ו-uvicorn הן artefacts מקומפלים. אם האינסטינקט שלכם הוא שספירת dependencies היא מה שצריך להדאיג, השורה הזו היא הדוגמה הנגדית.

ה-interpreter של Python מתחיל מהר יותר מזה של Node, ולא בקצת — 11.1 ms מול 19.4 ms על תוכנית ריקה. לכן ה-565 ms בשורת ה-cold-start אינם השפה. זה ה-SDK, ושורת החבילות הנטענות מסבירה למה:

third-party modules loaded to answer one tools/list over stdioTEXT
TypeScript   8 of 94   sdk, zod, zod-to-json-schema, ajv, ajv-formats,
                       fast-deep-equal, fast-uri, json-schema-traverse

Python      18 of 28   mcp, mcp_types, pydantic, pydantic_core, anyio,
                       starlette, uvicorn, sse_starlette, httpx2,
                       cryptography, _cffi_backend, opentelemetry, click, …

שרת שכל ה-I/O שלו הוא pipe עושה import לשרת web מסוג ASGI, ל-HTTP client ולספריית TLS לפני שהוא קורא את השורה הראשונה שלו. ה-SDK של TypeScript מביא גם Express, Hono, jose ו-eventsource — הם יושבים על הדיסק בלי להיקרא, כי גבול ה-package משאיר אותם מחוץ ל-import של server/stdio.js. ה-package של Python הוא גרף import אחד, ולכן import mcp הוא כולו: python -X importtime מייחס 727 ms ל-import mcp.server.mcpserver — מספר שנמדד תחת import profiler, ולכן יוצא מעל 709 ms שלוקחת ההרצה הלא-מפוריילת מ-spawn עד תשובה — ו-269 מהם לתת-העץ mcp.types לבדו — טיפוסי ה-wire הם מודלי Pydantic, מחלקה אחת לכל הודעת פרוטוקול בכל revision, ובנייתם היא עבודה שנעשית בזמן import. זה trade-off תכנוני, לא רשלנות — imports להוטים הם הסיבה שה-SDK של Python יכול לתת לכם run(transport="streamable-http") בשורה הבאה בלי התקנה שנייה.

ואז השורה האחרונה של בלוק הפתיחה מבטלת את הטיעון. ארזו את שרת ה-TypeScript כמו שצריך — entry של bin, shebang, npm link, שום דבר להורדה — והפעילו אותו דרך npx עם --no-install, וכך שרת stdio מפורסם באמת מתחיל:

median of 25, spawn → tools/list answeredTEXT
node ./incidents.js       144.5 ms
npx incidents-mcp         712.6 ms      (+568.1 ms of launcher)
python incidents.py       709.4 ms

ה-launcher עולה 568 ms לכל התחלה — פי ארבעה וחצי מכל import ה-SDK של TypeScript — והוא משולם בכל launch, כי MCP host מתחיל שרת stdio על ידי הרצת הפקודה הזו. לכן הצורה הכנה של ״TypeScript מתחיל פי חמישה מהר יותר״ היא: כן, עד שמפיצים אותו בדרך הרגילה. אותו סייג כנראה חל על uvx; במכונה הזו לא היה uv מותקן, ולכן השורה הזו לא קיימת. שום דבר שלא נמדד לא נכנס לטבלה.

פרק 26 כיסה את ה-framing של stdio. שני דברים הוא השאיר לכאן.

הראשון: הרצת שרת עם npx או uvx היא ה-transport של stdio. אין ״מצב package״ נפרד. הקונפיגורציה של host מציינת פקודה וארגומנטים; ה-host מריץ אותה ומדבר על ה-pipes. לכן ״איך מפיצים את זה״ ו-״באיזה transport זה מדבר״ הן שאלה אחת מקומית, ולכן עלות ה-launcher שייכת לפרק על שילוח.

השני: ל-stdio אין סעיף authorization בכלל, והמפרט אומר זאת בשורה אחת — מימושים המשתמשים ב-stdio ״SHOULD NOT follow this specification, and instead retrieve credentials from the environment״.4 מודל האבטחה שלו הוא של מערכת ההפעלה, וכך גם המגבלה שלו: תת-תהליך מקומי משרת בדיוק מכונה אחת ומשתמש אחד.

ה-transport החי האחר הוא Streamable HTTP: endpoint יחיד שמקבל POST, בקשת HTTP אחת לכל הודעת JSON-RPC, ו-header של Accept שחייב למנות גם את application/json וגם את text/event-stream כי השרת בוחר בכל בקשה עם מי מהשניים להשיב.5 פרק 14 פירק את ה-event stream הזה ידנית, ולכן שום דבר ב-wire format אינו חדש — רק מה שעוטף אותו. שלוש חובות של ה-revision הנוכחי קל לפספס וכולן ניתנות לבדיקה:

ה-version header חייב להסכים עם ה-body

קישור למקטע: ה-version header חייב להסכים עם ה-body

כל POST נושא MCP-Protocol-Version, והערך שלו חייב להתאים ל-protocolVersion בתוך ה-_meta של הבקשה עצמה. אי-התאמה היא 400 עם שגיאת header-mismatch, לא משיכת כתפיים.5

שני headers נוספים נדרשים לצורך compliance

קישור למקטע: שני headers נוספים נדרשים לצורך compliance

Mcp-Method משקף את ה-method בכל בקשה; Mcp-Name משקף את params.name או params.uri על tools/call, resources/read ו-prompts/get. הם קיימים כדי ש-proxy יוכל לנתב בלי לפרק bodies.5

הצורות הישנות נעלמו, ועונות בסירוב

קישור למקטע: הצורות הישנות נעלמו, ועונות בסירוב

ה-GET stream, Mcp-Session-Id וחידוש Last-Event-ID הוסרו כולם. שרת שמדבר רק את ה-revision הזה צריך לענות 405 Method Not Allowed ל-GET או DELETE, להתעלם מ-session header בלי להנפיק אחד, ולהתעלם מ-Last-Event-ID.5

עכשיו המדידה שממסגרת מחדש את כל הפרק. שלחו בקשה ב-revision הנוכחי לכל שרת מעל HTTP.

POST /mcp, MCP-Protocol-Version: 2026-07-28TEXT
Python   200  {"result":{"resultType":"complete","cacheScope":"private","ttlMs":0,
              "tools":[…],"_meta":{"io.modelcontextprotocol/serverInfo":{…}}}}

TypeScript    {"error":{"code":-32000,"message":"Bad Request: Unsupported protocol
              version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18,
              2025-03-26, 2024-11-05, 2024-10-07)"}}

הקבועים מסכימים עם ההתנהגות: LATEST_PROTOCOL_VERSION של ה-SDK ב-Python קורא 2026-07-28, זה של ה-SDK ב-TypeScript קורא 2025-11-25. שלחו את בקשת ה-header-mismatch מהשלב למעלה והשרת ב-Python עונה 400 עם שגיאה -32020 וההודעה ״mcp-protocol-version header does not match the request envelope's protocol version״; ל-SDK של TypeScript אין קוד כזה, כי הוא לא מיישם את ה-revision שמגדיר אותו.

העמוד שמציג את שניהם כ-Tier 1 אומר גם ״Each SDK provides the same functionality״.1 בתאריך שלמטה, עבור ה-revision הנוכחי, המשפט הזה הוא שאיפה. בדקו את LATEST_PROTOCOL_VERSION ב-SDK שאתם עומדים להתקין; זו שורה אחת, והטענה היחידה בפרק הזה שעוד תהיה חשובה בעוד שנה.

העבירו שרת מהלפטופ שלכם ו-client של זר מגיע עם token. זה החצי שפרק 26 השאיר בצד והחצי שמוצר רב-משתמשים לא יכול לדלג עליו.

המפרט מציב את שרת ה-MCP בתפקיד OAuth 2.1 ונותן לו שם: שרת MCP מוגן הוא resource server, ה-client הוא OAuth client, וה-authorization server הוא בעיה של מישהו אחר.4 מתוך התפקיד הזה, ארבעה סעיפים מחייבים, מצוטטים במלואם כי פרפראזה היא הדרך שבה הטעות נוצרת:

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. […] MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server. MCP servers MUST only accept tokens that are valid for use with their own resources. MCP servers MUST NOT accept or transit any other tokens.4

״Must not accept or transit״ הוא כלל האנטי-passthrough, ולכן כל מנגנון ה-audience קיים. שרת שמנגן מחדש את ה-bearer token שנמסר לו אל API של צד שלישי הוא confused deputy: הוא משאיל את האמון שלו למי שקרא לו. הכלל אוסר את השימוש החוזר, לא רק את האחסון.

כדי להפוך את זה לאכיף צריך ארבעה RFCs, תפקיד אחד לכל אחד.6 RFC 9728 הוא הדרך שבה ה-client מוצא בכלל את ה-authorization server: שרת ה-MCP מגיש מסמך protected-resource-metadata ו-401 מצביע אליו. RFC 8707 הוא פרמטר ה-resource — ה-client חייב לשלוח את ה-URI הקנוני של השרת גם בבקשת ההרשאה וגם בבקשת ה-token, ״regardless of whether authorization servers support it״, כך שה-token שהונפק מציין את ה-audience שלו. RFC 9207 סוגר את הלולאה מהצד השני: ה-client מתעד את ה-issuer לפני redirect ומשווה את ה-iss שחזר לפי מחרוזת מדויקת, בלי normalisation — בלי case folding, בלי השמטת פורט ברירת מחדל, בלי slash בסוף. ו-RFC 7591, Dynamic Client Registration, עכשיו deprecated לטובת Client ID Metadata Documents, ״retained for backwards compatibility with authorization servers that do not support״ אותם.4

חברו את זה בשני השרתים עם token verifier שלא עושה דבר חוץ מבדיקת audience. הסולם של TypeScript:

POST /mcp — TypeScript, with requireBearerAuthTEXT
no token            401  WWW-Authenticate: Bearer error="invalid_token",
                         error_description="Missing Authorization header",
                         scope="incidents:read",
                         resource_metadata="…/.well-known/oauth-protected-resource/mcp"
aud=other server    401  error_description="token audience is not this server"
no exp claim        401  error_description="Token has no expiration time"
right aud, no scope 403  error="insufficient_scope", scope="incidents:read"
right aud + scope   200  {"result":{"tools":[…]}}
GET /.well-known/oauth-protected-resource/mcpTEXT
{"resource":"http://127.0.0.1:8931/mcp",
 "authorization_servers":["https://auth.example.com/"],
 "scopes_supported":["incidents:read","incidents:write"],
 "resource_name":"Incidents"}

שני ה-SDKs מגישים את המסמך הזה ושניהם מצביעים אליו עם 401, וזה כל סיפור ה-discovery: client שמעולם לא ראה את השרת שלכם לומד מהסירוב איפה להזדהות. ה-403 הוא יצור אחר — ה-token תקין, ה-scope לא — וה-challenge מציין מה חסר כדי שה-client יוכל לעלות מדרגה במקום להתחיל מחדש.

שתי מדרגות שונות, ואף אחד מההבדלים אינו במפרט. ה-SDK של TypeScript מסרב ל-token בלי expiry claim; זה של Python מחזיר 200, כי expires_at אופציונלי ב-AccessToken שלו ו-None פירושו ״אין דעה״. וגם ה-403 של Python נושא error_description="Required scope: incidents:read" בלי פרמטר ה-scope שהמפרט אומר ששרתים צריכים לכלול. verifier הוא לא מקום לקבל בו ברירת מחדל של ספרייה: בדיקת ה-audience היא שלכם לכתוב בכל שפה, וכך גם ה-expiry.

ניט אחד כן מאותה הרצה. GET על ה-endpoint ענה 404 בחיווט Express ו-400 Bad Request: Missing session ID בזה של Python, במקום שבו המפרט מבקש 405 Method Not Allowed ובמקום שבו ״session ID״ הוא אוצר מילים שה-revision הזה הסיר. אף אחד מהם לא מסוכן; שניהם צורה של אקוסיסטם באמצע migration.

החלק האחרון של shipping הוא איפה מפרסמים, ויש לו תשובה עם מספר. נסרק היום, כל שרת ב-registry הרשמי בגרסתו האחרונה:7

servers
סך הכול (גרסה אחרונה, לא נמחק)28,170
פעיל / deprecated27,853 / 317
מפיץ לפחות package אחד ניתן להתקנה13,065
remote בלבד — URL, בלי מה להתקין14,696
npm8,275
PyPI3,603
OCI images867
bundles של mcpb706
NuGet / Cargo107 / 43

שתי קריאות, מצביעות לכיוונים מנוגדים. לפי שרתים שפורסמו, npm מוביל 2.3 ל-1 — המספר שאנשים מצטטים כשהם אומרים שהאקוסיסטם הוא TypeScript. לפי הורדות, Python מוביל: בשלושים הימים האחרונים mcp לקח 286.7 מיליון מול @modelcontextprotocol/sdk עם 194.7 מיליון, לפני שמוסיפים את fastmcp עם 72.1 מיליון.7 שניהם Tier 1, ה-schema הנורמטיבי הוא schema.ts, והמדריך הרשמי ״Build an MCP server״ נפתח בלשונית Python.1 איזה חצי מזה שהיה לכם בראש, החצי השני גם נכון.

והשורה שחשובה יותר משניהם: יותר מחצי מה-registry — 14,696 מתוך 28,170 — אין להם מה להתקין. אלה web services. ספירות ה-transport מסכימות מהצד השני: מתוך 14,290 רשומות package, 13,787 מצהירות stdio; מתוך 16,640 רשומות remote, 15,570 מצהירות Streamable HTTP ו-1,070 עדיין מצהירות HTTP+SSE ה-deprecated. לכן ״שרת MCP הוא תת-תהליך על הלפטופ שלכם״ מתאר מיעוט מצטמק, וכל אחד מה-14,696 צריך את הסעיף למעלה ולא environment variable.

הצגת פרטים

דו-לשוניות בכוונה, והתקדים לכך.

זה הפרק הדו-לשוני היחיד בקורס, כי התשובה הכנה מתפצלת: ה-registry הוא npm-first וההורדות הן Python-first, באותו זמן, היום. כתיבת אחת מהשתיים הייתה מוותרת על חצי מהשאלה ומתארת את האקוסיסטם לא נכון תוך כדי. יש לכך תקדים פתוח — Hugging Face MCP Course מציין בין הדרישות המקדימות שלו ״Experience with at least one programming language (Python or TypeScript examples will be shown)״, ומלמד את שתיהן.8 פרוטוקול שכל הערך שלו הוא מספר המימושים הוא מקום גרוע להיות בו חד-לשוני.

סעיף מתוארך: כל מה שלמעלה שיש לו חיי מדף

קישור למקטע: סעיף מתוארך: כל מה שלמעלה שיש לו חיי מדף

נקרא ונמדד ב-7 בספטמבר 2026, מול protocol revision 2026-07-28.

value
@modelcontextprotocol/sdk1.30.0, פורסם ב-27 ביולי 2026; 4,322,438 בתים לאחר unpack, 693 קבצים, 17 direct dependencies
ה-revision האחרון שהוא מיישם2025-11-25
mcp (PyPI)2.1.1, פורסם ב-25 באוגוסט 2026; wheel של 357,912 בתים, plus mcp-types 2.1.1 ב-69,656 בתים
ה-revision האחרון שהוא מיישם2026-07-28
SDK tiersTypeScript, Python, C#, Go, Rust ב-Tier 1; Java, Ruby ב-Tier 2; Swift, PHP, Kotlin ב-Tier 3
שרתי registry28,170
הורדות, 30 הימים האחרוניםmcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333

הערת migration אחת שאינה מספר. ב-mcp 2.x, השם של FastMCP שונה ל-MCPServer, וכמעט כל tutorial ברשת עדיין נפתח עם ה-import הישן. ה-SDK כולל מודול שכל מטרתו להסביר את זה, וזה ה-deprecation המתחשב ביותר בפרק הזה:

from mcp.server.fastmcp import FastMCPTEXT
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x,
where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import
MCPServer) and other APIs changed; see the migration guide … or pin 'mcp<2'
to keep running v1 code.

כשהטבלה מולכם, ההמלצה משעממת, וזה סימן טוב.

אם השרת חי בתוך web application שאתם כבר מריצים, כתבו אותו ב-TypeScript. אותו תהליך, אותו deploy, אותו request handler; Streamable HTTP הוא endpoint שמוסיפים ליד האחרים; ו-13.9 MiB ו-145 ms הם בחינם כי ה-runtime כבר היה למעלה. זה רוב 14,696 השרתים ה-remote.

אם השרת עוטף כלי נתונים, כתבו אותו ב-Python. מה שאתם חושפים הוא pandas, client למחסן נתונים, טרנספורמציות בהיקף של notebook, ושרת בשפה אחרת היה קריאת subprocess שלובשת schema. שבע מאות מילישניות של import בשירות שמתחיל פעם אחת אינן עלות; ב-subprocess ש-host מריץ מחדש כל היום, כן.

ולעת עתה, שורת ה-revision גוברת על שניהם. אם אתם צריכים 2026-07-28 — בקשות מרובות round-trip, resultType, cache hints, server/discover — לאחד משני ה-SDKs יש את זה היום ולשני לא.

עכשיו אתם יכולים לשלוח את אותו שרת בכל אחת מהשפות, להגן על הבחירה עם טבלה במקום העדפה, להריץ אותו מעל שני ה-transports החיים, ולמסור לו token שהוא יסרב לקבל.

מה שבניתם הוא עדיין function: schema, endpoint, דבר דטרמיניסטי שהמודל מפעיל. מחלקה שלמה של ידע לא נכנסת לצורה הזו — איך אנחנו כותבים postmortem, אילו שדות דוחות האירוע שלנו צריכים, סדר הדברים שאנחנו עושים ולמה. זה procedure, זה prose, ולדחוף את זה לתיאור כלי זו הדרך שבה system prompts גדלים לאלפיים tokens שמשולמים בכל turn יחיד, בין אם השיחה עוסקת באירועים ובין אם לא.

פרק 28 הוא התשובה האחרת: תיקייה עם SKILL.md בתוכה שהמודל קורא במקום לקרוא לה, נטענת בשלוש רמות כך שחומר הייחוס כמעט לא עולה דבר עד ה-turn שבו הוא נחוץ. אין לה שפה ראשית, וזה הדבר הראשון שהיא מלמדת.


כל מה שכאן נמדד ב-7 בספטמבר 2026, על Node 22.22.3 ו-Python 3.14.4, מול @modelcontextprotocol/sdk 1.30.0 עם zod 3.25.76 ו-mcp 2.1.1, כל אחד מותקן לתיקייה חד-פעמית משלו. זמני הריצה הם חציונים של 25 launches, wall clock מ-spawn עד השורה שנושאת את תגובת tools/list; ספירות token הן o200k_base דרך tiktoken על ה-JSON של כל definition. לא נקרא API בתשלום: שום דבר כאן לא צריך מודל.

שני השרתים הם 81 ו-63 שורות לא ריקות; אחד משלושת הכלים שלהם משוחזר למעלה בשתי השפות, וארבעת הרישומים האחרים שונים רק כפי שתואר. מדיניות גילוי השגיאות של ה-SDK ב-Python מצוטטת מתוך ה-docstrings של ToolError ו-UnexpectedToolError ב-mcp/server/mcpserver/exceptions.py; ברירת המחדל של pretty-printing היא pydantic_core.to_json(result, fallback=str, indent=2) ב-mcp/server/mcpserver/resources/types.py ו-utilities/func_metadata.py. קבועי גרסת הפרוטוקול הם LATEST_PROTOCOL_VERSION ב-mcp_types/version.py וב-types.js של ה-SDK ב-TypeScript, שניהם נקראו מה-packages המותקנים ולא מ-changelog.

  1. SDKs, modelcontextprotocol.io/docs/sdk, ו-Build an MCP server, modelcontextprotocol.io/docs/develop/build-server, שניהם נקראו ב-7 בספטמבר 2026. מקור טבלת ה-tier, המשפט ״Each SDK provides the same functionality but follows the idioms and best practices of its language״, סדר לשוניות השפה במדריך (Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go), וכלל ה-logging שצוטט על print() ו-stdout. 2 3 4

  2. stdio transport, .../basic/transports/stdio. מקור ה-newline framing וכלל הטוהר של stdout. פרק 26 קורא את העמוד הזה במלואו; הוא מצוטט כאן עבור השורה שהשרת השבור מפר.

  3. MCP Inspector, modelcontextprotocol.io/docs/2026-07-28/tools/inspector, נקרא ב-7 בספטמבר 2026. package אחד, שלושה clients מאחורי binary אחד — web, --cli ו---tui — חולקים core אחד, סט transports אחד ומצב OAuth אחד על הדיסק. ה-CLI הפיק כאן את עקבות הקטלוג.

  4. Authorization, modelcontextprotocol.io/specification/2026-07-28/basic/authorization, נקרא ב-7 בספטמבר 2026. מקור תפקיד ה-resource-server; ארבעת סעיפי טיפול ה-token המצוטטים במלואם; הדרישה ששרתים יממשו RFC 9728 וש-clients ישתמשו בו ל-discovery; כללי הפרמטר resource והגדרת ה-URI הקנוני; טבלת אימות ה-issuer; ה-deprecation של Dynamic Client Registration; טבלת 401/403/400 ו-challenge ה-insufficient_scope; ופטור ה-stdio, ״Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.״ 2 3 4

  5. Streamable HTTP, .../basic/transports/streamable-http, ו-Transports overview, .../basic/transports. מקור כלל ה-POST ל-endpoint יחיד, דרישת ה-Accept הכפולה, ה-header MCP-Protocol-Version וכלל החובה שלו להתאים ל-body, ה-headers Mcp-Method ו-Mcp-Name שמתוארים כ-״REQUIRED for compliance״, הסרת ה-GET stream, sessions ו-Last-Event-ID, הנחיית 405, אימות ה-Origin המחייב, והסיווג של transport HTTP+SSE מ-2024-11-05 כ-Deprecated תחת SEP-2596. 2 3 4

  6. הארבעה שעליהם המפרט נשען, עם הטיוטה שהוא יוצר לה פרופיל: The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-13. Campbell, B., Bradley, J. and Tschofenig, H., Resource Indicators for OAuth 2.0, RFC 8707, February 2020 — פרמטר ה-resource וה-audience שהוא קושר. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata, RFC 9728, April 2025 — המסמך ש-401 מצביע אליו. Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification, RFC 9207, March 2022 — פרמטר ה-iss והשוואת המחרוזת המדויקת. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol, RFC 7591, July 2015, deprecated לשימוש הזה. וגם Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750, October 2012, section 3, עבור צורת ה-challenge של WWW-Authenticate למעלה.

  7. ה-registry הרשמי של MCP, registry.modelcontextprotocol.io/v0/servers, נסרק ב-7 בספטמבר 2026 עם version=latest: 282 עמודים, 28,170 שרתים, נספרו על ידי registryType על פני שמות שרתים מובחנים. נתוני הורדות: api.npmjs.org/downloads/point/last-month עבור @modelcontextprotocol/sdk (194,679,333 עבור 8 באוגוסט – 6 בספטמבר 2026) ו-pypistats.org/api/packages/<name>/recent עבור mcp ו-fastmcp, שניהם נקראו באותו יום. גדלי package מגיעים ממסמך ה-registry של npm ומ-PyPI JSON API. 2

  8. MCP Course, Hugging Face, huggingface.co/learn/mcp-course, unit 0, נקרא ב-7 בספטמבר 2026: בין הדרישות המקדימות, ״Experience with at least one programming language (Python or TypeScript examples will be shown)״.


נוצר על ידי

David Vicente Campos

מייסד NeuraLIA Labs ושותף-מייסד MyRealFood

אני מהנדס מחשבים, בוגר אוניברסיטת לאון. הייתי שותף בהקמת MyRealFood, שם, כסמנכ״ל טכנולוגיות, בניתי את האפליקציה שמיליוני אנשים השתמשו בה כדי לאכול בריא יותר, והקמתי את NeuraLIA Labs, שם אני בונה מוצרי בינה מלאכותית. כאן אני כותב על מה שהייתי צריך להבין לאורך הדרך, כפי שהייתי רוצה שמישהו היה מסביר לי בזמנו.

עוד על המחבר

פורסם על ידי NeuraLIA Labs.

פוסטים חדשים ישירות לתיבת הדואר

חדשות AI, מדריכים ועדכוני מוצר — מייל קצר כשאנחנו מפרסמים משהו ששווה את הזמן שלך.

תוכן הקורס

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 דקות קריאה

מודל ה-AI Jev נבנה להחלטות, לא לפרוזה

Jev של TypeSafe AI מושך תשומת לב כי הוא מתייחס לאינטליגנציית תוכנה כאל בעיית הסתברות: לבחור את ההסתעפות הנכונה, להצמיד ביטחון, ולהימנע מתשלום ל-LLM כדי שיכתוב טקסט כשהקוד צריך החלטה.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering10 דקות קריאה

הנדסת הקשר לסוכני AI ארוכי־טווח

סוכנים שרצים לאורך זמן לא נכשלים רק כי החלון קטן. הם נכשלים כשקבצים, פלטי כלים והיסטוריה מיושנת דוחקים החוצה את המשימה שהסוכן היה אמור להשלים.

מוכנים לתת ל-LIA לבחור?

בנו עם כל מודלי ה-AI במקום אחד — התחילו בחינם עוד היום.