در طراحی ابزارهای متصل به مدلهای زبانی از طریق پروتکل زمینهی مدل (Model Context Protocol یا به اختصار MCP)، گاهی عملیاتی حساس رخ میدهد؛ مثلاً کاربر میخواهد رکوردی حذف شود یا ابهام میان چند گزینهی مشابه برطرف گردد. در نسخههای پیشین MCP، بسیاری از توسعهدهندگان به ریشهها (Roots) تکیه میکردند یا تصور میکردند تعیین مسیر از سوی کلاینت بهمعنای داشتن مجوز کافی برای دسترسی به فایلهاست؛ فرضی نادرست که میتواند آسیبپذیریهای امنیتی جدی پدید آورد.
در این درس با مشخصات مدرن MCP آشنا میشوید و یاد میگیرید چگونه دامنهی دسترسی را بهصورت آرگومانهای صریح تعریف کنید، اعتبارسنجی و مهار مسیر (Containment) را سمت سرور به انجام برسانید و فرآیند دریافت تعاملی ورودی از کاربر را با الگوی بیوضعیت MRTR و بدون نیاز به نشستهای مداوم مدیریت کنید.
قابلیت ریشهها (Roots) در نسخهی 2026-07-28 پروتکل MCP منسوخ شده است و اساساً هیچوقت هم سندباکس امنیتی نبوده است. دامنهی دسترسی (scope) را داخل آرگومانهای پیدای ابزار یا URIهای منبع بگذارید، مجوز آن را سمت سرور صادر کنید و هر جا ابزار واقعاً به ورودی کاربر نیاز دارد، از MRTR استفاده کنید. کاربر تصمیم را میبیند، مدل شناسه را دریافت میکند و هر نمونهای از سرور میتواند تلاش مجدد (retry) را پردازش کند.
اهداف یادگیری
- جایگزینی ریشههای منسوخشده با پارامترهای صریح محیط کاری (workspace)، URIهای منبع یا پیکربندی سمت سرور.
- تفکیک راهنماهای دامنه (scope hints) از مجوزدهی (authorization)، مهار مسیر (path containment) و سندباکسسازی در سطح سیستمعامل.
- ارائهی
elicitation/createدر حالت فرم از طریق نتیجهیinput_requiredدر الگوی MRTR. - اعلان پشتیبانی از استخراج ورودی (elicitation) در قابلیتهای کلاینت بهازای هر درخواست و رد حالتهای پشتیبانینشده.
- اعتبارسنجی مقادیر
accept،declineوcancelبهعنوان پیامدهای کاملاً متمایز. - متصلکردن تأییدیهی عملیات مخرب به هویت احرازشده (authenticated principal)، آرگومانهای اصلی، مجموعهی گزینهها و زمان انقضا.
دو مسئله که شبیه به نظر میرسند
یک ابزار مدیریت یادداشت این درخواست را دریافت میکند: «گزارش قدیمی TPS را حذف کن.»
سرور باید به دو پرسش کاملاً متفاوت پاسخ دهد:
- این عملیات مجاز به دسترسی به کدام محیط کاری است؟
- منظور کاربر دقیقاً کدامیک از سه یادداشتِ مشابه بوده است؟
مورد اول مربوط به دامنه و مجوزدهی است؛ مورد دوم مربوط به ابهامزدایی (disambiguation) تعاملی است. یکی دانستن این دو به طراحیهای خطرناکی ختم میشود؛ مثل اینکه پوشهی ارسالی کلاینت را دلیلی بر این بدانیم که فراخواننده حق دارد همهچیز را داخل آن حذف کند.
ریشهها بستری برای مهاجرت هستند
در نسخههای قبلی MCP، کلاینت میتوانست ریشهها را اعلان کند و هروقت این فهرست تغییر کرد به سرور خبر دهد. ریشهها صرفاً نقش راهنمایی و اطلاعرسانی داشتند؛ نه مانع از خواندن دادهها توسط فرآیند سرور میشدند، نه به فراخواننده مجوزی میدادند و نه هیچ سندباکسی در سطح سیستمعامل میساختند.
نسخهی 2026-07-28 متدهای roots/list و notifications/roots/list_changed را برای طراحیهای جدید منسوخ کرده است. بهتر است از یکی از این جایگزینهای صریح استفاده کنید:
- آرگومان ابزار مثل
workspaceUriیاdirectory، وقتی دامنه در هر فراخوانی تغییر میکند. - یک URI منبع، وقتی عملیات از قبل روی یک منبع مشخص متمرکز است.
- پیکربندی سمت سرور، وقتی یک استقرار فقط به یک محیط کاریِ ثابت اختصاص دارد.
- سندباکس فرآیند یا سیستمفایل محصور (jailed filesystem)، وقتی کد از نظر فنی نباید تحت هیچ شرایطی بتواند به بیرون فرار کند.
اگر یک سامانهی فعلی 2026-07-28 در طول دورهی گذار هنوز به roots/list نیاز داشته باشد، سرور آن را داخل inputRequests در MRTR قرار میدهد و نباید درخواست معکوسِ زنده (live reverse request) بفرستد. این روش یک تطبیقدهنده (adapter) برای مهاجرت است؛ هندلرهای جدید باید مستقیماً دامنهی صریح را بپذیرند.
مدل میتواند شناسهی صریح را ببیند و همان را تکرار کند. در مقابل، دامنهای که در نشست بستر انتقال (transport session) پنهان شده باشد، بررسی، بازپخش، ممیزی و مسیریابیاش بهمراتب دشوارتر است.
قانون سهلایه
یک URI صریح هم بهخودیخود به خودش مجوز نمیدهد. هر سه لایه را پیادهسازی کنید:
- مجوزدهی: آیا این هویتِ احرازشده اجازهی استفاده از این محیط کاری را دارد؟
- مهار (Containment): آیا URI هدفِ نرمالسازیشده داخل مرز محیط کاریِ مجاز باقی میماند؟
- سندباکس: آیا در صورت نفوذ به سرور، سیستمعامل میتواند مانع خروج فرآیند به بخشهای دیگر شود؟
سروری که در این درس اجرا میکنیم، فهرستی مجاز (allowlist) از URIهای محیط کاری نگه میدارد، مسیرهای کدگذاریشده با کاراکتر درصد را نرمال میکند، مرز اجزای مسیر واقعی را میسنجد و درست پیش از حذف، مهار مسیر را دوباره بررسی میکند.
بررسیهای سطحی و ابتدایی با پیشوند رشتهای اشتباه هستند:
allowed: file:///work/notes
attacker: file:///work/notes-evil/secret.md
traversal: file:///work/notes/%2e%2e/private.md
هر دو مسیر خصمانهی بالا با رشتهای فریبنده شروع میشوند. ابتدا مسیر را نرمال کنید و سپس اجزای آن را بسنجید. سرور فایل در محیط عملیاتی باید در برابر تداخل لینکهای نمادین (symbolic-link races) و رفتار اختصاصی مسیرها در پلتفرمهای مختلف هم ایمن باشد.
استخراج ورودی هنوز وجود دارد، اما شیوهی ارسال تغییر کرده است
استخراج ورودی قابلیت فعلی کلاینت برای دریافت ورودی از کاربر حین اجرای tools/call، prompts/get یا resources/read است. نام متد همچنان elicitation/create مانده، اما جهت جریان داده روی بستر ارتباطی عوض شده است.
در نسخهی 2026-07-28، سرور دیگر درخواست معکوس JSON-RPC نمیفرستد، بلکه یک InputRequiredResult برمیگرداند:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"delete_choice": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Choose one matching note and confirm deletion.",
"requestedSchema": {
"type": "object",
"properties": {
"note_id": {
"type": "string",
"enum": ["note-3", "note-7", "note-14"]
},
"confirm": {"type": "boolean"}
},
"required": ["note_id", "confirm"]
}
}
}
},
"requestState": "integrity-protected-delete-state"
}
}
میزبان فرم را نمایش میدهد. کاربر میتواند آن را بپذیرد، صریحاً رد کند یا ببندد. سپس کلاینت درخواست اصلی tools/call را با یک شناسهی تازه تکرار میکند:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "notes_delete",
"arguments": {
"workspaceUri": "file:///Users/alice/Documents/Notes",
"title": "TPS report"
},
"inputResponses": {
"delete_choice": {
"action": "accept",
"content": {"note_id": "note-14", "confirm": true}
}
},
"requestState": "integrity-protected-delete-state",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {"form": {}}
}
}
}
}
بین این دو فراخوانی هیچ نشست پروتکلی وجود ندارد. سرور وضعیت بازگشتی را بازبینی میکند، پاسخ را بر اساس اسکیمای مورد انتظار میسنجد، مطمئن میشود یادداشت انتخابی جزو گزینههای امضاشده بوده است، دوباره مجوز محیط کاری را بررسی میکند، مهار مسیر را میسنجد و در نهایت عملیات حذف را انجام میدهد.
مذاکرهی قابلیتها بهازای هر درخواست است
کلاینتی که از استخراج ورودی در حالت فرم پشتیبانی میکند، این را اعلان میکند:
{
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {"form": {}}
}
}
برای حفظ سازگاری، اعلانِ خالیِ قابلیت استخراج بهصورت "elicitation": {} همچنان معادل پشتیبانی انحصاری از حالت فرم است. مقدار صریح "elicitation": {"form": {}} هم از حالت فرم پشتیبانی میکند. اما اعلانِ صرفاً URL بهصورت "elicitation": {"url": {}} شامل فرم نمیشود. سرور نباید حالتی را داخل پاسخ بگنجاند که در قابلیتهای درخواست فعلی وجود ندارد؛ حتی اگر درخواستی قبلی آن را اعلان کرده باشد.
همهی درخواستها فیلد io.modelcontextprotocol/protocolVersion را هم همراه دارند. اگر این نسخه ارسال نشود یا از نوع رشته نباشد، خطای -32602 برمیگردد. رشتهی پشتیبانینشده خطای -32022 را همراه با دادههای دقیق supported و requested برمیگرداند. اگر قابلیت استخراج اعلام نشود یا فقط حالت URL فرستاده شود، خطای -32021 با مقدار data.requiredCapabilities برابر با {"elicitation":{"form":{}}} بازگردانده میشود.
پاکتی که فاقد id در JSON-RPC باشد، اعلان (notification) است. آن را پردازش کنید، بدون اینکه پاسخ موفقیت یا خطای JSON-RPC بفرستید. در بستر Streamable HTTP، اعلانِ پذیرفتهشده کد 202 Accepted را بدون بدنه میگیرد.
بهتر است clientInfo برای خطایابی ارسال شود، اما چون این داده را خود کلاینت اعلام میکند، نمیتوان برای مجوزدهی به آن تکیه کرد.
سرور متد server/discover را پیادهسازی میکند و مقادیر supportedVersions، قابلیتها، ttlMs و cacheScope را با resultType: "complete" برمیگرداند. این سرور در این طراحی مدرن ریشهها را اعلان نمیکند. چون ابزارها را معرفی میکند، پیادهسازی متد الزامی tools/list هم ضروری است. این خروجی توصیفگرِ قطعیِ notes_delete، یک inputSchema معتبر از نوع شیء، متادیتای شناسهی سرور و راهنماهای کَش عمومی را برمیگرداند.
حالت فرم
حالت فرم از یک اسکیمای JSON محدود استفاده میکند که برای پنجرههای تعاملیِ کاربردی طراحی شده است. ریشهی آن یک شیء است و ویژگیهایش فیلدهای اولیهی مسطح یا آرایههایی از enumهای پشتیبانیشده هستند. اشیای عمیقاً تودرتو و اسکیماهای همهمنظورهی سندها جایی در دیالوگهای تأیید ندارند.
از حالت فرم برای این موارد استفاده کنید:
- انتخاب یکی از چند گزینهی موجود؛
- تأیید یک عملیات مخرب؛
- دریافت اولویتهای غیرحساس کاربر؛
- جمعآوری تعداد اندکی مقدار که تصمیمگیرندهی آنها باید خود کاربر باشد، نه مدل.
از حالت فرم برای گذرواژهها، کلیدهای API، توکنهای دسترسی یا اطلاعات پرداخت استفاده نکنید. این دادههای محرمانه از کلاینت MCP عبور میکنند و ممکن است به لاگها یا کانتکست مدل برسند.
سرور محتوای بازگشتی را دوباره اعتبارسنجی میکند. اعتبارسنجی فرم سمت کلاینت رابط کاربری را روانتر میکند، اما اعتماد امنیتی ایجاد نمیکند.
حالت URL
حالت URL یک آدرس وب امن برای تعامل خارج از کانال (out-of-band) ارسال میکند:
{
"method": "elicitation/create",
"params": {
"mode": "url",
"message": "Connect the report service to continue.",
"url": "https://mcp.example.com/connect/report-service"
}
}
از این حالت زمانی استفاده کنید که اطلاعات حساس باید مستقیماً وارد یک جریان وبیِ تحت کنترل سرور شوند؛ مثل مجوزدهی به سرویسهای شخص ثالث. کلاینت مقصد کامل را به کاربر نشان میدهد و پیش از باز کردن آن رضایت میگیرد. کلاینت نباید این URL را از قبل واکشی کند (prefetch).
پاسخ accept یعنی کاربر با باز کردن URL موافقت کرده است؛ اما اثبات نمیکند که جریان خارجی واقعاً به پایان رسیده باشد. سرور در تلاش مجدد، وضعیت داخلی خودش را چک میکند و یا کار را تمام میکند یا یک نتیجهی input_required دیگر پس میدهد.
استخراج ورودی از طریق URL جایگزین اعطای مجوز میان کلاینت MCP و سرور MCP نیست. این سازوکار برای تعاملی خارجی است که سرور MCP باید به نیابت از کاربر انجام دهد. سرور باید کاربری را که در مرورگر است به همان هویت احرازشدهای متصل کند که عملیات MCP را آغاز کرده بود.
شاخههای پاسخ
با این عملیاتها مثل تصمیمهای محصولی برخورد کنید، نه نامهای جایگزین:
| عمل | معنا | رفتار امن سرور |
|---|---|---|
accept |
کاربر تعامل را ثبت کرد | محتوا را اعتبارسنجی کن و ادامه بده |
decline |
کاربر صریحاً رد کرد | یک خروجیِ کامل و بدون خطا از نوع امتناع برگردان |
cancel |
کاربر پنجره را بست یا نتوانست تمام کند | کار را در نقطهای امن متوقف کن و اجازهی تلاش مجدد در آینده را بده |
هرگز نبودِ محتوا را بهمعنای موافقت در نظر نگیرید. هرگز رد درخواست (decline) را به یک حلقهی تکراری از پرامپتها تبدیل نکنید.
محافظت از وضعیت مخرب در الگوی MRTR
فهرست گزینهها نباید صرفاً داخل پرامپت یا یک مقدار Base64 بدون امضا قرار بگیرد. کلاینت روی تمام دادههایی که برمیگرداند کنترل دارد.
در این درس، وضعیت ذخیرهشده شامل این موارد است و امضا میشود:
- هویت احرازشده؛
- متد مبدأ؛
- چکیدهی
workspaceUriوtitle؛ - شناسههای مجاز یادداشتها که در فرم نمایش داده شدهاند؛
- مرحلهی عملیات؛
- زمان انقضای کوتاه.
سرور پیش از اعمال تغییر، وضعیت لحظهای رکورد یادداشت را هم بررسی میکند. این کار جلوی تداخل همزمانی در حذف و جابهجایی هدف به خارج از محیط کاری پس از نمایش فرم را میگیرد.
برای عملیاتهای مالی یکباره یا بازگشتناپذیر، کد HMAC بهتنهایی جلوی بازپخش یک وضعیت معتبر پیش از انقضا را نمیگیرد. یک نانس (nonce) را دقیقاً یک بار در مخزن بازپخشی که بین همهی نمونههای هندلر به اشتراک گذاشته شده ذخیره و مصرف کنید. در این درس یک مخزن محدود تزریق میشود که دادههای منقضی را با TTL پاکسازی میکند و همزمان با اجرای حذف در حافظه، مالکیت این نانس را انحصاراً نگه میدارد. در پایگاه دادههای عملیاتی، ثبت نانس و اعمال تغییر باید درون یک تراکنش یا مرز نوشتنِ شرطیِ معادل پیوند بخورند.
پیش از ثبت و مصرف نانس، تعامل را اعتبارسنجی کنید. پاسخی که ساختار نادرست داشته باشد یا لغو (cancel) شود، تغییری ایجاد نمیکند و وضعیت تا زمان انقضا برای تلاش مجدد باقی میماند. رد صریح (decline) پایانی است؛ بنابراین در این درس نانس بدون حذف هیچ دادهای مصرف میشود.
پیادهسازی از صفر
فایل code/main.py یک ابزار مدرن notes_delete را پیادهسازی میکند:
- متد
tools/listیک توصیفگرِ قطعی و قابل کَش همراه با اسکیمای لازم برای محیط کاری و عنوان برمیگرداند. - دامنه یک آرگومان صریح به نام
workspaceUriاست. - پیکربندی سرور به آن محیط کاری برای هویتِ این درس مجوز میدهد.
- نرمالسازی URI تداخل پیشوندها و پیمایش دایرکتوری با کاراکترهای کدگذاریشده را رد میکند.
- هر حذفِ مخرب نیازمند استخراج ورودی در حالت فرم است.
- استخراج ورودی داخل ساختار
resultType: "input_required"ارسال میشود. - مقدارِ امضاشدهی
requestStateفهرست دقیق گزینهها و آرگومانهای اصلی را مقید میکند. - مخزنِ بازپخشِ تزریقشده مانع از این میشود که یک وضعیتِ پذیرفتهشده یا ردشده روی دو نمونهی مختلف سرور دوباره اجرا شود.
- تلاش مجدد با یک شناسه درخواستِ تازه انجام میشود و خروجی
resultType: "complete"برمیگرداند.
مخزن داده در حافظه نگهداری میشود تا رفتار پروتکل بهراحتی قابل بررسی باشد. قوانین امنیتی در صورت استفاده از پایگاه داده هم دقیقاً همین خواهند بود.
اجرای عملی
از ریشهی مخزن اجرا کنید:
cd phases/13-tools-and-protocols/12-mcp-roots-and-elicitation/code
python3 main.py
python3 -m unittest discover tests -v
چکپوینتهای مورد انتظار:
- شناسایی (Discovery) ابزارها را بدون ریشهها اعلان میکند.
- شناسایی ابزارها متد
notes_deleteرا همراه باresultType، متادیتای شناسهی سرور و راهنماهای کَش برمیگرداند. - شناسهی درخواست
1فرم را درinputRequests.delete_choiceبرمیگرداند. - شناسهی درخواست
2وضعیت امضاشده را بازتاب میدهد و عملیات حذف را به پایان میرساند. - هر دو مسیر پیشوندی و پیمایش کدگذاریشده در شرط مهار مسیر رد میشوند.
- عنوانی که تغییر کرده باشد نمیتواند از وضعیت تأیید اولیهاش دوباره استفاده کند.
- رد عملیات، یادداشت را دستنخورده باقی میگذارد.
- دو شیء سرور که وضعیت یادداشت و بازپخش را به اشتراک دارند، نمیتوانند هر دو یک تأییدیه را اجرا کنند.
- اعلانهای خالی و صریح فرم کار میکنند، در حالی که پشتیبانی از صرفاً URL دقیقاً خطای الزامات فرم
-32021را بازمیگرداند. - خطاهای نسخهی پشتیبانینشده دقیقاً ساختار دادهی
-32022را برمیگردانند. - اعلانِ بدون شناسه هیچ پاسخ JSON-RPC تولید نمیکند.
خروجی این درس
فایل outputs/skill-elicitation-form-designer.md دامنهی صریح، بررسیهای مجوز، فرم MRTR، شاخههای پاسخ و پیوند وضعیت را طراحی میکند. این فایل ریشههای منسوخ را سندباکس در نظر نمیگیرد و از جمعآوری اطلاعات حساس از طریق حالت فرم پرهیز میکند.
تمرینها
- مخزن بازپخش درونحافظهای را با SQLite جایگزین کنید. با یک تراکنش نانس را مصرف و یادداشت را حذف کنید؛ سپس نشان دهید دو فرآیند نمیتوانند هر دو عملیات commit را با هم انجام دهند.
- مذاکرهی قابلیت
urlو یک جریان راهاندازی خارج از کانال را اضافه کنید. اطلاعات کاربری سرویس شخص ثالث را بیرون ازinputResponsesنگه دارید. - نگاشت درونحافظهای یادداشت را با یک پایگاه دادهی موقت SQLite عوض کنید. مجوز و مهار مسیر را دوباره داخل همان تراکنشِ تغییر بازبینی کنید.
- برای پیادهسازی واقعی روی سیستمفایل، سیاست پیوند نمادین (symlink) اضافه کنید. توضیح دهید چرا بررسی صرفاً واژگانیِ مهار در URI نمیتواند جلوی گریز از طریق پیوند نمادین را بگیرد.
- یک تطبیقدهنده برای نسخهی 2025-11-25 طراحی کنید که خروجی هندلر مدرن MRTR را به استخراج ورودی قدیمیِ آغازشده توسط سرور تبدیل کند. این منطق را از هندلر فعلی جدا نگه دارید.
اصطلاحات کلیدی
| اصطلاح | معنا در نسخهی 2026-07-28 |
|---|---|
| ریشهها (Roots) | راهنماهای منسوخ و اطلاعرسانی از محیط کاری؛ نه مجوزدهی هستند نه سندباکس |
| دامنهی صریح (Explicit scope) | شناسهی محیط کاری، دایرکتوری یا منبع که در آرگومانهای درخواست پیدا است |
| مهار (Containment) | بررسی اجزای مسیر نرمالشده برای نگهداشتن هدف در داخل یک مرز مشخص |
| استخراج ورودی (Elicitation) | قابلیت کلاینت برای دریافت ورودی کاربر حین اجرای عملیات MCP |
| حالت فرم (Form mode) | دریافت ورودی ساختاریافتهی کاربر درون کانال با یک اسکیمای مسطح و محدود |
| حالت URL | تعامل خارج از کانال برای جریانهای کاری حساس یا خارجی |
| الگوی MRTR | نتیجهی بیوضعیتِ نیازمند ورودی که بعد از آن یک تلاش مجددِ تازه صورت میگیرد |
وضعیت درخواست (requestState) |
دادهی مات و محرمانهای که کلاینت عیناً برمیگرداند و یکپارچگی آن سمت سرور بررسی میشود |
| رد صریح (Decline) | امتناع مستقیم و واضح کاربر |
| لغو (Cancel) | بستن پنجره یا نیمهکاره ماندن تعامل بدون اعلام موافقت |
سازگاری با نسخههای قدیمی
برای سیستمی که روی نسخهی 2025-11-25 قفل شده است، ممکن است متدهای roots/list، notifications/roots/list_changed و استخراج ورودی زنده و آغازشده توسط سرور (elicitation/create) همچنان وجود داشته باشند. چنین تطبیقدهندهای را در کد بهعنوان نسخهی قدیمی برچسب بزنید. اجازه ندهید فهرست ریشههای قدیمی بررسیهای مجوز سمت سرور را دور بزند و فرضیات نشست پروتکل را وارد هندلر مدرن نکنید.
