MCP Docs ревизия 2026-07-28

Начало / Как написать свой MCP-сервер на Python

Как написать свой MCP-сервер на Python

Инструмент, ресурс и шаблон запроса на официальном SDK — с проверкой и запуском по stdio и HTTP

РуководствоКод проверен: Python 3.13, пакет mcp 2.3.0 — в памяти, по stdio и по HTTPОснова: Документация Python SDKСверено 13.10.2026, ревизия 2026-07-28Открыть .md

Соберём небольшой сервер каталога товаров: один инструмент, один ресурс и один шаблон запроса. Официальный Python SDK второй версии сам строит схемы параметров, разбирает сообщения и отвечает по ревизии 2026-07-28 — остаётся написать функции.

Путь сервера: три функции с декораторами, проверка в памяти, запуск по stdio для локальных клиентов или по HTTP для удалённых.

Установка#

Нужен Python 3.10 или новее.

Установка SDK
pip install "mcp[cli]"
# или в проекте на uv:
uv add "mcp[cli]"

Дополнение [cli] ставит команду mcp: mcp dev для проверки в MCP Inspector, mcp run для запуска и mcp install для регистрации в Claude Desktop.

Код сервера#

catalog_server.py
"""MCP-сервер «Каталог»: инструмент, ресурс и шаблон запроса."""
import sys

from mcp.server import MCPServer

mcp = MCPServer("Каталог", instructions="Каталог мебели: поиск товаров и остатков.")

PRODUCTS = [
    {"sku": "A-100", "name": "Стол обеденный", "price": 18900, "stock": 4},
    {"sku": "A-200", "name": "Стул мягкий", "price": 5400, "stock": 0},
    {"sku": "B-310", "name": "Полка настенная", "price": 2300, "stock": 17},
]


@mcp.tool()
def find_products(query: str, in_stock_only: bool = False) -> list[dict]:
    """Найти товары по части названия. price — цена в рублях, stock — остаток."""
    q = query.lower()
    return [
        p for p in PRODUCTS
        if q in p["name"].lower() and (p["stock"] > 0 or not in_stock_only)
    ]


@mcp.resource("catalog://categories")
def categories() -> str:
    """Разделы каталога."""
    return "Столы\nСтулья\nПолки"


@mcp.prompt()
def compare(first_sku: str, second_sku: str) -> str:
    """Сравнить два товара по артикулу."""
    return f"Сравни товары {first_sku} и {second_sku}: цена, наличие, назначение."


if __name__ == "__main__":
    if "--http" in sys.argv:
        mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)
    else:
        mcp.run()  # stdio по умолчанию

Что здесь происходит:

  • @mcp.tool() превращает функцию в инструмент. Имя, описание и схему параметров SDK берёт из имени функции, строки документации и подсказок типов: query: str, in_stock_only: bool = False и есть схема.
  • Возвращаемый тип list[dict] даёт структурированный результат: клиент получит и текст, и поле structuredContent.
  • @mcp.resource("catalog://categories") — ресурс: данные, которые приложение само подкладывает модели.
  • @mcp.prompt() — шаблон запроса, который пользователь выбирает явно.
  • mcp.run() без аргументов — транспорт stdio. Запуск стоит под if __name__ == "__main__", потому что команды mcp dev и mcp run импортируют файл.

Проверка без клиента#

SDK содержит клиент, который подключается к объекту сервера прямо в памяти — без процесса и порта. Это основа автотестов:

test_server.py
"""Проверка сервера без клиента и без процесса: Client(mcp) подключается в памяти."""
import asyncio

from mcp import Client

from catalog_server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        tools = await client.list_tools()
        print([t.name for t in tools.tools])

        result = await client.call_tool("find_products", {"query": "стол", "in_stock_only": True})
        print(result.structured_content)

        res = await client.read_resource("catalog://categories")
        print(res.contents[0].text)


