پرش به محتوا
27/30فصل 27 از 30

انتشار یک MCP Server: TypeScript و Python، با اندازه‌گیری واقعی

یک server یکسان، دو بار نوشته شد: سه tool، یک resource، یک prompt. 94 package در برابر 28 و cold start 145 ms در برابر 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 server خط اول است، اما به همان شکلی اجرا شده که واقعاً توزیع می‌شود — و فقط سه میلی‌ثانیه با Python فاصله دارد.

فصل 26 Model Context Protocol را با مشخصات خودش و با JSON-RPC خام خواند، چون JSON-RPC خام زبان ندارد. این فصل دو زبان دارد، و وزن استدلال همین‌جاست: همان server، دو بار نوشته‌شده. سه tool، یک resource، یک prompt، هر دو SDK، بدون میان‌بر در هیچ سمت. بعد transportها، inspector، خطای 401، و عددهایی که هیچ‌کس منتشر نکرده است.

Server، و این‌که چرا این پنج چیز را در خود دارد

لینک به بخش: Server، و این‌که چرا این پنج چیز را در خود دارد

یک incident log. سه tool، چون جداسازی خواندن و نوشتن در فصل 18 باید قابل دیدن باشد: search_incidents می‌خواند، open_incident می‌نویسد و یک handle برمی‌گرداند، resolve_incident آن handle را می‌گیرد و می‌بندد. یک resource، incidents://open، چون خواندن فهرست فعلی چیزی است که application وصل می‌کند. یک prompt، postmortem، چون «این را بنویس» slash command یک انسان است. این همان سلسله‌مراتب کنترل فصل 26 است — model، application، انسان — که به پنج ثبت تبدیل شده.

اهمیت handle بیشتر از چیزی است که به نظر می‌رسد. فصل 26 یک تقویم اسباب‌بازی را با نگه‌داشتن state در یک آرایه در سطح module خراب کرد: protocol هیچ session ندارد، پس یک creation tool یک شناسه opaque برمی‌گرداند و هر فراخوانی بعدی آن را مثل یک argument معمولی می‌گیرد. هیچ‌چیز در هیچ‌کدام از دو file فرض نمی‌کند caller همان processی است که آن را باز کرده.

اینجا همان tool را در هر دو زبان، کنار هم ثبت‌شده، می‌بینید:

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.")

اول چیزی را بخوانید که یکسان است، چون یافته همین است. هر دو یک name، یک description، دو string argument توصیف‌شده و سه annotation تعریف می‌کنند؛ هر دو یک function هستند؛ هیچ‌کدام JSON-RPC، framing، stdout یا نسخه protocol را ذکر نمی‌کند. دو SDK به یک شکل یکسان همگرا شده‌اند، و «Tier 1» هم باید همین معنا را بدهد.1

دو تفاوت واقعی‌اند و هر دو بعداً برمی‌گردند. TypeScript argumentها را با یک schema library توصیف می‌کند — اینجا Zod — و schema مقداری است که شما می‌نویسید. Python آن‌ها را با type hintهای خود function توصیف می‌کند و هنگام import می‌خواند؛ برای همین چیزهایی درباره function می‌داند که file TypeScript هرگز به او نگفته. و مسیر error: TypeScript یک tool result با isError برمی‌گرداند، Python exception پرتاب می‌کند. این را نگه دارید.

چهار ثبت دیگر از نظر ساختاری هیچ تفاوتی ندارند. Resource برابر است با server.registerResource("open-incidents", "incidents://open", …) در برابر @server.resource("incidents://open", …)؛ prompt برابر است با registerPrompt در برابر @server.prompt. خط آخر هر file transport است: await server.connect(new StdioServerTransport()) در برابر server.run().

کل fileها: 81 خط غیرخالی و 3,060 byte از TypeScript در برابر 63 و 2,555. با همان احتیاطی که لازم است بخوانید — line count به اندازه زبان، formatter را هم اندازه می‌گیرد؛ به همین دلیل هیچ‌کدام از این عددها در جدول headline پایین نیامده‌اند.

اثبات نامرئی‌بودن زبان، یک 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));

آن را به نوبت به هر server بدهید. خروجی واقعی، کوتاه‌شده:

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}"}]

