فاز 13 · درس 7

ساخت سرور MCP بی‌وضعیت با پایتون و تایپ‌اسکریپت

نوع
ساختنی
زمان
حدود 85 دقیقه
زبان‌ها
Python, TypeScript
طرحی مینیمال از دو جعبه شفاف با پایپ‌های ورودی و خروجی استاندارد که داده‌ها را بدون ذخیره‌سازی در جریان نگه می‌دارند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. حلقه‌ی مدرن توزیع درخواست (Dispatch loop)
  5. اعتبارسنجی درخواست
  6. متد اجباری Discover
  7. ابزارها
  8. منابع
  9. پرامپت‌ها
  10. تمام نتایج موفق نوع‌دار هستند
  11. ممنوعیت درخواست‌های آغازشده از سمت سرور
  12. سازگاری صریح با نسخه‌های قدیمی
  13. استفاده از ابزارهای واقعی
  14. خروجی این درس
  15. تمرین‌ها
  16. اصطلاحات کلیدی
  17. برای مطالعه‌ی بیشتر

در معماری‌های توزیع‌شده و مقیاس‌پذیر، نگهداری وضعیت نشست (Session State) یکی از بزرگ‌ترین چالش‌های توسعه‌دهندگان است. در نسخه‌های مدرن پروتکل MCP (نسخه‌ی 2026-07-28)، سرور دیگر نیازی به ذخیره‌ی وضعیت دست‌دادن (Handshake) ندارد و هر درخواست با داشتن متادیتای کامل، به‌صورت مستقل پردازش می‌شود. این طراحی مانع از تداخل داده‌ها میان ورکرها و کلاینت‌های مختلف در محیط‌های ابری می‌شود.

اگر می‌خواهید ابزارهای مدل‌های زبانی را به‌شکل استاندارد و بدون باگ‌های رایج نشست‌های قدیمی پیاده‌سازی کنید، یادگیری قراردادهای مدرن MCP، متد اجباری server/discover و مدیریت اعتبارسنجی مستقل برای هر درخواست حیاتی است. در این درس یاد می‌گیرید چگونه با استفاده از صرفاً کتابخانه‌های استاندارد Python و TypeScript یک سرور امن، قطعی و بی‌وضعیت بسازید.

یک سرور مدرن MCP هیچ دست‌دادنی (Handshake) را به خاطر نمی‌سپارد؛ متادیتا را در تک‌تک درخواست‌ها اعتبارسنجی می‌کند، یک گرداننده (Handler) را اجرا می‌کند و یک نتیجه‌ی نوع‌دار برمی‌گرداند.

اهداف یادگیری

  • پیاده‌سازی متد اجباری server/discover برای نسخه‌ی 2026-07-28 پروتکل MCP.
  • اعتبارسنجی نسخه‌ی پروتکل و قابلیت‌های کلاینت در هر درخواست.
  • ارائه‌ی ابزارها، منابع و پرامپت‌ها با ترتیب قطعی در فهرست‌ها.
  • بازگرداندن resultType، هویت سرور و راهنماهای حافظه‌ی موقت (Cache hints) روی نتایج درست.
  • ارائه‌ی همین قرارداد بی‌وضعیت (Stateless) روی stdio تفکیک‌شده با خط جدید در Python و TypeScript.

صورت مسئله

ساخت سروری که قابلیت‌های کلاینت را بعد از پیام اول ذخیره می‌کند ساده است، اما اجرا و نگهداری آن در محیط عملیاتی دردسرساز خواهد بود. یک فرایند (Process) واحد ممکن است به چند کلاینت متوالی پاسخ دهد؛ یک درخواست از راه دور ممکن است روی ورکر متفاوتی بنشیند؛ و اعلام قابلیت‌های منسوخ می‌تواند رفتار سیستم را فراتر از مرزهای مجاز نشت دهد.

نسخه‌ی 2026-07-28 پروتکل MCP این بخش از مسئله را در لایه‌ی پروتکل حل می‌کند: هر درخواست باید خودتوصیف‌گر باشد. برنامه‌ی شما همچنان می‌تواند یادداشت‌های پایدار، جاب‌ها یا هندل‌های صریح وضعیت را نگه دارد؛ چیزی که حق نگهداری آن را ندارد، وضعیت پنهان پروتکل است؛ وضعیتی که نحوه‌ی رمزگشایی (Decode) درخواست‌های بعدی را تغییر دهد.

در این درس یک سرور یادداشت را دو بار می‌سازیم. نسخه‌های Python و TypeScript برای هسته‌ی پروتکل فقط از کتابخانه‌های استاندارد خود استفاده می‌کنند. هر دو نسخه متدهای یکسانی ارائه می‌دهند و قرارداد انتقال یکسانی را اعمال می‌کنند.

