إطلاق خادم MCP: TypeScript وPython بالأرقام
الخادم نفسه مكتوب مرتين: ثلاث أدوات ومورد وprompt، ثم قياسه: 94 حزمة مقابل 28، وبدء بارد 145 ms مقابل 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 الخام لا يملك لغة. هذا الفصل يملك لغتين، وثقل الحجة يقع هنا: الخادم نفسه، مكتوب مرتين. ثلاث أدوات، مورد واحد، prompt واحد، كلا SDKين، بلا اختصارات في أي طرف. ثم وسائل النقل، وInspector، و401، والأرقام التي لم ينشرها أحد.
الخادم، ولماذا يحتوي على هذه الأشياء الخمسة
رابط إلى القسم: الخادم، ولماذا يحتوي على هذه الأشياء الخمسةسجل حوادث. ثلاث أدوات، لأن فصل الفصل 18 بين القراءة والكتابة يجب أن يكون مرئيًا: search_incidents يقرأ، وopen_incident يكتب ويعيد handle، وresolve_incident يأخذ ذلك handle ويغلق. مورد واحد، incidents://open، لأن قراءة القائمة الحالية شيء تُلحقه التطبيقات. وprompt واحد، postmortem، لأن «اكتب هذا بصيغة تقرير» هو أمر slash لشخص. ذلك هو تسلسل التحكم في الفصل 26 — model، application، person — وقد تحوّل إلى خمسة تسجيلات.
الـhandle أهم مما يبدو. كسر الفصل 26 تقويمًا بسيطًا لأنه أبقى حالته في مصفوفة على مستوى module: البروتوكول لا يملك session، لذلك تعيد أداة الإنشاء معرفًا opaque وكل استدعاء لاحق يأخذه كوسيط عادي. لا شيء في أي من الملفين يفترض أن المستدعي هو العملية التي فتحته.
إليك الأداة نفسها في اللغتين، مسجلة جنبًا إلى جنب:
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.")اقرأ ما هو متماثل أولًا، لأن هذه هي النتيجة. كلاهما يعلن اسمًا ووصفًا ووسيطين نصيين موصوفين وثلاث annotations؛ كلاهما function واحدة؛ ولا يذكر أي منهما JSON-RPC أو framing أو stdout أو إصدار بروتوكول. تقارب SDKان على الشكل نفسه، وهذا ما يفترض أن يعنيه «Tier 1».1
هناك فرقان حقيقيان وكلاهما يعود لاحقًا. يصف TypeScript الوسائط بمكتبة schema — Zod هنا — والـschema قيمة تكتبها. أما Python فيصفها عبر type hints الخاصة بالfunction نفسها ويقرأها وقت الاستيراد، ولهذا يعرف أشياء عن الfunction لم يخبره بها ملف TypeScript قط. ومسار الخطأ: يعيد TypeScript نتيجة أداة مع isError، بينما يرفع Python استثناء. احتفظ بهذه النقطة.
التسجيلات الأربعة الأخرى لا تختلف في أي بنية. المورد هو server.registerResource("open-incidents", "incidents://open", …) مقابل @server.resource("incidents://open", …)؛ والـprompt هو registerPrompt مقابل @server.prompt. السطر الأخير في كل ملف هو النقل: await server.connect(new StdioServerTransport()) مقابل server.run().
الملفان كاملان: 81 سطرًا غير فارغ و3,060 بايت من TypeScript مقابل 63 و2,555. خذ ذلك بالقدر المستحق من التحفظ — عدد الأسطر يقيس formatter بقدر ما يقيس لغة، ولهذا لا يظهر أي من الرقمين في جدول العناوين أدناه.
عميل واحد، خادمان
رابط إلى القسم: عميل واحد، خادمانالدليل على أن اللغة غير مرئية هو تشغيل عميل واحد مرتين، في أحد عشر سطرًا:
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 نفسه. لا يستطيع عميل TypeScript أن يعرف بماذا كُتب الخادم، ولا يسأل أبدًا. هذا هو وعد البروتوكول كاملًا، وقد صمد.
انظر الآن إلى المسافات البيضاء في النتيجة الثانية، لأنها ليست تجميلية: SDK الخاص بـPython يسلسل payloads باستخدام pydantic_core.to_json(result, fallback=str, indent=2). عند قراءة المورد وفي القائمة حادثتان، يبلغ جسم TypeScript 136 حرفًا و37 token من o200k_base؛ أما جسم Python فـ185 و62. tokens أكثر بنسبة ثمانية وستين في المئة للصفوف نفسها، يدفع ثمنها كل من يقرأ المورد داخل prompt، كل مرة.
يحكي الفهرس القصة نفسها ولكن بسبب أكبر. كلا الخادمين، الأدوات الثلاث نفسها، وtools/list موزون مفتاحًا بمفتاح:
| المفتاح | TypeScript | Python |
|---|---|---|
name | 21 | 21 |
description | 46 | 46 |
annotations | 46 | 46 |
inputSchema | 211 | 192 |
outputSchema | — | 187 |
execution | 27 | — |
| الإجمالي | 342 | 480 |
Schemas الإدخال في Python أرخص — جسر Zod في TypeScript يطبع $schema وadditionalProperties على كل واحدة. الفجوة الكاملة البالغة 138 token هي schema خرج لم يكتبها أحد. resolve_incident معلّمة بـ-> Incident، لذلك اشتق SDK JSON Schema لنوع الإرجاع وشحنها. هذا مفيد فعلًا — فهو ما يتيح للعميل التحقق من structuredContent — لكنه 187 token من context window تصل لأن هناك type hint. تنطبق قاعدة الفصل 24 عن مزاحمة التعريفات للمادة المهمة على schemas لم تكن تعرف أنك تملكها.
اكسره عمدًا: رسالة الخطأ التي تسرّبت
رابط إلى القسم: اكسره عمدًا: رسالة الخطأ التي تسرّبتمسارا الخطأ أعلاه ليسا خيار أسلوب. أعطِ كل خادم أداة تفشل كما تفشل integration حقيقية، واقرأ ما يصل إلى model.
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 عنوانًا داخليًا ومنفذًا واسم قاعدة بيانات وحساب خدمة داخل سياق model. أما SDK الخاص بـPython فلم يضع شيئًا من ذلك هناك؛ ذهب traceback إلى stderr وبقي على الخادم.
ليس أي منهما bug. كلاهما قرار، وقرار Python مكتوب في docstring الخاصة به: إن ToolError هو «فشل توقّعته» وتُعاد رسالته «في content ليقرأها model»؛ وأي شيء آخر «يُعامل كانهيار: لا يرى model إلا Error executing tool <name>، ويسجل الخادم traceback عند ERROR». وتقول class الخاصة بحالة الانهيار الباقي صراحة — «لا يصل شيء من الأصل إلى العميل».
كلا السلوكين خاطئ نصف الوقت. جادل الفصل 18 بأن خطأ التحقق ينبغي أن يعود كنتيجة أداة يمكن لـmodel قراءتها وتصحيحها، لأن ذلك هو السطر الأعلى أثرًا في معظم integrations؛ على جانب Python يتطلب ذلك رفع ToolError صراحة، وValueError عارٍ يرمي الجملة المفيدة بعيدًا. تسير حجة الفصل 30 في الاتجاه المعاكس: كل ما تعيده أداة يهبط في سياق يستطيع 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، لذلك يُflush السطر الشارد عندما يقرر buffer — هنا، عند الخروج، بعد رد كُتب قبله. لا يظهر الفساد في المكان الذي توجد فيه bug. أضف flush=True، أو مكتبة تعمل flush، فينتقل.
ثم الجزء الذي يفسر لماذا يصل هذا إلى الإنتاج. أرسل الخادم المكسور إلى ثلاثة عملاء:
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 ذو السبعة أسطر فورًا. أما العميل الرسمي وInspector فيتجاهلان الأمر — يتخطيان السطر ويتابعان. القاعدة التي لا تكسر إلا العملاء الذين لا يستخدمهم أحد هي قاعدة تصل إلى الإنتاج سليمة، ولهذا يستحق الأمر كسرها عمدًا هنا بدلًا من سجل عميل.
وضع CLI في Inspector هو النصف الذي يُنسى: npx @modelcontextprotocol/inspector --cli <command> --method tools/list يطبع فهرسًا ويخرج، ما يجعله قابلًا للبرمجة بطريقة لا تتيحها واجهة المتصفح.3
الجدول
رابط إلى القسم: الجدولتثبّت كلا SDKين نظيفًا، كل في دليله، بلا أي شيء مشترك:
| TypeScript | Python | |
|---|---|---|
| الحزمة | @modelcontextprotocol/sdk 1.30.0 + zod 3.25.76 | mcp 2.1.1 |
| أحدث مراجعة بروتوكول منفّذة | 2025-11-25 | 2026-07-28 |
| الحزم المتعدية المثبتة | 94 | 28 |
| الحجم المثبت | 13.9 MiB | 44.3 MiB |
| الملفات على القرص | 3,386 | 2,018 |
| حزم الطرف الثالث المحمّلة لخدمة stdio | 8 من 94 | 18 من 28 |
| بدء المفسر العاري، الوسيط | 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 dependency هي منظومة npm وهي تتصرف على طبيعتها — fast-deep-equal، es-errors، dunder-proto. أما 28 الخاصة بـPython فهي أقل عددًا وضخمة: cryptography وpydantic-core وuvicorn artifacts مترجمة. إن كان حدسك أن عدد dependencies هو ما ينبغي القلق منه، فهذا الصف هو المثال المضاد.
يبدأ مفسر 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 يستورد خادم ويب ASGI وعميل HTTP ومكتبة TLS قبل أن يقرأ أول سطر. يشحن SDK الخاص بـTypeScript أيضًا Express وHono وjose وeventsource — لكنها تبقى على القرص غير مقروءة، لأن حدود الحزمة تبقيها خارج import server/stdio.js. حزمة Python عبارة عن import graph واحد، لذلك import mcp هو كل شيء: ينسب python -X importtime 727 ms إلى import mcp.server.mcpserver — رقم مقاس تحت import profiler، ولهذا يأتي أعلى من 709 ms التي تستغرقها الجولة غير المراقبة من spawn حتى الإجابة — و269 منها إلى subtree mcp.types وحدها — أنواع wire هي models من Pydantic، class واحد لكل رسالة بروتوكول لكل revision، وبناؤها عمل يُنجز وقت الاستيراد. هذه مقايضة تصميم، لا إهمال — imports المبكرة هي سبب قدرة SDK الخاص بـPython على إعطائك run(transport="streamable-http") في السطر التالي بلا تثبيت ثانٍ.
ثم يأتي الصف الأخير من الكتلة الافتتاحية لينقض الحجة. حزّم خادم TypeScript كما ينبغي — مدخل 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 — وتُدفع في كل تشغيل، لأن مضيف MCP يبدأ خادم stdio بتشغيل ذلك الأمر. لذلك فالصيغة الصادقة لعبارة «TypeScript يبدأ أسرع بخمس مرات» هي: نعم، إلى أن توزّعه بالطريقة المعتادة. ينطبق التحفظ نفسه على الأرجح على uvx؛ لم يكن على هذا الجهاز uv مثبتًا، لذلك لا يوجد ذلك الصف. لا يدخل الجدول شيء غير مقاس.
وسيلتا نقل، واثنتان فقط
رابط إلى القسم: وسيلتا نقل، واثنتان فقطغطى الفصل 26 framing الخاص بـstdio. ترك شيئين لهذا الموضع.
الأول: تشغيل خادم باستخدام npx أو uvx هو نقل stdio. لا يوجد «وضع حزمة» منفصل. تسمّي إعدادات المضيف command وarguments؛ يشغّله المضيف ويتحدث عبر pipes. ولهذا فإن «كيف أوزع هذا» و«أي transport يتكلم» هما سؤال واحد محليًا، ولهذا تنتمي تكلفة launcher إلى فصل عن الشحن.
الثاني: لا يملك stdio قسم تفويض إطلاقًا، وتقول المواصفة ذلك في سطر واحد — implementations التي تستخدم stdio «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 نموذجه الأمني هو نموذج نظام التشغيل، وكذلك حدّه: subprocess محلي يخدم جهازًا واحدًا ومستخدمًا واحدًا.
النقل الحي الآخر هو Streamable HTTP: endpoint واحد يقبل POST، وطلب HTTP واحد لكل رسالة JSON-RPC، وheader Accept يجب أن يدرج كلًا من application/json وtext/event-stream لأن الخادم يختار لكل طلب أيهما يجيب به.5 حلّل الفصل 14 ذلك event stream يدويًا، لذلك لا جديد في wire format — الجديد فقط ما يغلّفه. ثلاث التزامات في المراجعة الحالية يسهل تفويتها وكلها قابلة للاختبار:
يجب أن يتفق header الإصدار مع الجسم
رابط إلى القسم: يجب أن يتفق header الإصدار مع الجسميحمل كل POST MCP-Protocol-Version، ويجب أن تطابق قيمته protocolVersion داخل _meta الخاصة بالطلب نفسه. عدم التطابق هو 400 مع خطأ header-mismatch، لا تجاهل.5
يلزم headerان آخران للامتثال
رابط إلى القسم: يلزم headerان آخران للامتثالMcp-Method يعكس method في كل طلب؛ وMcp-Name يعكس params.name أو params.uri على tools/call وresources/read وprompts/get. وُجدا حتى يستطيع proxy التوجيه بلا parsing للأجسام.5
الأشكال القديمة اختفت، وتجيب بالرفض
رابط إلى القسم: الأشكال القديمة اختفت، وتجيب بالرفضأُزيلت كل من GET stream وMcp-Session-Id وLast-Event-ID resumption. ينبغي لخادم لا يتكلم إلا هذه المراجعة أن يجيب 405 Method Not Allowed على GET أو DELETE، ويتجاهل session header بلا minting لواحد، ويتجاهل Last-Event-ID.5
والآن القياس الذي يعيد تأطير الفصل كله. أرسل طلب مراجعة حالية إلى كل خادم عبر 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، ويقرأ الخاص بـTypeScript 2025-11-25. أرسل طلب header-mismatch من الخطوة أعلاه فيجيب خادم Python بـ400 مع الخطأ -32020 والرسالة «mcp-protocol-version header does not match the request envelope's protocol version»؛ أما SDK الخاص بـTypeScript فلا يملك مثل هذا الكود، لأنه لا ينفّذ المراجعة التي تعرّفه.
تقول الصفحة التي تدرج الاثنين في Tier 1 أيضًا «Each SDK provides the same functionality».1 في التاريخ أدناه، وبالنسبة للمراجعة الحالية، هذه الجملة طموحية. افحص LATEST_PROTOCOL_VERSION في SDK الذي توشك على تثبيته؛ إنه سطر واحد، والادعاء الوحيد في هذا الفصل الذي سيظل مهمًا بعد عام.
401، والجملة التي ينبغي اقتباسها
رابط إلى القسم: 401، والجملة التي ينبغي اقتباسهاانقل خادمًا خارج حاسوبك المحمول وسيظهر عميل غريب ومعه token. هذا هو النصف الذي تركه الفصل 26 جانبًا والنصف الذي لا يستطيع منتج متعدد المستخدمين تخطيه.
تضع المواصفة خادم MCP في دور OAuth 2.1 وتسميه: خادم MCP المحمي هو resource server، والعميل هو 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 هو كيف يجد العميل authorization server أصلًا: يخدم خادم MCP مستند protected-resource-metadata ويشير إليه 401. RFC 8707 هو parameter resource — يجب على العميل إرسال URI القانونية للخادم في كل من طلب التفويض وطلب token، «سواء كانت authorization servers تدعمه أم لا»، حتى يسمّي token الصادر جمهوره. RFC 9207 يغلق الحلقة من الجهة الأخرى: يسجل العميل issuer قبل redirect ويقارن iss العائد كسلسلة مطابقة تمامًا، بلا normalisation — لا case folding، ولا حذف default-port، ولا trailing slash. وRFC 7591، Dynamic Client Registration، أصبح الآن deprecated لصالح Client ID Metadata Documents، «محفوظًا للتوافق الخلفي مع authorization servers التي لا تدعمها».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"}يخدم كلا SDKين ذلك المستند ويشير كلاهما بـ401 إليه، وهذه هي قصة discovery كاملة: عميل لم ير خادمك قط يتعلم أين يتوثق من رفض. أما 403 فهو كائن مختلف — token سليم، لكن scope ليست كذلك — ويسمي challenge ما ينقص حتى يستطيع العميل الترقّي بدل البدء من جديد.
يختلف درجتان، ولا يوجد أي من الاختلافين في المواصفة. يرفض SDK الخاص بـTypeScript token بلا expiry claim؛ أما Python فيعيد 200، لأن expires_at اختياري على AccessToken لديه وNone تعني «لا رأي». كما يحمل 403 في Python error_description="Required scope: incidents:read" بلا parameter scope الذي تقول المواصفة إن على الخوادم تضمينه. لا مكان في verifier لقبول default مكتبة: فحص audience عليك كتابته في أي لغة، وكذلك expiry.
ملاحظة صغيرة صادقة من التشغيل نفسه. أجاب GET على endpoint بـ404 في توصيل Express وبـ400 Bad Request: Missing session ID في Python، حيث تطلب المواصفة 405 Method Not Allowed وحيث إن «session ID» مفردات أزالتها هذه المراجعة. لا يشكل أي منهما خطرًا؛ كلاهما شكل منظومة في منتصف الهجرة.
أين تعيش الخوادم فعليًا
رابط إلى القسم: أين تعيش الخوادم فعليًاآخر قطعة في الشحن هي أين تنشر، ولها إجابة برقم. جرى الزحف اليوم إلى كل خادم في السجل الرسمي عند أحدث إصدار له:7
| الخوادم | |
|---|---|
| الإجمالي، أحدث إصدار وغير محذوف | 28,170 |
| نشط / deprecated | 27,853 / 317 |
| يشحن حزمة واحدة قابلة للتثبيت على الأقل | 13,065 |
| remote فقط — URL ولا شيء يُثبّت | 14,696 |
| npm | 8,275 |
| PyPI | 3,603 |
| OCI images | 867 |
حزم 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 أي نصف من ذلك كان في ذهنك، فالنصف الآخر صحيح أيضًا.
والصف الأهم من كليهما: أكثر من نصف السجل — 14,696 من 28,170 — لا يملك شيئًا يُثبّت. تلك خدمات ويب. وتتفق إحصاءات transport من الجهة الأخرى: من بين 14,290 إدخال حزمة، يعلن 13,787 عن stdio؛ ومن بين 16,640 إدخال remote، يعلن 15,570 عن Streamable HTTP و1,070 ما زالت تعلن HTTP+SSE deprecated. لذلك فعبارة «خادم MCP هو subprocess على حاسوبك المحمول» تصف أقلية آخذة في الانكماش، وكل واحد من الـ14,696 يحتاج القسم أعلاه لا environment variable.
عرض التفاصيل
ثنائي اللغة عمدًا، والسابقة لذلك.
هذا هو الفصل الوحيد ثنائي اللغة في الدورة، لأن الجواب الصادق ينقسم: السجل npm-first والتنزيلات Python-first، في الوقت نفسه، اليوم. كتابة واحدة من اللغتين ستتنازل عن نصف السؤال وتسيء وصف المنظومة أثناء ذلك. هناك سابقة علنية — تدرج Hugging Face MCP Course ضمن متطلباتها «Experience with at least one programming language (Python or TypeScript examples will be shown)»، وتدرّس الاثنين.8 البروتوكول الذي تتمثل قيمته كلها في عدد implementations مكان سيئ لأن يكون أحادي اللغة.
قسم مؤرخ: كل ما سبق له مدة صلاحية
رابط إلى القسم: قسم مؤرخ: كل ما سبق له مدة صلاحيةقُرئ وقيس في 7 سبتمبر 2026، مقابل مراجعة البروتوكول 2026-07-28.
| القيمة | |
|---|---|
@modelcontextprotocol/sdk | 1.30.0، نُشر في 27 يوليو 2026؛ 4,322,438 بايت بعد الفك، 693 ملفًا، 17 dependency مباشرة |
| أحدث مراجعة ينفذها | 2025-11-25 |
mcp (PyPI) | 2.1.1، نُشر في 25 أغسطس 2026؛ wheel بحجم 357,912 بايت، إضافة إلى mcp-types 2.1.1 عند 69,656 بايت |
| أحدث مراجعة ينفذها | 2026-07-28 |
| طبقات SDK | TypeScript وPython وC# وGo وRust في Tier 1؛ Java وRuby في Tier 2؛ Swift وPHP وKotlin في Tier 3 |
| خوادم السجل | 28,170 |
| التنزيلات، آخر 30 يومًا | mcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333 |
ملاحظة هجرة واحدة ليست رقمًا. في mcp 2.x، أُعيدت تسمية FastMCP إلى MCPServer، وما زال معظم الدروس على الإنترنت تقريبًا يفتح بالاستيراد القديم. يشحن SDK module هدفها الوحيد شرح ذلك، وهو ألطف 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.إذن أيهما
رابط إلى القسم: إذن أيهمامع الجدول أمامك، التوصية مملة، وهذه علامة جيدة.
إذا كان الخادم يعيش داخل تطبيق ويب تشغّله بالفعل، فاكتبه بـTypeScript. العملية نفسها، والـdeploy نفسه، وrequest handler نفسه؛ Streamable HTTP هو endpoint تضيفه بجانب الآخرين؛ و13.9 MiB و145 ms مجانيان لأن runtime كان يعمل أصلًا. هذا هو حال معظم الخوادم remote البالغ عددها 14,696.
إذا كان الخادم يغلف أدوات بيانات، فاكتبه بـPython. ما تكشفه هو pandas، وعميل warehouse، وتحويلات بحجم notebook، والخادم بلغة أخرى سيكون subprocess call يرتدي schema. سبعمئة ميلي ثانية من import في خدمة تبدأ مرة واحدة ليست كلفة؛ في subprocess يعيد المضيف تشغيله طوال اليوم، هي كلفة.
وفي الوقت الحالي، يتفوق صف المراجعة على الاثنين. إذا احتجت 2026-07-28 — طلبات متعددة الرحلات، وresultType، وcache hints، وserver/discover — فأحد SDKين يملكها اليوم والآخر لا.
إلى أين يذهب هذا بعد ذلك
رابط إلى القسم: إلى أين يذهب هذا بعد ذلكيمكنك الآن شحن الخادم نفسه بأي من اللغتين، والدفاع عن الاختيار بجدول بدل تفضيل، وتشغيله فوق كلا وسيلتي النقل الحيتين، وتسليمه token سيرفضه.
ما بنيته ما زال function: schema، وendpoint، وشيء deterministic يستدعيه model. هناك فئة كاملة من المعرفة لا تلائم هذا الشكل — كيف نكتب نحن postmortem، وما الحقول التي تحتاجها تقارير الحوادث لدينا، والترتيب الذي نفعل به الأشياء ولماذا. إنها procedure، وهي prose، وإجبارها على دخول وصف أداة هو الطريقة التي تنمو بها system prompts إلى ألفي token مدفوعة في كل turn مفرد سواء كانت المحادثة عن الحوادث أم لا.
الفصل 28 هو الجواب الآخر: مجلد فيه SKILL.md يقرأه model بدلًا من استدعائه، محمّلًا على ثلاثة مستويات بحيث لا تكلف المادة المرجعية شيئًا تقريبًا حتى 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 تشغيلًا، wall clock من spawn إلى السطر الحامل لرد tools/list؛ وعدد tokens هو o200k_base عبر tiktoken على JSON لكل تعريف. لم تُستدعَ أي API مدفوعة: لا يحتاج أي شيء هنا إلى model.
الخادمان يبلغان 81 و63 سطرًا غير فارغ؛ أُعيد إنتاج واحدة من أدواتهما الثلاث أعلاه باللغتين، ولا تختلف التسجيلات الأربعة الأخرى إلا كما وُصف. سياسة كشف الأخطاء في SDK الخاص بـPython مقتبسة من docstrings الخاصة بـToolError وUnexpectedToolError في mcp/server/mcpserver/exceptions.py؛ وdefault الخاص بـpretty-printing هو pydantic_core.to_json(result, fallback=str, indent=2) في mcp/server/mcpserver/resources/types.py وutilities/func_metadata.py. ثوابت protocol-version هي LATEST_PROTOCOL_VERSION في mcp_types/version.py وفي types.js الخاصة بـSDK TypeScript، وكلاهما قُرئ من الحزم المثبتة لا من changelog.
المراجع
رابط إلى القسم: المراجع-
SDKs،
modelcontextprotocol.io/docs/sdk، وBuild an MCP server،modelcontextprotocol.io/docs/develop/build-server، وكلاهما قُرئ في 7 سبتمبر 2026. مصدر جدول tiers، وجملة «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. مصدر framing بأسطر جديدة وقاعدة نقاءstdout. يقرأ الفصل 26 هذه الصفحة كاملة؛ وتُذكر هنا للسطر الذي يخالفه الخادم المكسور. ↩ -
MCP Inspector،
modelcontextprotocol.io/docs/2026-07-28/tools/inspector، قُرئ في 7 سبتمبر 2026. حزمة واحدة، وثلاثة عملاء خلف binary واحد — web و--cliو--tui— تشترك في core واحد، ومجموعة transports واحدة، وحالة OAuth واحدة على القرص. أنتج CLI آثار الفهرس هنا. ↩ -
Authorization،
modelcontextprotocol.io/specification/2026-07-28/basic/authorization، قُرئ في 7 سبتمبر 2026. مصدر دور resource-server؛ والفقرات الأربع المتعلقة بالتعامل مع token والمقتبسة كاملة؛ واشتراط أن تنفذ الخوادم RFC 9728 وأن يستخدمه العملاء للاكتشاف؛ وقواعد parameter resourceوتعريف canonical-URI؛ وجدول issuer-validation؛ و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المزدوج، وheader MCP-Protocol-Versionوقاعدته التي تشترط مطابقة الجسم، و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 -
الأربعة التي تستند إليها المواصفة، مع المسودة التي تفصلها: 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 — parameter 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 — parameter 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، القسم 3، لشكل challenge WWW-Authenticateأعلاه. ↩ -
سجل 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، وكلها قُرئت في اليوم نفسه. أحجام الحزم من مستند npm registry وPyPI JSON API. ↩ ↩2 -
MCP Course، Hugging Face،
huggingface.co/learn/mcp-course، الوحدة 0، قُرئ في 7 سبتمبر 2026: ضمن المتطلبات، «Experience with at least one programming language (Python or TypeScript examples will be shown)». ↩