Resources ו-Prompts

Resources — הקשר בשליטת האפליקציה

בפרק 1 ראינו את היררכיית השליטה: tools בשליטת המודל, resources בשליטת האפליקציה. resource הוא נתון שהשרת חושף, מזוהה ב-URI ייחודי: קובץ, סכמה של מסד נתונים, דף תיעוד, issue. ההבדל מכלי הוא לא בתוכן אלא במי שמחליט להכניס אותו לקונטקסט.

ה-spec משאיר ל-host את ההחלטה איך להשתמש ב-resources. הוא יכול להציג אותם ב-UI לבחירה מפורשת (עץ או רשימה), לאפשר חיפוש וסינון, או לצרף אותם אוטומטית לפי היוריסטיקות או לפי בחירה של המודל. הפרוטוקול לא מכתיב דפוס מסוים.

למה צריך את שני המנגנונים? כלי מתאים כשהמודל צריך להחליט בעצמו, באמצע משימה, מה לשלוף. resource מתאים כשהמשתמש או האפליקציה כבר יודעים מה רלוונטי. למשל, משתמש שמצרף קובץ לשיחה, או host שמצרף תמיד את קובץ ה-README של הפרויקט. resource גם מאפשר ל-host לשמור ב-cache, להירשם לשינויים ולהציג תצוגה מקדימה, דברים שקשה לעשות עם תוצאה של כלי.

שרת שתומך ב-resources חייב להצהיר על היכולת resources. היא כוללת שני דגלים אופציונליים ובלתי תלויים: listChanged, להודעות כשהרשימה משתנה, ו-subscribe, לעדכונים על resource מסוים. כמו ב-tools, הרשימה לא יכולה להשתנות לפי החיבור, אבל כן לפי ההרשאה.

resources/list

resources/list מחזירה מערך resources, עם pagination ו-caching בדיוק כמו tools/list (פרק 5). לכל Resource יש uri, המזהה הייחודי, ו-name, השם הפרוגרמטי. שדות אופציונליים: title לתצוגה, description, mimeType, size בבתים, icons ו-annotations.

size שימושי ל-host: לפני שהוא קורא resource, הוא יכול להעריך כמה tokens הוא יתפוס בקונטקסט ולהחליט אם לצרף אותו, לקצר אותו או לשאול את המשתמש.

שימו לב ל-cacheScope של private בדוגמה. רשימת ה-issues תלויה בהרשאות של המשתמש, ולכן אסור ל-cache משותף להגיש אותה למשתמש אחר.

JSON
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "resultType": "complete",
    "resources": [
      {
        "uri": "issues://812",
        "name": "issue-812",
        "title": "#812 Login fails on Safari 17",
        "mimeType": "text/markdown",
        "size": 2140,
        "annotations": { "audience": ["assistant"], "lastModified": "2026-09-20T08:14:00Z" }
      },
      {
        "uri": "file:///project/CONTRIBUTING.md",
        "name": "CONTRIBUTING.md",
        "title": "Contribution guide",
        "mimeType": "text/markdown"
      }
    ],
    "nextCursor": "eyJwYWdlIjoyfQ==",
    "ttlMs": 60000,
    "cacheScope": "private"
  }
}

resources/read — טקסט ובינארי

resources/read מקבלת uri ומחזירה מערך contents. כל פריט מכיל uri ו-mimeType, ובנוסף text לתוכן טקסטואלי או blob לתוכן בינארי בקידוד base64. שרת רשאי להחזיר כמה פריטים לבקשה אחת, למשל את כל הקבצים כשקוראים resource של תיקייה.

גם כאן התשובה חייבת לכלול ttlMs ו-cacheScope. תוכן שתלוי במשתמש הוא בדרך כלל private. resources/read יכולה להחזיר גם input_required, למשל כשצריך אישור מהמשתמש לפני קריאת קובץ רגיש (פרק 7).