asyncio.run(main())
Вывод
['find_products']
{'result': [{'sku': 'A-100', 'name': 'Стол обеденный', 'price': 18900, 'stock': 4}]}
Столы
Стулья
Полки

Посмотреть сервер глазами клиента можно в MCP Inspector (нужен Node.js): uv run mcp dev catalog_server.py.

Запуск по HTTP#

Тот же сервер становится удалённым одной строкой: mcp.run(transport="streamable-http", host="127.0.0.1", port=8000). В примере это включает ключ --http:

Запуск
python catalog_server.py --http
# адрес сервера: http://127.0.0.1:8000/mcp
client_http.py
"""Клиент для сервера, запущенного командой: python catalog_server.py --http"""
import asyncio

from mcp import Client


async def main() -> None:
    async with Client("http://127.0.0.1:8000/mcp") as client:
        result = await client.call_tool("find_products", {"query": "полка"})
        print(result.structured_content)


asyncio.run(main())

Параметры транспорта передаются в run(), а не в конструктор MCPServer: конструктор описывает, что это за сервер, run() — как его обслуживать.

Что уходит по сети#

По ревизии 2026-07-28 рукопожатия нет: каждый запрос несёт версию и возможности клиента в _meta. Клиент может сначала вызвать server/discover. Вот настоящий ответ нашего сервера (тексты раскодированы):

Ответ на server/discover
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": {
      "tools": {"listChanged": true},
      "resources": {"listChanged": true, "subscribe": true},
      "prompts": {"listChanged": true}
    },
    "instructions": "Каталог мебели: поиск товаров и остатков.",
    "cacheScope": "private", "ttlMs": 0,
    "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Каталог", "version": ""}}
  }
}

И ответ на вызов инструмента find_products с запросом «полка»:

Ответ на tools/call
{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "resultType": "complete",
    "isError": false,
    "content": [{"type": "text", "text": "{\n  \"sku\": \"B-310\", …}"}],
    "structuredContent": {
      "result": [{"sku": "B-310", "name": "Полка настенная", "price": 2300, "stock": 17}]
    },
    "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Каталог", "version": ""}}
  }
}

Обязательные поля resultType, ttlMs и cacheScope, заголовки Mcp-Method и Mcp-Name SDK заполняет и проверяет сам. Разбор полей — в разделах «Протокол» и «Транспорты».

Подключение к клиенту#

Claude Desktop и Claude Code
uv run mcp install catalog_server.py          # Claude Desktop
claude mcp add catalog -- uv run --with "mcp[cli]" mcp run /абсолютный/путь/catalog_server.py

Для Cursor, VS Code и OpenCode — в разделе «Подключение».

Как проектировать инструменты#

  • Вокруг задач, а не методов API. «Найти товар» и «проверить статус заказа» лучше десятка обёрток над каждым адресом API.
  • Описание — для модели. Одно-два предложения: что делает, когда вызывать, что значат поля результата.
  • Ограничения в коде. Лимит строк, только чтение, проверка прав — на сервере, а не в надежде на модель.
  • Ошибки — понятным текстом. raise ToolError("Заказ 10452 не найден") модель прочитает и исправит запрос; любое другое исключение она увидит как «вызов не удался».
  • Стабильный порядок списка. Спецификация требует отдавать инструменты в стабильном порядке: клиенты кэшируют список.

Старые клиенты#

Многие клиенты на дату проверки ещё работают по ревизии 2025-11-25 и открывают сессии. SDK второй версии обслуживает их тем же кодом. Отдельно о совместимости — в разделе «Версии и совместимость».

Перед публикацией#

  1. Сервер отвечает на server/discover и выдаёт инструменты в стабильном порядке.
  2. По HTTP — только HTTPS и авторизация, если данные закрытые.
  3. Журнал пишется в stderr, а не в stdout.
  4. Удалённый сервер прошёл бесплатную проверку по ревизии 2026-07-28.

Тот же сервер на TypeScript — в руководстве «Свой сервер на TypeScript».