در طراحی سرورهای مبتنی بر پروتکل کانتکست مدل (Model Context Protocol)، تفکیک وظایف اهمیت بسیار بالایی دارد. معمولاً این اشتباه رخ میدهد که هر نوع بازخوانی اطلاعات به شکل یک ابزار (Tool) یا پرامپت پیادهسازی میشود، در حالی که پروتکل MCP برای محتوای خواندنی و پایدار، سازوکار مشخصی به نام منابع (Resources) و برای جریانهای کاری ساختیافته، مفهوم پرامپتها (Prompts) را در نظر گرفته است.
این درس به شما کمک میکند معماری تعامل مدل و سرور را بر مبنای قراردادهای بدون وضعیت (Stateless) تنظیم کنید. با درک نحوهی اعلام قابلیتها در متد `server/discover`، تعیین سیاستهای امنیتی و راهنماهای کش، و پیادهسازی سیستم اشتراک رویدادها از طریق استریمهای پایدار، سروری استاندارد، پیشبینیپذیر و ایمن میسازید که بدون وابستگی به تاریخچهی اتصال کلاینت، پاسخها را با دقت بازمیگرداند.
ابزارها عملیات انجام میدهند. منابع محتوای آدرسپذیر را ارائه میدهند. پرامپتها قالبهای پیامِ انتخابی کاربر را بستهبندی میکنند. یک سرور MCP خوب این قراردادها را تفکیکشده و قابلپیشبینی نگه میدارد.
اهداف یادگیری
- انتخاب درست میان ابزارها، منابع و پرامپتها بر اساس نیت مصرفکننده.
- اعلام دامنهی منابع و پرامپتها از طریق متد الزامی
server/discover. - ساخت نتایج قطعی و تکرارپذیر برای
resources/listوprompts/list. - بهکارگیری
ttlMsوcacheScopeبدون نشت دادههای اختصاصی کاربران. - بازگرداندن خطای JSON-RPC با کد
-32602برای URIهای نامعتبر یا ناشناختهی منابع. - باز کردن استریم پاسخ POST برای
subscriptions/listenو تطبیق هر رویداد با شناسه اشتراک. - برخورد با محتوای منابع و قالبهای پرامپت بهعنوان خروجی غیرقابلاعتمادِ سرور.
از مصرفکننده شروع کنید
راحتترین راه برای استفادهی نادرست از MCP این است که کار را با کد پیادهسازی شروع کنید. یک پرسوجوی دیتابیس تبدیل به ابزار میشود، صرفاً چون با مفهوم تابع آشنایید؛ یک گردش کارِ با قابلیت استفادهی مجدد تبدیل به منبع میشود چون در یک فایل ذخیره شده است؛ و یک پرامپت تبدیل به سیاستهای پنهان میشود چون میزبان میتواند آن را تزریق کند.
نقطهی شروع باید این باشد: چه کسی انتخاب میکند و چه انتظاری دارد؟
| مولفهی پایه (Primitive) | نیت اصلی | چه کسی آن را انتخاب میکند؟ | خروجی معمول |
|---|---|---|---|
| ابزار (Tool) | انجام یک عملیات | مدل یا برنامه کاربردی | نتیجهی ساختاریافتهی یک اقدام |
| منبع (Resource) | خواندن محتوا در یک URI مشخص | میزبان، برنامه یا کاربر | محتوای متنی یا باینری |
| پرامپت (Prompt) | شروع یک گردش کار پیامیِ قابلاستفادهی مجدد | کاربر از طریق رابط کاربری میزبان | یک یا چند پیام پرامپت |
یک یادداشت در مسیر notes://note-1 یک منبع است، چون محتوایی آدرسپذیر است. delete_note یک ابزار است، چون وضعیت (state) را تغییر میدهد. review_note یک پرامپت است، چون کاربر یک گردش کارِ آمادهی بررسی را انتخاب میکند.
صرفاً برای اینکه همهچیز کامل به نظر برسد، یک عملیات را در قالب هر سه مولفه پیاده نکنید. هر سطح و رابط اضافه، نیازمند اکتشاف، تعیین سطح دسترسی، کشکردن، مدیریت خطا، تست و مستندسازی است.
بستهی بیوضعیت ویرایش 2026-07-28
این درس نسخهی 2026-07-28 پروتکل MCP را هدف قرار داده است. در این ساختار، هیچ دستدادن (handshake) برای مقداردهی اولیه یا نشست پروتکلی (protocol session) وجود ندارد. هر درخواست، نسخهی پروتکل و قابلیتهای کلاینتِ خود را در کلیدهای رزروشدهی _meta همراه دارد.
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "course-client",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
پیادهسازی متد server/discover برای سرور الزامی است. خروجی این متد نسخههای پشتیبانیشده، قابلیتهای منابع و پرامپتها، هویت پیادهسازی و راهنماهای کش را اعلام میکند. کلاینت میتواند مستقیماً متد دیگری را فراخوانی کند، اما این اکتشاف پیش از ساخت رابط کاربری یک اسنپشات پایدار در اختیارش میگذارد.
{
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"resources": {"listChanged": true, "subscribe": true},
"prompts": {"listChanged": true}
},
"ttlMs": 3600000,
"cacheScope": "public"
}
یک پاسخ عادی، وضعیت "resultType": "complete" را اعلام میکند. فیلد _meta در پاسخ، پیادهسازی ارائهدهنده را با io.modelcontextprotocol/serverInfo مشخص میکند. این اطلاعات برای عیبیابی مفید است، اما هویتی برای احراز هویت محسوب نمیشود. اگر درخواستی نسخهای پشتیبانینشده را ارسال کند، خطای -32022 همراه با نسخهی درخواستی و نسخههای پشتیبانیشدهی سرور برگردانده میشود.
این قراردادِ بیوضعیت، درک شما را از طراحی تغییر میدهد. هیچ فهرستی نباید به فراخوانی پیشین در طول همان اتصال وابسته باشد. اعتبارسنجی ممکن است مجموعهی قابلمشاهده را تغییر دهد چون اطلاعات هویتی ورودیِ درخواست هستند، اما تاریخچهی اتصال نباید چنین تأثیری بگذارد.
منابع قراردادهای پایدار URI هستند
منبع، محتوایی است که با یک URI شناخته میشود. ساختار URI را پیش از نوشتن هندلر طراحی کنید.
ویژگیهای یک URI مناسب:
- بهاندازهی کافی پایدار باشد تا بتوان آن را بوکمارک کرد یا بین درخواستها فرستاد.
- در فضای نامِ (namespace) دامنهی سرور تعریف شده باشد.
- از شناسهی پردازه (process ID) یا اتصال مستقل باشد.
- پیش از دسترسی به فضای ذخیرهسازی اعتبارسنجی شود.
- در هر بار خواندن، سطح دسترسی آن بررسی شود.
آدرس notes://note-1 از note-1 بهتر است، چون فضای نام آن مشخص و صریح است. یک سرور فایل ممکن است از URIهای file:// استفاده کند، اما همچنان باید پس از حل کردن لینکهای نمادین (symlinks) و بخشهای نسبی مسیر، مرزهای دایرکتوریهای مجازِ پیکربندیشده را بررسی کند.
متد resources/list منابعی را برمیگرداند که در حال حاضر برای فراخواننده قابلمشاهده هستند. خروجی را بر اساس یک کلید پایدار مثل URI مرتب کنید. این نظم قطعی مانع از دست رفتن بیهودهی کش (cache misses)، تغییر اسنپشاتها و پرش رابط کاربری میزبان بین تازهسازیها میشود.
{
"resultType": "complete",
"resources": [
{
"uri": "notes://note-1",
"name": "Architecture decision",
"description": "Why the service uses a stateless boundary",
"mimeType": "text/markdown"
}
],
"ttlMs": 300000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "notes-server",
"version": "2.0.0"
}
}
}
متد resources/read یک یا چند آیتم محتوایی را برمیگرداند. یک URI ناشناخته به معنی خواندنِ موفق یک محتوای خالی نیست. بر اساس مشخصات فعلی منابع در MCP، برای URIهای نامعتبر یا ناشناختهی منابع، خطای پارامترهای نامعتبرِ JSON-RPC با کد -32602 در نظر گرفته شده است.
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Unknown or invalid resource URI",
"data": {
"uri": "notes://missing"
}
}
}
این تفکیک به کلاینت اجازه میدهد نبود سند را از یک سند معتبر اما خالی تشخیص دهد. همچنین از بازگشت ناخواستهی سیستم به یک جستوجوی گستردهتر جلوگیری میکند.
قالبهای منابع
یک قالب منبع (resource template)، خانوادهای از URIهای پارامتربندیشده را توصیف میکند. وقتی فهرست کردن تکتک آیتمهای عینی هزینهبر یا بیپایان باشد، از قالب استفاده کنید. برای مثال، notes://projects/{project}/decisions/{decision} به کلاینت نشان میدهد چطور بدون نیاز به دریافت تکتک تصمیمها، آدرس معتبری بسازد.
استفاده از قالب، اعتبارسنجی را ضعیف نمیکند. متغیرها را تجزیه کنید، اعتبارسنجی سطح دسترسی را اعمال کنید، محدودیت طول و کاراکترها را در نظر بگیرید و پرسوجوهای ذخیرهسازی را با پارامترهای تایپشده بسازید. هرگز بخش انتهاییِ دلخواه یک URI را مستقیماً به مسیر فایلسیستم یا دستور دیتابیس الحاق نکنید.
محتوا دستورالعمل قابلاعتماد نیست
متن منابع ممکن است حاوی تزریق پرامپت، دادههای محرمانه، دستورات گمراهکننده یا نشانهگذاریهای معیوب باشد. میزبان باید منشأ و سندیت را حفظ کند و با محتوای منبع صرفاً به عنوان داده برخورد کند. سرور نیز باید حجم محتوا را محدود کند، نوع MIME دقیق برگرداند، فیلدهایی را که فراخواننده به آنها دسترسی ندارد پنهان کند و از بازگرداندن رکوردهای غیرمرتبط بپرهیزد.
پرامپتها قالبهای تحت کنترل کاربر هستند
پرامپتهای MCP برای انتخاب صریح توسط کاربر طراحی شدهاند. میزبان میتواند آنها را در قالب دستورات اسلش (slash commands)، گزینههای منو یا دکمههای گردش کار نمایش دهد. پروتکل رابط کاربری خاصی را تحمیل نمیکند.
خروجی prompts/list برای یک درخواست با سطح دسترسی یکسان، باید همیشه قطعی و یکدست باشد. هر پرامپت به نامی پایدار، توضیحی مفید و تعریف آرگومانهایی نیاز دارد که به میزبان امکان دهد پیش از فراخوانی prompts/get، ورودیها را جمعآوری کند.
{
"resultType": "complete",
"prompts": [
{
"name": "review_note",
"title": "Review a note",
"description": "Review one note for a named concern",
"arguments": [
{
"name": "uri",
"description": "The note resource URI",
"required": true
}
]
}
],
"ttlMs": 600000,
"cacheScope": "public"
}
متد prompts/get آرگومانها را به پیام تبدیل میکند، اما جایگزین دستورالعملهای سیستمی میزبان نمیشود. میزبان خودش تصمیم میگیرد پیامهای برگشتی چگونه وارد زمینهی مدل شوند و سیاستهای معتبر خود را در اولویت بالاتری نگه میدارد.
آرگومانهای پرامپت را در مرز ورودی سرور اعتبارسنجی کنید. URI پرامپت باید همان بررسیهای دسترسیِ متد خواندن مستقیم منبع را پشت سر بگذارد. نگذارید پرامپت به کانالی فرعی برای دور زدن محدودیتهای دسترسی به منابع تبدیل شود.
راهنماهای کش بخشی از درستی عملکرد هستند
فیلد ttlMs به کلاینت میگوید یک نتیجه تا چه مدت قابلاستفادهی مجدد است. فیلد cacheScope نیز مشخص میکند چه کسانی میتوانند آن مقدار کششده را به اشتراک بگذارند.
| دامنه (Scope) | معنی | کاربرد معمول |
|---|---|---|
public |
در صورت مجاز بودن دسترسی، میتواند بین کاربران مختلف استفاده شود | کاتالوگ عمومی پرامپتها |
private |
محدود به کاربر یا بستر هویتیِ درخواستدهنده | محتوای یادداشت متعلق به یک کاربر |
مقدار TTL را بر اساس نرخ تغییر دادهها و میزان آسیبی که کهنگی داده وارد میکند تعیین کنید. برای مثال، 5 دقیقه ممکن است برای یک کاتالوگ عمومی پرامپت مناسب باشد؛ اما خواندن یک یادداشت شخصی شاید به 1 دقیقه زمان نیاز داشته باشد.
پروتکل MCP فقط public و private را به عنوان مقادیر مجاز برای cacheScope تعریف کرده است. برای نتایج حساس حاوی اطلاعات محرمانه یا نتایجی که بهسرعت تغییر میکنند، مقدار cacheScope: "private" را همراه با ttlMs: 0 برگردانید و سپس قوانین سختگیرانهترِ عدم ذخیرهسازی را در سیاستهای کش میزبان اعمال کنید. خودِ no-store یکی از مقادیر معتبر cacheScope در MCP نیست.
راهنماهای کش هرگز جایگزین کنترل دسترسی نمیشوند. کلید کش باید شامل تمام ابعاد درخواست باشد که بر میزان دسترسی تأثیر میگذارند؛ از جمله شناسه مستأجر (tenant)، کاربر، دامنه، زبان و مکان (locale) و نشانگر صفحهبندی (cursor). اگر یک کش مشترک نمیتواند این ابعاد را با امنیت مدیریت کند، از دامنهی private با TTL صفر و سیاست عدم ذخیرهسازی در سطح میزبان استفاده کنید.
اشتراکها از استریم پاسخی استفاده میکنند که کلاینت باز کرده است
الگوی مدرن اشتراکها جایگزین متد قدیمی RPC با عنوان resources/subscribe و اندپوینت قدیمی رویدادها مبتنی بر HTTP GET شده است.
کلاینت متد subscriptions/listen را به عنوان یک درخواست عادی JSON-RPC ارسال میکند. در بستر Streamable HTTP، این یک درخواست POST است که پاسخ آن به عنوان یک استریم SSE باز میماند. شیء notifications نقش یک لیست مجاز (allowlist) را دارد؛ سرور نباید رویدادهایی با انواعی که درخواست نشدهاند ارسال کند.
{
"jsonrpc": "2.0",
"id": 17,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "course-client",
"version": "1.0.0"
}
},
"notifications": {
"resourcesListChanged": true,
"promptsListChanged": true,
"resourceSubscriptions": [
"notes://note-1"
]
}
}
}
شناسهی درخواست، همان شناسه اشتراک است. پیش از ارسال هر رویداد درخواستشده، سرور رویداد notifications/subscriptions/acknowledged را میفرستد. فیلتر موجود در این پیام تنها شامل زیرمجموعهای است که سرور آن را پذیرفته است.
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": 17
},
"notifications": {
"resourcesListChanged": true,
"resourceSubscriptions": [
"notes://note-1"
]
}
}
}
تمام رویدادهای بعدی در آن استریم نیز همان متادیتا را با خود دارند.
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"_meta": {
"io.modelcontextprotocol/subscriptionId": 17
},
"uri": "notes://note-1"
}
}
این اعلان میگوید منبع تغییر کرده است. سپس کلاینت مجدداً آن را از طریق resources/read و با توجه به سطح دسترسی فعلی میخواند. کلاینت فرض نمیکند که متن رویداد حتماً شامل سند جدید باشد.
چندین اشتراک میتوانند از یک کانال stdio مشترک استفاده کنند. شناسه اشتراک به کلاینت امکان میدهد آنها را تفکیک کند. در بستر HTTP، بستن استریم پاسخ به معنای لغو اشتراک است. سروری که استریم را بهدرستی خاتمه میدهد، یک پاسخ نهایی با وضعیت resultType: "complete" مرتبط با درخواست اصلی ارسال میکند.
از استریم اشتراک به عنوان نشست پروتکل استفاده نکنید. درخواست خواندنِ بعدی همچنان یک درخواست مستقل و کامل است که میتواند به هر نمونهی سالمی از سرور ارسال شود.
کارگاه تعاملی
با استفاده از تصویر، 5 قابلیت یک سیستم مدیریت پروژه را دستهبندی کنید: جزئیات ایشو (issue details)، ایجاد ایشو (create issue)، قالب بررسی اسپرینت (sprint review template)، سیاستهای پروژه (project policy) و بستن ایشو (close issue). سپس مشخص کنید کدام لیستها میتوانند بهصورت عمومی کش شوند، کدام خواندنها باید خصوصی بمانند و کدام منابع شایستهی دریافت اعلانهای بهروزرسانی هستند.
برای هر دستهبندی، مشخص کنید چه کسی انتخابکننده است. اگر مدل اقدامی انجام میدهد، از ابزار استفاده کنید. اگر میزبان محتوایی آدرسپذیر با URI را میخواند، از منبع استفاده کنید. اگر کاربر یک گردش کارِ آماده از پیامها را شروع میکند، از پرامپت استفاده کنید.
تمرین عملی
شبیهساز را از ریشهی مخزن اجرا کنید:
cd phases/13-tools-and-protocols/10-mcp-resources-and-prompts/code
python3 main.py
python3 -m unittest discover tests -v
گزارش اجرا را به همین ترتیب بررسی کنید:
- مطمئن شوید
server/discoverنسخهی فعلی و هر دو قابلیت را اعلام میکند. - مطمئن شوید هر دو نتیجهی فهرست مرتب شدهاند و از
resultType: "complete"استفاده میکنند. - مطمئن شوید نتایج فهرست و خواندن، راهنماهای کش هدفمندی دارند.
- آدرس خواندن را به
notes://missingتغییر دهید و خطای-32602را مشاهده کنید. - مطمئن شوید تأیید اشتراک پیش از رویداد منبع ارسال میشود.
- مطمئن شوید رویداد و پایان عادی استریم، هر دو شناسه اشتراک
5را دارند.
مدل پایتون اتصال واقعی HTTP برقرار نمیکند؛ بلکه پیامهایی را نشان میدهد که SDK باید روی استریم پاسخی با دامنهی درخواست قرار دهد. برای بخش بستهبندی دادهها و بستر انتقال در محیط عملیاتی، از SDKهای رسمی استفاده کنید.
خروجی این درس
فایل outputs/skill-primitive-splitter.md یک بازبینیِ طراحیِ قابلاستفادهی مجدد برای انتخاب مولفههای پایهی MCP است. این سند اکنون بررسی اکتشاف قطعی، محدودهی کش، رفتار در مواجهه با URI نامعتبر و فیلترهای اشتراک مدرن را پوشش میدهد.
همچنین فایل assets/primitive-split.svg ارائه شده که نسخهای ثابت از مرز مولفههای پایه و اشتراکها برای مطالعهی آفلاین است.
اعتبارسنجی
cd phases/13-tools-and-protocols/10-mcp-resources-and-prompts/code
python3 main.py
python3 -m unittest discover tests -v
نتیجهی مورد انتظار: برنامهی اصلی یک خروجی JSON چاپ میکند و دستور تست موفقیت حداقل 12 تست را گزارش میدهد.
ارتباط با پروژه پایانی
زمانی که سرور پروژهی پایانی شما دانشی آدرسپذیر را در کنار عملیاتها ارائه میدهد، از این قرارداد استفاده کنید. کار شما باید شامل یک اسنپشات قطعی از کاتالوگ، یک خواندن منبع با احراز هویت، حل یک پرامپت، یک حالت URI نامعتبر و گزارش کامل یک اشتراک باشد.
شواهد شما باید نشان دهند که هیچ فهرستی به تاریخچهی اتصال وابسته نیست و رویدادِ اشتراک هرگز به خودی خود دسترسی به منبع اصلی را فراهم نمیکند.
تمرینها
- قالب منبع
notes://projects/{project}/notes/{id}را اضافه کرده و هر دو متغیر را اعتبارسنجی کنید. - صفحهبندی را به
resources/listاضافه کنید، طوری که ترتیب خروجی قطعی و پایدار بماند. - محدودهی کش یک منبع را به
cacheScope: "private"باttlMs: 0تغییر دهید، سیاست عدم ذخیرهسازی در سطح میزبان را اضافه کنید و تهدیدی را که دلیل استفاده از هر دو کنترل است توضیح دهید. - یک اشتراک برای تغییرات فهرست پرامپتها اضافه کنید و ثابت کنید وقتی فیلتر فاقد
promptsListChangedاست، هیچ رویدادی ارسال نمیشود. - دو اشتراک همزمان بسازید و نشان دهید هر رویداد شناسهی درخواست درست را حمل میکند.
- یک شناسه کاربر (subject) برای اعتبارسنجی دسترسی به هندلر خواندن اضافه کنید و نشان دهید مقدار کششده نمیتواند بین کاربران مختلف به اشتراک گذاشته شود.
اصطلاحات کلیدی
- منبع (Resource): محتوای آدرسپذیر با URI که توسط یک سرور MCP ارائه میشود.
- پرامپت (Prompt): قالب پیامی تحت کنترل کاربر که توسط یک سرور MCP ارائه میشود.
- فهرست قطعی (Deterministic list): نتیجهی یک فرایند اکتشاف که با ورودیهای یکسان، همیشه اعضا و ترتیب پایداری دارد.
ttlMs: مدتزمان تازگی کش به میلیثانیه.cacheScope: مرز بهاشتراکگذاری یک نتیجهی کششده.subscriptions/listen: درخواستی با طول عمر بالا که استریم پاسخ آن اعلانهای صراحتاً فیلترشده را تحویل میدهد.- شناسه اشتراک (Subscription ID): شناسهی درخواست listen اصلی که در متادیتای اعلان تکرار میشود.
- پارامترهای نامعتبر (Invalid parameters): خطای JSON-RPC با کد
-32602که برای URI نامعتبر یا ناشناختهی منبع استفاده میشود. - نسخه پروتکل پشتیبانینشده (Unsupported protocol version): خطای JSON-RPC با کد
-32022که شامل نسخههایsupportedوrequestedاست. server/discover: متد الزامی سرور که نسخههای پشتیبانیشده، قابلیتها، هویت و راهنماهای اختیاری کش را برمیگرداند.