מקרה מיוחד: אם ה-URI מתחיל ב-https://, ה-client רשאי להביא את התוכן ישירות מהרשת בלי לעבור דרך השרת. לכן ה-spec ממליץ לשרת להשתמש ב-https רק כשה-client באמת יכול לטעון את ה-resource בעצמו. אם השרת צריך להוסיף הרשאות, לעבד את התוכן או לגשת לרשת פנימית, עדיף scheme אחר או scheme מותאם, גם אם השרת עצמו מוריד את התוכן מהאינטרנט.

JSON
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "resultType": "complete",
    "contents": [
      {
        "uri": "issues://812",
        "mimeType": "text/markdown",
        "text": "# Login fails on Safari 17. Steps: open /login, submit valid credentials, page reloads without session."
      },
      {
        "uri": "issues://812/attachments/screenshot.png",
        "mimeType": "image/png",
        "blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
      }
    ],
    "ttlMs": 30000,
    "cacheScope": "private"
  }
}

URI schemes ואבטחת resources

ה-spec מתאר כמה schemes נפוצים, והרשימה לא סגורה. https:// מייצג resource ברשת, עם ההגבלה מהסעיף הקודם. file:// מייצג resources שמתנהגים כמו מערכת קבצים, גם אם הם לא קבצים פיזיים. לרכיבים שאינם קבצים רגילים, כמו תיקיות, אפשר להשתמש ב-MIME type כמו inode/directory. git:// מייצג אינטגרציה עם git.

schemes מותאמים, כמו issues:// בדוגמאות שלנו, מותרים לגמרי, בתנאי שהם עומדים ב-RFC 3986. scheme מותאם עוזר ל-host לזהות מאיזה סוג resource מדובר, ומונע בלבול עם קבצים אמיתיים.

דרישות האבטחה מה-spec: שרת חייב (MUST) לאמת כל URI, וחייב לנקות נתיבים ב-file:// כדי למנוע directory traversal. בקשה ל-file:///project/../../etc/passwd צריכה להיכשל גם אם השרת בנוי על מערכת קבצים אמיתית. בנוסף, צריך (SHOULD) לממש בקרת גישה ל-resources רגישים ולבדוק הרשאות לפני כל פעולה, ותוכן בינארי חייב להיות מקודד כראוי.

Resource templates ו-URI Templates

לא כל resource אפשר למנות מראש. לשרת של issue tracker יש אלפי issues, ורשימה מלאה ב-resources/list לא הגיונית. לשם כך יש resource templates: תבניות URI לפי RFC 6570, עם משתנים בסוגריים מסולסלים. resources/templates/list מחזירה מערך resourceTemplates, שבו לכל תבנית יש uriTemplate, name, ו-title, description, mimeType ו-icons אופציונליים.

ה-host מציג את התבנית למשתמש (או למודל), ממלא את המשתנים, ושולח resources/read עם ה-URI המלא. למשל, owner=acme, repo=web ו-number=812 הופכים ל-repo://acme/web/issues/812. את הערכים אפשר להשלים אוטומטית דרך completion, בסוף הפרק.

RFC 6570 מגדיר ארבע רמות: רמה 1 היא החלפה פשוטה של {var} עם קידוד של תווים שמורים, ורמות 2 עד 4 מוסיפות אופרטורים כמו {+path} (בלי קידוד של /) ו-{?query} (בניית query string). בסקיצה בסוף הפרק נממש רמה 1.

JSON
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "resultType": "complete",
    "resourceTemplates": [
      {
        "uriTemplate": "repo://{owner}/{repo}/issues/{number}",
        "name": "repo-issue",
        "title": "Repository issue",
        "description": "A single issue with its description and comments",
        "mimeType": "text/markdown"
      }
    ],
    "ttlMs": 300000,
    "cacheScope": "public"
  }
}
למעלה התבנית repo://{owner}/{repo}/issues/{number}, ושלושה ערכים (acme, web, 812) עולים אל המשתנים שלה. מתחת ה-URI המלא repo://acme/web/issues/812, שממנו יוצאת בקשת resources/read לשרת. חץ אפור מקווקו מהשרת חזרה לתבנית מסומן match.
ה-client ממלא את משתני התבנית ל-URI מלא, והשרת מפרק אותו חזרה לפרמטרים