مفاهیم پایه

حلقه‌ی مدرن توزیع درخواست (Dispatch loop)

read one JSON-RPC line
parse the envelope
if it is a notification, do not respond
validate params._meta for this request
route by method
wrap success with resultType and serverInfo
write one JSON-RPC response line
forget request-scoped metadata

سه قاعده‌ی stdio همچنان اهمیت دارند:

  • فقط پیام‌های JSON-RPC را در stdout بنویسید؛ اطلاعات تشخیصی (Diagnostics) را به stderr بفرستید.
  • پیام‌ها را با خط جدید جدا کنید و خروجی هر پاسخ را خالی (Flush) کنید.
  • با رسیدن stdin به انتهای فایل (EOF)، بلافاصله خارج شوید.

طول عمر فرایند، همان طول عمر بستر انتقال (Transport) است، نه یک نشست مدرن MCP.

اعتبارسنجی درخواست

هر درخواست باید شامل این موارد باشد:

{
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "notes-client",
        "version": "1.0.0"
      }
    }
  }
}

دو فیلد اول اجباری هستند؛ ارائه‌ی clientInfo توصیه می‌شود. ساختار هویتی ارائه‌شده را اعتبارسنجی کنید، اما آن را احراز هویت تلقی نکنید.

اگر نسخه پشتیبانی نمی‌شود، کد خطای -32022 را همراه با مقادیر requested و supported برگردانید. نبود متادیتای درخواست به‌معنای پارامترهای نامعتبر است (کد -32602). هرگز فیلدهای خالی را بر اساس فراخوانی‌های قبلی پر نکنید.

متد اجباری Discover

سرورهای مدرن حتماً باید server/discover را پیاده‌سازی کنند. یک نتیجه‌ی کامل discovery شامل نسخه‌های مدرن پشتیبانی‌شده، قابلیت‌ها، دستورالعمل‌های اختیاری، راهنماهای حافظه‌ی موقت، و هویت سرور در فیلد _meta است:

{
  "resultType": "complete",
  "supportedVersions": ["2026-07-28"],
  "capabilities": {
    "tools": {"listChanged": false},
    "resources": {"listChanged": false, "subscribe": false},
    "prompts": {"listChanged": false}
  },
  "ttlMs": 3600000,
  "cacheScope": "public",
  "_meta": {
    "io.modelcontextprotocol/serverInfo": {
      "name": "notes-server",
      "version": "2.0.0"
    }
  }
}

متد Discovery قفل سرور را باز نمی‌کند. کلاینت می‌تواند بدون فراخوانی Discovery مستقیماً tools/list را صدا بزند، چون tools/list هم‌اکنون نیز همان متادیتای درخواست را همراه خود دارد.

ابزارها

متد tools/list فهرستی قطعی از توصیف‌کننده‌های ابزار را برمی‌گرداند. ترتیب پایدار آیتم‌ها کش پاسخ‌ها را بهبود می‌دهد و کانتکست مدل را پایدار نگه می‌دارد. نتیجه‌ی این متد به ttlMs و cacheScope هم نیاز دارد.

متد tools/call بلوک‌های محتوا و مقدار isError را برمی‌گرداند. وقتی انولوپ پروتکل یا پارامترهای متد نامعتبر هستند، از خطای JSON-RPC استفاده کنید. اما زمانی که فراخوانی ابزار معتبر است ولی خود ابزار با شکست مواجه می‌شود، مقدار isError: true را برگردانید.

یادداشت‌های توصیفی ابزارها (Tool annotations) صرفاً راهنما هستند، نه سازوکار اعمال اجبار:

  • readOnlyHint
  • destructiveHint
  • idempotentHint
  • openWorldHint

میزبان (Host) باید از این فیلدها برای تأیید گرفتن از کاربر و نحوه‌ی نمایش رابط کاربری استفاده کند. خود سرور همچنان موظف است اعطای مجوز واقعی را اعمال کند.

منابع

متد resources/list توصیف‌کننده‌های پایدار URI را برمی‌گرداند. متد resources/read محتواهای نوع‌دار را تحویل می‌دهد. هر دو متد در نسخه‌ی 2026-07-28 کش‌پذیر هستند؛ بنابراین هر دو شامل ttlMs و cacheScope می‌شوند.

برای داده‌های یادداشتِ مختص کاربر، از "cacheScope: "private استفاده کنید. یک حافظه‌ی موقت اشتراکی نباید پاسخی با دامنه‌ی خصوصی را در دیگر بسترهای احراز دسترسی استفاده کند.

ارسال تغییرات در ساختار مدرن از resources/subscribe استفاده نمی‌کند. کلاینت استریم subscriptions/listen را باز می‌کند و دسته‌بندی‌های resourceSubscriptions یا تغییرات فهرست را درخواست می‌دهد. این روند را در درس 10 پیاده‌سازی می‌کنیم.