همان toolها، همان ترتیب، همان handle. یک TypeScript client نمی‌تواند بفهمد server با چه زبانی نوشته شده، و هرگز هم نمی‌پرسد. این کل وعده یک protocol است، و پابرجا می‌ماند.

حالا به whitespace در نتیجه دوم نگاه کنید، چون تزئینی نیست: Python SDK payloadها را با pydantic_core.to_json(result, fallback=str, indent=2) serialise می‌کند. در resource read با دو incident در فهرست، body در TypeScript برابر 136 character و 37 o200k_base token است؛ body در Python برابر 185 و 62 است. شصت‌وهشت درصد token بیشتر برای rowهای یکسان، هر بار به هزینه هر کسی که resource را داخل prompt می‌خواند.

Catalogue نیز همان داستان را با علت بزرگ‌تری دارد. هر دو server، همان سه tool، tools/list کلیدبه‌کلید وزن شد:

کلیدTypeScriptPython
name2121
description4646
annotations4646
inputSchema211192
outputSchema187
execution27
مجموع342480

Schemaهای input در Python ارزان‌ترند — bridge مربوط به Zod در TypeScript روی هرکدام یک $schema و یک additionalProperties می‌زند. کل فاصله 138-token یک output schema است که هیچ‌کس ننوشته. resolve_incident با -> Incident annotation شده، پس SDK یک JSON Schema برای return type استخراج کرده و فرستاده. واقعاً مفید است — همان چیزی است که به client اجازه می‌دهد structuredContent را validate کند — و 187 token از context window شماست که به‌خاطر یک type hint وارد می‌شود. قاعده فصل 24 درباره این‌که definitionها جای material مهم را می‌گیرند، درباره schemaهایی هم صدق می‌کند که نمی‌دانستید دارید.

عمداً خرابش کنید: error messageی که نشت کرد

لینک به بخش: عمداً خرابش کنید: error messageی که نشت کرد

دو مسیر error بالا انتخاب سبک نیستند. به هر server یک tool بدهید که همان‌طور fail شود که یک integration واقعی fail می‌شود، و ببینید چه چیزی به model می‌رسد.

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}

TypeScript SDK یک آدرس داخلی، یک port، یک database name و یک service account را داخل context مدل گذاشت. Python SDK هیچ‌کدام را آنجا نگذاشت؛ traceback به stderr رفت و روی server ماند.

هیچ‌کدام bug نیست. هر دو تصمیم‌اند، و تصمیم Python در docstring خودش نوشته شده: یک ToolError «شکستی است که انتظارش را داشتید» و message آن «در content برای خواندن model» برگردانده می‌شود؛ هر چیز دیگر «مثل crash تلقی می‌شود: model فقط Error executing tool <name> را می‌بیند، و server traceback را در ERROR log می‌کند». کلاس مربوط به حالت crash بقیه را بلند می‌گوید — «هیچ‌چیز از original به client نمی‌رسد».

هر دو رفتار نیمی از مواقع غلط‌اند. فصل 18 استدلال کرد که validation error باید به‌صورت tool result برگردد تا model بتواند بخواند و اصلاح کند، چون در بیشتر integrationها همان خط بیشترین leverage را دارد؛ در سمت Python این مستلزم آن است که ToolError را صراحتاً raise کنید، و یک ValueError ساده جمله مفید را دور می‌اندازد. استدلال فصل 30 در جهت مخالف می‌رود: هر چیزی که یک tool برمی‌گرداند وارد contextی می‌شود که prompt injection بعدی می‌تواند تلاش کند دوباره آن را بیرون بکشد، و متن exception بازبینی‌نشده کم‌ممیزی‌ترین متن در سیستم شماست.

قاعده‌ای که از هر دو جان سالم به در می‌برد: برای هر tool تصمیم بگیرید یک failure مجاز است چه چیزی بگوید، و آن string را خودتان بنویسید. هرگز اجازه ندهید متن پیش‌فرض یک exception در هیچ‌کدام از دو زبان تصمیم بگیرد.

عمداً خرابش کنید: یک خط روی standard output

لینک به بخش: عمداً خرابش کنید: یک خط روی standard output

Tutorial رسمی قاعده را بی‌ابهام می‌گوید: «برای serverهای مبتنی بر STDIO: هرگز روی stdout ننویسید. نوشتن روی stdout پیام‌های JSON-RPC را corrupt می‌کند و server شما را می‌شکند. function print() به‌طور پیش‌فرض روی stdout می‌نویسد، پس آن را کاملاً از یک STDIO server بیرون نگه دارید.»1 فصل 26 نسخه normative را نقل کرد — یک server «MUST NOT write anything to its stdout that is not a valid MCP message».2

