در پیادهسازی ابزارهای تعاملی با هوش مصنوعی بر پایهی پروتکل زمینهی مدل (Model Context Protocol یا MCP)، یکی از چالشهای اصلی انتخاب شیوهی انتقال داده میان کلاینت و سرور است. در نسخههای اولیهی پروتکل، مدیریت نشستها و اتصالات وابسته به وضعیت، مقیاسپذیری سرورهای ابری و توزیعشده را دشوار میکرد و وابستگی به پردازههای خاص، ریسک خطاهای قطعی اتصال را بالا میبرد.
در نسخهی مدرن پروتکل، ارتباطات شبکه از طریق Streamable HTTP بازطراحی شده و کاملاً مستقل از وضعیت (Stateless) و بر پایهی درخواستهای مجزای POST تعریف میشود. در کنار آن، ارتباط محلی پردازهها روی بستر ورودی/خروجی استاندارد (stdio) همچنان سادهترین و امنترین راهکار برای پردازههای فرزند است.
این درس به شما نشان میدهد چطور قرارداد جدید Streamable HTTP را پیادهسازی کنید، هدرهای متادیتا را با بدنهی JSON-RPC اعتبارسنجی نمایید و بدون نیاز به نشستهای سنتی، سیستمهایی پایدار و مقیاسپذیر در معماری توزیعشده بسازید.
بستر انتقال (Transport) پیامهای MCP را منتقل میکند، اما وضعیت ناموجود پروتکل را تأمین نمیکند. در نسخهی
2026-07-28، هم stdio محلی و هم Streamable HTTP در مخزن راهدور، درخواستهای خودتوصیف را حمل میکنند.
اهداف یادگیری
- انتخاب stdio برای پردازههای فرزند محلی و Streamable HTTP برای سرویسهای شبکه.
- پیادهسازی قرارداد مدرن Streamable HTTP بهصورت تکاندپوینت و فقط با متد POST.
- همگامسازی (Mirror) و اعتبارسنجی هدرهای نسخهی MCP، متد و نام در برابر بدنهی JSON-RPC.
- ارائهی صحیح رویدادهای ارسالی از سرور (SSE) در دامنهی درخواست و استریمهای طولانیمدت
subscriptions/listen. - مهاجرت از استقرارهای مبتنی بر نشست (Session) و مدل قدیمی HTTP+SSE بدون قالبکردن رفتار قدیمی بهعنوان رویکرد مدرن.
صورت مسئله
ویرایشهای اولیهی Streamable HTTP، مذاکرهی پروتکل را با رفتار اتصال و نشست درهم آمیخته بودند. سرور میتوانست هدر Mcp-Session-Id بسازد، یک استریم مستقل GET در اختیارتان بگذارد، متد DELETE را برای خاتمهی نشست بپذیرد و استریمهای SSE را با Last-Event-ID از سر بگیرد.
نسخهی 2026-07-28 پروتکل MCP، این سازوکارها را کاملاً از بستر انتقال مدرن حذف میکند. حالا هر درخواستی میتواند به هر ورکر سالمی برسد، چون نسخهی پروتکل و قابلیتهای کلاینت در بدنهی همان درخواست جابهجا میشوند. هدرهای HTTP فیلدهای مشخصی را صرفاً برای مسیریابی و اعمال سیاستها بازتاب میدهند، اما سرور پیش از اجرا، این هدرها را با بدنه تطبیق میدهد و اعتبارسنجی میکند.
این رویکرد، مقیاسپذیری و درک سیستم را بسیار سادهتر میکند. همچنین یعنی اگر سروری بستر انتقال سال 2025 را بهعنوان گزینهی فعلی آموزش دهد، در واقع مدل خرابی و امنیت اشتباهی را معرفی کرده است.
مفاهیم کلیدی
بستر stdio
اتصال stdio برای پردازهی فرزندی است که کلاینت اجرا میکند:
- کلاینت در هر خط از stdin یک پیام JSON-RPC با انکودینگ UTF-8 مینویسد.
- سرور در هر خط از stdout یک پیام JSON-RPC با انکودینگ UTF-8 مینویسد.
- سرور پیامهای خطایابی را روی stderr ارسال میکند.
- سرور بهمحض رسیدن به EOF در stdin، بیدرنگ بسته میشود.
- در نسخهی مدرن، هر درخواست نسخه و قابلیتهای کلاینت را درون
params._metaارسال میکند.
شاید پردازه برای فراخوانیهای متعددی زنده بماند، اما این رفتار بهمعنای نشست در پروتکل مدرن نیست. اگر پردازه ناگهان بسته شود، همهی درخواستهای در جریان (In-flight) از دست میروند. در این وضعیت باید پردازه را ریاستارت کنید، مراحل شناسایی و فهرستگیری را دوباره انجام دهید، اشتراکها را از اول باز کنید و عملیاتهای امن را با شناسههای درخواست جدید تکرار کنید.
پروتکل Streamable HTTP در نسخهی 2026-07-28
یک سرور مدرن تنها یک اندپوینت MCP مثل /mcp ارائه میدهد که متد POST را میپذیرد.
هر درخواست یا اعلان JSON-RPC در قالب یک درخواست جدید HTTP POST ارسال میشود. بدنهی درخواست فقط حاوی یک پیام JSON-RPC است. کلاینتها هیچ پاسخی در قالب JSON-RPC به سرور نمیفرستند.
سرور برای پاسخ به یک درخواست، یکی از این دو مورد را برمیگرداند:
- هدر
Content-Type: application/jsonهمراه با یک پاسخ JSON-RPC؛ یا - هدر
Content-Type: text/event-streamهمراه با اعلانهای مرتبط با همان درخواست، که در ادامه پاسخ نهایی JSON-RPC ارسال میشود.
سرور برای اعلانی که پذیرفته است، پاسخ 202 Accepted را بدون بدنه برمیگرداند.
کلاینتها پشتیبانی از هر دو نوع پاسخ را اعلام میکنند:
Accept: application/json, text/event-stream
فقط POST یعنی دقیقاً فقط POST
در Streamable HTTP مدرن، نه استریم مستقل GET داریم و نه اندپوینت DELETE برای پایاندادن به نشست.
- درخواست
GET /mcpخطای405 Method Not Allowedبرمیگرداند. - درخواست
DELETE /mcpخطای405 Method Not Allowedبرمیگرداند. - هدر
Mcp-Session-Idکاملاً نادیده گرفته میشود؛ سرور هرگز آن را نمیسازد و بازتاب نمیدهد. - هدر
Last-Event-IDنادیده گرفته میشود، چون استریمهای مدرن امکان ازسرگیری ندارند.
اگر استریمی در دامنهی درخواست، پیش از دریافت پاسخ نهایی قطع شود، کلاینت آن درخواستِ در جریان را از دست داده است. در این حالت، اگر تکرار عملیات امن باشد، کلاینت میتواند درخواست جدیدی با یک شناسهی JSON-RPC تازه بفرستد، اما نباید برای ازسرگیری استریم تلاش کند.
اعتبارسنجی مبدأ (Origin)
سرورها هدر Origin را در ارتباطات ورودی اعتبارسنجی میکنند تا جلوی حملات DNS Rebinding را بگیرند. اگر این هدر ارسال شده باشد ولی در دامنههای مجاز تعریف نشده باشد، خطای 403 Forbidden برگردانید. کلاینتهای غیرمرورگری میتوانند هدر Origin را ارسال نکنند و قواعد رسمی بستر انتقال نیز این اجازه را به آنها میدهد.
سرورهای محلی باید بهجای گوشدادن روی همهی رابطها، روی 127.0.0.1 متصل شوند. سرویسهای تحت شبکه همچنان در تکتک درخواستها به احراز هویت و تعیین سطح دسترسی نیاز دارند. اعتبارسنجی مبدأ همان احراز هویت نیست.
مقایسهی مبدأ را پس از اعمال تنظیمات استاندارد، بهصورت کاملاً دقیق (Exact match) انجام دهید. بررسیهای پیشوندی مثل origin.startswith("https://trusted.example") ناامن هستند، چون ممکن است پسوندهای تحت کنترل مهاجم را بپذیرند.
هدرهای متادیتای اجباری در HTTP
هر درخواست مدرن POST شامل این موارد است:
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: notes_search
قوانین هدرها:
- ارسال هدر
MCP-Protocol-Versionاجباری است و باید دقیقاً برابر باparams._meta.io.modelcontextprotocol/protocolVersionباشد. - ارسال هدر
Mcp-Methodاجباری است و باید برابر باmethodدر JSON-RPC باشد. - ارسال هدر
Mcp-Nameبرای متدهایtools/call،resources/readوprompts/getالزامی است. - مقدار
Mcp-Nameبرابر باparams.name، یا برایresources/readبرابر باparams.uriاست. - نام هدرها حساس به بزرگی و کوچکی حروف نیست، ولی مقادیر آنها حساس به بزرگی و کوچکی حروف است.
برای مقادیر ناامن یا غیر اسکی (Non-ASCII) در Mcp-Name، دقیقاً از ساختار نشانهگذار Base64 در UTF-8 استفاده کنید:
=?base64?{Base64EncodedValue}?=
سرور این مقدار را قبل از مقایسه با بدنه، رمزگشایی (Decode) میکند.
اگر هدرهای بازتابی فرستاده نشوند، نامعتبر باشند، یا با بدنه همخوانی نداشته باشند، پاسخ HTTP 400 با کد خطای JSON-RPC به صورت -32020 برگردانده میشود. اگر هدر و بدنه روی نسخهای هماهنگ باشند که سرور از آن پشتیبانی نمیکند، پاسخ HTTP 400 با کد -32022 و ساختار خطای دقیق زیر را برگردانید: {"supported":["2026-07-28"],"requested":"2027-01-01"}.
اگر متد ارسالی ناشناخته باشد، سرور پاسخ HTTP 404 با کد JSON-RPC معادل -32601 میدهد. این بدنهی JSON-RPC حیاتی است، چون کلاینتهای سازگار با هر دو نسل به کمک آن خطای مدرن را از مسیر نیافتن روی اندپوینتهای قدیمی تشخیص میدهند.
رویدادهای ارسالی سرور در دامنهی درخواست (Request-scoped SSE)
سرور میتواند برای درخواستی که زمان زیادی میبرد، استریم SSE را انتخاب کند:
POST tools/call id=41
<- notifications/progress related to id=41
<- notifications/progress related to id=41
<- JSON-RPC response id=41
stream closes
سرور نباید روی این استریم درخواستهای مستقل JSON-RPC بفرستد. تعاملات مربوط به Sampling، Elicitation و Roots با استفاده از نتایج Multi Round-Trip Request مدیریت میشوند. بستن استریم پاسخ، همان درخواست را لغو میکند.
شناسهی رویداد SSE را برای بازپخش اضافه نکنید؛ قابلیت ازسرگیری با Last-Event-ID در این ویرایش مدرن پشتیبانی نمیشود.
مدیریت تغییرات زنده با subscriptions/listen
اعلانهای تغییرات بهجای متد مستقل GET، از طریق درخواستی مدیریت میشوند که خودِ کلاینت باز کرده است:
{
"jsonrpc": "2.0",
"id": "listen-1",
"method": "subscriptions/listen",
"params": {
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["notes://note-1"]
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "course-client",
"version": "1.0.0"
}
}
}
}
پاسخ این درخواست POST، یک استریم طولانیمدت SSE است. اولین پیام پروتکل در این استریم، notifications/subscriptions/acknowledged خواهد بود. این پیام تأیید، تکتک اعلانهای تغییر و نتیجهی نهایی، همگی فیلد io.modelcontextprotocol/subscriptionId را داخل _meta دارند که برابر با شناسهی همان درخواست listen است. سرور میتواند برای پایداری اتصال، کامنتهای SSE بفرستد. هر زمان استریم قطع شود، کلاینت متد subscriptions/listen را با یک شناسهی درخواست جدید دوباره میفرستد و دادههای تغییریافته را مجدداً دریافت میکند.
متدهای resources/subscribe و resources/unsubscribe متعلق به معماری قدیمی هستند؛ از آنها روی ارتباط مدرن استفاده نکنید.
صریحسازی وضعیت در سطح برنامه (Explicit Application State)
حذف نشستهای پروتکل مانع ایجاد جریانهای کاری دارای وضعیت نیست. سرور میتواند یک دستگیرهی وضعیت (State handle) کدر بسازد و آن را بهعنوان نتیجهی معمولی یک ابزار برگرداند. کلاینت در فراخوانیهای بعدی، این دستگیره را بهعنوان یک آرگومان صریح میفرستد.
دستگیرهها را به هویت احرازشده (Principal) متصل کنید، آنها را غیرقابلحدس بسازید، برایشان زمان انقضا بگذارید و در هر بار استفاده دسترسی را بررسی کنید. با این کار، وضعیت بهجای پنهانماندن در سطح اتصال (Transport affinity)، در لایهی برنامه آشکار و شفاف میشود.
مشکل ذخیرهی وضعیت در نمونههای سرویس (Replica)، ماهیتی فنی دارد:
- درخواست A به Replica 1 میرسد و یک پیشنویس در حافظهی رم آن پردازه میسازد.
- پاسخ سرور دستگیرهی پیشنویس را برنمیگرداند، چون فرض کرده که اتصال، خودِ پیشنویس را مشخص میکند.
- درخواست B در قالب یک POST جدید فرستاده میشود و به Replica 2 میرسد.
- نمونهی Replica 2 متادیتای پروتکل را دارد، ولی هیچ راهی برای شناسایی یا بارگذاری آن پیشنویس ندارد؛ در نتیجه عملیات شکست میخورد یا شیء محلی اشتباهی را میخواند.
- مسیریابی چسبنده (Sticky routing) شاید این مشکل را ظاهراً بپوشاند، اما بهمحض ریاستارت، دیپلوی، تغییر زمانبندی یا Failover، درخواست بعدی جابهجا شده و خطا رخ میدهد.
مرزبندی درست شامل دو بخش است: زمینهی پروتکل در هر درخواست باقی میماند، اما وضعیت پایدار برنامه در پایگاه دادهی مشترک و تحت یک دستگیرهی صادرشده از سمت سرور ذخیره میشود که به کلاینت تحویل داده شده است. فراخوانی بعدی این دستگیره را ارسال میکند، هر Replica میتواند همان رکورد را بخواند و لایهی احراز هویت و دسترسی، رکورد را با هویت و مستأجر (Tenant) تطبیق میدهد. حافظهی رم سرور میتواند دادهها را کش کند، اما برای کارکرد درست سیستم نباید تنها نسخهی نگهداری اطلاعات باشد.
سازوکار ذخیرهی وضعیت را با توجه به طول عمر آن انتخاب کنید. متغیرهای محلی درخواست (Request-local) برای یک فراخوانی کافی هستند. یک فرایند کوتاهمدت MRTR میتواند از requestState همراه با بررسی صحت داده استفاده کند. یک پیشنویس یا وظیفهی بادوام به دستگیرهی صریح بههمراه ذخیرهسازی مشترک، انقضا، کنترل همروندی و ویژگی خنثیبودن تکرار (Idempotency) نیاز دارد. هیچکدام از این موارد، یک نشست در پروتکل MCP محسوب نمیشوند.
سازگاری دو دورهای در HTTP
کلاینتی که از سرورهای مدرن و قدیمی پشتیبانی میکند، ابتدا یک POST مدرن میفرستد. اگر با خطای HTTP 400، 404 یا 405 روبهرو شود، بدنه را بررسی میکند:
- خطای استاندارد و شناختهشدهی JSON-RPC در ساختار مدرن نشان میدهد سرور بهروز است. در این حالت باید درخواست را اصلاح کنید یا با نسخهای معتبر دوباره بفرستید؛ هرگز دانگرید نکنید.
- بدنهی خالی یا پاسخ ناشناخته ممکن است نشاندهندهی یک سرور قدیمی HTTP+SSE باشد. فقط در این حالت میتوانید اندپوینت قدیمی GET را امتحان کنید و منتظر رویداد قدیمی
endpointبمانید.
یک سرور هم در دورهی مهاجرت میتواند از هر دو نسل پشتیبانی کند؛ کافی است متادیتای مدرن را به پیادهسازی مدرنِ مبتنی بر POST هدایت کند و اندپوینتهای جداگانهای برای کلاینتهای قدیمی نگه دارد. اما هرگز رفتارهایی مثل GET مستقل، متد DELETE، شناسهی نشست یا بازپخش رویدادها را بخشی از نسخهی 2026-07-28 معرفی نکنید.
پیادهسازی عملی
فایل code/main.py یک سرور محدود و مدرن Streamable HTTP را فقط با کتابخانهی استاندارد Python پیادهسازی میکند. این کد Origin و هدرهای بازتابی را اعتبارسنجی میکند، هدرهای حذفشدهی نشست را نادیده میگیرد، برای فراخوانیهای عادی خروجی JSON میدهد و استریم SSE محدودی از subscriptions/listen را اجرا میکند.
cd code
python3 main.py --probe
python3 -m unittest discover tests -v
کد ارزیابی این موارد را بررسی میکند:
- رد شدن Origin نامعتبر؛
- موفقیتآمیز بودن مرحلهی شناسایی بدون شناسهی نشست؛
- نادیدهگرفتن
Mcp-Session-IdوLast-Event-ID؛ - بازگرداندن خطای
-32020در صورت عدم تطابق هدر با بدنه؛ - بازگرداندن خطای
-32022همراه با اطلاعات دقیق فیلدهایsupportedوrequestedدر صورت عدم پشتیبانی از نسخه؛ - بازگرداندن پاسخ HTTP
202بدون بدنه هنگام پذیرش اعلانهای بدون شناسه؛ - بازگرداندن خطای
405برای متدهای GET و DELETE؛ - اجرای
subscriptions/listenبهصورت استریم پاسخ POST که در تأییدیه، اعلانها و نتیجهی نهایی، شناسهی اشتراک را برمیگرداند.
آمادهسازی خروجی نهایی
در این بخش، فایل outputs/skill-mcp-transport-migrator.md آماده میشود. این فایل نشستهای پروتکل را حذف میکند، اعتبارسنجی هدر و بدنه را میافزاید، رویکرد subscriptions/listen را جایگزین GET مستقل میسازد و لایههای سازگاری با سیستمهای قدیمی را کاملاً مجزا نگه میدارد.
تمرینها
- هدر
Mcp-Methodرا از یک درخواست POST حذف کنید. مطمئن شوید که پاسخ سرور HTTP400و خطای-32020است. - نسخهی
2027-01-01را بهطور یکسان در هدر و بدنه بفرستید. بررسی کنید که پاسخ سرور HTTP400با کد خطای-32022و محتوای دقیق{"supported":["2026-07-28"],"requested":"2027-01-01"}باشد. - یک مقدار Base64 نشانهگذاریشده در هدر
Mcp-Nameبرای URI غیر اسکی ارسال کنید. بررسی کنید که سرور پس از رمزگشایی (Decode)، آن را باparams.uriمقایسه کند. - استریم محدود listen را پیش از پایان پاسخ قطع کنید. سپس آن را با یک شناسهی JSON-RPC جدید دوباره بفرستید و ابزارها را مجدداً دریافت کنید.
- یک دستگیرهی صریح به ابزار ping اضافه کنید. بدون استفاده از چسبندگی اتصال به سروری خاص، دستگیره را به هویت مجاز احراز هویت مقید کنید.
اصطلاحات کلیدی
| اصطلاح | معنی |
|---|---|
| stdio | تبادل خطبهخط پیامهای JSON-RPC از طریق پردازهی فرزندی که کلاینت اجرا کرده است |
| Streamable HTTP | تکاندپوینتی که در آن هر پیام مدرن، یک درخواست جدید POST است |
| Request-scoped SSE | استریم پاسخ یک POST که شامل اعلانهای مرتبط و پاسخ نهایی است |
subscriptions/listen |
درخواست طولانیمدت POST برای دریافت اعلانهای تغییراتِ انتخابشده |
| Header mismatch | دریافت HTTP 400 و JSON-RPC -32020 هنگام مغایرت هدرهای بازتابی با بدنه |
| Origin validation | محافظت در برابر حملات DNS Rebinding در اتصالات ورودی، نه احراز هویت |
| Explicit state handle | توکن لایهی برنامه که مثل یک پارامتر معمولی ارسال میشود، نه وضعیت پنهان نشست |
| Legacy bridge | سازوکارهای نسخههای قبلی که صرفاً برای سازگاری بهصورت مجزا نگه داشته شدهاند |