Annotations ושגיאות

resources, templates ו-content blocks תומכים ב-annotations. audience הוא מערך עם user, assistant או שניהם, ומציין למי התוכן מיועד. priority הוא מספר בין 0 ל-1: 1 פירושו חשוב מאוד (למעשה נדרש), ו-0 פירושו אופציונלי לגמרי. lastModified הוא timestamp בפורמט ISO 8601. host משתמש בהם כדי לסנן לפי קהל, לתעדף מה נכנס לקונטקסט כשהתקציב מוגבל, ולמיין לפי עדכניות.

שגיאות: resource שלא קיים מחזיר JSON-RPC error עם -32602 (Invalid params), ועדיף עם data.uri. שגיאה פנימית מחזירה -32603. שרת לא יכול (MUST NOT) להחזיר contents ריק עבור resource שלא קיים, כי מערך ריק דו-משמעי: הוא יכול להיות resource קיים וריק או resource שלא קיים.

בגרסה הקודמת, 2025-11-25, resource not found דווח בקוד -32002. client צריך (SHOULD) לקבל גם אותו, כדי לעבוד מול שרתים ישנים.

JSON
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32602,
    "message": "Resource not found",
    "data": { "uri": "repo://acme/web/issues/99999" }
  }
}

עדכונים: list_changed ו-resources/updated

יש שני סוגי הודעות על שינויים ב-resources, ושניהם עוברים דרך subscriptions/listen (פרק 4). resourcesListChanged: true במסנן מבקש את notifications/resources/list_changed כשהרשימה משתנה. resourceSubscriptions עם רשימת URIs מבקש את notifications/resources/updated על resources ספציפיים, בתנאי שהשרת הצהיר על subscribe.

ההודעה updated נושאת רק את ה-uri, בלי התוכן החדש. ה-client שמקבל אותה שולח resources/read כדי לקבל את הגרסה העדכנית. זה שומר על ההודעות קטנות, ומשאיר ל-client להחליט אם התוכן עדיין רלוונטי. אם הוא לא, אין סיבה לקרוא אותו מחדש.

בגרסה הקודמת, 2025-11-25, ה-client נרשם לכל resource בנפרד עם resources/subscribe וביטל עם resources/unsubscribe, והשרת שלח את ההודעות על ה-session הפתוח. ב-2026-07-28 שתי הבקשות האלה הוחלפו ב-subscriptions/listen. הדוגמה היא בקשת subscribe בסגנון הישן, כדי שתזהו אותה בשרתים קיימים.

JSON
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "resources/subscribe",
  "params": {
    "uri": "file:///project/config.json"
  }
}

Prompts — תבניות בשליטת המשתמש

prompts הם תבניות של הודעות שהשרת מציע, והמשתמש בוחר להפעיל אותן במפורש, בדרך כלל כ-slash command. ה-spec מדגיש ש-user-controlled מתייחס למי שמחליט מתי להשתמש ב-prompt, ולא למי שכותב אותו. התוכן עצמו מוגדר בשרת.

למה שרת יספק prompts? כי מי שבנה את השרת יודע איך נכון להשתמש בו. שרת של issue tracker יכול להציע prompt בשם triage_issue שמנסח למודל בדיוק איך לסווג issue, אילו שדות לבדוק ואיזה פורמט להחזיר. המשתמש מקבל תהליך עבודה מוכן בפקודה אחת. זה דומה ל-skills שראינו בנושא AI Agents, אבל בגרסה שהמשתמש מפעיל ידנית.

prompts/list מחזירה מערך prompts. לכל Prompt יש name, ו-title, description, icons ו-arguments אופציונליים. arguments הוא מערך של PromptArgument עם name, description ו-required. ה-host משתמש בו כדי לבנות טופס קטן או שורת פקודה עם פרמטרים.

