Ошибка 1: Пустые или неинформативные описания инструментов
Посмотрим правде в глаза: большинство MCP-серверов, которые я видел за последние полгода, выглядят так, будто их писали под диктовку GPT-4 в 1:30 ночи. Самая частая проблема — отсутствие внятного description у инструмента. LLM, которая будет дёргать ваш сервер, не умеет читать ваши мысли. Она полагается исключительно на текстовое описание. Если вы напишете:
@mcp.tool()
def get_weather(city: str) -> str:
"""Получить погоду"""
...Модель не поймёт, в каком формате вы отдаёте погоду, что означает возвращаемая строка, какие единицы измерения. В итоге — неверный вызов или странные ответы. Как надо: распишите, что делает функция, какие параметры, что возвращает, в каких единицах.
@mcp.tool(description="Возвращает текущую погоду в указанном городе в градусах Цельсия и влажности. Параметр city: название города на английском. Возвращает JSON с полями temp, humidity, description.")
def get_weather(city: str) -> str:
...Это не «документационная» придирка. От качества описания напрямую зависит, сможет ли LLM правильно вызвать ваш инструмент. Потратьте на описание 2 минуты — сэкономите часы отладки.
Ошибка 2: Игнорирование ToolAnnotations и неявная обработка ошибок
В FastMCP начиная с версии 0.4.x (а на 2026 год актуальна 0.6) появились ToolAnnotations — метаданные, которые говорят агенту, как обрабатывать результат. Самая распространённая ошибка — не использовать их, а потом удивляться, почему модель не переспрашивает или не делает повторный вызов при ошибке.
Вот как не надо:
@mcp.tool()
def search_database(query: str) -> list:
try:
results = db.search(query)
return results if results else ["Ничего не найдено"]
except Exception as e:
return [f"Ошибка: {e}"]Проблема тут в следующем: модель видит список строк и не понимает, была ли ошибка на самом деле. Она может интерпретировать "Ошибка: Timeout" как валидный результат. Правильный подход — использовать ToolAnnotations и выбрасывать исключения для неуспешных случаев:
from fastmcp import ToolAnnotations
@mcp.tool(
annotations=ToolAnnotations(
success_indicator=lambda response: "error" not in response.lower(),
retry_on_failure=True
)
)
def search_database(query: str) -> list:
if not query.strip():
raise ValueError("Запрос не может быть пустым")
results = db.search(query)
if not results:
return ["Ничего не найдено"]
return resultsОбратите внимание: мы выбросили исключение для пустого запроса — FastMCP сам передаст агенту ошибку, и он примет решение о повторной попытке или запросе уточнения. ToolAnnotations дают агенту понять, что результат успешен или нет. Это критически важно для production-среды.
Подробнее о настройке конфигураций MCP-серверов читайте в статье MCP Doctor: как автоматизировать отладку конфигов.
Ошибка 3: Неверная типизация и игнорирование встроенных валидаторов
FastMCP умеет валидировать параметры на основе type hints. Но многие пишут **kwargs или используют Any везде. Так делать нельзя — вы ломаете автодополнение для модели и теряете возможность валидации.
Пример ошибки:
@mcp.tool()
def send_email(recipient, subject, body) -> bool:
...Такие голые параметры — зло. Модель не знает, что recipient — строка, а body — может быть многострочным. Исправляем:
from pydantic import EmailStr
from typing import Optional
@mcp.tool()
def send_email(
recipient: EmailStr,
subject: str,
body: str,
cc: Optional[list[EmailStr]] = None
) -> bool:
...FastMCP использует pydantic для валидации, поэтому можно использовать EmailStr, Field с ограничениями. Это не только улучшает безопасность, но и даёт модели более точные подсказки.
Кстати, на практике MCP-серверы с грамотной типизацией реже попадают в список «мёртвых», который описывается в статье Кризис Model Context Protocol: 52% удаленных серверов мертвы. Не будьте частью этой статистики.
Ошибка 4: Синхронные операции в асинхронном сервере
FastMCP по умолчанию работает с asyncio. Если вы внутри async-функции вызываете time.sleep() или синхронный HTTP-запрос через requests, вы блокируете event loop. Это убивает производительность.
Как не надо:
import time, requests
@mcp.tool()
async def fetch_data(url: str) -> dict:
time.sleep(2) # блокирует всех!
resp = requests.get(url)
return resp.json()Используем asyncio-эквиваленты:
import asyncio
import aiohttp
@mcp.tool()
async def fetch_data(url: str) -> dict:
await asyncio.sleep(2)
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.json()Если же вам нужно запустить синхронный код (например, библиотеку, не поддерживающую async), оберните его в run_in_executor или используйте встроенный @mcp.tool(sync=True) (если функция синхронная, FastMCP сам запустит её в пуле потоков).
Ошибка 5: Отсутствие обработки контекста и сессий
MCP-сервер может обслуживать несколько клиентов одновременно. Если вы храните состояние в глобальных переменных, данные разных пользователей перемешаются. Типичный антипаттерн:
user_sessions = {} # глобальный словарь
@mcp.tool()
def login(username: str, password: str) -> str:
token = authenticate(...)
user_sessions[id] = token # откуда id? из космоса?
return tokenFastMCP предоставляет объект Context, который можно внедрить через аргумент ctx. Он содержит session_id и другие метаданные:
from fastmcp import Context
@mcp.tool()
def get_user_profile(ctx: Context) -> str:
# ctx.session_id — уникальный идентификатор сессии
# ctx.client_info — информация о клиенте
return f"Привет, пользователь {ctx.session_id}!"Для хранения данных сессии используйте ctx.session (если включено session management) или внешнее хранилище (Redis, база данных). Никогда не полагайтесь на глобальные переменные.
Ошибка 6: Игнорирование безопасности — открытые injection-векторы
Поскольку MCP-сервер вызывается LLM-агентом, а агент может быть скомпрометирован, ваш сервер — первая линия обороны. Самая популярная ошибка — передача пользовательского ввода напрямую в команду shell или SQL-запрос.
Пример:
import subprocess
@mcp.tool()
def run_script(script_path: str) -> str:
result = subprocess.run(["bash", script_path], capture_output=True)
return result.stdout.decode()Тут script_path может быть ; rm -rf / — да, маловероятно, но модель может быть инжектирована промптом. Как защититься?
- Ограничьте список разрешённых путей.
- Используйте
subprocess.runсshell=False(уже сделано), но обязательно проверяйте аргументы. - Для SQL используйте параметризованные запросы (никакой конкатенации!).
ALLOWED_PATHS = ["/home/user/scripts/"]
@mcp.tool()
def run_script(script_name: str) -> str:
# проверяем, что script_name разрешён
full_path = os.path.join(ALLOWED_PATHS[0], script_name)
if not os.path.realpath(full_path).startswith(os.path.realpath(ALLOWED_PATHS[0])):
raise PermissionError("Доступ запрещён")
result = subprocess.run(["bash", full_path], capture_output=True)
return result.stdout.decode()Подробнее про угрозы prompt injection в MCP читайте в статье Как защитить AI-агентов от prompt injection. Там описан реальный кейс атаки через Claude.
Ошибка 7: Неправильная обработка потока (stream) и больших ответов
Некоторые MCP-серверы должны возвращать большие данные (например, логи, результаты поиска). Если вы возвращаете всё сразу как одну строку, агент может не уложиться в лимит контекста или просто запутаться.
FastMCP поддерживает стриминг результатов через AsyncGenerator.
Как не надо:
@mcp.tool()
def read_large_file(filename: str) -> str:
with open(filename) as f:
return f.read()Как надо:
from typing import AsyncIterator
@mcp.tool()
async def read_large_file(filename: str) -> AsyncIterator[str]:
with open(filename) as f:
for line in f:
yield line
await asyncio.sleep(0) # передаём управлениеТеперь агент может обрабатывать данные порциями, не перегружая себя. Кроме того, используйте ToolAnnotations с streaming=True, чтобы модель знала, что ответ будет частями.
Бонус: как отлаживать MCP-сервер без головной боли
Классическая ситуация: вы запустили сервер, Claude Desktop или Cursor его не видит, а в логах — тишина. Раньше я тратил часы на перепроверку конфигов JSON. Теперь использую MCP Doctor — он валидирует конфиги, проверяет доступность эндпоинта и даже подсказывает, какой версии FastMCP не хватает. Если ещё не знакомы — прочитайте MCP Doctor: как автоматизировать отладку конфигов — сэкономит вам день.
А для тестирования вызовов в реальном времени отлично подходит MCP Chat Studio v2 — это Postman для MCP, с возможностью экспорта в Python.
Итог: не будьте тем разработчиком, чей сервер умирает в 52%
Ошибки, описанные выше — не теоретические. Они взяты из реальных проектов и обсуждений в сообществе. Я специально не стал писать «заключение» с перечислением каждого пункта. Вместо этого дам один совет: перед деплоем любого MCP-сервера прогоните его через валидатор ToolAnnotations и проверьте типизацию. Даже если кажется, что всё работает, — одна неправильная аннотация может похоронить совместимость с агентами.
А как вы боретесь с ошибками в своих MCP-серверах? Делитесь в комментариях — возможно, ваш опыт ляжет в основу следующей статьи.