پرامپت‌ها

متد prompts/list کش‌پذیر و قطعی است. متد prompts/get یک پرامپت نام‌گذاری‌شده را با آرگومان‌های ارسالی رندر می‌کند. نتیجه‌ی رندر پرامپت کامل است، اما جزو نتایج خواندنی یا فهرستی کش‌پذیر نیست که به راهنماهای کش نیاز داشته باشد.

تمام نتایج موفق نوع‌دار هستند

در این مثال‌ها برای تمام پاسخ‌های موفق از یک پوشش‌دهنده (Wrapper) مشترک استفاده می‌کنیم:

def complete(payload):
    return {
        "resultType": "complete",
        **payload,
        "_meta": {SERVER_INFO_KEY: SERVER_INFO},
    }

گرداننده‌های List، Read و Discovery مقادیر ttlMs و cacheScope را هم اضافه می‌کنند. متمرکز کردن این پوشش‌دهنده جلوی این را می‌گیرد که یک گرداننده فیلدهای مدرن پاسخ را بی‌سرصدا از قلم بیندازد.

ممنوعیت درخواست‌های آغازشده از سمت سرور

یک سرور مدرن می‌تواند اعلان‌های (Notifications) مربوط به درخواست کلاینت را ارسال کند، یا روی استریم subscriptions/listen که کلاینت باز کرده اعلان بفرستد؛ اما خودش هرگز نباید یک درخواست مستقل JSON-RPC ایجاد کند.

وقتی گرداننده به نمونه‌برداری (Sampling)، استخراج داده (Elicitation) یا ورودی‌های Roots نیاز دارد، یک پاسخ input_required برمی‌گرداند. کلاینت درخواست‌های ورودیِ تعبیه‌شده را برآورده می‌کند و متد اصلی را با شناسه‌ی درخواست (Request id) جدید دوباره فراخوانی می‌کند. در درس 11 این الگوی درخواست چندرفت‌وبرگشتی (Multi Round-Trip Request) را بررسی خواهیم کرد.

سازگاری صریح با نسخه‌های قدیمی

سروری که از هر دو دوره پشتیبانی می‌کند، می‌تواند دست‌دادن نسخه‌ی 2025-11-25 را روی یک branch کاملاً جداگانه برای کدهای قدیمی پیاده‌سازی کند. این سرور وقتی فیلدهای اجباری _meta مدرن حضور دارند رفتار مدرن را پیش می‌گیرد، و وقتی پیام initialize را دریافت می‌کند به سراغ رفتار قدیمی می‌رود.

درخواست‌های 2026-07-28 را به مسیر دست‌دادن قدیمی هدایت نکنید. همچنین فیلدهای مدرن resultType را روی نتایج مقداردهی اولیه‌ی قدیمی قرار ندهید. کد این درس عمداً فقط به‌صورت مدرن نوشته شده است تا اصول تغییرناپذیر آن واضح و مشهود بماند.

حلقه توزیع (DISPATCH LOOP)یک خط JSON ورودی، یک خط متناظر خروجی
DISPATCH LOOPThe server reads one JSON object per line from stdin. A message with an id is a request and must produce exactly one response carrying the same id on stdout. A notification has no id and produces nothing. Anything else printed to stdout corrupts the wire, which is why debug output goes to stderr.stdinstdoutچیزی نوشته نشدتوزیعشناسه دارد: پاسخبی‌شناسه: مصرفلاگ به stderrدرخواست id:7پاسخ id:7اعلان
سرور در هر خط یک آبجکت JSON را از stdin می‌خواند. پیامی با شناسه (id) یک درخواست است و باید دقیقاً یک پاسخ با همان شناسه در stdout تولید کند. یک اعلان (notification) شناسه‌ای ندارد و هیچ خروجی‌ای تولید نمی‌کند. نوشتن هر چیز دیگری در stdout جریان داده را مخدوش می‌کند، به همین دلیل خروجی‌های اشکال‌زدایی به stderr فرستاده می‌شوند.

استفاده از ابزارهای واقعی

دموی محدود و تست‌های سرور Python را اجرا کنید:

cd code
python3 main.py --demo
python3 -m unittest discover tests -v

نسخه‌ی TypeScript را با یک رانر TypeScript اجرا کنید:

npx tsx main.ts --demo

این دمو متد server/discover را صدا می‌زند، تک‌تک بخش‌های پایه را فهرست می‌کند، ابزارها را فرامی‌خواند و نحوه‌ی نمایش خطای عدم پشتیبانی از نسخه را نشان می‌دهد. هر درخواست مدرن متادیتا را تکرار می‌کند و هر پاسخ موفق، هویت سرور را به همراه دارد.

