Всё стремительнее на глазах формируется новый виток в развитии инструментов для работы с искусственным интеллектом: если ещё недавно внимание разработчиков было приковано к no-code/low-code платформам вроде n8n и Make, то сегодня в центр внимания выходят ИИ-агенты, MCP-серверы и собственные тулзы, с помощью которых нейросети не просто генерируют текст, но и учатся действовать. Это не просто тренд — это новая парадигма от «что мне сделать?» к «вот как я это сделаю сам».
Вместе с этим появляется множество вопросов:
Что такое MCP? Зачем вообще нужны тулзы? Как ИИ может использовать код, написанный мной? И почему всё больше разработчиков создают собственные MCP-серверы, вместо того чтобы довольствоваться готовыми решениями?
Эта статья — путеводитель по новой реальности. Без лишней теории, с большим количеством практики:
Мы поговорим о том, что из себя представляют MCP-серверы и как они взаимодействуют с нейросетями
Разберёмся, как создавать собственные инструменты (тулзы) и подключать их к ИИ
И, главное, на простых примерах покажу, как научить нейросеть работать с вашим кодом: будь то калькулятор, AI-интерфейс к API, или даже полноценный агент для автоматизации действий
К концу статьи вы сможете не просто понимать, что такое MCP, а писать собственные серверы и подключать их к ИИ, готовые к использованию в реальных проектах.
Рекомендую также заглянуть в мою предыдущую статью «Как научить нейросеть работать руками: создание полноценного ИИ-агента с MCP и LangGraph за час», — она отлично дополнит сегодняшний материал.
Поехали.
Отличие MCP и инструментов (тулзы, tools)
Начнём с самого частого вопроса у тех, кто только начинает разбираться в теме: что такое MCP, что такое инструменты (тулзы), и в чём между ними разница. Давайте разберёмся.
MCP vs Tools: метафора для понимания
Путаница возникает не случайно — эти понятия действительно близки. Чтобы проще понять, представьте, что:
MCP-сервер — это как библиотека или фреймворк на любом языке программирования.
Инструмент (tool) — это отдельная функция, выполняющая конкретную задачу.
Таким образом, инструмент — это кирпич, а MCP — это здание, собранное из этих кирпичей и обёрнутое в удобный интерфейс, с которым может взаимодействовать ИИ-агент.
Зачем всё это вообще нужно
Всё внимание к теме MCP объясняется очень просто:
теперь вы можете написать абсолютно любой код, будь то:
простой скрипт на Python
REST API-эндпоинт
локальная функция
…и дать нейросети возможность самостоятельно вызывать его — как будто она понимает, что делает.
Простой пример — как это работает
Допустим, у вас есть обычная функция, которая принимает два аргумента: city
и days
. Вы вручную вызываете её как:
get_weather(city="Москва", days=4)
Она возвращает погоду на 4 дня — всё просто.
Теперь представьте: Вы задаёте нейросети вопрос:
«Дружище, подскажи, какая там погода будет в Краснодаре в ближайшие четыре дня?»
ИИ-агент сам:
Извлекает из запроса нужные переменные (
city = "Краснодар"
,days = 4
)Вызывает вашу функцию
Получает результат
И сам же формирует осмысленный ответ для пользователя — будто всё это сделал человек вручную.
Причём, даже если нейросеть работает локально, без доступа к интернету, она всё равно справляется — потому что задача не в поиске данных, а в использовании доступных инструментов.
Когда инструментов становится много
А теперь представьте, что у вас не одна такая функция, а целый набор:
create_file()
,delete_file()
,read_file()
,list_files()
и десятки других
Все они работают вокруг общей логики — например, с файлами.
В какой-то момент вы или другой разработчик можете объединить эти функции в единый набор с общей структурой, описанием и интерфейсом. Вот это уже и будет MCP-сервер — полноценная коллекция инструментов, с которой может работать нейроагент.
Так и родилось понятие MCP:
это не просто набор случайных тулзов, а логически объединённая система инструментов, с которой ИИ может взаимодействовать как с фреймворком.
Как нейросеть понимает, как работать с инструментами?
Это, пожалуй, один из самых важных вопросов во всей теме: как ИИ вообще осознаёт, что и когда нужно вызывать? Как он «узнаёт», что у нас есть функция, которая может выполнить нужное действие?
Старый формат общения: чат и текст
До недавнего времени взаимодействие с ИИ выглядело просто:
— Вы писали в чат нейросети свой запрос
— Она генерировала текст в ответ
Может быть, вы даже прикладывали файлы и просили что-то сделать с ними, но на этом всё. ИИ был «в голове», но без рук.
Новый подход: инструменты и действия
Теперь всё меняется. У нас появилась возможность давать нейросети инструменты — буквально, расширять её возможности через функции. Мы можем:
написать свои собственные функции
объединить их в MCP-сервер
взять чужой код или готовый набор инструментов
и… подключить всё это к ИИ
Сегодня я покажу, как это делается. А стало возможным всё это благодаря MCP-протоколу (Model Context Protocol) — разработке компании Anthropic, которая задала единый стандарт описания инструментов для использования нейросетями.
То есть компания Anthropic придумала некое общепринятое описание правил создания кода для нейросетей. К нему можно отнести, например, специальный формат аннотаций и документации в каждой функции-инструменте.
Магия в описании: как ИИ «видит» ваши функции
Представьте, что вы написали функцию для работы с погодой:
def get_weather(city: str) -> dict:
"""
Получает текущую погоду для указанного города.
Args:
city (str): Название города на русском или английском языке
Returns:
dict: Словарь с данными о погоде (температура, влажность, описание)
"""
# ваш код здесь
Когда вы подключаете эту функцию к ИИ, например, через LangGraph, нейросеть получает не только сам код, но и полное описание: что делает функция, какие параметры принимает, что возвращает.
Как работает «мозг» ИИ-агента
Процесс принятия решений выглядит примерно так:
Пользователь пишет: «Какая сейчас погода в Москве?»
ИИ анализирует: «Нужна информация о погоде в конкретном городе»
ИИ сканирует доступные инструменты: «У меня есть функция
get_weather
, которая принимает название города»ИИ принимает решение: «Это именно то, что нужно!»
ИИ вызывает функцию:
get_weather("Москва")
ИИ получает результат и формулирует ответ пользователю
LangGraph как умный координатор
LangGraph делает этот процесс ещё более элегантным. Он работает как граф состояний, где каждый узел может:
Анализировать текущую ситуацию
Выбирать нужный инструмент
Передавать управление следующему узлу
Благодаря этому ИИ может выполнять сложные многошаговые задачи: сначала получить погоду, потом на основе неё предложить одежду, а затем найти ближайший магазин.
В рамках сегодняшней статьи мы не будем глубоко погружаться в тему графов, так как это заслуживает, серии публикаций и, если я увижу ваш отклик на статью, которую вы сейчас читаете — с меня серия публикаций по LangGraph в рамках которой я разложу тему цепочек (графов), от А до Я, а сегодня ограничимся только инструментами и MCP.
Главный секрет успеха
80% успеха любого MCP-сервера — это качественные описания инструментов. Чем подробнее и точнее вы опишете, что делает ваша функция, тем лучше ИИ поймёт, когда её использовать.
Плохое описание: «Делает расчёты»
Хорошее описание: «Вычисляет сложные проценты по вкладу с учётом капитализации за указанный период»
Именно поэтому далее в статье мы уделим особое внимание правильному оформлению функций и их документации.
Подготовка к практике
Уверен, вы уже хотите поскорее приступить к коду — и правильно! Но прежде чем мы начнём, есть пара важных моментов.
Рекомендуется к прочтению
Для более глубокого понимания очень желательно ознакомиться с моей предыдущей статьёй:
«Как научить нейросеть работать руками: создание полноценного ИИ-агента с MCP и LangGraph за час»
Также рекомендую заглянуть в мой Telegram-канал «Лёгкий путь в Python». Именно там я уже опубликовал:
Исходный код из этой и прошлой статьи
Эксклюзивные материалы, которых нет на Хабре
Полные практические примеры MCP-серверов, скриптов и тулзов
Что потребуется
Для полноценной работы нам понадобится API-токен одного из LLM-провайдеров. Подойдут:
DeepSeek (я буду использовать его в примерах)
Claude (Anthropic)
OpenAI (ChatGPT)
или локальные решения вроде Ollama
Если вы читали прошлую статью — вы уже знаете, как подключать любой из этих вариантов к LangGraph.
Подготовка среды
Сегодня всё будем писать на Python, так что первым делом — создаём виртуальное окружение и устанавливаем зависимости.
python -m venv venv
source venv/bin/activate # или venv\Scripts\activate на Windows
Создаём .env
файл и помещаем туда ваши токены. Пример:
OPENAI_API_KEY=sk-proj-123
DEEPSEEK_API_KEY=sk-12345
ANTROPIC_API_KEY=sk-12345
OPENROUTER_API_KEY=sk-or-v1-2123123
Выберите подходящего вам провайдера — LangGraph поддерживает их все.
Устанавливаем зависимости
Создайте файл requirements.txt
и добавьте в него зависимости. Полный список (актуальный на момент написания):
fastmcp==2.10.6
langchain==0.3.26
langchain-deepseek==0.1.3
langchain-mcp-adapters==0.1.9
langchain-ollama==0.3.5
langchain-openai==0.3.28
langgraph==0.5.3
mcp==1.12.0
ollama==0.5.1
openai==1.97.0
pydantic-settings==2.10.1
python-dotenv==1.1.1
uvicorn==0.35.0
faker==37.4.2
Запускаем установку:
pip install -r requirements.txt
Новое
Из нового здесь:
fastmcp
— мощная библиотека для быстрой сборки и публикации MCP-серверов.faker
— удобная библиотека для генерации тестовых (фейковых) данных. Сегодня она нам пригодится при создании демонстрационных инструментов.
План действий
Вот что мы сегодня сделаем шаг за шагом:
Научимся писать свои инструменты (тулзы) и подключать их напрямую к ИИ-агенту
Разберёмся, как подключать готовые MCP-серверы и использовать их инструменты в своём проекте
Создадим свой собственный MCP-сервер
Задеплоим его в облако с помощью Amvera Cloud — Это быстро, удобно, бюджетно, и вы получите HTTPS-домен, готовый для интеграции с LangGraph и любыми LLM-агентами. К тому же, Amvera предоставляет не только хостинг приложений, но и облачную инфраструктуру с собственным инференсом LLM без иностранной карты и встроенное проксирование до Claude, Gemini, Grok, GPT — всё в одном месте для ваших ИИ-проектов.
Готовы? Тогда переходим к практике сразу после небольшого, но очень важного, теоритического отступления.
Два подхода к работе с инструментами в LangGraph / LangChain
Когда вы начинаете подключать свои инструменты (tools) к нейросети через LangGraph или LangChain, у вас есть два основных пути: биндить инструменты вручную или использовать готовый ReAct‑агент. Оба имеют свои плюсы и минусы — разберём их.
1. bind_tools — биндинг инструментов напрямую к модели
Вы определяете функции с декоратором
@tool
, снабжаете их описанием (doc‑string), затем передаёте список инструментов модели через.bind_tools()
.Модель знает о каждом инструменте и может сгенерировать запрос — вызов той или иной функции — если это необходимо.
Пример сценария: чат-бот, где нужен единичный вызов инструмента (например, калькулятор или API запрос). После этого модель возвращает обычный ответ.
Ограничение: модель может вызвать только один инструмент за сессию или игнорировать биндинг, если недостаточно уверен обязан ли вызывать. Подходит, если вы хотите тонко контролировать, когда и какой инструмент используется.
Преимущества:
Низкая задержка, менее затратный способ.
Гибкость: вы самостоятельно решаете, когда и как обрабатывать tool_call.
Что важно:
Обязательно качественные описания инструментов — иначе модель может их не заметить.
2. create_react_agent — готовый ReAct‑агент из LangGraph / LangChain
LangGraph предоставляет
create_react_agent
, который сам управляет циклом ReAct (Reasoning‑Acting‑Loop): модель может вызвать инструмент, получить результат, проанализировать его и продолжить до финального ответа.Этот подход называют «реактивным агентом», он автоматически вызывает нужные инструменты до тех пор, пока не сформируется окончательный ответ.
В коде вы просто передаёте провайдера модели и список инструментов, например:
agent = create_react_agent("model_name", tools)
response = await agent.ainvoke({...})
Подходит для сложных задач, где агенту нужно взаимодействовать с несколькими инструментами, несколько шагов подряд.
Преимущества:
Удобство и автоматизация tool‑calling: вам не нужно контролировать вложенность вызовов.
Подходит для сценариев с несколькими инструментами за запрос.
Что важно:
Меньшая гибкость: агент сам решает, какие инструменты и когда вызывать.
Иногда может не распознать нужный tool, если описание не точное, или модель не поддерживает tool-calling нативно.
Пример создания простых инструментов с биндом
Для разогрева начнем с простого практического примера — напишем несколько функций, инициируем LLM и научим нашего нейро-товарища использовать эти инструменты.
Подготовка: импорты и настройка
Начнем с импортов:
from typing import Annotated, Sequence, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import (
BaseMessage,
SystemMessage,
HumanMessage,
AIMessage,
)
from langchain_deepseek import ChatDeepSeek
from langchain_core.tools import tool
from langgraph.graph.message import add_messages
from langgraph.graph import StateGraph, END, START
from langgraph.prebuilt import ToolNode
import os
import asyncio
Основную «магию» нам позволит оформить импорт tool
из langchain_core
и использование специального сервисного узла ToolNode
. Просто чтобы вы оставались в контексте — узел это логическое звено или точка, через которую проходит логика графа. Сам граф — это как дорожная карта. Обязательно подробнее это обсудим.
Сразу вызываем:
load_dotenv()
Это нужно, чтобы использовать переменные из файла .env
.
Описание состояния агента
Сразу опишем состояние, в котором будем хранить наши сообщения:
class AgentState(TypedDict):
"""Состояние агента, содержащее последовательность сообщений."""
messages: Annotated[Sequence[BaseMessage], add_messages]
Берите на вооружение. Несмотря на то что подход простой — он позволяет удобно сохранять контекст общения с нейросетью, ну а состояния — это основная движущая сила графов.
Создание функций-инструментов
Теперь опишем 2 простые асинхронные функции-инструмента:
async def add(a: int, b: int) -> int:
"""Складывает два целых числа и возвращает результат."""
await asyncio.sleep(0.1)
return a + b
async def list_files() -> list:
"""Возвращает список файлов в текущей папке."""
await asyncio.sleep(0.1)
return os.listdir(".")
Мягко говоря, зачем тут асинхронность, спросите вы, и я вам отвечу. Сейчас идет большая мода на асинхронность в Python и, несмотря на то что тут у нас нет в ней необходимости — этим простым примером я решил показать вам, что LangGraph прекрасно справляется с асинхронной логикой.
Вы видите 2 простейшие функции. Одна принимает на вход 2 числа и складывает, вторая выводит список файлов.
Для того чтобы эти функции могли использовать нейросети, мы уже проделали часть работы, а именно внутри функции дали описание того, что они делают. Это описание мы даем именно для нейросетей, поэтому, во-первых, не забудьте его добавить, а во-вторых, сделайте чтобы это описание было понятным!
Превращение функций в инструменты
Теперь нам нужно на каждую функцию повесить специальный декоратор:
@tool
async def add(a: int, b: int) -> int:
# ...
@tool
async def list_files() -> list:
# ...
Этим простым действием мы подготовили наши функции к интеграции.
Теперь создадим простую переменную (список), в который поместим наши инструменты:
tools = [add, list_files]
Аргументы передавать не нужно — нейросеть сама разберется!
Инициализация модели и привязка инструментов
Теперь выполним инициализацию модели (подробно говорили об этом в прошлой статье) и забиндим к ней наши инструменты:
llm = ChatDeepSeek(model="deepseek-chat").bind_tools(tools)
Создание узла агента
Теперь напишем функцию, которая будет вызывать нашу модель с заготовленным промптом:
async def model_call(state: AgentState) -> AgentState:
system_prompt = SystemMessage(
content="Ты моя система. Ответь на мой вопрос исходя из доступных для тебя инструментов"
)
messages = [system_prompt] + list(state["messages"])
response = await llm.ainvoke(messages)
return {"messages": [response]}
Тут уже начинается работа с состоянием. Если вы имеете опыт в создании телеграм-ботов на Aiogram 3 (кстати, у меня на Хабре штук 10 статей, в которых я рассказал о процессе создания ботов), то вы могли сталкиваться с таким понятием как FSM (машина состояний). Тут все работает похожим образом. У нас есть некое состояние, в котором мы храним все сообщения (сообщения от ИИ, системные сообщения, сообщения от человека и сообщения от инструментов), и при каждом вызове нейронки мы обновляем это состояние, пробрасывая все сообщения в контекст.
Условная логика: продолжать или завершать
Теперь опишем функцию с простым условием:
async def should_continue(state: AgentState) -> str:
"""Проверяет, нужно ли продолжить выполнение или закончить."""
messages = state["messages"]
last_message = messages[-1]
# Если последнее сообщение от AI и содержит вызовы инструментов - продолжаем
if isinstance(last_message, AIMessage) and last_message.tool_calls:
return "continue"
# Иначе заканчиваем
return "end"
Тут вот какая логика. Мы хотим сделать, чтобы нейронка не просто вызвала наши функции (скажем по правде, мы это и сами прекрасно сделаем), нет, мы хотим, чтобы она после вызова сделала что-то с этой информацией.
Например, у нас есть функция, которая на вход принимает PDF-документ и извлекает из него текст. Мы хотим не просто получить извлеченный текст, а чтобы нейросеть эту информацию использовала далее или, как минимум, дала по ней summary. В реализации подобной логики поможет эта функция.
Сборка и запуск графа
Теперь остается это дело запустить. Сейчас мы опишем главную функцию. Я дам ее полный код, а после прокомментирую:
async def main():
# Создание графа
graph = StateGraph(AgentState)
graph.add_node("our_agent", model_call)
tool_node = ToolNode(tools=tools)
graph.add_node("tools", tool_node)
# Настройка потока
graph.add_edge(START, "our_agent")
graph.add_conditional_edges(
"our_agent", should_continue, {"continue": "tools", "end": END}
)
graph.add_edge("tools", "our_agent")
# Компиляция и запуск
app = graph.compile()
result = await app.ainvoke(
{
"messages": [
HumanMessage(
content="Посчитай общее количество файлов в этой директории и прибавь к этому значению 10"
)
]
}
)
# Показываем результат
print("=== Полная история сообщений ===")
for i, msg in enumerate(result["messages"]):
print(f"{i+1}. {type(msg).__name__}: {getattr(msg, 'content', None)}")
if hasattr(msg, "tool_calls") and msg.tool_calls:
print(f" Tool calls: {msg.tool_calls}")
# Финальный ответ
for msg in reversed(result["messages"]):
if isinstance(msg, AIMessage) and not getattr(msg, "tool_calls", None):
print(f"\n=== Финальный ответ ===")
print(msg.content)
break
else:
print("\n=== Финальный ответ не найден ===")
Разбор концепции графов
Тут мы уже сталкиваемся с графом. Постараюсь коротко прокомментировать. Все в LangGraph держится на 4 основных «китах»:
Сам граф или некая дорожная карта
Узел (нода) или некие точки на этой карте
Ребра — связки между нодами
Состояния (некие чекпоинты в рамках «дорожной карты»)
Создание графа
Процесс начинается с создания графа:
graph = StateGraph(AgentState)
Добавление узлов
Затем мы привязываем к нему все существующие узлы (ноды):
graph.add_node("our_agent", model_call)
tool_node = ToolNode(tools=tools)
graph.add_node("tools", tool_node)
Ноды всегда принимают имя и некую функцию (в некоторых случаях достаточно использовать безымянные функции). Функции могут быть как наши, так и сервисные, как в примере с ToolNode
.
Связывание узлов
Далее нам необходимо узлы между собой связать. Связывать можно как обычными ребрами, так и условными.
Пример обычного ребра:
graph.add_edge(START, "our_agent")
Тут мы связали 2 узла: системный узел (START
, который ранее импортировали) и наш узел. Для связки в таких узлах используется имя узлов.
Пример условного ребра:
graph.add_conditional_edges(
"our_agent", should_continue, {"continue": "tools", "end": END}
)
Он принимает имя узла, от которого должно пойти ребро. Далее, вторым параметром, принимает название условной функции (она всегда строки возвращает), и далее мы описываем простое условие:
если условная функция вернула «continue», то мы вызываем узел tools
, иначе мы вызываем узел END
, тем самым завершая работу графа.
Понимаю, что сейчас может быть не все понятно, но когда-то, если это будет вам нужно, я более детально и подробно разложу концепт графов в формате мини-курса на Хабре.
Замыкание цикла
Если мы вызвали узел инструментов, то с него мы выполняем переход обратно на нашего агента, а тот уже, когда увидит, что инструменты не вызывались, просто завершит работу — END
.
graph.add_edge("tools", "our_agent")
Компиляция и запуск
Далее нам нужно скомпилировать граф:
app = graph.compile()
И остается только запустить:
result = await app.ainvoke({
"messages": [
HumanMessage(
content="Посчитай общее количество файлов в этой директории и прибавь к этому значению 10"
)
]
})
Далее я просто в подробном виде отобразил ответ нашего агента.


