Начало / Как написать свой MCP-сервер на Python
Как написать свой MCP-сервер на Python
Инструмент, ресурс и шаблон запроса на официальном SDK — с проверкой и запуском по stdio и HTTP
Соберём небольшой сервер каталога товаров: один инструмент, один ресурс и один шаблон запроса. Официальный Python SDK второй версии сам строит схемы параметров, разбирает сообщения и отвечает по ревизии 2026-07-28 — остаётся написать функции.
Путь сервера: три функции с декораторами, проверка в памяти, запуск по stdio для локальных клиентов или по HTTP для удалённых.
Установка#
Нужен Python 3.10 или новее.
pip install "mcp[cli]"
# или в проекте на uv:
uv add "mcp[cli]"Дополнение [cli] ставит команду mcp: mcp dev для проверки в MCP Inspector, mcp run для запуска и mcp install для регистрации в Claude Desktop.
Код сервера#
"""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 содержит клиент, который подключается к объекту сервера прямо в памяти — без процесса и порта. Это основа автотестов:
"""Проверка сервера без клиента и без процесса: 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"""Клиент для сервера, запущенного командой: 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. Вот настоящий ответ нашего сервера (тексты раскодированы):
{
"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 с запросом «полка»:
{
"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 заполняет и проверяет сам. Разбор полей — в разделах «Протокол» и «Транспорты».
Подключение к клиенту#
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 второй версии обслуживает их тем же кодом. Отдельно о совместимости — в разделе «Версии и совместимость».
Перед публикацией#
- Сервер отвечает на
server/discoverи выдаёт инструменты в стабильном порядке. - По HTTP — только HTTPS и авторизация, если данные закрытые.
- Журнал пишется в stderr, а не в stdout.
- Удалённый сервер прошёл бесплатную проверку по ревизии 2026-07-28.
Тот же сервер на TypeScript — в руководстве «Свой сервер на TypeScript».