خروجی این درس

خروجی این درس فایل outputs/skill-mcp-server-scaffolder.md است. این فایل ساختار سروری مدرن را همراه با قرارداد Discovery، اعتبارسنجی در تک‌تک درخواست‌ها، فهرست‌های قطعیِ کش‌پذیر و یک تطبیق‌دهنده‌ی (Adapter) اختیاری و مجزا برای نسخه‌های قدیمی تولید می‌کند.

تمرین‌ها

  1. قابلیت‌ها را از یک درخواست حذف کنید و ثابت کنید سرور ادعای مطرح‌شده در درخواست قبلی را بازیافت نمی‌کند.
  2. ترتیب درج در TOOLS، PROMPTS و یادداشت‌ها را معکوس کنید؛ سپس تأیید کنید خروجی همه‌ی فهرست‌ها همچنان ثابت و پایدار باقی می‌ماند.
  3. یک ابزار تخریبی به نام notes_delete اضافه کنید و داخل مجری (Executor) آن یک بررسی اعطای مجوز قرار دهید. فیلد destructiveHint را صرفاً به عنوان یک راهنمای رابط کاربری نگه دارید.
  4. متد resources/templates/list را همراه با ttlMs، cacheScope و ترتیب قطعی اضافه کنید.
  5. یک تطبیق‌دهنده‌ی جداگانه برای نسخه‌ی قدیمی 2025-11-25 بسازید و با نوشتن تست ثابت کنید که درخواست مدرن هرگز وارد این مسیر نمی‌شود.

اصطلاحات کلیدی

اصطلاح معنا
سرور بی‌وضعیت (Stateless server) هر درخواست را بر اساس متادیتای خودش پردازش می‌کند، بدون اینکه وضعیت نشست پروتکل را به خاطر بسپارد
server/discover متد اجباری نسخه‌های مدرن که نسخه‌ها و قابلیت‌ها را اعلام می‌کند
نتیجه‌ی کامل (Complete result) پاسخ موفق مدرن با مقدار "resultType: "complete
نتیجه‌ی کش‌پذیر (Cacheable result) نتیجه‌ی متدهای Discovery، List یا Resource-read همراه با ttlMs و cacheScope
فهرست قطعی (Deterministic list) رجیستری منطقی یکسان که همیشه آیتم‌ها را با همان ترتیب ثابت تولید می‌کند
هویت سرور (Server identity) فیلد توصیه‌شده‌ی io.modelcontextprotocol/serverInfo در بخش _metaی نتیجه
خطای ابزار (Tool error) فراخوانی معتبر ابزار که محتوا را همراه با isError: true برمی‌گرداند
خطای پروتکل (Protocol error) درخواست نامعتبر در لایه‌ی JSON-RPC یا MCP که در قالب شیء error برگردانده می‌شود

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسیک سرور MCP مدرن قابلیت‌های کلاینت را از کجا می‌خواند؟
  2. حین درسیک سرور مدرن ملزم به پیاده‌سازی کدام متد است؟
  3. حین درسکدام فیلدها در نتیجه موفق tools/list الزامی هستند؟
  4. حین درسچرا سرور باید ابزارها را به‌صورت قطعی (deterministic) مرتب کند؟
  5. پس از درسیک فراخوانی ابزار معتبر اجرا می‌شود اما عملیات دامنه با شکست مواجه می‌شود. کدام ساختار مناسب است؟
  6. پس از درسچه چیزی باید به طول عمر بستر انتقال سرور stdio پایان دهد؟

سؤالات متداول

چرا سرور مدرن MCP نباید وضعیت پروتکل را ذخیره کند؟

چون یک پروسس ممکن است به کلاینت‌های مختلف پاسخ دهد یا درخواست‌ها روی ورکرهای متفاوتی قرار گیرند؛ ذخیره‌ی وضعیت پنهان باعث نشست اطلاعات و رفتار نامعتبر در مرزهای دسترسی می‌شود.

تفاوت خطای ابزار (Tool error) و خطای پروتکل چیست؟

خطای پروتکل زمانی رخ می‌دهد که ساختار JSON-RPC یا پارامترهای درخواست نامعتبر باشند، اما خطای ابزار زمانی است که درخواست درست اجرا شده ولی خروجی ابزار با شکست روبه‌رو شده و فیلد isError: true برمی‌گرداند.

آیا متد server/discover برای استفاده از ابزارها الزامی است؟

خیر؛ کلاینت می‌تواند بدون فراخوانی discover مستقیماً tools/list را صدا بزند، چون در معماری مدرن MCP تمام متادیتای لازم درون خود درخواست ارسال می‌شود.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.