Биндим собственные инструменты и инструменты чужого MCP-сервера
Тут нужно понимать, что для того чтобы появилась техническая возможность у ваших ИИ-агентов использовать инструменты из MCP-серверов — вам нужно каким-то образом подключиться к ним. Для этого на данный момент существует 2 основных вида транспорта:
stdio: когда вы физически запускаете на своей локальной машине или VPS-сервере MCP, извлекаете набор тулзов и передаете их ИИ-агенту (через bind или через react_agent)
streamable_http: та же логика, но с удаленным подключением по HTTP-протоколу
Если вы разобрались с биндом обычных тулзов, то и вопросов бинда тулзов от MCP-сервера у вас тоже возникнуть не должно. Все сводится к следующему:
Объединяем все наши кастомные тулзы в 1 список (если они есть)
Объединяем тулзы MCP-сервера (серверов) в другой список
Объединяем эти 2 списка в 1 список и биндим к агенту
Давайте теперь проверим это на практике.
Создание кастомного инструмента
Чтобы было интереснее — напишем тулзу, которая будет принимать пол (male | female) и будет возвращать мужское или женское имя с фамилией:
@tool
async def get_random_user_name(gender: str) -> str:
"""
Возвращает случайное мужское или женское имя в зависимости от условия:
male - мужчина, female - женщина
"""
faker = Faker("ru_RU")
gender = gender.lower()
if gender == "male":
return f"{faker.first_name_male()} {faker.last_name_male()}"
return f"{faker.first_name_female()} {faker.last_name_female()}"
Подключение MCP-адаптера
Теперь давайте импортируем специальный адаптер, который позволит извлечь инструменты из подключенных MCP-серверов:
from langchain_mcp_adapters.client import MultiServerMCPClient
Теперь объединим в список все наши существующие тулзы:
custom_tools = [get_random_user_name]
Функция для получения всех инструментов
Теперь давайте напишем функцию, которая будет извлекать инструменты из подключенных MCP-серверов:
async def get_all_tools():
"""Получение всех инструментов: ваших + MCP"""
# Настройка MCP клиента
mcp_client = MultiServerMCPClient(
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"transport": "stdio",
},
"context7": {
"transport": "streamable_http",
"url": "https://mcp.context7.com/mcp",
},
}
)
# Получаем MCP инструменты
mcp_tools = await mcp_client.get_tools()
# Объединяем ваши инструменты с MCP инструментами
return custom_tools + mcp_tools
Разбор подключенных серверов
Давайте разбираться.
Благодаря MultiServerMCPClient
мы смогли подключиться к 2-м MCP-серверам:
context7 по
streamable_http
— очень полезный MCP-сервер, который возвращает актуальную информацию по самым ходовым библиотекам и фреймворкам. При разработке — незаменимая вещь!filesystem по
stdio
— хороший MCP-сервер, инструменты которого позволяют взаимодействовать с файловой системой: создавать, изменять файлы, выводить список и так далее.
Важные моменты установки
Важный момент по поводу локальных MCP (транспорт stdio
). Для того чтобы они работали, часто требуется локальная установка. В случае с server-filesystem
MCP установка будет иметь следующий вид:
npm install -g @modelcontextprotocol/server-filesystem
Также, в зависимости от команды, возможно, вам необходимо будет установить дополнительный софт. Например, Python с библиотекой uv, Node.js последней версии, npm и так далее.
Результат объединения
На выходе функция get_all_tools
просто вернет список всех доступных тулзов — как кастомных, так и родом из подключенных MCP.
Следующий шаг
Далее, в случае с прямым биндом, отличий от предыдущего примера, где мы биндили только кастомные тулзы, особо не будет, так что останавливаться на этом не будем.
Кому будет интересно — в моем бесплатном телеграм-канале «Легкий путь в Python» уже лежит полный исходный код с примерами и с MCP-сервером.
Переходим к «черному ящику» — react_agent
.
Тулзы с «Черным ящиком» React Agent LangGraph
Теперь посмотрим, как работает React Agent и каким образом он принимает тулзы для работы. Думаю, что вы будете удивлены, когда узнаете, что кода с React Agent для прикрепления тулзов будет даже меньше, чем в примере с биндом.
Что такое React Agent?
React Agent — это предварительно настроенный агент из LangGraph, который реализует паттерн ReAct (Reasoning + Acting). Это означает, что агент:
Размышляет (Reasoning) — анализирует задачу и планирует действия
Действует (Acting) — выполняет нужные инструменты
Наблюдает — получает результаты и корректирует план
Повторяет цикл до получения финального ответа
В отличие от ручной сборки графа, React Agent автоматически управляет всей логикой принятия решений. Вам не нужно думать о состояниях, узлах и ребрах — это уже реализовано внутри.
Простая инициализация
Первый этап, где мы объединяем в один список тулзы (кастомные и от MCP-агентов), отличаться не будет, но самое главное отличие будет далее и заключаться оно будет в инициализации агента. В данном примере нам не пригодятся графы.
1. Получаем список всех инструментов:
all_tools = await get_all_tools()
2. Инициируем агента:
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model=ChatDeepSeek(model="deepseek-chat"),
tools=all_tools,
prompt="Ты дружелюбный ассистент, который может генерировать фейковых пользователей, \
выполнять вычисления и делиться интересными фактами.",
)
При инициализации мы передаем:
Модель (обратите внимание, без явного бинда — просто инициализация модели)
Передаем список наших инструментов в параметре
tools
Пишем пользовательский промпт, который определяет поведение агента
Магия ReAct Agent
Вся магия заключается в том, что create_react_agent
под капотом создает сложный граф с:
Узлом для вызова модели
Узлом для выполнения инструментов
Условной логикой для принятия решений
Управлением состоянием и сообщениями
Но от вас это скрыто — вы получаете готового к работе агента одной строкой!
Продвинутый вызов с логированием
Для примера я использовал вызов через astream
. Такой подход нужен для более удобного логирования ответов нейросети и инструментов. Вот полный код:
async def run_query(agent, query: str):
"""Выполняет один запрос к агенту с читаемым выводом"""
print(f"? Запрос: {query}")
step_counter = 0
processed_messages = set() # Для избежания дублирования
async for event in agent.astream(
{"messages": [{"role": "user", "content": query}]},
stream_mode="values",
):
if "messages" in event and event["messages"]:
messages = event["messages"]
# Обрабатываем только новые сообщения
for msg in messages:
msg_id = getattr(msg, 'id', str(id(msg)))
if msg_id in processed_messages:
continue
processed_messages.add(msg_id)
# Получаем тип сообщения
msg_type = getattr(msg, 'type', 'unknown')
content = getattr(msg, 'content', '')
# 1. Сообщения от пользователя
if msg_type == 'human':
print(f"? Пользователь: {content}")
print("-" * 40)
# 2. Сообщения от ИИ
elif msg_type == 'ai':
# Проверяем наличие вызовов инструментов
tool_calls = getattr(msg, 'tool_calls', [])
if tool_calls:
step_counter += 1
print(f"? Шаг {step_counter}: Агент использует инструменты")
# Размышления агента (если есть)
if content and content.strip():
print(f"? Размышления: {content}")
# Детали каждого вызова инструмента
for i, tool_call in enumerate(tool_calls, 1):
# Парсим tool_call в зависимости от формата
if isinstance(tool_call, dict):
tool_name = tool_call.get('name', 'unknown')
tool_args = tool_call.get('args', {})
tool_id = tool_call.get('id', 'unknown')
else:
# Если это объект с атрибутами
tool_name = getattr(tool_call, 'name', 'unknown')
tool_args = getattr(tool_call, 'args', {})
tool_id = getattr(tool_call, 'id', 'unknown')
print(f"? Инструмент {i}: {tool_name}")
print(f" ? Параметры: {tool_args}")
print(f" ? ID: {tool_id}")
print("-" * 40)
# Финальный ответ (без tool_calls)
elif content and content.strip():
print(f"? Финальный ответ:")
print(f"? {content}")
print("-" * 40)
# 3. Результаты выполнения инструментов
elif msg_type == 'tool':
tool_name = getattr(msg, 'name', 'unknown')
tool_call_id = getattr(msg, 'tool_call_id', 'unknown')
print(f"? Результат инструмента: {tool_name}")
print(f" ? Call ID: {tool_call_id}")
# Форматируем результат
if content:
# Пытаемся распарсить JSON для красивого вывода
try:
import json
if content.strip().startswith(('{', '[')):
parsed = json.loads(content)
formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
print(f" ? Результат:")
for line in formatted.split('\n'):
print(f" {line}")
else:
print(f" ? Результат: {content}")
except:
print(f" ? Результат: {content}")
print("-" * 40)
# 4. Другие типы сообщений (для отладки)
else:
if content:
print(f"❓ Неизвестный тип ({msg_type}): {content[:100]}...")
print("-" * 40)
print("=" * 80)
print("✅ Запрос обработан")
print()
Простой вызов без логирования
Основная «длина» кода выше обусловлена детальным логированием результата. В целом, для простого вызова было бы достаточно всего одной строки:
# Простейший вызов
result = await agent.ainvoke({"messages": [{"role": "user", "content": "Твой запрос"}]})
print(result["messages"][-1].content)
Как видите, с React Agent мы получили мощного агента буквально в несколько строк кода!
FastMCP: быстрый старт
Думаю, что к этому моменту вы поняли, что никакой особой сложности или «магии» за MCP-серверами не стоит. Это просто набор разрозненных функций, объединенных между собой какой-то общей задачей.
Следовательно — настало время разобраться с тем, как писать собственные MCP-серверы!
Так как материала уже получилось много — сейчас я проведу короткий экспресс-курс «молодого бойца» в знакомстве с FastMCP. Когда-то, возможно, вернемся и более детально распакуем этого зверя.
Что такое FastMCP?
FastMCP — это высокоуровневый Python-фреймворк, который делает создание MCP-серверов максимально простым. Он разработан так, чтобы быть быстрым и Pythonic — в большинстве случаев достаточно просто декорировать функцию.
Главное, что нужно понять — FastMCP 1.0 оказался настолько успешным, что был интегрирован в официальный MCP Python SDK. А FastMCP 2.0 — это активно развиваемая версия с расширенным функционалом.
Транспорты и возможности
Главное, что нужно понять, так это то, что на FastMCP вы можете создавать MCP-серверы, которые будут работать:
Локально по stdio (сегодня рассматривать не будем)
По streamable_http (в FastMCP просто
transport="http"
)
Технически все будет сводиться к тому, чтобы объединить несколько инструментов в одно целое.
Три способа описания функционала
Сами инструменты можно описывать 3-мя основными способами:
1. Tools (инструменты)
Инструменты позволяют LLM выполнять действия, вызывая ваши Python-функции (синхронные или асинхронные). Идеально подходят для вычислений, API-вызовов или побочных эффектов (как POST/PUT).
Примерно такая же логика и синтаксис, как в LangGraph:
from fastmcp import FastMCP
mcp = FastMCP("Мой сервер")
@mcp.tool
def add(a: int, b: int) -> int:
"""Складывает два числа"""
return a + b
@mcp.tool
async def fetch_weather(city: str) -> str:
"""Получает погоду для города"""
# Здесь может быть вызов API
return f"В городе {city} сегодня солнечно"
2. Resources (ресурсы)
Ресурсы предоставляют источники данных только для чтения (как GET-запросы). Они позволяют LLM получать информацию из ваших данных.
@mcp.resource("user://profile/{user_id}")
def get_user_profile(user_id: str) -> str:
"""Получает профиль пользователя по ID"""
return f"Профиль пользователя {user_id}: активный, премиум-подписка"
@mcp.resource("docs://readme")
def get_readme() -> str:
"""Возвращает README проекта"""
with open("README.md", "r") as f:
return f.read()
3. Prompts (промпты)
Промпты определяют шаблоны взаимодействия для LLM (переиспользуемые шаблоны для взаимодействий с LLM).
@mcp.prompt
def debug_code(error_message: str) -> str:
"""Помогает отладить код по сообщению об ошибке"""
return f"""
Анализируй эту ошибку и предложи решение:
Ошибка: {error_message}
Дай пошаговые инструкции для исправления.
"""
@mcp.prompt
def review_code(code: str) -> list:
"""Создает промпт для ревью кода"""
return [
{"role": "user", "content": f"Проверь этот код:\n\n{code}"},
{"role": "assistant", "content": "Я помогу проверить код. Что конкретно тебя беспокоит?"}
]
Простой пример: собираем всё вместе
Давайте создадим небольшой MCP-сервер, который демонстрирует все три подхода:
from fastmcp import FastMCP
import json
import datetime
# Создаем сервер
mcp = FastMCP(
name="Demo Assistant",
instructions="Ассистент для демонстрации возможностей MCP"
)
# === ИНСТРУМЕНТЫ ===
@mcp.tool
def calculate_age(birth_year: int) -> int:
"""Вычисляет возраст по году рождения"""
current_year = datetime.datetime.now().year
return current_year - birth_year
@mcp.tool
async def generate_password(length: int = 12) -> str:
"""Генерирует случайный пароль"""
import random, string
chars = string.ascii_letters + string.digits + "!@#$%"
return ''.join(random.choice(chars) for _ in range(length))
# === РЕСУРСЫ ===
@mcp.resource("system://status")
def system_status() -> str:
"""Возвращает статус системы"""
return json.dumps({
"status": "online",
"timestamp": datetime.datetime.now().isoformat(),
"version": "1.0.0"
})
@mcp.resource("help://{topic}")
def get_help(topic: str) -> str:
"""Возвращает справку по теме"""
help_docs = {
"password": "Используйте generate_password для создания безопасных паролей",
"age": "Используйте calculate_age для вычисления возраста",
"status": "Проверьте system://status для мониторинга системы"
}
return help_docs.get(topic, f"Справка по теме '{topic}' не найдена")
# === ПРОМПТЫ ===
@mcp.prompt
def security_check(action: str) -> str:
"""Создает промпт для проверки безопасности действия"""
return f"""
Ты специалист по информационной безопасности.
Проанализируй это действие на предмет безопасности: {action}
Оцени:
1. Потенциальные риски
2. Рекомендации по безопасности
3. Альтернативные подходы
"""
@mcp.prompt
def explain_result(tool_name: str, result: str) -> str:
"""Объясняет результат работы инструмента"""
return f"""
Объясни пользователю простыми словами результат работы инструмента '{tool_name}':
Результат: {result}
Сделай объяснение понятным и полезным.
"""
# Запуск сервера
if __name__ == "__main__":
mcp.run(transport="http", port=8000)
Тестирование FastMCP-сервера
Для тестирования вашего MCP-сервера у вас есть несколько вариантов, от простых до продвинутых.
1. MCP Inspector (быстрое тестирование)
FastMCP поставляется с встроенным инструментом отладки — MCP Inspector, который предоставляет удобный веб-интерфейс:
# Запуск инспектора
fastmcp dev demo_server.py
Откроется браузер с интерфейсом, где вы сможете:
Во вкладке Tools тестировать инструменты с реальными параметрами
Во вкладке Resources проверять ресурсы
Во вкладке Prompts генерировать промпты
2. Программный клиент (для серьезного тестирования)
Для более серьезного тестирования стоит написать программный клиент. Вот пример полноценного тест-клиента для нашего Demo Assistant:
import asyncio
import json
from fastmcp import Client
from dotenv import load_dotenv
load_dotenv()
def safe_parse_json(text):
"""Безопасно парсит JSON или возвращает исходный текст"""
try:
return json.loads(text)
except json.JSONDecodeError:
return text
async def test_demo_server():
"""Полноценное тестирование Demo Assistant MCP-сервера."""
print("? Подключаемся к Demo Assistant серверу...")
client = Client("http://127.0.0.1:8000/mcp/")
async with client:
try:
# Проверяем соединение
print("✅ Сервер запущен!\n")
# Получаем возможности сервера
tools = await client.list_tools()
resources = await client.list_resources()
prompts = await client.list_prompts()
# Отображаем что доступно
print(f"? Доступно инструментов: {len(tools)}")
for tool in tools:
print(f" • {tool.name}: {tool.description}")
print(f"\n? Доступно ресурсов: {len(resources)}")
for resource in resources:
print(f" • {resource.uri}")
print(f"\n? Доступно промптов: {len(prompts)}")
for prompt in prompts:
print(f" • {prompt.name}: {prompt.description}")
print("\n? ТЕСТИРУЕМ ФУНКЦИОНАЛ:")
print("-" * 50)
# === ТЕСТИРУЕМ ИНСТРУМЕНТЫ ===
# 1. Тест расчета возраста
print("1️⃣ Тестируем calculate_age:")
result = await client.call_tool("calculate_age", {"birth_year": 1990})
age_data = safe_parse_json(result.content[0].text)
print(f" Возраст человека 1990 г.р.: {age_data} лет")
# 2. Тест генерации пароля
print("\n2️⃣ Тестируем generate_password:")
result = await client.call_tool("generate_password", {"length": 16})
password_data = safe_parse_json(result.content[0].text)
print(f" Сгенерированный пароль (16 символов): {password_data}")
# === ТЕСТИРУЕМ РЕСУРСЫ ===
# 3. Тест системного статуса
print("\n3️⃣ Читаем system://status:")
resource = await client.read_resource("system://status")
status_content = resource[0].text
status_data = safe_parse_json(status_content)
print(f" Статус системы: {status_data['status']}")
print(f" Время: {status_data['timestamp']}")
print(f" Версия: {status_data['version']}")
# 4. Тест динамического ресурса помощи
print("\n4️⃣ Читаем help://password:")
resource = await client.read_resource("help://password")
help_content = resource[0].text
print(f" Справка: {help_content}")
# === ТЕСТИРУЕМ ПРОМПТЫ ===
# 5. Тест промпта безопасности
print("\n5️⃣ Генерируем security_check промпт:")
prompt = await client.get_prompt("security_check", {
"action": "открыть порт 3000 на сервере"
})
security_prompt = prompt.messages[0].content.text
print(f" Промпт создан (длина: {len(security_prompt)} символов)")
print(f" Начало: {security_prompt[:100]}...")
# 6. Тест промпта объяснения
print("\n6️⃣ Генерируем explain_result промпт:")
prompt = await client.get_prompt("explain_result", {
"tool_name": "generate_password",
"result": "Tj9$mK2pL8qX"
})
explain_prompt = prompt.messages[0].content.text
print(f" Промпт создан (длина: {len(explain_prompt)} символов)")
print(f" Начало: {explain_prompt[:100]}...")
print("\n? ВСЕ ТЕСТЫ ПРОШЛИ УСПЕШНО!")
print("? Статистика:")
print(f" ✅ Инструментов протестировано: 2/{len(tools)}")
print(f" ✅ Ресурсов протестировано: 2/{len(resources)}")
print(f" ✅ Промптов протестировано: 2/{len(prompts)}")
except Exception as e:
print(f"❌ Ошибка при тестировании: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(test_demo_server())
3. Как запускать тесты
Для запуска тестов в одном окне запускам FastMCP приложение, а в другом окне — файл с клиентом для тестирования.
Приступим к созданию собственного MCP сервера!
Практика: создаем полноценный математический MCP-сервер
Думаю, что к этому моменту теории достаточно — пора переходить к практике! В этом разделе мы создадим полноценный математический MCP-сервер, который продемонстрирует все возможности FastMCP: инструменты, ресурсы и промпты.
В целом, вам не обязательно повторять мой код в данном блоке. Если у вас есть мысли или собственные идеи по созданию своего MCP-сервера — воплощайте! Если особых идей нет, то предлагаю воплотить вместе со мной математический MCP-сервер, который обработает все три типа компонентов: инструменты, ресурсы и промпты.
Структура проекта
Предлагаю создать отдельный проект под MCP-сервер. Логика та же: поднимаем виртуальное окружение, устанавливаем зависимости (fastmcp==2.10.6
) и прочие, которые будет требовать ваш проект.
Подготовим структуру проекта:
math_mcp_server/
├── server.py # Главный файл сервера
├── routes/ # Модули с логикой
│ ├── __init__.py
│ ├── basic_math.py # Базовые математические операции
│ ├── geometry.py # Геометрические вычисления
│ ├── statistics.py # Статистика и анализ данных
│ ├── resources.py # Математические ресурсы
│ └── prompts.py # Генераторы промптов
├── requirements.txt
└── test_client.py # Клиент для тестирования
Почему такая структура? Мы разбиваем функционал на логические модули, чтобы код был читаемым и легко расширяемым. Каждый модуль отвечает за свою область математики.
Базовые математические операции
Начнем с модуля базовых операций. Я приведу полный код этого модуля, чтобы вы увидели логику выстраивания кода:
# routes/basic_math.py
import math
from datetime import datetime
from fastmcp import FastMCP
def setup_basic_math_routes(server: FastMCP):
"""Настройка базовых математических операций."""
@server.tool
def calculate_basic(expression: str) -> dict:
"""Вычислить базовое математическое выражение."""
try:
# Безопасное вычисление только математических выражений
allowed_names = {
k: v for k, v in math.__dict__.items()
if not k.startswith("__")
}
allowed_names.update({"abs": abs, "round": round, "pow": pow})
result = eval(expression, {"__builtins__": {}}, allowed_names)
return {
"expression": expression,
"result": result,
"type": type(result).__name__,
"calculated_at": datetime.now().isoformat()
}
except Exception as e:
return {
"expression": expression,
"error": str(e),
"calculated_at": datetime.now().isoformat()
}
@server.tool
def solve_quadratic(a: float, b: float, c: float) -> dict:
"""Решить квадратное уравнение ax² + bx + c = 0."""
discriminant = b**2 - 4*a*c
if discriminant > 0:
x1 = (-b + math.sqrt(discriminant)) / (2*a)
x2 = (-b - math.sqrt(discriminant)) / (2*a)
return {
"equation": f"{a}x² + {b}x + {c} = 0",
"discriminant": discriminant,
"roots": [x1, x2],
"type": "two_real_roots"
}
elif discriminant == 0:
x = -b / (2*a)
return {
"equation": f"{a}x² + {b}x + {c} = 0",
"discriminant": discriminant,
"roots": [x],
"type": "one_real_root"
}
else:
real_part = -b / (2*a)
imaginary_part = math.sqrt(abs(discriminant)) / (2*a)
return {
"equation": f"{a}x² + {b}x + {c} = 0",
"discriminant": discriminant,
"roots": [
f"{real_part} + {imaginary_part}i",
f"{real_part} - {imaginary_part}i"
],
"type": "complex_roots"
}
@server.tool
def factorial(n: int) -> dict:
"""Вычислить факториал числа."""
if n < 0:
return {"error": "Факториал не определен для отрицательных чисел"}
result = math.factorial(n)
return {
"number": n,
"factorial": result,
"formula": f"{n}!",
"steps": " × ".join(str(i) for i in range(1, n + 1)) if n > 0 else "1"
}
Ключевые принципы:
Модульность: мы назначаем основную функцию
setup_basic_math_routes()
, которая аргументом всегда принимает наш сервер —server
. Далее последующая логика ничем не будет отличаться от той, которую мы рассматривали ранее.Безопасность: в
calculate_basic
мы ограничиваем доступные функции, чтобы предотвратить выполнение опасного кода.Подробные ответы: каждая функция возвращает структурированную информацию с пояснениями.
Геометрические вычисления
По остальным модулям приведу основные функции с комментариями:
# routes/geometry.py
import math
from fastmcp import FastMCP
def setup_geometry_routes(server: FastMCP):
"""Настройка геометрических функций."""
@server.tool
def circle_properties(radius: float) -> dict:
"""Вычислить свойства окружности по радиусу."""
if radius <= 0:
return {"error": "Радиус должен быть положительным числом"}
return {
"radius": radius,
"diameter": 2 * radius,
"circumference": 2 * math.pi * radius,
"area": math.pi * radius**2,
"formulas": {
"circumference": "2πr",
"area": "πr²"
}
}
@server.tool
def triangle_area(base: float, height: float) -> dict:
"""Вычислить площадь треугольника."""
if base <= 0 or height <= 0:
return {"error": "Основание и высота должны быть положительными"}
area = 0.5 * base * height
return {
"base": base,
"height": height,
"area": area,
"formula": "½ × основание × высота"
}
@server.tool
def distance_between_points(x1: float, y1: float, x2: float, y2: float) -> dict:
"""Вычислить расстояние между двумя точками."""
distance = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
return {
"point1": {"x": x1, "y": y1},
"point2": {"x": x2, "y": y2},
"distance": distance,
"formula": "√[(x₂-x₁)² + (y₂-y₁)²]"
}
Промпты для обучения математике
Промпты — это мощный инструмент для создания образовательного контента:
# routes/prompts.py
from fastmcp import FastMCP
def setup_math_prompts(server: FastMCP):
"""Настройка математических промптов."""
@server.prompt
def explain_solution(problem: str, solution: str, level: str = "intermediate") -> str:
"""Промпт для объяснения математического решения."""
level_instructions = {
"beginner": "Объясни очень простыми словами, как будто учишь школьника",
"intermediate": "Дай подробное объяснение с промежуточными шагами",
"advanced": "Включи математическое обоснование и альтернативные методы решения"
}
instruction = level_instructions.get(level, level_instructions["intermediate"])
return f"""
Ты математический преподаватель. {instruction}.
Задача: {problem}
Решение: {solution}
Твоя задача:
1. Объясни каждый шаг решения
2. Укажи какие математические правила применялись
3. Покажи почему именно так решается задача
4. Дай советы для решения похожих задач
Используй ясный язык и приводи примеры где это уместно.
"""
@server.prompt
def create_practice_problems(topic: str, difficulty: str = "medium", count: int = 5) -> str:
"""Промпт для создания практических задач."""
difficulty_descriptions = {
"easy": "простые задачи для начинающих",
"medium": "задачи среднего уровня сложности",
"hard": "сложные задачи для продвинутых учеников"
}
diff_desc = difficulty_descriptions.get(difficulty, "задачи среднего уровня")
return f"""
Создай {count} {diff_desc} по теме "{topic}".
Требования:
1. Каждая задача должна иметь четкое условие
2. Укажи правильный ответ для каждой задачи
3. Задачи должны быть разнообразными
4. Приведи краткое решение для каждой
Формат:
Задача 1: [условие]
Ответ: [правильный ответ]
Решение: [краткие шаги]
Тема: {topic}
Сложность: {difficulty}
Количество: {count}
"""
Математические ресурсы-справочники
Ресурсы предоставляют справочную информацию:
# routes/resources.py
import json
import math
from fastmcp import FastMCP
def setup_math_resources(server: FastMCP):
"""Настройка математических ресурсов-справочников."""
@server.resource("math://formulas/basic")
def basic_formulas() -> str:
"""Основные математические формулы."""
formulas = {
"Алгебра": {
"Квадратное уравнение": "ax² + bx + c = 0, x = (-b ± √(b²-4ac)) / 2a",
"Разность квадратов": "a² - b² = (a + b)(a - b)",
"Квадрат суммы": "(a + b)² = a² + 2ab + b²",
"Квадрат разности": "(a - b)² = a² - 2ab + b²"
},
"Геометрия": {
"Площадь круга": "S = πr²",
"Длина окружности": "C = 2πr",
"Площадь треугольника": "S = ½ × основание × высота",
"Теорема Пифагора": "c² = a² + b²",
"Площадь прямоугольника": "S = длина × ширина"
},
"Тригонометрия": {
"Основное тригонометрическое тождество": "sin²α + cos²α = 1",
"Формула синуса двойного угла": "sin(2α) = 2sin(α)cos(α)",
"Формула косинуса двойного угла": "cos(2α) = cos²α - sin²α"
}
}
return json.dumps(formulas, ensure_ascii=False, indent=2)
@server.resource("math://constants/mathematical")
def math_constants() -> str:
"""Математические константы."""
constants = {
"π (Пи)": {
"value": math.pi,
"description": "Отношение длины окружности к её диаметру",
"approximation": "3.14159"
},
"e (Число Эйлера)": {
"value": math.e,
"description": "Основание натурального логарифма",
"approximation": "2.71828"
},
"φ (Золотое сечение)": {
"value": (1 + math.sqrt(5)) / 2,
"description": "Золотое сечение",
"approximation": "1.61803"
},
"√2": {
"value": math.sqrt(2),
"description": "Квадратный корень из 2",
"approximation": "1.41421"
}
}
return json.dumps(constants, ensure_ascii=False, indent=2)
Статистика и анализ данных
# routes/statistics.py
import statistics
from typing import List
from fastmcp import FastMCP
def setup_statistics_routes(server: FastMCP):
"""Настройка статистических функций."""
@server.tool
def analyze_dataset(numbers: List[float]) -> dict:
"""Полный статистический анализ набора данных."""
if not numbers:
return {"error": "Пустой набор данных"}
n = len(numbers)
return {
"dataset": numbers,
"count": n,
"sum": sum(numbers),
"mean": statistics.mean(numbers),
"median": statistics.median(numbers),
"mode": statistics.mode(numbers) if len(set(numbers)) < n else "Нет моды",
"range": max(numbers) - min(numbers),
"min": min(numbers),
"max": max(numbers),
"variance": statistics.variance(numbers) if n > 1 else 0,
"std_deviation": statistics.stdev(numbers) if n > 1 else 0,
"quartiles": {
"q1": statistics.quantiles(numbers, n=4)[0] if n >= 4 else None,
"q2": statistics.median(numbers),
"q3": statistics.quantiles(numbers, n=4)[2] if n >= 4 else None
}
}
@server.tool
def correlation_coefficient(x_values: List[float], y_values: List[float]) -> dict:
"""Вычислить коэффициент корреляции Пирсона между двумя наборами данных."""
if len(x_values) != len(y_values):
return {"error": "Наборы данных должны быть одинакового размера"}
if len(x_values) < 2:
return {"error": "Нужно минимум 2 точки данных"}
try:
correlation = statistics.correlation(x_values, y_values)
# Интерпретация силы корреляции
abs_corr = abs(correlation)
if abs_corr >= 0.8:
strength = "очень сильная"
elif abs_corr >= 0.6:
strength = "сильная"
elif abs_corr >= 0.4:
strength = "умеренная"
elif abs_corr >= 0.2:
strength = "слабая"
else:
strength = "очень слабая"
direction = "положительная" if correlation > 0 else "отрицательная"
return {
"x_values": x_values,
"y_values": y_values,
"correlation_coefficient": correlation,
"interpretation": {
"strength": strength,
"direction": direction,
"description": f"{strength} {direction} корреляция"
}
}
except Exception as e:
return {"error": f"Ошибка вычисления: {str(e)}"}
Сборка проекта: главный файл сервера
Для сборки проекта в корне опишем файл server.py
:
# server.py
from datetime import datetime
from fastmcp import FastMCP
from routes.basic_math import setup_basic_math_routes
from routes.prompts import setup_math_prompts
from routes.resources import setup_math_resources
from routes.statistics import setup_statistics_routes
from routes.geometry import setup_geometry_routes
def create_math_server() -> FastMCP:
"""Создать и настроить математический MCP-сервер."""
server = FastMCP("Mathematical Calculator & Tutor")
# Подключаем все модули
setup_basic_math_routes(server)
setup_statistics_routes(server)
setup_geometry_routes(server)
setup_math_resources(server)
setup_math_prompts(server)
# Дополнительные общие инструменты
@server.tool
def server_info() -> dict:
"""Информация о математическом сервере."""
return {
"name": "Mathematical Calculator & Tutor",
"version": "1.0.0",
"description": "Полнофункциональный математический MCP-сервер",
"capabilities": {
"tools": [
"Базовые вычисления",
"Решение квадратных уравнений",
"Статистический анализ",
"Геометрические вычисления",
"Факториалы"
],
"resources": [
"Математические формулы",
"Константы",
"Справка по статистике",
"Примеры решений"
],
"prompts": [
"Объяснение решений",
"Создание задач",
"Репетиторство",
"Анализ ошибок"
]
},
"created_at": datetime.now().isoformat()
}
# ================================
# ЗАПУСК СЕРВЕРА
# ================================
if __name__ == "__main__":
math_server = create_math_server()
math_server.run(transport="http", port=8000, host="0.0.0.0")
Деплой и тестирование через ИИ-агентов
И так, мы подняли с вами собственный MCP-сервер, к которому можно подключаться удаленно (по HTTP-протоколу), но без деплоя в этом большого смысла не будет, так как подключиться сейчас к нашему серверу можно только на локальном компьютере. По сути, сейчас он работает не как transport="http"
, а как stdio
.
Давайте исправлять эту ситуацию!
Зачем нужен деплой MCP-сервера?
Локальный запуск ограничивает возможности:
Сервер доступен только с вашего компьютера
Нельзя поделиться с коллегами или интегрировать в продакшн
ИИ-агенты не могут подключиться удаленно
Нет постоянной доступности (выключили компьютер — сервер недоступен)
Деплой решает эти проблемы:
Доступность 24/7 из любой точки мира
Возможность интеграции с ИИ-платформами
Масштабируемость и надежность
Простое подключение через URL
Выбор платформы для деплоя
Самое простое решение для деплоя — взять сервис, на который достаточно будет доставить свое FastMCP-приложение и на котором это приложение запустится автоматически. Кроме того, чтобы не тратиться на покупке доменного имени, хотелось бы, чтобы его нам дали в подарок.
Такое решение — облачный хостинг Amvera Cloud.
Почему Amvera?
Бесплатный домен в подарок
111 рублей на баланс за регистрацию
Автоматический деплой из Git или через интерфейс
Простая настройка через конфиг‑файл
Автоматическое обновление при изменении кода
Стабильный доступ к LLM API — на Amvera, Claude и ChatGPT работают без VPN и прокси «из коробки», что критично для продакшн‑проектов в России
Можно подключить API LLM с оплатой рублями. Не нужно иметь иностранную карту.
Подготовка к деплою
Весь процесс деплоя будет сводиться к тому, чтобы доставить файлы вашего приложения с заготовленным конфиг-файлом в созданный на сайте Amvera проект. Доставить можно как просто перетягиванием файлов через интерфейс на сайте, так и через стандартные команды Git (тут как кто привык).
1. Создаем файл конфигурации Amvera
Подготовим файл с настройками amvera.yml
:
meta:
environment: python
toolchain:
name: pip
version: "3.11"
build:
requirementsPath: requirements.txt
run:
scriptName: server.py
persistenceMount: /data
containerPort: 8000
Что означают параметры:
environment: python
— используем Python-окружениеtoolchain.version: "3.11"
— версия PythonrequirementsPath
— путь к файлу с зависимостямиscriptName
— главный файл для запускаcontainerPort: 8000
— порт приложения (должен совпадать с тем, что в коде)
2. Подготавливаем requirements.txt
В нашем случае содержимое файла минимальное:
fastmcp==2.10.6
При необходимости добавьте другие зависимости, которые использует ваш проект.
Пошаговый деплой на Amvera
Этого достаточно! Теперь действуем пошагово:
Шаг 1: Регистрация и создание проекта
Заходим на сайт amvera.ru и регистрируемся (за регистрацию, кстати, получаем 111 рублей на внутренний баланс — достаточно для бесплатного старта)
Кликаем на «Создать проект». Даем ему имя (например, «math‑mcp‑server») и выбираем тариф. Для тестов будет достаточно «Начальный плюс»

Шаг 2: Загрузка файлов
-
На экране загрузки файлов выбираем удобный способ. Я выбрал «Через интерфейс». Загружаем файлы:
amvera.yml
requirements.txt
Папку
routes/
со всеми модулями
Жмем «Далее»

Шаг 3: Проверка настроек
Если вы загрузили файл с настройками, то на новом экране вы увидите заполненные поля. Проверяем, чтобы все было корректно, и нажимаем «Завершить»
Шаг 4: Активация домена
Проваливаемся в проект, там выбираем вкладку «Домены» и активируем бесплатное доменное имя. Не забываем передвинуть ползунок для активации!

Шаг 5: Ожидание запуска
После этого ждем 2–3 минуты и ваш сервис доступен по выделенному доменному имени. Если доменное имя не применилось — просто кликаем на кнопку «Пересобрать проект», но обычно этого не требуется.

Получаем URL для подключения
В моем случае ссылка на доступ к MCP-серверу будет иметь следующий вид:
https://math-mcp-server-yakvenalex.amvera.io/mcp/
И, следовательно, для подключения к моему MCP-серверу мне будет достаточно указать следующую конструкцию:
"math_mcp": {
"transport": "streamable_http",
"url": "https://math-mcp-server-yakvenalex.amvera.io/mcp/"
}
Пример с кода:
async def get_all_tools():
"""Получение всех инструментов: ваших + MCP"""
# Настройка MCP клиента
mcp_client = MultiServerMCPClient(
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"transport": "stdio",
},
"match_mcp": {
"transport": "streamable_http",
"url": "https://mcpserver-yakvenalex.amvera.io/mcp/",
},
"context7": {
"transport": "streamable_http",
"url": "https://mcp.context7.com/mcp",
},
}
)
# Получаем MCP инструменты
mcp_tools = await mcp_client.get_tools()
# Объединяем ваши инструменты с MCP инструментами
return custom_tools + mcp_tools
Имя сервера (math_mcp
) может быть любым.
MCP-сервер отлично взаимодействует как с LangGraph и LangChain, так и с другими агентами, такими как Cursor, Claude Code, Claude Desktop, Gemini Cli и другими.
Заключение
Вот и подошла к концу наша большая статья о MCP-серверах и ИИ-агентах. Признаюсь честно — когда я начинал её писать, думал, что получится что-то покороче. Но тема оказалась настолько увлекательной и многогранной, что остановиться было просто невозможно!
Что мы с вами прошли
За это время мы проделали немалый путь:
Разобрались, что такое MCP и чем он отличается от обычных инструментов
Научились создавать собственные тулзы и биндить их к нейросетям
Освоили подключение готовых MCP-серверов
Поняли разницу между ручным биндом и React Agent
Создали полноценный математический MCP-сервер с нуля
И даже задеплоили его в облако Amvera Cloud!
Мои впечатления
Знаете, что меня больше всего поражает в этой теме? Скорость развития. Буквально полгода назад о MCP мало кто слышал, а сегодня это уже стандарт де-факто для ИИ-агентов. И темп только нарастает — каждую неделю появляются новые фреймворки, новые возможности, новые горизонты.
Но самое классное — это простота. Помните, как раньше интеграция с ИИ была болью? Нужно было разбираться с API, форматами, протоколами... А сейчас? Написал функцию, повесил декоратор @tool
— и вуаля, нейросеть уже может её использовать!
Что дальше?
Эта статья — только начало. В планах у меня ещё много интересного:
Детальный разбор LangGraph (если увижу отклик на эту статью)
Создание сложных многоагентных систем
Интеграция MCP с популярными инструментами разработки
Может быть, даже видеокурс по теме
Призыв к действию
А пока — экспериментируйте! Создавайте свои MCP-серверы, подключайте их к разным ИИ-моделям, делитесь результатами. Именно сейчас, когда технология только набирает обороты, у каждого из нас есть шанс стать частью этой революции.
Где найти меня
Весь код из статьи, дополнительные материалы и эксклюзивный контент — в моём Telegram-канале «Лёгкий путь в Python». Там я делюсь не только готовыми решениями, но и процессом разработки — со всеми ошибками, инсайтами и «эврика-моментами».
Последние слова
ИИ-агенты перестают быть фантастикой — они становятся частью нашей повседневной работы. И те, кто научится создавать для них правильные инструменты, получат огромное преимущество.
Удачи в ваших экспериментах с MCP! И помните — лучшее время посадить дерево было 20 лет назад, а второе лучшее время — сегодня. То же самое с изучением ИИ-агентов.
P.S. Если статья была полезной — не забудьте поставить лайк и поделиться с коллегами. А ещё лучше — напишите в комментариях, какие MCP-серверы создали вы! Всегда интересно посмотреть на чужие решения.