Контекст≈ 25 мин с практикой

Ресурсы и prompts: передайте правила и получите готовое сообщение

Инструмент выполняет действие, ресурс отдаёт контекст по URI, а prompt собирает сообщения по аргументам. На той же лаборатории создадим постоянный ресурс с правилами подписи и шаблон объяснения. Проверка прочитает ресурс и получит сообщение, не вызывая модель. Это позволяет проверить сервер независимо от того, какой текст затем сгенерирует ИИ-клиент.

В результате

Ресурс с правилами, готовое сообщение prompt и обработка двух ошибок.

Перед началом

Окружение первого урока и lab_session.py.

Скачать руководство целиком · Markdown
Граница версии

Python SDK v1 · mcp==1.30.0 · протокол 2025-11-25. Для SDK v2 требуется отдельная миграция.

Перед началом

В папке mcp-label-lab должны быть .venv с SDK 1.30.0 и lab_session.py из контракта. Исходный сервер и его конфигурация остаются в лаборатории. Новая версия будет отдельным context_server.py.

1. Сохраните сервер контекста

Создайте context_server.py:

context_server.pyPython
Скачать
"""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")
context_server.pyPython
"""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:

check_context.pyPython
Скачать
"""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())
check_context.pyPython
"""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
PowerShellPowerShell
.\.venv\Scripts\python.exe check_context.py

Ожидается:

Ожидаемый вывод
PASS: resource read, prompt get, two expected errors
Ожидаемый вывод
PASS: resource read, prompt get, two expected errors

В resource.contents[0] ожидаются URI workshop://rules, mimeType text/plain и полный RULES. В prompt.messages[0] роль будет user, а текст:

Ожидаемый вывод
Explain this label using these rules: A label contains 1 to 80 characters after trimming. Internal spaces are preserved.
Label: Draft
Ожидаемый вывод
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 (в новой вкладке), prompts 2025-11-25 (в новой вкладке), клиент SDK v1 (в новой вкладке), FastMCP v1 (в новой вкладке).

Вопросы по материалу

Вызов review_label уже проверяет подпись с помощью модели?

Нет. Этот prompt собирает сообщение из правил и переданного text. Получение сообщения подтверждает работу шаблона; вызов модели и оценка её ответа в упражнение не входят.

Почему workshop://rules не открывается как сайт?

Это URI ресурса MCP, а не HTTP-адрес. В упражнении клиент читает его через resources/read и получает фиксированный текст правил. Веб-сервер для этого не нужен.

Продукты ATOMNO

Другие задачи для AI-помощника

Это отдельные продукты ATOMNO для задач с данными. Они не входят в учебные примеры.

Все продукты ATOMNO (в новой вкладке)