به هر server یک خط اضافه کنید و raw 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 نیست. Processی که stdout آن pipe باشد نه terminal، streamی block-buffered می‌گیرد؛ بنابراین خط اضافی هر وقت buffer تصمیم بگیرد flush می‌شود — اینجا، هنگام exit، بعد از responseی که قبل از آن نوشته شده بود. Corruption جایی ظاهر نمی‌شود که bug هست. flush=True را اضافه کنید، یا libraryای که flush می‌کند، و جابه‌جا می‌شود.

بعد بخشی که توضیح می‌دهد چرا این وارد production می‌شود. Server خراب را به سه client بدهید:

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 هر دو شانه بالا می‌اندازند — خط را skip می‌کنند و ادامه می‌دهند. قاعده‌ای که فقط clientهایی را می‌شکند که هیچ‌کس استفاده نمی‌کند، قاعده‌ای است که سالم به production می‌رسد؛ برای همین ارزش دارد اینجا عمداً آن را خراب کنیم، نه در log مشتری.

حالت CLI در Inspector همان نیمه‌ای است که فراموش می‌شود: npx @modelcontextprotocol/inspector --cli <command> --method tools/list یک catalogue چاپ می‌کند و خارج می‌شود، و همین آن را به شکلی scriptable می‌کند که browser UI نیست.3

هر دو SDK تمیز، در directoryهای جداگانه خودشان و بدون چیز shared نصب شدند:

TypeScriptPython
package@modelcontextprotocol/sdk 1.30.0 + zod 3.25.76mcp 2.1.1
آخرین protocol revision پیاده‌سازی‌شده2025-11-252026-07-28
transitive packageهای نصب‌شده9428
اندازه نصب‌شده13.9 MiB44.3 MiB
fileهای روی disk3,3862,018
third-party packageهای loadشده برای serve کردن stdio8 از 9418 از 28
شروع bare interpreter، median19.4 ms11.1 ms
spawn → پاسخ tools/list، median از 25144.5 ms709.4 ms
catalogue tools/list، tokenهای o200k_base342480

هر row در جهت متفاوتی غافلگیر می‌کند، و به همین دلیل ارزش دارد مقایسه را اجرا کنید نه این‌که فرض بگیرید.

TypeScript بیش از سه برابر package نصب می‌کند و کمتر از یک‌سوم byte. 94 dependency یعنی اکوسیستم npm همان خودش است — fast-deep-equal، es-errors، dunder-proto. 28 تای Python کمترند و عظیم: cryptography، pydantic-core و uvicorn artefactهای compiled هستند. اگر غریزه شما این است که باید نگران تعداد dependency باشید، این row نمونه نقض است.

Interpreter در Python سریع‌تر از Node شروع می‌شود، و فاصله کم نیست — 11.1 ms در برابر 19.4 ms روی یک برنامه خالی. پس 565 ms در row مربوط به cold-start زبان نیست. SDK است، و row packageهای loadشده می‌گوید چرا:

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, …

Serverی که تنها I/O آن یک pipe است، پیش از خواندن اولین خط خود یک ASGI web server، یک HTTP client و یک TLS library import می‌کند. TypeScript SDK هم Express، Hono، jose و eventsource را ship می‌کند — اما روی disk خوانده‌نشده می‌مانند، چون مرز package آن‌ها را از import مربوط به server/stdio.js بیرون نگه می‌دارد. Package در Python یک import graph است، پس import mcp همه آن است: python -X importtime مقدار 727 ms را به import mcp.server.mcpserver نسبت می‌دهد — عددی که زیر import profiler اندازه‌گیری شده، و برای همین بالاتر از 709 msی درمی‌آید که اجرای بدون profiler از spawn تا answer طول می‌کشد — و 269 ms از آن‌ها را فقط به subtree مربوط به mcp.types نسبت می‌دهد — wire typeها Pydantic model هستند، یک class برای هر protocol message در هر revision، و ساختنشان کاری است که هنگام import انجام می‌شود. این یک design trade است، نه شلختگی — importهای eager همان دلیلی‌اند که Python SDK می‌تواند در خط بعد run(transport="streamable-http") را بدون install دوم به شما بدهد.