JSON
{
  "jsonrpc": "2.0",
  "id": 9,
  "result": {
    "resultType": "complete",
    "prompts": [
      {
        "name": "triage_issue",
        "title": "Triage an issue",
        "description": "Classifies an issue by severity and area, and suggests an owner",
        "arguments": [
          { "name": "issue_id", "description": "The issue number", "required": true },
          { "name": "area", "description": "Product area, if already known", "required": false }
        ]
      }
    ],
    "ttlMs": 600000,
    "cacheScope": "public"
  }
}

prompts/get — מתבנית להודעות

prompts/get מקבלת name ו-arguments (אובייקט של מחרוזות), ומחזירה description אופציונלי ומערך messages. כל PromptMessage מכיל role, user או assistant, ו-content מאחד מסוגי ה-content blocks שראינו בפרק 5: text, image, audio, resource_link או resource מוטמע. ה-host מכניס את ההודעות לשיחה, ומשם השיחה ממשיכה כרגיל.

היכולת להטמיע resource היא מה שהופך prompts לחזקים. בדוגמה, השרת לא רק מנסח הוראה אלא מצרף את התוכן העדכני של ה-issue ואת מדריך הסיווג של הצוות. המשתמש כתב /triage_issue 812, והמודל מקבל את כל ההקשר הדרוש.

שגיאות: שם prompt לא תקין או ארגומנט חובה חסר מחזירים -32602, ושגיאה פנימית -32603. prompts/get יכולה גם להחזיר input_required (פרק 7). ה-spec דורש (MUST) לאמת את הקלטים והפלטים של prompts כדי למנוע הזרקות. שימו לב שהתוכן שחוזר מ-prompts/get נכנס לשיחה עם המודל, ולכן הוא ערוץ שבו שרת זדוני יכול להחדיר הוראות (פרק 10).

JSON
{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "resultType": "complete",
    "description": "Triage issue #812",
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "resource",
          "resource": {
            "uri": "issues://812",
            "mimeType": "text/markdown",
            "text": "# Login fails on Safari 17. Steps: open /login, submit valid credentials, page reloads without session."
          }
        }
      },
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Triage the issue above. Return severity (S1-S4), area, and a suggested owner team, following our triage guide."
        }
      }
    ]
  }
}

Completion — השלמה אוטומטית של ארגומנטים

כשמשתמש ממלא ארגומנט של prompt או משתנה של resource template, השרת יכול להציע השלמות, בדומה להשלמת קוד ב-IDE. שרת שתומך בזה מצהיר על היכולת completions, וה-client שולח completion/complete.

הבקשה מכילה ref: או ref/prompt עם name, או ref/resource עם uri (URI או URI template). בנוסף, argument עם name ו-value, מה שהמשתמש הקליד עד עכשיו. אם כבר מולאו ארגומנטים אחרים, ה-client שולח אותם ב-context.arguments כדי שההשלמה תתחשב בהם. בדוגמה, ההשלמה של number יודעת לחפש רק ב-acme/web.

התשובה מכילה completion עם values (עד 100 הצעות, ממוינות לפי רלוונטיות), total אופציונלי ו-hasMore. ההנחיות מה-spec: ה-client צריך לבצע debounce להקלדה מהירה ולשמור תוצאות ב-cache. השרת צריך להגביל קצב, לאמת קלטים, ולהקפיד שההשלמות לא יחשפו מידע שהמשתמש לא מורשה לראות. כך, למשל, השלמה של מספרי issues לא צריכה להציע issues פרטיים.

