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

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

> Страница сайта MCP Docs — руководство (https://mcpdocs.ru/svoy-server/). Тип: Руководство. Код проверен: Python 3.13, пакет mcp 2.3.0 — в памяти, по stdio и по HTTP. Ревизия спецификации 2026-07-28, сверено 13.10.2026.

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

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

## Установка

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

_Установка SDK_

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

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

## Код сервера

_catalog_server.py_

```python
"""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()` превращает функцию в [инструмент](https://mcpdocs.ru/concepts/tools/). Имя, описание и схему параметров SDK берёт из имени функции, строки документации и подсказок типов: `query: str, in_stock_only: bool = False` и есть схема.
- Возвращаемый тип `list[dict]` даёт структурированный результат: клиент получит и текст, и поле `structuredContent`.
- `@mcp.resource("catalog://categories")` — [ресурс](https://mcpdocs.ru/concepts/resources/): данные, которые приложение само подкладывает модели.
- `@mcp.prompt()` — [шаблон запроса](https://mcpdocs.ru/concepts/prompts/), который пользователь выбирает явно.
- `mcp.run()` без аргументов — транспорт stdio. Запуск стоит под `if __name__ == "__main__"`, потому что команды `mcp dev` и `mcp run` импортируют файл.

> По stdio стандартный вывод — это канал протокола. Не используйте `print()` для отладки: пишите журнал через модуль `logging`, он выводит в stderr.

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

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

_test_server.py_

```python
"""Проверка сервера без клиента и без процесса: 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())
```

_Вывод_

```text
['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`:

_Запуск_

```bash
python catalog_server.py --http
# адрес сервера: http://127.0.0.1:8000/mcp
```

_client_http.py_

```python
"""Клиент для сервера, запущенного командой: 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_

```json
{
  "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_

```json
{
  "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 заполняет и проверяет сам. Разбор полей — в разделах [«Протокол»](https://mcpdocs.ru/protocol/) и [«Транспорты»](https://mcpdocs.ru/protocol/transports/).

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

_Claude Desktop и Claude Code_

```bash
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 — в разделе [«Подключение»](https://mcpdocs.ru/podklyuchenie/).

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

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

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

Многие клиенты на дату проверки ещё работают по ревизии 2025-11-25 и открывают сессии. SDK второй версии обслуживает их тем же кодом. Отдельно о совместимости — в разделе [«Версии и совместимость»](https://mcpdocs.ru/protocol/versions/).

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

1. Сервер отвечает на `server/discover` и выдаёт инструменты в стабильном порядке.
2. По HTTP — только HTTPS и [авторизация](https://mcpdocs.ru/svoy-server/avtorizatsiya/), если данные закрытые.
3. Журнал пишется в stderr, а не в stdout.
4. Удалённый сервер прошёл [бесплатную проверку](https://mcpprotocol.ru/mcp-check/) по ревизии 2026-07-28.

Тот же сервер на TypeScript — в руководстве [«Свой сервер на TypeScript»](https://mcpdocs.ru/svoy-server/typescript/).

> Готовые серверы — в [каталоге MCP-серверов](https://mcpprotocol.ru/katalog/) справочника. Свой удалённый сервер можно проверить на соответствие ревизии 2026-07-28 [бесплатной проверкой](https://mcpprotocol.ru/mcp-check/).