و بعد آخرین row از block آغازین کل استدلال را برمی‌گرداند. TypeScript server را درست package کنید — یک entry bin، یک shebang، npm link، بدون چیزی برای download — و آن را از طریق npx با --no-install اجرا کنید، یعنی همان‌طور که یک stdio server منتشرشده واقعاً start می‌شود:

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 برای هر start برابر 568 ms هزینه دارد — چهارونیم برابر کل import مربوط به TypeScript SDK — و در هر launch پرداخت می‌شود، چون یک MCP host یک stdio server را با اجرای همان command start می‌کند. پس شکل صادقانه جمله «TypeScript پنج برابر سریع‌تر start می‌شود» این است: بله، تا وقتی آن را به روش معمول توزیع نکنید. احتمالاً همین caveat برای uvx هم صدق می‌کند؛ روی این machine هیچ uv نصب نبود، پس چنین rowای وجود ندارد. هیچ چیز اندازه‌گیری‌نشده‌ای وارد جدول نمی‌شود.

فصل 26 framing مربوط به stdio را پوشش داد. دو چیز را برای اینجا گذاشت.

اول: اجرای یک server با npx یا uvx همان stdio transport است. هیچ «package mode» جداگانه‌ای وجود ندارد. Configuration یک host یک command و argumentها را نام می‌برد؛ host آن را spawn می‌کند و از طریق pipeها حرف می‌زند. برای همین «این را چطور توزیع کنم» و «کدام transport را صحبت می‌کند» در حالت local یک سؤال‌اند، و به همین دلیل هزینه launcher به فصلی درباره shipping تعلق دارد.

دوم: stdio اصلاً بخش authorization ندارد، و specification این را در یک خط می‌گوید — implementationهایی که از stdio استفاده می‌کنند «SHOULD NOT follow this specification, and instead retrieve credentials from the environment».4 مدل security آن مدل operating system است، و محدودیتش هم همین: یک local subprocess دقیقاً به یک machine و یک user سرویس می‌دهد.

Transport زنده دیگر Streamable HTTP است: یک endpoint واحد که POST می‌پذیرد، یک HTTP request برای هر JSON-RPC message، و header Accept که باید هر دو application/json و text/event-stream را list کند، چون server برای هر request انتخاب می‌کند با کدام‌یک پاسخ دهد.5 فصل 14 آن event stream را دستی parse کرد، پس هیچ‌چیز در wire format جدید نیست — فقط چیزی که دور آن پیچیده. سه الزام revision فعلی به‌راحتی از قلم می‌افتند و هر سه قابل تست‌اند:

Version header باید با body هم‌خوان باشد

لینک به بخش: Version header باید با body هم‌خوان باشد

هر POST دارای MCP-Protocol-Version است، و مقدار آن باید با protocolVersion داخل _meta خود request match کند. Mismatch یک 400 با header-mismatch error است، نه شانه بالا انداختن.5

دو header دیگر برای compliance لازم‌اند

لینک به بخش: دو header دیگر برای compliance لازم‌اند

Mcp-Method روی هر request روش را mirror می‌کند؛ Mcp-Name روی tools/call، resources/read و prompts/get، params.name یا params.uri را mirror می‌کند. وجود دارند تا proxy بتواند بدون parse کردن bodyها route کند.5

شکل‌های قدیمی حذف شده‌اند، و با امتناع پاسخ می‌دهند

لینک به بخش: شکل‌های قدیمی حذف شده‌اند، و با امتناع پاسخ می‌دهند

GET stream، Mcp-Session-Id و resumption مربوط به Last-Event-ID همگی حذف شدند. Serverی که فقط این revision را صحبت می‌کند باید به GET یا DELETE با 405 Method Not Allowed پاسخ دهد، session header را بدون mint کردن session نادیده بگیرد، و Last-Event-ID را ignore کند.5

حالا اندازه‌گیری‌ای که کل فصل را از نو قاب‌بندی می‌کند. یک current-revision request را از طریق HTTP به هر server بفرستید.

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)"}}

