مدلهای زبانی و ابزارهای متنی سنتی میتوانند اطلاعات را در قالب متون ساده یا جدولهای محدود توصیف کنند، اما خروجیهای تعاملی مانند نمودارهای پویا، خطوط زمانی قابل فیلتر یا پنلهای عملیاتی نیازمند واسط کاربری هستند. افزونهی MCP Apps بدون زیر پا گذاشتن استانداردهای وب، امکان اجرای امن رابطهای کاربری تعاملی را در کلاینتهای مدلهای زبانی فراهم میکند.
در معماری جدید پروتکل MCP، نشستهای طولانیمدت و دستدادنهای سنگین حذف شدهاند و ارتباطات به صورت بیوضعیت (stateless) انجام میشوند. این موضوع توسعهدهندگان را با چالش جدیدی مواجه میکند: چگونه میتوان یک برنامهی وب پویا را درون ابزاری فراخوانی کرد که هیچ نشست پایداری در سطح پروتکل ندارد؟
در این درس یاد میگیرید که چگونه افزونهی Apps را از طریق `server/discover` معرفی کنید، منبع نمایشی با طرح شناسه `ui://` را پیش از فراخوانی ابزار مقید سازید و تبادل پیامها را با رعایت اصول سندباکس، سیاست امنیت محتوا (CSP) و اعتبارسنجی دقیق مبدأ در کلاینت مدیریت کنید.
یک نتیجهی تعاملی همچنان همان تبادل ابزار و منبع در پروتکل MCP است. هستهی نسخهی 2026-07-28 این تبادل را خودکفا (self-contained) میکند و افزونهی Apps نیز بستر سندباکسشدهی مرورگر (sandboxed browser surface) را به آن میافزاید.
اهداف یادگیری
- معرفی MCP Apps از طریق
server/discoverو قابلیتهای افزونه بهازای هر درخواست. - تعریف منبع
ui://روی ابزار، پیش از فراخوانی آن. - بازگرداندن نتایج کامل ابزار و منبع بر بستر انتقال (transport) بیوضعیت (stateless) نسخهی 2026-07-28.
- تفکیک پیام رابطِ (bridge)
ui/initializeدر Apps از فرایند دستدادنِ (handshake) حذفشده در هستهی MCP. - اِعمال اعتبارسنجی مبدأ، سندباکس، سیاست امنیت محتوا (CSP) و دسترسیهای حداقللازم (least-privilege).
صورت مسئله
یک نتیجهی متنی میتواند یک خط زمانی را توصیف کند، اما نمیتواند خط زمانی تعاملی در اختیارتان بگذارد تا بتوانید آن را فیلتر یا بررسی کنید و رویش اقدامی انجام دهید.
افزونهی MCP Apps این مشکل نمایش را با یک افزونهی اختیاری حل میکند. تعریف ابزار (Tool Definition) به یک منبع ui:// اشاره دارد. میزبان میتواند پیش از اجرای ابزار، آن منبع را دریافت و بازبینی کند، آن را داخل یک iframe سندباکسشده رندر نماید و تمام اقدامات برنامه را از طریق یک رابطِ JSON-RPC هدایت کند.
پروتکل اصلی در نسخهی 2026-07-28 تغییر کرده است. برنامهی خود را در چرخهی حیات اتصالات قدیمی نپیچید:
- درخواست
initializeو اعلانnotifications/initializedدر هستهی پروتکل حذف شدهاند. - هدر
Mcp-Session-Idدیگر وجود ندارد. - هر درخواست، نسخهی پروتکل و قابلیتهای کلاینت را داخل
params._metaارسال میکند. - سرور متد
server/discoverرا پیادهسازی میکند تا کلاینتها بتوانند نسخهها، قابلیتهای اصلی و افزونهها را بررسی کنند. - تمام نتایج موفق یک تفکیککنندهی
resultTypeدارند. - پروتکل Streamable HTTP بهازای هر درخواست از یک متد POST استفاده میکند. درگاههای GET و DELETE در نسخهی مدرن خطای 405 برمیگردانند.
رابط Apps هنوز متدی به نام ui/initialize دارد، اما این متد متعلق به ساختار postMessage در iframe است و نشست هستهی MCP را دوباره زنده نمیکند.
مفاهیم پایه
دو پروتکل در قالب یک قابلیت
مرز میان لایهها را دقیق نگه دارید:
- هستهی MCP وظیفهی انتقال متدهای
server/discover،tools/list،tools/call،resources/listوresources/readرا بر عهده دارد. - افزونهی MCP Apps واسط کاربری را تعریف میکند و رابطِ میان iframe و میزبان را شکل میدهد.
- قوانین سندباکس مرورگر دسترسیهای واسط کاربری را محدود میکنند.
شناسهی این افزونه io.modelcontextprotocol/ui است و فعالسازی آن با توافق هر دو طرف انجام میشود. کلاینت در هر درخواست، پشتیبانی از افزونه را درون شیء قابلیتها (capabilities) ارسال میکند:
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {}
}
},
"io.modelcontextprotocol/clientInfo": {
"name": "timeline-host",
"version": "1.0.0"
}
}
}
}
پیشنهاد میشود از clientInfo برای خطایابی استفاده کنید. این فیلد صرفاً دادهای اظهاری از سمت کلاینت است، نه هویتی برای احراز صلاحیت.
شناسایی پیش از رندر
نتیجهی شناسایی در سرور، این افزونه را معرفی میکند:
{
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"resources": {},
"extensions": {
"io.modelcontextprotocol/ui": {}
}
},
"ttlMs": 300000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "timeline-app-server",
"version": "2.0.0"
}
}
}
سرور باید از شناسایی پشتیبانی کند. با این حال نیازی نیست کلاینت پیش از هر اقدامی حتماً متد شناسایی را فراخوانی کند؛ چون هر اقدام قابلیتهای خاص خودش را همراه دارد.
تعریف واسط کاربری در تعریف ابزار
قرارداد مدرن Apps، واسط کاربری را در tools/list به ابزار مقید میکند:
{
"name": "notes_timeline",
"description": "Render a timeline of notes.",
"inputSchema": {
"type": "object",
"properties": {}
},
"_meta": {
"ui": {
"resourceUri": "ui://notes/timeline.html"
}
}
}
این فراداده عمداً پیش از فراخوانی ارسال میشود. به این ترتیب میزبان میتواند پیش از آنکه نتیجهی ابزار نیازمند نمایش باشد، فایل HTML را پیشبارگذاری و کش کرده و از نظر امنیتی بازبینی کند. کدهای سازگار با نسخههای قبلی شاید کلیدهای متنی ساده و مسطح قدیمی را هم بپذیرند، اما سرورهای جدید باید ساختار تودرتوی _meta.ui.resourceUri را ارسال کنند.
متد tools/list در هستهی فعلی قابل کشکردن است. پاسخ را بهصورت مرتبشدهی قطعی (deterministic) و همراه با ttlMs و دامنهی (scope) کش یعنی cacheScope بازگردانید. اگر ابزارهای در دسترس بهازای هر کاربر یا توکن فرق میکند، cacheScope را روی private بگذارید.
ارسال داده و واگذاری اتصال نما به میزبان
فراخوانی ابزار (Tool call) محتوای عادی را در کنار دادههای ساختاریافته برمیگرداند:
{
"resultType": "complete",
"content": [
{"type": "text", "text": "Timeline ready."}
],
"structuredContent": {
"notes": [
{"id": "note-1", "title": "Discover", "created": "2026-07-28"}
]
},
"isError": false
}
میزبان از پیش میداند چه نمایی (View) به این ابزار تعلق دارد؛ بنابراین نیازی نیست صرفاً برای تکرار URI یک بلوک محتوایی جدید بسازید.
ارائهی برنامه در قالب یک منبع
سرور قابلیت resources را در مرحلهی شناسایی معرفی میکند؛ بنابراین باید متد الزامی resources/list را هم پیادهسازی کند. آیتمهای این لیست قطعی شامل URI استاندارد، نام ثابت، توضیحات و نوع MIME است. پاسخِ لیست درست مانند لیست قطعی ابزارها شامل resultType، فرادادهی هویت سرور، ttlMs و cacheScope خواهد بود.
میزبان resources/read را ارسال میکند. این درخواست در Streamable HTTP شامل موارد زیر است:
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: resources/read
Mcp-Name: ui://notes/timeline.html
مقادیر هدر و بدنهی JSON-RPC باید دقیقاً یکسان باشند. عدم تطابق آنها خطای پروتکل -32020 را به دنبال دارد.
پاسخ شامل منبع HTML و راهنماهای کش (cache hints) است:
{
"resultType": "complete",
"contents": [
{
"uri": "ui://notes/timeline.html",
"mimeType": "text/html;profile=mcp-app",
"text": "<!doctype html>...",
"_meta": {
"ui": {
"csp": {
"connectDomains": [],
"resourceDomains": [],
"frameDomains": [],
"baseUriDomains": []
},
"permissions": {}
}
}
}
],
"ttlMs": 60000,
"cacheScope": "public"
}
کشکردن منابع واسط کاربری بهعنوان محتوای اجرایی
منبع یک برنامه با یک متن معمولی قابل تعویض نیست. دادههای کششدهی آن میتوانند کدهای رابط را اجرا کنند، دادههای ابزار را رندر نمایند و کارهایی را با وساطت میزبان درخواست دهند. کلید کش را بر اساس URI استاندارد ui://، هویت و نسخهی تأییدشدهی سرور، خلاصهی محتوای منبع (digest) و در صورت وجود دامنهی خصوصی برای کش، بافت احراز هویت تعیین کنید. هرگز یک منبع برنامهی خصوصی را میان کاربران مختلف به اشتراک نگذارید؛ چون حتی با یکسان بودن URI، ممکن است فایل HTML یا فرادادههای امنیتی آن متفاوت باشد.
با منقضیشدن ttlMs، تغییر پیوند _meta.ui.resourceUri در ابزار، تغییر نسخهی سرور یا شناسهی تأییدشده، یا دریافت اعلان تغییر منبع برای آن URI، کش را باطل کنید. پیش از رندر مجدد، منبع را دوباره دریافت کرده و CSP و مجوزها را بازبینی کنید. یک iframe قدیمی نباید فقط به این دلیل که نسخهی جدید هنوز بارگذاری نشده، دسترسیهای گستردهتر خود را حفظ کند.
اعتبارسنجی ترافیک خام پیش از سیاستهای دسترسی
اعتبارسنجی ترتیب مشخصی دارد: ابتدا ساختار JSON-RPC را بررسی کنید و مطمئن شوید فرادادههای پروتکل از نوع رشته و نگاشت قابلیتهای کلاینت از نوع شیء هستند. سپس هدرهای مسیربندی را با بدنه مطابقت دهید. فقط پس از آن بررسی کنید که آیا نسخهی پروتکل تطبیقیافته پشتیبانی میشود یا خیر. این ترتیب مانع از تفسیر متناقض درخواست بین پروکسی و سرور میشود.
| وضعیت | کد HTTP | خطای JSON-RPC |
|---|---|---|
| عدم تطابق نسخه، متد یا نام در هدر و بدنه | 400 | -32020 |
| توافق هدر و بدنه روی نسخهای پشتیبانینشده | 400 | -32022، با مقدار دقیق data برابر با {"supported":["2026-07-28"],"requested":"<actual>"} |
نبود قابلیت افزونهی Apps در resources/read |
400 | -32021، با مسیر data.requiredCapabilities.extensions.io.modelcontextprotocol/ui |
| متد ناشناخته باشد | 404 | -32601 |
اعلانهای JSON-RPC فیلد id ندارند؛ بنابراین سرور هرگز پاسخی در قالب JSON-RPC برای آنها ارسال نمیکند. یک اعلان پذیرفتهشده در بستر HTTP با وضعیت 202 و بدنهی خالی پاسخ داده میشود. بروز خطا ممکن است وضعیت HTTP را تغییر دهد، اما باز هم نمیتواند بدنهی خطای JSON-RPC برای اعلانها بسازد.
سندباکس فقط یک مرز امنیتی است، نه تأیید اعتماد
میزبان کنترل کامل iframe را در دست دارد. برنامه نمیتواند کوکیها، حافظهی محلی یا DOM صفحهی میزبان را مستقیماً بخواند. تمام کارهای نیازمند دسترسی باید از رابط عبور کنند.
این پیشفرضها را به کار بگیرید:
- تمام دامنههای مجاز در CSP را در ابتدا خالی بگذارید و فقط مبدأهای مورد نیاز برنامه را اضافه کنید. از
connectDomainsبرای fetch، XHR و WebSocket و ازresourceDomainsبرای اسکریپتها، استایلها، تصاویر و فونتها استفاده کنید. - تا حد ممکن کدها و دادهها را بستهبندی (bundle) کنید.
- هیچ مجوزی برای دوربین، میکروفون یا موقعیت مکانی نگیرید، مگر اینکه قابلیتی ملموس در برنامه به آن نیاز داشته باشد.
- متد
postMessageرا دقیقاً به مبدأ طرف مقابل مقید کنید و رویدادهای سایر مبدأها را نپذیرید. - آرگومانهای ابزار، نتایج آن، متن منبع و پیامهای رابط را ورودیهای نامطمئن در نظر بگیرید.
- اخذ رضایت کاربر را درون خود میزبان نگه دارید. یک iframe نمیتواند اقدامات پیامددار را خودش تأیید کند.
یک ویژگی sandbox ثابت را از آموزشهای دیگر در تمام میزبانها کپی نکنید. میزبان باید پرچمهای آن را متناسب با مدل مبدأ برنامه و ساختار ایزولهسازی خود تعیین کند.
یک دامنهی مجاز همچنان میتواند مسیری برای نشت اطلاعات باشد. تنظیم connectDomains: ["https://api.example.com"] یعنی هر اسکریپتی داخل برنامه میتواند دادههای مجاز را به آن آدرس بفرستد. تطبیق دقیق مبدأ جلوی ابهام در مقصد را میگیرد، اما مناسببودن محتوای ارسالی را تعیین نمیکند. دسترسیهای connect را بهطور پیشفرض خالی بگذارید، از قرار دادن توکنهای حامل (bearer tokens) در iframe خودداری کنید، تا جای ممکن عملیات محدود را از طریق میزبان هدایت (proxy) کنید، حجم درخواستها و پاسخها را محدود نگه دارید و مشخص کنید هر درخواست بر اثر کدام اقدام کاربر ایجاد شده است. با resourceDomains جدا از connectDomains رفتار کنید؛ مجوز بارگذاری یک فونت یا اسکریپت نباید اجازهی بارگذاری دادههای دلخواه را صادر کند.
چرخهی حیات مستقل رابط Apps
رابط Apps یک ساختار JSON-RPC بر بستر postMessage است. این ساختار میتواند پیامهای ui/initialize و اعلانهای ui/* را منتقل کند و متدهای شبیه به هستهی پروتکل مثل tools/call را به عنوان پروکسی اجرا نماید.
نما (View) پیام ui/initialize را به همراه appInfo و شیء appCapabilities ارسال میکند. میزبان در پاسخ، قابلیتها و بافت (context) خود را برمیگرداند. تنها پس از این پاسخ است که نما پیام ui/notifications/initialized را ارسال میکند. میزبان باید قبل از ارسال هر پیامی به نما، منتظر این اعلانِ Apps بماند.
این دستدادن (handshake) محلی، رابطی میان یک iframe و یک فریمِ میزبان میسازد. این کار نه نسخهی پروتکل MCP را تعیین میکند، نه وضعیتی در سرور به وجود میآورد و نه نشستی بر بستر انتقال (transport) میسازد. به پیشوندها دقت کنید: اعلان هسته یعنی notifications/initialized حذف شده، در حالی که اعلانِ Apps یعنی ui/notifications/initialized باقی مانده است. هر درخواستی در هستهی پروتکل که از طریق فراخوانی ابزار در رابط شکل میگیرد، درخواستی جدید و خودکفا با یک شناسهی JSON-RPC تازه و فرادادههای کامل است.
بافت میزبان، اقدامات و لغو دسترسی
میزبان حتی پس از راهاندازی رابط، مرجع نهایی باقی میماند. نما تنها زمانی میتواند اجرای ابزار، جابهجایی در صفحات، استفاده از کلیپبورد یا سایر کارهای حساس را درخواست کند که میزبان قابلیت آن را قبلاً اعلام کرده باشد. میزبان صحت درخواست نوعبندیشده، کاربر فعلی، مقصد و آرگومانها را بررسی میکند، سیاستهای تأیید را به کار میبندد و حتی میتواند آن را رد کند. کلیک روی دکمه و ارسال یک پیام معتبر در رابط فقط نیت کاربر (Intent) را نشان میدهند؛ هیچکدام مجوزی برای اجرا صادر نمیکنند.
تنظیمات تم، اندازه و دسترسیپذیری را به چشم بافتی متغیر در میزبان ببینید، نه ورودیهای ثابت در رندر اولیه:
- توکنهای رنگ و قلمِ ارائهشده توسط میزبان را اِعمال کنید و به تغییرات تم یا کنتراست واکنش نشان دهید.
- اجازه دهید نما ابعاد مدنظرش را اعلام کند، اما کنترل ابعاد نهایی iframe را در دست بگیرید تا محتوا از چیدمان بیرون نزند یا لایههای فریبنده نسازد.
- ترتیب کلیدهای کیبورد، نشانگر فوکوسِ مشهود، نامهای استاندارد دسترسیپذیری، وضعیت صفحهخوانها، کنتراست مناسب، بزرگنمایی و رفتار کاهش حرکت (reduced-motion) را داخل iframe رعایت کنید.
- انتقال فوکوس میان کنترلهای میزبان و کنترلهای نما را پس از تغییر اندازه و رندر مجدد دوباره بررسی کنید.
دسترسیها ممکن است حین باز بودن برنامه لغو شوند؛ چه به دلیل تغییر حساب کاربری، تغییر سیاستها، قرنطینهشدن یک سرور یا محدودشدن رضایت کاربر در میزبان. سطح دسترسی و مجوزها را در لحظهی انجام هر اقدام ارزیابی کنید، نه فقط در مرحلهی ui/initialize. هنگام لغو دسترسی، فراخوانیهای حساسِ در انتظار را رد کنید، فعالیتهای شبکهایِ خارج از چارچوب جدید را متوقف سازید، وضعیتهای حساس رندرشده را پاک کنید و در صورتی که خودِ منبع واسط کاربری دیگر پذیرفته نیست، نما را دوباره سوار کرده یا به متن بازگردید. نما باید پاسخ رد را بهعنوان یک نتیجهی عادی بپذیرد، نه اینکه درخواست را آنقدر تکرار کند تا میزبان تسلیم شود.
حفظ سازگاری در غیاب واسط کاربری
سروری که از Apps پشتیبانی میکند، همچنان باید به میزبانهایی که قابلیت واسط کاربری را اعلام نمیکنند پاسخ دهد:
- همان ابزار را در
tools/listبدون_meta.uiبرگردانید. - یک خروجی متنی کاربردی برای
tools/callنگه دارید. - درخواست
resources/readبرای منبع واسط کاربری را با خطای کمبود قابلیت رد کنید. - برای تشخیص اتمام موفق یک ابزار، هرگز فرض نکنید که یک iframe حتماً وجود دارد.
پیادهسازی
کد code/main.py یک مدل پروتکل درونپروسهای و کوچک را بدون نیاز به SDK میسازد. این کد بستهی درخواستها و مقادیر مسیربندی در Streamable HTTP را اعتبارسنجی میکند، قابلیتهای Apps را از طریق server/discover اعلام مینماید، ابزارها و منابع را فهرست میکند، ابزار را به اجرا درمیآورد و یک منبع HTML خودکفا تحویل میدهد.
این مدل دادههای از پیش تجزیهشده و هدرهای مسیربندی را دریافت میکند؛ بنابراین یک تطبیقدهندهی (Adapter) کامل HTTP نیست و Content-Type یا Accept را پردازش نمیکند. برای پیادهسازی کامل تطبیقدهندهی Streamable HTTP که نیازمند هدر Content-Type: application/json و هدر Accept حاوی هر دو مقدار application/json و text/event-stream است، درس 09 را ببینید.
آن را اجرا کنید:
cd phases/13-tools-and-protocols/14-mcp-apps
python3 code/main.py
python3 -m unittest discover code/tests -v
پنج مورد را در خروجی بررسی کنید:
- تمام فراخوانیها مستقل از هم هستند.
- تمام درخواستها قابلیتهای خود را در
_metaارسال میکنند. - متد
resources/listپیش از خواندن هر منبع، یک توصیفگر ثابت ارائه میدهد. - تمام نتایج دارای
resultTypeو فرادادههای هویت سرور هستند. - هیچ اثری از شناسهی نشست هستهی پروتکل دیده نمیشود.
استفاده عملی
کار را با server/discover شروع کنید. مطمئن شوید io.modelcontextprotocol/ui در نگاشت افزونههای سرور دیده میشود. سپس tools/list را دو بار فراخوانی کنید؛ یک بار با قابلیت Apps و بار دیگر بدون آن. پاسخ اول منبع واسط کاربری را اعلام میکند، در حالی که دومی به عنوان یک ابزار متنی ساده و کاربردی باقی میماند.
منبع ui://notes/timeline.html را بخوانید. در متن HTML به دنبال hostOrigin و شرط محافظتی event.origin بگردید. وجود این دو خط، حداقل نشانهی مشهودی است که نشان میدهد رابط برنامه از مقصدهای نامشخص (wildcard) استفاده نمیکند.
انتشار
این درس فایل outputs/skill-mcp-apps-spec.md را ارائه میدهد. پیش از نوشتن کدهای فریمورک، از آن برای بازبینی قرارداد برنامه استفاده کنید. این فایل شما را ملزم میکند بستهی کنونی هسته، مذاکرهی افزونهها، حالت بازگشت به متن (fallback)، منبع واسط کاربری، سیاستهای کش، CSP، مجوزها، متدهای رابط و مرزهای رضایت کاربر را دقیق و صریح مشخص کنید.
تمرینها
- قابلیت کلاینت را به یک نگاشت خالی از افزونهها تغییر دهید. مطمئن شوید
tools/listابزار را حفظ میکند اما پیوند واسط کاربری را حذف مینماید. - هدر
Mcp-Name: ui://notes/other.htmlرا همراه با بدنهای که خط زمانی را میخواند بفرستید و خطای-32020را بررسی کنید. - دامنهی کش را به
cacheScope: privateتغییر دهید و شرطِ وابسته به کاربری را که این کار را توجیه میکند شرح دهید. - اسکریپت را به آدرس
https://static.example.com/app.jsمنتقل کنید. این مبدأ را درresourceDomainsوارد نمایید و ریسک زنجیرهی تأمین ناشی از آن را توضیح دهید. - یک ابزار به نام
notes_openاضافه کنید و کلیک روی دکمه را از طریق میزبان هدایت نمایید. تصمیمگیری برای تأیید کاربر را درون خود میزبان نگه دارید.
اصطلاحات کلیدی
| اصطلاح | معنی |
|---|---|
| MCP Apps | افزونهای اختیاری برای نمایش تعاملی HTML توسط میزبان MCP |
io.modelcontextprotocol/ui |
شناسهی افزونه که هر دو طرف آن را اعلام میکنند |
ui:// |
طرح شناسه (scheme) برای قالب واسط کاربری برنامه |
text/html;profile=mcp-app |
نوع MIME اختصاصی برای صفحات HTML برنامههای MCP |
server/discover |
متد کنونی RPC برای شناسایی پروتکل و قابلیتها |
resources/list |
متد الزامی دریافت لیست منابع، زمانی که سرور منابع را معرفی میکند |
resultType |
تفکیککنندهی الزامی برای نتایج موفق در نسخههای جدید |
ui/initialize |
اولین پیام رابط Apps، مجزا از فرایند اولیهسازی حذفشده در هسته |
ui/notifications/initialized |
اعلان آمادگی نمای برنامه که پس از پاسخ میزبان ارسال میشود |
| CSP | سیاست مرورگر برای محدودسازی اسکریپتها، استایلها، تصاویر و مبدأهای شبکه |
| حالت بازگشت به متن (Text fallback) | رفتار پشتیبان ابزار برای پاسخ به میزبانهایی که از Apps پشتیبانی نمیکنند |
