Вы пишете сервис на Python. В нём уже есть функция, которая подсказывает, с чего начать диагностику: для API — проверить HTTP-ответ и журнал, для фонового обработчика — очередь задач. Пока этой функцией пользуется только ваш код, всё просто. Но другому приложению понадобятся понятный способ обнаружить её и правила вызова.
MCP-сервер предоставляет такой интерфейс. Он сообщает клиенту, какие инструменты доступны, какие аргументы они принимают и как получить результат. Мы создадим сервер на FastMCP, добавим инструмент find_runbook и вызовем его из Python-клиента.
Сначала проверим работу сервера в одном процессе, затем запустим его через stdio и убедимся, что клиент может общаться с отдельным процессом. Для примера хватит Python и локальных данных: языковая модель, облачный аккаунт и API-ключи не потребуются.
Установка Python и FastMCP
Для работы нужен Python 3.12 и терминал. Используем fastmcp==4.0.5 — фиксированную версию самостоятельной библиотеки FastMCP (страница выпуска на PyPI). Пример также использует модель Pydantic для ответа, поэтому её зависимость укажем явно, а не будем полагаться на косвенную установку.
Создайте каталог mcp-first-server и сохраните в нём файл requirements.txt:
fastmcp==4.0.5
pydantic==2.14.2
Затем в терминале Linux или macOS выполните:
cd mcp-first-server
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pip show fastmcp
В выводе последней команды найдите Version: 4.0.5. Это поможет сразу заметить, если вы установили другую версию пакета. На Windows изменится путь к Python внутри .venv; способы установки подробно описаны в документации FastMCP.
Если удобнее начать с готовых файлов, используйте закреплённый commit проекта XNova. В нём находятся сервер, оба smoke-сценария, pytest-тесты и CI для Linux, macOS и Windows.
В примерах в интернете можно встретить похожие названия классов из разных библиотек. Самостоятельная библиотека FastMCP использует from fastmcp import FastMCP. В официальном Python SDK mcp версии 2 высокоуровневый класс теперь называется MCPServer; это другой API. Поэтому дальше используем импорты только выбранной библиотеки. Отличия перечислены в описании изменений SDK v2.
После установки можно переходить к серверу. В качестве данных возьмём небольшой словарь: его содержимое известно заранее, поэтому результат вызова легко проверить.
Создаём MCP-сервер с одним инструментом
Напишем функцию, которая принимает название сервиса и возвращает первую диагностическую подсказку. Для api и worker ответы будут разными; неизвестное имя мы тоже обработаем. Создайте server.py:
from typing import Literal
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("Runbook Demo")
RUNBOOKS: dict[str, str] = {
"api": "Проверьте HTTP-статус, журнал и подключение к базе.",
"worker": "Проверьте очередь задач и ошибки обработки.",
}
class RunbookResult(BaseModel):
service: str
status: Literal["ok", "not_found"]
hint: str
@mcp.tool
def find_runbook(service: str) -> RunbookResult:
"""Возвращает первую диагностическую проверку для сервиса."""
if service not in RUNBOOKS:
return RunbookResult(
service=service,
status="not_found",
hint="Сервис не найден.",
)
return RunbookResult(service=service, status="ok", hint=RUNBOOKS[service])
if __name__ == "__main__":
mcp.run()
Здесь четыре ключевых элемента. FastMCP("Runbook Demo") создаёт объект сервера. Декоратор @mcp.tool регистрирует функцию find_runbook как доступный клиенту инструмент. Параметр service: str сообщает FastMCP, что клиент должен передать строку; библиотека использует сигнатуру и документацию функции для формирования схемы аргументов. Модель RunbookResult задаёт структуру ответа, а Literal ограничивает status двумя значениями. Эти механизмы описаны в разделе Tools.
На выходе получаем три поля: service (запрошенное имя), status (результат поиска) и hint (что проверить). FastMCP преобразует типизированный объект в структурированный ответ MCP. Неизвестный сервис вернёт not_found как обычный результат функции: клиенту не придётся угадывать, произошёл ли сбой протокола.
В конце mcp.run() запускает сервер: по умолчанию FastMCP использует stdio (Quickstart). Через стандартный вывод идут протокольные сообщения, поэтому обычные print() для отладки туда добавлять нельзя. Диагностику направляйте в stderr, иначе клиент может перестать разбирать ответы.
Теперь сервер умеет объявлять инструмент, но нам ещё нужно увидеть его глазами клиента. Для этого сначала разберём два сообщения протокола, а затем напишем проверку.
Как клиент узнаёт об инструменте
При прямом вызове Python-кода обе стороны должны знать функцию find_runbook. MCP-клиенту достаточно знать адрес или способ запуска сервера. Он сначала узнаёт, какие инструменты тот предлагает, а потом выбирает один из них и передаёт аргументы.
В протоколе это операции tools/list и tools/call. Список содержит имя, описание и схему аргументов инструмента. Для find_runbook обязательный аргумент — service типа string. По этой схеме клиент понимает, какие данные отправить. Формат операций описан в спецификации MCP Tools.
В нашем примере проверим два разных способа соединения:
- В одном процессе.
Client(mcp)связывается непосредственно с объектом FastMCP в памяти. Мы проверим регистрацию и вызов инструмента без запуска дочернего процесса. - Через
stdio. Клиент запускаетserver.pyкак отдельный процесс, передаёт ему протокольные сообщения через стандартный ввод и получает ответы через стандартный вывод. Этот вариант ближе к работе локального MCP-хоста.
Одинаковый результат в этих проверках отвечает на разные вопросы: правильно ли работает инструмент и может ли клиент связаться с отдельным процессом. Подробности — в документации транспортов FastMCP. Начнём с первого вопроса.
Подключаем Python-клиента к MCP-серверу
Создайте рядом с server.py файл smoke.py:
import asyncio
from fastmcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
tools = await client.list_tools()
names = sorted(tool.name for tool in tools)
assert names == ["find_runbook"], names
found = await client.call_tool("find_runbook", {"service": "api"})
assert found.data.status == "ok"
missing = await client.call_tool("find_runbook", {"service": "unknown"})
assert missing.data.status == "not_found"
print("tools: find_runbook")
print("known: ok")
print("unknown: not_found")
if __name__ == "__main__":
asyncio.run(main())
Здесь async with Client(mcp) открывает соединение с сервером в памяти. Через list_tools() мы узнаём зарегистрированные инструменты и убеждаемся, что среди них есть find_runbook. Затем дважды вызываем его: сначала с известным сервисом, потом с неизвестным. result.data содержит разобранный типизированный результат, поэтому к полям можно обращаться как к атрибутам. Работу методов подробнее объясняют FastMCP Client и Calling Tools.
В терминале запустите:
.venv/bin/python smoke.py
При успешном запуске вы увидите:
tools: find_runbook
known: ok
unknown: not_found
Первая строка означает, что клиент обнаружил инструмент. Две следующие — что он получил ожидаемые статусы для api и unknown. Если какое-либо условие assert нарушится, программа завершится с ошибкой. Так мы не примем неправильный результат за успешный.
Осталось проверить важную деталь: сможет ли клиент запустить тот же сервер как отдельный процесс и обменяться с ним MCP-сообщениями. Для этого используем stdio.
Проверяем соединение через stdio
Для проверки отдельного процесса создайте stdio_smoke.py рядом с двумя предыдущими файлами:
import asyncio
import sys
from pathlib import Path
from fastmcp import Client
from fastmcp.client.transports import StdioTransport
async def main() -> None:
server_file = Path(__file__).resolve().with_name("server.py")
transport = StdioTransport(command=sys.executable, args=[str(server_file)])
async with Client(transport) as client:
tools = await client.list_tools()
assert [tool.name for tool in tools] == ["find_runbook"]
result = await client.call_tool("find_runbook", {"service": "worker"})
assert result.data.status == "ok"
assert result.data.service == "worker"
print("stdio: find_runbook ok")
if __name__ == "__main__":
asyncio.run(main())
Здесь StdioTransport запускает server.py с тем же Python-интерпретатором, что и клиент. Это важно: сервер и клиент используют одно установленное окружение FastMCP. Код запрашивает список инструментов, вызывает find_runbook для worker и проверяет ответ. Способ создания StdioTransport(command=..., args=...) описан в документации Client Transports.
Запустите:
.venv/bin/python stdio_smoke.py
При успешном запуске появится stdio: find_runbook ok. В отличие от предыдущей проверки, здесь клиент действительно запустил отдельный процесс и вызвал его инструмент. Если команда завершилась с ошибкой, проверьте сначала, что происходит при обычном вызове smoke.py: так проще отделить ошибку сервера от ошибки соединения.
Что проверить, если вызов не работает
Начинайте с того места, где результат отличается от ожидаемого:
ModuleNotFoundError: fastmcp. Убедитесь, что команда выполняется через Python из созданного.venv, а пакет установлен именно туда.list_tools()вернул пустой список. Проверьте, что@mcp.toolстоит над функцией, а клиент импортирует правильный объектmcp.- Не прошёл
assertнаresult.data. Проверьте имя инструмента и аргументservice. Значениеunknownдолжно вернутьnot_foundкак обычный ответ нашей функции. - Клиент
stdioне получает ответ. Проверьте путь кserver.pyи путь к интерпретатору, а также отсутствие обычныхprint()в протокольномstdout. Сообщения об ошибках смотрите вstderr. - Сервер запущен вручную, но ничего не напечатал. Сам
.venv/bin/python server.pyзапускает процесс, ожидающий MCP-сообщения. Используйтеstdio_smoke.pyдля реального обмена, а не ожидайте текстового приветствия.
После исправления повторите smoke.py, затем stdio_smoke.py. Первый тест подтверждает инструмент, второй — соединение через отдельный процесс.
Результат и дальнейшие шаги
Теперь у вас есть MCP-сервер с одним инструментом, а рядом — два клиента для проверки. smoke.py подтверждает, что инструмент зарегистрирован и возвращает предусмотренные ответы. stdio_smoke.py добавляет проверку запуска отдельного процесса и обмена MCP-сообщениями. Это полезное различие: если первая проверка проходит, а вторая нет, искать причину стоит в запуске процесса или соединении.
В примере данные хранятся в словаре, а инструмент только читает их. Мы не проверяли авторизацию, подключение к конкретному редактору или агенту, работу по HTTP и безопасность доступа к реальным данным. Прежде чем подключать внешние сервисы или разрешать действия, которые изменяют состояние, потребуется отдельно проверить права и обработку ошибок.
Попробуйте добавить в словарь ещё один сервис и вызвать его из обоих клиентов. Когда новый результат появится и в проверке через stdio, можно переходить к собственному источнику данных — API или базе. Принцип останется тем же: объявить инструмент, проверить аргументы, выполнить вызов и убедиться, что клиент получил ожидаемый результат.
Источники
- Установка и проверка FastMCP
- Минимальный сервер, декоратор, stdio и команды запуска
- Декоратор tools и схема аргументов
- Client с объектом сервера в памяти
- list_tools, call_tool и result.data
- Варианты in-memory и stdio transport
- Переименование FastMCP в MCPServer в официальном SDK v2
- MCP tools/list и tools/call
- Подтверждённый выпуск FastMCP 4.0.5
- Закреплённая версия воспроизводимого примера