Constantها با رفتار هم‌خوان‌اند: LATEST_PROTOCOL_VERSION در Python SDK برابر 2026-07-28 است، در TypeScript SDK برابر 2025-11-25. Header-mismatch request مرحله بالا را بفرستید و Python server با 400، error -32020 و message «mcp-protocol-version header does not match the request envelope's protocol version» پاسخ می‌دهد؛ TypeScript SDK چنین codeای ندارد، چون revisionی را که آن را تعریف می‌کند پیاده نکرده.

صفحه‌ای که هر دو را در Tier 1 list می‌کند همچنین می‌گوید «Each SDK provides the same functionality».1 در تاریخ پایین، برای revision فعلی، آن جمله آرزومندانه است. در SDKی که می‌خواهید install کنید LATEST_PROTOCOL_VERSION را check کنید؛ یک خط است، و تنها ادعای این فصل است که یک سال دیگر هم هنوز مهم خواهد بود.

خطای 401، و جمله‌ای که باید نقل کرد

لینک به بخش: خطای 401، و جمله‌ای که باید نقل کرد

Server را از laptop خود بیرون ببرید و client یک غریبه با token از راه می‌رسد. این همان نیمه‌ای است که فصل 26 دست‌نخورده گذاشت و نیمه‌ای که یک product چندکاربره نمی‌تواند skip کند.

Specification، MCP server را در یک نقش OAuth 2.1 قرار می‌دهد و نامش را هم می‌گوید: یک MCP server محافظت‌شده resource server است، client یک OAuth client است، و authorization server مشکل شخص دیگری است.4 از دل آن نقش، چهار بند اجباری، کامل نقل شده‌اند چون paraphrase کردنشان همان‌جایی است که اشتباه رخ می‌دهد:

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 است، و به همین دلیل کل apparatus مربوط به audience وجود دارد. Serverی که bearer tokenی را که به او داده شده در یک third-party API replay می‌کند، یک confused deputy است: اعتماد خودش را به هر کسی که او را صدا زده قرض می‌دهد. قاعده reuse را ممنوع می‌کند، نه فقط storage را.

قابل اجرا کردن این قاعده چهار RFC می‌خواهد، هرکدام با یک کار.6 RFC 9728 راهی است که client اصلاً authorization server را پیدا می‌کند: MCP server یک protected-resource-metadata document serve می‌کند و یک 401 به آن اشاره می‌کند. RFC 8707 همان parameter resource است — client باید URI canonical server را در هر دو authorization request و token request بفرستد، «regardless of whether authorization servers support it»، تا token صادرشده audience خودش را نام ببرد. RFC 9207 حلقه را از سمت دیگر می‌بندد: client پیش از redirect، issuer را record می‌کند و iss برگشتی را با exact string compare می‌کند، بدون normalisation — نه case folding، نه حذف default-port، نه trailing slash. و RFC 7591، Dynamic Client Registration، اکنون به نفع Client ID Metadata Documents deprecated شده، «retained for backwards compatibility with authorization servers that do not support» آن‌ها.4

این را روی هر دو server با token verifierای wire کنید که هیچ کاری جز check کردن audience انجام نمی‌دهد. Ladder در 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"}

هر دو SDK آن document را serve می‌کنند و هر دو یک 401 را به آن point می‌کنند، که کل داستان discovery همین است: clientی که هرگز server شما را ندیده، از یک refusal یاد می‌گیرد کجا authenticate کند. 403 موجود دیگری است — token خوب است، scope خوب نیست — و challenge چیزی را که کم است نام می‌برد تا client بتواند step up کند نه این‌که از نو شروع کند.

دو پله فرق دارند، و هیچ‌کدام از این تفاوت‌ها در specification نیست. TypeScript SDK tokenی را که هیچ expiry claim ندارد رد می‌کند؛ نسخه Python 200 برمی‌گرداند، چون expires_at روی AccessToken آن optional است و None یعنی «نظری ندارم». و 403 در Python، error_description="Required scope: incidents:read" را بدون parameter scope حمل می‌کند که specification می‌گوید serverها باید شامل کنند. Verifier جای پذیرش default یک library نیست: audience check را شما باید در هر زبان بنویسید، expiry را هم همین‌طور.

یک نکته کوچک و صادقانه از همان run. GET روی endpoint در wiring مربوط به Express با 404 پاسخ داد و در Python با 400 Bad Request: Missing session ID، جایی که specification 405 Method Not Allowed می‌خواهد و جایی که «session ID» واژگان revisionی است که حذف شده. هیچ‌کدام خطرناک نیست؛ هر دو شکل اکوسیستمی هستند که وسط migration است.

