השקת שרת MCP: TypeScript ו-Python, במדידה
אותו שרת נכתב פעמיים — שלושה כלים, resource ו-prompt — ואז נמדד: 94 חבילות מול 28, ו-cold start של 145ms מול 709.
בעמוד הזה
הנה כל הוויכוח על השפה, במדידה, לפני שנאמרה עליו מילה.
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, ולכן כלי יצירה מחזיר מזהה אטום וכל קריאה מאוחרת יותר מקבלת אותו כארגומנט רגיל. שום דבר באף אחד מהקבצים לא מניח שהקורא הוא התהליך שפתח אותו.
הנה אותו כלי בשתי השפות, רשום זה לצד זה:
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 }) }] };
},
);@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 אחד, שני שרתיםההוכחה שהשפה בלתי נראית היא הרצה אחת של client פעמיים, באחת-עשרה שורות:
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));כוונו אותו לכל שרת בתורו. פלט אמיתי, מקוצר:
$ 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 נשקל מפתח-מפתח:
| key | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| total | 342 | 480 |
ה-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 שלא ידעתם שיש לכם.
לשבור בכוונה: הודעת השגיאה שדלפה
קישור למקטע: לשבור בכוונה: הודעת השגיאה שדלפהשני מסלולי השגיאה למעלה אינם בחירת סגנון. תנו לכל שרת כלי שנכשל כמו שאינטגרציה אמיתית נכשלת, וקראו מה מגיע למודל.
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 הגולמי:
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:
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 הותקנו נקי, כל אחד לתיקייה שלו, בלי שום דבר משותף:
| TypeScript | Python | |
|---|---|---|
| package | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| גרסת הפרוטוקול האחרונה שמיושמת | 2025-11-25 | 2026-07-28 |
| חבילות transitive שהותקנו | 94 | 28 |
| גודל מותקן | 13.9 MiB | 44.3 MiB |
| קבצים בדיסק | 3,386 | 2,018 |
| חבילות צד שלישי שנטענו כדי לשרת stdio | 8 מתוך 94 | 18 מתוך 28 |
| הפעלת interpreter חשוף, חציון | 19.4 ms | 11.1 ms |
spawn → tools/list נענה, חציון מתוך 25 | 144.5 ms | 709.4 ms |
קטלוג tools/list, tokens של o200k_base | 342 | 480 |
כל שורה מפתיעה בכיוון אחר, ולכן כדאי להריץ את ההשוואה ולא להניח.
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, ושורת החבילות הנטענות מסבירה למה:
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 מפורסם באמת מתחיל:
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 מותקן, ולכן השורה הזו לא קיימת. שום דבר שלא נמדד לא נכנס לטבלה.
שני transports, ורק שניים
קישור למקטע: שני transports, ורק שנייםפרק 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 נוספים נדרשים לצורך complianceMcp-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.
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 שאתם עומדים להתקין; זו שורה אחת, והטענה היחידה בפרק הזה שעוד תהיה חשובה בעוד שנה.
ה-401, והמשפט שצריך לצטט
קישור למקטע: ה-401, והמשפט שצריך לצטטהעבירו שרת מהלפטופ שלכם ו-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:
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":[…]}}{"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 |
| פעיל / deprecated | 27,853 / 317 |
| מפיץ לפחות package אחד ניתן להתקנה | 13,065 |
| remote בלבד — URL, בלי מה להתקין | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
bundles של mcpb | 706 |
| NuGet / Cargo | 107 / 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/sdk | 1.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 tiers | TypeScript, Python, C#, Go, Rust ב-Tier 1; Java, Ruby ב-Tier 2; Swift, PHP, Kotlin ב-Tier 3 |
| שרתי registry | 28,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 המתחשב ביותר בפרק הזה:
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.
הפניות
קישור למקטע: הפניות-
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 -
stdio transport,
.../basic/transports/stdio. מקור ה-newline framing וכלל הטוהר שלstdout. פרק 26 קורא את העמוד הזה במלואו; הוא מצוטט כאן עבור השורה שהשרת השבור מפר. ↩ -
MCP Inspector,
modelcontextprotocol.io/docs/2026-07-28/tools/inspector, נקרא ב-7 בספטמבר 2026. package אחד, שלושה clients מאחורי binary אחד — web,--cliו---tui— חולקים core אחד, סט transports אחד ומצב OAuth אחד על הדיסק. ה-CLI הפיק כאן את עקבות הקטלוג. ↩ -
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 -
Streamable HTTP,
.../basic/transports/streamable-http, ו-Transports overview,.../basic/transports. מקור כלל ה-POST ל-endpoint יחיד, דרישת ה-Acceptהכפולה, ה-headerMCP-Protocol-Versionוכלל החובה שלו להתאים ל-body, ה-headersMcp-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 -
הארבעה שעליהם המפרט נשען, עם הטיוטה שהוא יוצר לה פרופיל: 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למעלה. ↩ -
ה-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 -
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)״. ↩