JSON
{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "completion/complete",
  "params": {
    "ref": { "type": "ref/resource", "uri": "repo://{owner}/{repo}/issues/{number}" },
    "argument": { "name": "number", "value": "81" },
    "context": { "arguments": { "owner": "acme", "repo": "web" } },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

איך host מציג את שלושת ה-primitives

הפרוטוקול לא מכתיב UI, אבל הדפוסים הנפוצים נובעים ישירות מהיררכיית השליטה. tools נכנסים לרשימת הכלים שנשלחת למודל, עם סימון ברור בזמן הפעלה ובקשת אישור לפעולות רגישות, לפי ה-annotations (כשהשרת אמין) או לכל כלי שאינו מסומן כקריאה בלבד.

resources מופיעים בבורר עם חיפוש, בדומה לצירוף קובץ לשיחה. ה-host יכול להשתמש ב-priority וב-audience כדי להחליט מה לצרף אוטומטית ומה רק להציג. templates מוצגים כטופס עם שדות, ו-completion ממלא אותם.

prompts מופיעים כ-slash commands או בתפריט. בחירה ב-prompt פותחת טופס קטן לארגומנטים (עם completion), והתוצאה של prompts/get נכנסת לשיחה כהודעות. כך אותו שרת, למשל issue tracker, חושף כלים שהמודל מפעיל, issues שהמשתמש מצרף, ותהליך triage שהמשתמש מפעיל בפקודה. כל אחד מהם מגיע למקום אחר ב-host.

סקיצה: URI templates ברמה 1 וניתוב resources/read

compileTemplate הופכת תבנית כמו repo://{owner}/{repo}/issues/{number} ל-regex, ומחזירה שתי פעולות. match מפרקת URI מלא לפרמטרים, וזה מה שהשרת צריך כדי לנתב resources/read. expand בונה URI מפרמטרים, וזה מה שה-client צריך אחרי שהמשתמש מילא את הטופס.

במימוש רמה 1, כל משתנה תואם רצף של תווים שאינם /, ? או #, והערכים מקודדים ב-encodeURIComponent. זה קירוב טוב לכללי הקידוד של RFC 6570 ברמה הזו. לרמות גבוהות יותר השתמשו בספרייה ייעודית.

readResource מראה את הצד של השרת: היא עוברת על ה-templates, קוראת ל-handler של הראשון שמתאים, ומחזירה -32602 כש-URI לא מתאים לאף תבנית, כפי שה-spec דורש, ולא contents ריק.

TypeScript
type Params = Record<string, string>;

interface UriTemplate {
  match(uri: string): Params | null;
  expand(params: Params): string;
}

// RFC 6570 רמה 1 בלבד: {var} פשוט
function compileTemplate(template: string): UriTemplate {
  const names: string[] = [];
  const pattern = template.replace(/\{(\w+)\}|[.*+?^$()|[\]\\]/g, (m: string, name?: string) => {
    if (name) {
      names.push(name);
      return "([^/?#]+)";
    }
    return "\\" + m; // תווים מיוחדים ב-regex מקבלים escape
  });
  const regex = new RegExp(`^${pattern}$`);
  return {
    match(uri) {
      const m = regex.exec(uri);
      if (!m) return null;
      return Object.fromEntries(names.map((n, i) => [n, decodeURIComponent(m[i + 1])]));
    },
    expand(params) {
      return template.replace(/\{(\w+)\}/g, (_: string, n: string) => encodeURIComponent(params[n] ?? ""));
    },
  };
}

const templates = [
  {
    template: compileTemplate("repo://{owner}/{repo}/issues/{number}"),
    read: (p: Params) => `# Issue ${p.number} in ${p.owner}/${p.repo}`,
  },
];

function readResource(uri: string) {
  for (const { template, read } of templates) {
    const params = template.match(uri);
    if (params) {
      return {
        resultType: "complete",
        contents: [{ uri, mimeType: "text/markdown", text: read(params) }],
        ttlMs: 30000,
        cacheScope: "private",
      };
    }
  }
  return { error: { code: -32602, message: "Resource not found", data: { uri } } };
}

const t = templates[0].template;
console.log(t.expand({ owner: "acme", repo: "web app", number: "812" })); // repo://acme/web%20app/issues/812
console.log(t.match("repo://acme/web%20app/issues/812"));
console.log(readResource("repo://acme/web/issues/812"));
console.log(readResource("repo://acme/web/pulls/5"));