Serverها واقعاً کجا زندگی می‌کنند

لینک به بخش: Serverها واقعاً کجا زندگی می‌کنند

آخرین تکه shipping این است که کجا publish می‌کنید، و پاسخی عدددار دارد. امروز crawled شد، همه serverها در registry رسمی در آخرین version خودشان:7

serverها
مجموع (آخرین version، حذف‌نشده)28,170
active / deprecated27,853 / 317
حداقل یک installable package ship می‌کنند13,065
فقط remote — یک URL، بدون چیزی برای install14,696
npm8,275
PyPI3,603
OCI images867
bundleهای mcpb706
NuGet / Cargo107 / 43

دو خوانش، در دو جهت مخالف. بر اساس serverهای منتشرشده، npm با نسبت 2.3 به 1 جلوست — عددی که مردم وقتی می‌گویند اکوسیستم TypeScript است نقل می‌کنند. بر اساس download، Python جلوست: در سی روز گذشته mcp به 286.7 میلیون رسید در برابر @modelcontextprotocol/sdk با 194.7 میلیون، پیش از اضافه کردن fastmcp با 72.1 میلیون.7 هر دو Tier 1 هستند، schema normative یک schema.ts است، و tutorial رسمی «Build an MCP server» با tab مربوط به Python باز می‌شود.1 هر نیمه‌ای از این را در ذهن داشتید، نیمه دیگر هم درست است.

و rowای که از هر دو مهم‌تر است: بیش از نصف registry — 14,696 از 28,170 — چیزی برای install ندارد. آن‌ها web service هستند. شمارش transportها از سمت دیگر هم همین را تأیید می‌کند: از 14,290 package entry، 13,787 مورد stdio اعلام می‌کنند؛ از 16,640 remote entry، 15,570 مورد Streamable HTTP اعلام می‌کنند و 1,070 مورد هنوز HTTP+SSE deprecated را اعلام می‌کنند. پس «یک MCP server یک subprocess روی laptop شماست» اقلیتی رو به کاهش را توصیف می‌کند، و هر یک از آن 14,696 مورد به بخش بالا نیاز دارد نه به environment variable.

نمایش جزئیات

عمداً دوزبانه، و سابقه‌ای برای آن.

این تنها فصل دوزبانه دوره است، چون پاسخ صادقانه دو تکه می‌شود: registry امروز هم‌زمان npm-first است و downloadها Python-first. نوشتن فقط یکی از این دو، نصف سؤال را واگذار می‌کرد و هم‌زمان اکوسیستم را غلط توصیف می‌کرد. در فضای open سابقه وجود دارد — Hugging Face MCP Course در prerequisites خود می‌گوید «Experience with at least one programming language (Python or TypeScript examples will be shown)»، و هر دو را آموزش می‌دهد.8 Protocolی که کل ارزشش تعداد implementationهاست، جای بدی برای تک‌زبانه بودن است.

بخش تاریخ‌دار: هر چیز بالا که عمر مفید دارد

لینک به بخش: بخش تاریخ‌دار: هر چیز بالا که عمر مفید دارد

خوانده و اندازه‌گیری‌شده در 7 سپتامبر 2026، در برابر protocol revision 2026-07-28.

مقدار
@modelcontextprotocol/sdk1.30.0، منتشرشده در 27 ژوئیه 2026؛ 4,322,438 byte unpacked، 693 file، 17 dependency مستقیم
آخرین revisionی که پیاده‌سازی می‌کند2025-11-25
mcp (PyPI)2.1.1، منتشرشده در 25 اوت 2026؛ wheel با 357,912 byte، به‌علاوه mcp-types 2.1.1 با 69,656 byte
آخرین revisionی که پیاده‌سازی می‌کند2026-07-28
Tierهای SDKTypeScript، Python، C#، Go، Rust در Tier 1؛ Java، Ruby در Tier 2؛ Swift، PHP، Kotlin در Tier 3
serverهای registry28,170
downloadها، 30 روز گذشتهmcp 286,653,871 · fastmcp 72,097,269 · @modelcontextprotocol/sdk 194,679,333

