# Ресурсы и prompts: передайте правила и получите готовое сообщение

Инструмент выполняет действие, ресурс отдаёт контекст по URI, а prompt собирает сообщения по аргументам. На той же лаборатории создадим постоянный ресурс с правилами подписи и шаблон объяснения. Проверка прочитает ресурс и получит сообщение, не вызывая модель. Это позволяет проверить сервер независимо от того, какой текст затем сгенерирует ИИ-клиент.

## Перед началом

В папке `mcp-label-lab` должны быть `.venv` с SDK 1.30.0 и `lab_session.py` из [контракта](/uroki/chto-takoe-mcp/). Исходный сервер и его конфигурация остаются в лаборатории. Новая версия будет отдельным `context_server.py`.

## 1. Сохраните сервер контекста

Создайте `context_server.py`:

```python
"""Add a fixed resource and prompt to the local label workshop."""
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("label-context")
RULES = "A label contains 1 to 80 characters after trimming. Internal spaces are preserved."


@mcp.tool()
def normalize_label(text: str) -> dict[str, str]:
    """Trim a label; reject empty labels or more than 80 characters."""
    label = text.strip()
    if not 1 <= len(label) <= 80:
        raise ValueError("Expected 1 to 80 characters after trimming")
    return {"label": label}


@mcp.resource("workshop://rules")
def label_rules() -> str:
    """Read the fixed lab rules."""
    return RULES


@mcp.prompt()
def review_label(text: str) -> str:
    """Prepare a message explaining a label under the lab rules."""
    return f"Explain this label using these rules: {RULES}\nLabel: {text}"


if __name__ == "__main__":
    mcp.run(transport="stdio")
```

URI `workshop://rules` — идентификатор ресурса, не веб-адрес. Его читает MCP-клиент через `resources/read`; браузер и HTTP-сервер не участвуют. Текст фиксирован, не содержит пользовательских файлов или секретов. Шаблон `review_label` принимает text и возвращает сообщение с этими правилами.

Prompt в этом упражнении не является результатом работы модели и не запускает инструмент самостоятельно. В частности, `get_prompt` не обязан проверить подпись по правилам normalize_label. Клиент отдельно выбирает контекст, получает шаблон и решает, какие сообщения передать модели.

## 2. Сохраните проверку

Создайте `check_context.py`:

```python
"""Read a resource and obtain a prompt without calling a language model."""
import asyncio
import json
from mcp.shared.exceptions import McpError
from lab_session import lab_session

RULES = "A label contains 1 to 80 characters after trimming. Internal spaces are preserved."


async def main():
    async with lab_session("context_server.py") as (session, _):
        resources = await session.list_resources()
        assert [str(resource.uri) for resource in resources.resources] == ["workshop://rules"]
        rules = await session.read_resource("workshop://rules")
        assert rules.contents[0].text == RULES
        prompts = await session.list_prompts()
        assert [prompt.name for prompt in prompts.prompts] == ["review_label"]
        prompt = await session.get_prompt("review_label", {"text": "Draft"})
        assert prompt.messages[0].role == "user"
        assert prompt.messages[0].content.text == f"Explain this label using these rules: {RULES}\nLabel: Draft"
        errors = {}
        for name, action in [("missing_resource", lambda: session.read_resource("workshop://missing")), ("missing_prompt_argument", lambda: session.get_prompt("review_label", {}))]:
            try:
                await action()
            except McpError as error:
                errors[name] = {"code": error.error.code, "message": error.error.message}
            else:
                raise AssertionError("Expected an MCP error for " + name)
        print("PASS: resource read, prompt get, two expected errors")
        print(json.dumps({"resource": rules.model_dump(mode="json", exclude_none=True), "prompt": prompt.model_dump(mode="json", exclude_none=True), "known_errors": errors}, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    asyncio.run(main())
```

Тест проверяет оба списка, читает ресурс, сверяет содержимое сообщения и получает два ожидаемых исключения `McpError`. Это отличается от `isError: true` в результате tools/call: методы ресурсов и prompts возвращают протокольную ошибку, которую клиент должен обработать.

## 3. Получите контекст без модели

```powershell
.\.venv\Scripts\python.exe check_context.py
```

Ожидается:

```text
PASS: resource read, prompt get, two expected errors
```

В `resource.contents[0]` ожидаются URI `workshop://rules`, mimeType `text/plain` и полный RULES. В `prompt.messages[0]` роль будет user, а текст:

```text
Explain this label using these rules: A label contains 1 to 80 characters after trimming. Internal spaces are preserved.
Label: Draft
```

Этот текст подтверждает сборку шаблона. Он ничего не доказывает о качестве будущего ответа модели. Проверка не оплачивает генерацию и не требует API-ключа.

## Два известных отказа

`workshop://missing` должен дать Unknown resource. Вызов review_label без text должен дать Missing required arguments. В проверенной версии SDK оба диагностических объекта имеют код 0; не используйте этот код как универсальную классификацию всех ошибок MCP. Здесь тест утверждает именно факт исключения и сохраняет сообщение.

Если ресурс не найден, сравните URI полностью, включая scheme и имя. Если список prompts пуст, убедитесь, что клиент запустил context_server.py и декоратор `@mcp.prompt()` сохранён. При ошибке импорта восстановите lab_session.py. Обычные отладочные сообщения сервера отправляйте в stderr: stdout занят протоколом.

## Проверьте себя

В отдельной копии сервера замените RULES и получите обновлённый ресурс. Затем обновите ожидаемый текст в копии теста. Оба результата — read_resource и get_prompt — должны измениться согласованно, поскольку шаблон использует тот же RULES. Не приписывайте этому автоматическое разрешение внешнего доступа: наш ресурс фиксированный и локальный.

## Версия и источники

Упражнение закреплено за Python 3.11 и `mcp==1.30.0`, SDK v1, протоколом 2025-11-25. SDK v2 требует отдельной миграции. Примеры используют синтетические подписи, без внешнего API и модели. Документация сверена 8 октября 2026 года.

На реальном локальном stdio проверены resources/list, resources/read, prompts/list, prompts/get и два ожидаемых отказа. Модель и HTTP здесь не запускались.

Источники: [ресурсы 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), [prompts 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts), [клиент SDK v1](https://py.sdk.modelcontextprotocol.io/v1/client/), [FastMCP v1](https://py.sdk.modelcontextprotocol.io/v1/server/).
