وقتی یک برنامهی هوش مصنوعی یا ایجنت توسعه میدهید، بهندرت تنها با یک سرور ابزار سروکار دارید. چالش اصلی زمانی آغاز میشود که کلاینت شما باید بهطور همزمان با چندین سرور MCP ارتباط برقرار کند؛ سرورهایی که ممکن است برخی از آنها از نسخههای نوین پروتکل استفاده کنند و برخی دیگر نسخههای قدیمیتر باشند. در چنین وضعیتی، تشخیص درست نسخهی سرور بدون شکستن اتصالات یا تنزل نابجای نسخه، مهارتی حیاتی است.
در این درس یاد میگیرید که چگونه یک کلاینت قوی و چندمنظوره برای پروتکل MCP بسازید. یاد خواهید گرفت چطور فرایند کشف اولیه (Discovery) را روی بستر stdio پیادهسازی کنید، کاتالوگ ابزارهای چند سرور را بدون تداخل نامها ادغام کنید، فراخوانیها را به سرور مالک بفرستید و یک رویکرد ایمن و مهارشده (Fail-closed) برای پشتیبانی از سرورهای قدیمی در پیش بگیرید.
کلاینت مدرن MCP قرارداد خود را در تکتک درخواستها تکرار میکند. سختترین تصمیم سازگاری آن، تشخیص این نکته است که یک سرور قدیمی واقعاً قدیمی است یا یک سرور مدرن دارد خطایی قابلاصلاح را گزارش میدهد.
اهداف یادگیری
- ساخت همهی درخواستهای MCP نسخهی
2026-07-28همراه با متادیتای جاری. - بررسی سرورهای stdio با
server/discoverو انتخاب نسخهای با پشتیبانی متقابل. - مجاز شمردن کاوش مهارشدهی نسخههای قدیمی (legacy probe) فقط برای سرورهای مشخصاً در لیست مجاز (allowlisted peers).
- پذیرش دورهی قدیمی (legacy era) تنها پس از اعتبارسنجی نتیجهی مثبت
initializeبرای یک بازبینیِ پشتیبانیشده. - ادغام (merge) قطعی فهرست ابزارها بدون بازنویسی پنهانی تداخلها.
- هدایت فراخوانیها به سرور مالک هر ابزار، بدون ابداع سشنهای پروتکلی.
صورت مسئله
میزبان ایجنت معمولاً با بیش از یک سرور MCP تعامل دارد. این میزبان باید تکتک سرورها را شناسایی کند، کاتالوگ ابزارها را ادغام کند، نامهای تکراری را برطرف سازد، فراخوانیها را هدایت کرده و پس از نقص بستر انتقال (transport) بازیابی شود.
بازبینی 2026-07-28 وضعیت پایدار را سادهتر میکند؛ زیرا هر درخواست مستقل و خودکفا است. با این حال، موضوع سازگاری باعث میشود راهاندازی اولیه به دقت و ظرافت بیشتری نیاز داشته باشد. کلاینت ممکن است با موارد زیر روبهرو شود:
- سروری مدرن که از نسخهی مدنظر پشتیبانی میکند؛
- سروری مدرن که یک خطای شناختهشدهی مربوط به نسخه یا هدر را برمیگرداند؛
- سروری قدیمی که هرگز نام
server/discoverبه گوشش نخورده است؛ - سروری قدیمی که تا زمان دریافت
initializeساکت میماند.
در نظر گرفتن هر خطای کاوش بهعنوان سرور قدیمی خطرناک است. یک درخواست مدرن بدساختار، یک سرور زیر بار سنگین، یک فرایند متوقفشده و یک سرور قدیمی همگی میتوانند مهلت زمانی منقضیشده (timeout) یا قطع اتصالِ یکسانی به وجود آورند. این نشانهها مبهم هستند. کلاینت پیش از انتخاب دورهی قدیمی، باید نیت صریح اپراتور را با شواهد پروتکلی مثبت ترکیب کند.
مفاهیم کلیدی
یک طرف ارتباط (peer)، نه یک سشن پروتکل
برای هر فرایند سرور یا اندپوینت (endpoint)، یک رکورد برای طرف ارتباط در بستر انتقال نگه دارید:
- هندل بستر انتقال یا تابع ارسال؛
- دوره و نسخهی پروتکل انتخابشده؛
- آخرین قابلیتهای کشفشدهی سرور؛
- آخرین فهرست قطعی ابزارها؛
- شناسههای درخواستهای معلق برای تطبیق پاسخها (correlation)؛
- سلامت بستر انتقال.
این کار صرفاً ثبت سوابق داخلی کلاینت است و وضعیت سشن در پروتکل بهشمار نمیآید. در MCP مدرن، سرور همچنان نسخه و قابلیتهای جاری را در تکتک درخواستها دریافت میکند.
ساخت هر درخواست مدرن از پایه
def modern_request(request_id, method, params, version, capabilities):
return {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": {
**params,
"_meta": {
"io.modelcontextprotocol/protocolVersion": version,
"io.modelcontextprotocol/clientCapabilities": capabilities,
"io.modelcontextprotocol/clientInfo": CLIENT_INFO,
},
},
}
متادیتا را تنها یک بار به شیء اتصال متصل نکنید با این فرض که حتماً به دست سرور میرسد. درخواست نهایی سریالایزشده را نشانهگذاری و بررسی کنید.
کشف مدرن
متد server/discover نسخههای پشتیبانیشده، قابلیتهای سرور، دستورالعملها، راهنماهای کش و هویت توصیهشدهی سرور را برمیگرداند. کلاینت بالاترین نسخهی مدرنی را که هر دو طرف از آن پشتیبانی میکنند انتخاب میکند.
فرایند کشف برای کلاینتی که صرفاً مدرن است اختیاری است، اما در stdio توصیه میشود. برخی سرورهای قدیمی عملیات را پیش از مقداردهی اولیه میپذیرند؛ بنابراین ارسال tools/list در وهلهی اول میتواند به موفقیتی گمراهکننده منجر شود. متد server/discover یک مرز شفاف میان دورهها ایجاد میکند.
کاوش سازگاری stdio
یک کلاینت stdio با پشتیبانی از هر دو دوره، پیش از هر درخواست دیگری server/discover را با متادیتای مدرن مدنظر خود ارسال میکند. سه دستهی پیامد وجود دارد:
- نتیجهی کشف (DiscoverResult): سرور مدرن است. نسخهای با پشتیبانی مشترک را انتخاب کنید و کار را با متادیتای اختصاصی هر درخواست ادامه دهید.
- خطای مدرن شناختهشده: سرور مدرن است. برای کد
-32022، نسخهای را ازdata.supportedانتخاب کرده و با شناسهی درخواست جدید دوباره تلاش کنید. در صورت بروز خطاهای هدر یا قابلیتها، درخواست را اصلاح کنید. متدinitializeرا ارسال نکنید. - نشانهی مبهم: یک خطای ناشناختهی JSON-RPC، انقضای زمان، قطع اتصال یا پاسخ خالی، دورهی پروتکل را مشخص نمیکند. در صورتی که آن طرف ارتباط مشخصاً برای سازگاری با نسخههای قدیمی پیکربندی نشده باشد، سیستم باید به شکل امن متوقف شود (fail closed).
خطاهای مدرنِ شناختهشدهی پروتکل عبارتند از:
- کد
-32020HeaderMismatch - کد
-32021MissingRequiredClientCapability - کد
-32022UnsupportedProtocolVersion
خطاهای مدرن شناختهشده مدرن باقی میمانند، حتی زمانی که سرور در لیست مجاز نسخههای قدیمی باشد. وقتی سروری ثابت میکند دامنهی خطاهای مدرن را میفهمد، ارسال initialize تنزل نسخه محسوب میشود.
کد خطای -32601 را شواهد مثبت برای قدیمی بودن در نظر نگیرید. این کد فقط به سروری که صراحتاً در لیست مجاز قرار دارد اجازه میدهد یک بار کاوش قدیمی شود. همین قاعده برای انقضای زمان، قطع اتصال یا پاسخ خالی نیز برقرار است.
لیست مجاز نیت اپراتور است، نه شواهد
سازگاری با نسخههای قدیمی باید یک ویژگی صریح در پیکربندی اختصاصی یک سرور باشد:
client.add_server("archive", archive_transport, allow_legacy=True)
این انتخاب را دقیقاً به دستور یا اندپوینت پیکربندیشده مقید کنید. از الگوهای عام (wildcard) که به هر سرور دلخواهی اجازهی ورود به قواعد سستتر را میدهند استفاده نکنید. اگر سروری گزینهی allow_legacy=True نداشته باشد، پس از نتیجهی مبهم در کشف رد میشود و هرگز initialize را دریافت نمیکند.
لیست مجاز فقط مجوز کاوش را میدهد و تعیینکنندهی دوره نیست. کلاینت یک بار initialize را تحت یک مهلت زمانی اجباری در بستر انتقال ارسال میکند، سپس تحقق همهی موارد زیر الزامی است:
- پاسخی مبتنی بر JSON-RPC
2.0با شناسهی درخواست منطبق؛ - دقیقاً یک فیلد
resultو بدون هیچ فیلدerror؛ - فیلد
protocolVersionموجود در مجموعهی بازبینیهای قدیمیِ پیکربندیشده در کلاینت؛ - فیلد
capabilitiesکه مقدار آن یک شیء باشد؛ - شیء
serverInfoشامل فیلدهای متنی غیرخالیnameوversion.
انقضای مهلت زمانی، قطع اتصال، پاسخ خطا، نتیجهی بدساختار، عدم تطابق شناسه یا بازبینی پشتیبانینشده منجر به توقف امن سیستم (fail closed) میشود. فقط یک نتیجهی مثبت و از نظر ساختاری معتبر، دورهی قدیمی را برمیگزیند. کد مقدار legacy_probe_timeout_ms را به تطبیقدهندهی (adapter) بستر انتقال میدهد؛ یک تطبیقدهندهی واقعی stdio یا HTTP باید این مهلت را عملاً اعمال کند، نه اینکه صرفاً آن را ثبت کند.
دورهی انتخابشده را برای آن طرف ارتباط در بستر انتقال کش کنید و پیش از هر فراخوانی مجدداً عملیات کاوش را انجام ندهید.
نسخهی قدیمی یک branch سازگاری است
به محض اینکه کاوش مهارشده شواهد مثبت و معتبری از قدیمی بودن برگرداند، کلاینت دقیقاً طبق تعریف همان بازبینی، از نسخهی قدیمی انتخابشده استفاده میکند:
- بررسی ساختار پاسخ و شناسهی تطبیق؛
- بررسی اینکه بازبینی توافقشده در مجموعهی نسخههای قدیمی پیکربندیشده باشد؛
- ثبت قابلیتها و هویت اعتبارسنجیشدهی سرور؛
- ارسال
notifications/initializedتنها پس از تأیید تمام بررسیها؛ - استفاده از ساختارهای درخواست قدیمی در طول مدت فعال بودن آن بستر انتقال.
این branch صرفاً برای سازگاری با سرورهای شناختهشده وجود دارد و رویکرد پیشفرض برای سرورهای جدید یا درخواستهای تازه نیست. اگر بستر انتقال ریاستارت شود یا اندپوینت آن تغییر کند، کشِ دورهی مربوط به آن طرف ارتباط را پاک کرده و دوباره مذاکره کنید.
کشف و کش کردن ابزارها
برای هر سرور فعال، tools/list را فراخوانی کنید. نتیجهی مدرن شامل resultType، ttlMs و cacheScope است. با در نظر گرفتن زمینهی مجوزدهی صحیح، به توصیهی تازگی دادهها پایبند باشید. پس از انقضا یا پس از دریافت رویداد تغییر فهرست که در آن ثبتنام کردهاید، دوباره دادهها را بخوانید.
اگر سرور قدیمی فیلد resultType را برنگرداند، کلاینتها باید مقدار آن را "complete" در نظر بگیرند. از پاسخی که متعلق به دورهی توافقشدهی قدیمی است، انتظار فیلدهای مدرن کش را نداشته باشید.
سرور باید ترتیب قطعی و مشخصی برگرداند. کلاینت نیز باید قبل از ادغام مرتبسازی را انجام دهد تا ترتیب رجیستری محلی به زمانبندی راهاندازی فرایندها وابسته نباشد.
ادغام ایمن در برابر تداخل نامها
ممکن است دو سرور ابزاری به نام search ارائه دهند. یک سیاست اعلامشده را انتخاب کنید:
- افزودن پیشوند هنگام تداخل: اولین نام استاندارد را حفظ کنید و موارد بعدیِ دارای تداخل را بهصورت
<server>/<tool>نمایش دهید. - رد کردن هنگام تداخل: ابزار تکراری را بارگذاری نکنید و یک خطای پیکربندی واضح نشان دهید.
- بازنویسی پنهانی: هرگز از این روش استفاده نکنید؛ چون پنهان میکند کدام سرور اقدام انتخابشده توسط مدل را دریافت میکند.
هم نام استاندارد و هم نام محلی را ذخیره کنید. مدل نام استاندارد را میبیند، اما فراخوانی خروجی tools/call از نام محلی اعلامشده توسط سرور مالک استفاده میکند.
هدایت یک فراخوانی
مسیریابی صرفاً یک جستوجوی ساده است:
canonical tool name
-> peer name + local tool name
-> new JSON-RPC request id
-> modern request metadata or explicit legacy shape
-> matching response id
هنگامی که بستر انتقال سرور مالک در دسترس نیست، فراخوانی را ارسال نکنید. بستر انتقال را مجدداً وصل یا ریاستارت کنید، سپس کشف و tools/list را دوباره اجرا کنید. درخواستهای مدرنِ در حال پردازش که روی یک بستر انتقال قطعشده از بین رفتهاند، در صورتی که سیاست ایمنی عملیات اجازه دهد، میتوانند با شناسهی JSON-RPC جدید دوباره تکرار شوند.
اعلانها و ثبتنامها
تغییرات مدرنِ فهرستها و منابع، فقط از طریق استریم subscriptions/listen که توسط کلاینت باز شده دریافت میشوند. کلاینت فیلتر اعلان را میفرستد، منتظر notifications/subscriptions/acknowledged میماند، و رویدادها را بر اساس شناسهی درخواست شنود در متادیتای اعلان تطبیق میدهد.
هنگام قطع ارتباط، یک درخواست شنود جدید باز کنید و فهرستها یا منابع مرتبط را دوباره واکشی کنید. استریمهای مدرن با Last-Event-ID از سر گرفته نمیشوند.
عدم وجود درخواستهای مبدأ-سرور
سرورهای مدرن برای نمونهبرداری (sampling)، استخراج اطلاعات (elicitation) یا دریافت ریشهها (roots)، با درخواستهای مستقل JSON-RPC کلاینت را فراخوانی نمیکنند. آنها وضعیت input_required را برمیگردانند و کلاینت پس از تأمین ورودیهای ارائهشده، درخواست اصلی را دوباره تکرار میکند.
هنگام تأمین ورودی، روند خواندن پاسخ از سرور را مسدود نکنید. زنجیرهی تطبیق را حفظ کرده و برای تلاش مجدد، شناسهی JSON-RPC جدید بسازید.
استفادهی عملی
فایل code/main.py از توابع سرور درونفرایندی استفاده میکند تا تصمیمات مربوط به پروتکل شفاف بمانند. این اسکریپت به دو سرور مدرن و یک سرور قدیمی که عمداً در لیست مجاز قرار گرفته متصل میشود، سپس ابزارهای آنها را ادغام و هدایت میکند. تابع فراخوانیِ بستر انتقال بودجهی زمانی مشخصی دریافت میکند تا branch سازگاری نتواند کاوشهای بدون محدودیت زمانی را پنهان کند.
cd code
python3 main.py
python3 -m unittest discover tests -v
تستها مرزهایی را اثبات میکنند که دموهای معمولی متوجه آنها نمیشوند:
- درخواستهای مدرن متادیتا را تکرار میکنند؛
- کد خطای
-32022کشف مدرن را بدون مقداردهی اولیه تکرار میکند؛ - خطاهای مدرنِ شناختهشده هرگز به نسخهی پایینتر تنزل نمییابند، حتی برای سروری که در لیست مجاز باشد؛
- انقضای زمان، قطع اتصال، پاسخهای خالی و خطاهای ناشناخته بدون لیست مجاز منجر به اجرای
initializeنمیشوند؛ - سرور موجود در لیست مجاز تنها پس از دریافت نتیجهی معتبر و پشتیبانیشدهی
initializeبه دورهی قدیمی منتقل میشود؛ - نتایج قدیمیِ بدساختار یا پشتیبانینشده سرور را در حالت غیرقابلدسترس رها میکنند؛
- دورهای که با موفقیت انتخاب شده برای طول عمر آن بستر انتقال کش میشود.
خروجی نهایی
این درس خروجی outputs/skill-mcp-client-harness.md را ارائه میدهد. این خروجی چارچوب لازم برای نشانهگذاری درخواستهای مدرن، مذاکرهی دورهی پروتکل در stdio، ادغام قطعی فضاهای نام، هدایت فراخوانیها، و یک branch سازگاری fail-closed برای نسخههای قدیمی را فراهم میآورد.
تمرینها
- یک سرور ساختگی ایجاد کنید که کد
-32022را بدون هیچ نسخهی مشترک پشتیبانیشده برگرداند. بررسی کنید که کلاینت بهجای ارسالinitializeبا خطا متوقف شود. - یک سرور قدیمیِ ساختگی را در لیست مجاز بگذارید، کاری کنید که کاوش مهارشدهی
initializeآن با پایان مهلت زمانی مواجه شود، و نشان دهید که وضعیت سرور رویunknownو غیرقابلدسترس باقی میماند. - فهرست ابزارهایی با
cacheScope: "private"برای دو زمینهی مجوزدهی مجزا اضافه کنید. مطمئن شوید کلاینت هرگز نتیجهی کششدهی یک زمینه را با زمینهی دیگر به اشتراک نمیگذارد. - سیاست تداخل را به حالت رد کردن تغییر دهید و کاری کنید که راهاندازی اولیه متوقف شود و نام هر دو سرور در پیام خطا درج گردد.
- یک شبیهساز متناهی برای
subscriptions/listenبسازید. در صورت قطع استریم، با یک شناسهی درخواست جدید دوباره فرایند شنود را آغاز کنید و ابزارها را مجدداً بخوانید.
اصطلاحات کلیدی
| اصطلاح | معنا |
|---|---|
| طرف ارتباط (Peer) | رکورد سمت کلاینت برای بستر انتقال یک سرور و دادههای کشفشدهی آن |
| دورهی پروتکل (Protocol era) | متادیتای مدرن در سطح هر درخواست یا ساختار مقداردهی اولیهی قدیمی |
| کاوش کشف (Discovery probe) | ارسال اولیهی server/discover جهت شناسایی دورهی پروتکل در stdio |
| خطای مدرن شناختهشده (Recognized modern error) | خطایی که رفتار مدرن را اثبات کرده و بازگشت به شیوهی قدیمی را ممنوع میکند |
| لیست مجاز قدیمی (Legacy allowlist) | پیکربندی اپراتور که اجازهی یک بار کاوش سازگاری مهارشده را برای سروری مشخص صادر میکند |
| شواهد مثبت قدیمی (Positive legacy evidence) | نتیجهی معتبر و تطبیقیافتهی initialize برای یک بازبینی قدیمی که صراحتاً پشتیبانی میشود |
| فضای نام ادغامشده (Merged namespace) | نامهای استاندارد ابزارها در بین همهی سرورهای فعال |
| سیاست تداخل (Collision policy) | قاعدهی افزودن پیشوند یا رد کردن برای نامهای تکراری ابزارها |
| کش دوره (Era cache) | رفتار مدرن یا قدیمیِ انتخابشده که برای بستر انتقال یک سرور ذخیره میشود |
| بازیابی بستر انتقال (Transport recovery) | راهاندازی مجدد یا اتصال دوباره، کشف مجدد، دریافت مجدد فهرست، و تلاش دوبارهی ایمن با شناسهی جدید |