یک migration note که عدد نیست. در mcp 2.x، FastMCP به MCPServer rename شد، و تقریباً هر tutorial آنلاین هنوز با import قدیمی باز می‌شود. SDK moduleای ship می‌کند که تنها هدفش توضیح همین است، و این considerateترین 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.

با جدول روبه‌رویتان، recommendation حوصله‌سربر است، که نشانه خوبی است.

اگر server داخل web applicationی زندگی می‌کند که همین حالا اجرا می‌کنید، آن را با TypeScript بنویسید. همان process، همان deploy، همان request handler؛ Streamable HTTP یک endpoint است که کنار بقیه اضافه می‌کنید؛ و 13.9 MiB و 145 ms رایگان‌اند چون runtime از قبل بالا بوده. این وضعیت بیشتر همان 14,696 remote server است.

اگر server دور data tooling پیچیده می‌شود، آن را با Python بنویسید. چیزی که expose می‌کنید pandas، یک warehouse client، transformهایی به اندازه یک notebook، و serverی در زبان دیگر است که در عمل subprocess callای با schema پوشیده است. هفتصد میلی‌ثانیه import در serviceای که یک بار start می‌شود هزینه نیست؛ در subprocessی که host تمام روز relaunch می‌کند، هست.

و فعلاً، row مربوط به revision بر هر دو اولویت دارد. اگر به 2026-07-28 نیاز دارید — multi-round-trip requestها، resultType، cache hintها، server/discover — امروز یکی از این دو SDK آن را دارد و دیگری ندارد.

حالا می‌توانید همان server را در هرکدام از دو زبان ship کنید، انتخاب را با جدول دفاع کنید نه با ترجیح، آن را روی هر دو transport زنده اجرا کنید، و tokenی به آن بدهید که رد خواهد کرد.

چیزی که ساختید هنوز یک function است: یک schema، یک endpoint، چیزی deterministic که model invoke می‌کند. یک طبقه کامل از knowledge در این شکل جا نمی‌شود — این‌که ما postmortem را چطور می‌نویسیم، گزارش‌های incident ما چه fieldهایی لازم دارند، ترتیب کارهای ما چیست و چرا. این procedure است، prose است، و زورچپان کردنش داخل tool description همان راهی است که system promptها را به دو هزار token می‌رساند؛ هزینه‌ای که در تک‌تک turnها پرداخت می‌شود، چه conversation درباره incident باشد چه نباشد.

فصل 28 پاسخ دیگر است: folderای با یک SKILL.md داخلش که model به‌جای call کردن می‌خواند، در سه سطح load می‌شود تا reference material تقریباً هیچ هزینه‌ای نداشته باشد تا همان turnای که لازم می‌شود. زبان اصلی ندارد، و همین اولین چیزی است که یاد می‌دهد.


همه‌چیز اینجا در 7 سپتامبر 2026، روی Node 22.22.3 و Python 3.14.4، در برابر @modelcontextprotocol/sdk 1.30.0 با zod 3.25.76 و mcp 2.1.1 اندازه‌گیری شد، هرکدام نصب‌شده در directory دورریختنی خودش. Timingها median از 25 launch هستند، wall clock از spawn تا lineای که response مربوط به tools/list را حمل می‌کرد؛ token countها o200k_base از طریق tiktoken روی JSON هر definition هستند. هیچ paid APIای call نشد: هیچ‌چیز اینجا به model نیاز ندارد.

دو server دارای 81 و 63 خط غیرخالی‌اند؛ یکی از سه tool آن‌ها بالا در هر دو زبان بازتولید شده، و چهار ثبت دیگر فقط همان‌طور که توضیح داده شد تفاوت دارند. Policy مربوط به error-disclosure در Python SDK از docstringهای 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 است. Constantهای protocol-version برابر LATEST_PROTOCOL_VERSION در mcp_types/version.py و در types.js مربوط به TypeScript SDK هستند، هر دو از packageهای نصب‌شده خوانده شدند نه از 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»، ترتیب language-tabهای tutorial (Python، TypeScript، Java، Kotlin، C#، Ruby، Rust، Go)، و قاعده logging نقل‌شده درباره print() و stdout. 2 3 4

  2. stdio transport، .../basic/transports/stdio. منبع newline framing و قاعده purity مربوط به stdout. فصل 26 این صفحه را کامل می‌خواند؛ اینجا برای خطی cite شده که server خراب آن را نقض می‌کند.

  3. MCP Inspector، modelcontextprotocol.io/docs/2026-07-28/tools/inspector، خوانده‌شده در 7 سپتامبر 2026. یک package، سه client پشت یک binary — web، --cli و --tui — با یک core مشترک، یک مجموعه transport مشترک و یک OAuth state روی disk. CLI، catalogue traceهای اینجا را تولید کرد.

  4. Authorization، modelcontextprotocol.io/specification/2026-07-28/basic/authorization، خوانده‌شده در 7 سپتامبر 2026. منبع نقش resource-server؛ چهار بند token-handling که کامل نقل شدند؛ الزام پیاده‌سازی RFC 9728 توسط serverها و استفاده clientها از آن برای discovery؛ قواعد parameter resource و تعریف canonical-URI؛ جدول issuer-validation؛ deprecation مربوط به Dynamic Client Registration؛ جدول 401/403/400 و challenge مربوط به insufficient_scope؛ و exemption مربوط به 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. منبع قاعده single-endpoint POST، الزام dual Accept، header MCP-Protocol-Version و قاعده must-match-the-body آن، headerهای Mcp-Method و Mcp-Name که به‌عنوان «REQUIRED for compliance» توصیف شده‌اند، حذف GET stream، sessionها و Last-Event-ID، guidance مربوط به 405، validation اجباری Origin، و طبقه‌بندی transport مربوط به 2024-11-05 HTTP+SSE به‌عنوان Deprecated تحت SEP-2596. 2 3 4

  6. چهار موردی که specification بر آن‌ها تکیه دارد، همراه با draftی که profile می‌کند: 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، فوریه 2020 — parameter resource و audienceی که bind می‌کند. Jones, M.B., Hunt, P. and Parecki, A., OAuth 2.0 Protected Resource Metadata، RFC 9728، آوریل 2025 — documentی که یک 401 به آن point می‌کند. Meyer zu Selhausen, K. and Fett, D., OAuth 2.0 Authorization Server Issuer Identification، RFC 9207، مارس 2022 — parameter iss و exact-string comparison. Richer, J. (ed.) et al., OAuth 2.0 Dynamic Client Registration Protocol، RFC 7591، ژوئیه 2015، برای این use deprecated شده. و Jones, M. and Hardt, D., The OAuth 2.0 Authorization Framework: Bearer Token Usage، RFC 6750، اکتبر 2012، section 3، برای شکل challenge مربوط به WWW-Authenticate در بالا.

  7. Registry رسمی MCP، registry.modelcontextprotocol.io/v0/servers، crawled در 7 سپتامبر 2026 با version=latest: 282 page، 28,170 server، شمرده‌شده با registryType روی server nameهای distinct. ارقام download: api.npmjs.org/downloads/point/last-month برای @modelcontextprotocol/sdk (194,679,333 برای 8 اوت تا 6 سپتامبر 2026) و pypistats.org/api/packages/<name>/recent برای mcp و fastmcp، هر دو همان روز خوانده شدند. Package sizeها از npm registry document و PyPI JSON API می‌آیند. 2

  8. MCP Course، Hugging Face، huggingface.co/learn/mcp-course، unit 0، خوانده‌شده در 7 سپتامبر 2026: در میان prerequisites، «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.
jev12 دقیقه مطالعه

مدل هوش مصنوعی Jev برای تصمیم ساخته شده، نه نثر

Jev از TypeSafe AI توجه‌ها را جلب کرده چون هوشمندی نرم‌افزار را مسئله‌ای احتمالاتی می‌بیند: شاخه درست را انتخاب کنید، میزان اطمینان را کنار آن بگذارید، و وقتی کد به یک تصمیم نیاز دارد برای نوشتن متن به یک LLM پول ندهید.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering13 دقیقه مطالعه

مهندسی کانتکست برای عامل‌های AI بلندافق

عامل‌های طولانی‌اجرا فقط به‌خاطر کوچک بودن پنجره شکست نمی‌خورند. وقتی فایل‌ها، خروجی ابزارها و تاریخچهٔ کهنه وظیفه‌ای را که عامل قرار بود تمام کند کنار می‌زنند، شکست رخ می‌دهد.

آماده‌اید انتخاب مدل را به LIA بسپارید؟

با همه مدل‌های هوش مصنوعی در یک جا بسازید — همین امروز رایگان شروع کنید.