Всё стремительнее на глазах формируется новый виток в развитии инструментов для работы с искусственным интеллектом: если ещё недавно внимание разработчиков было приковано к 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"— версия Python -
requirementsPath— путь к файлу с зависимостями -
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-серверы создали вы! Всегда интересно посмотреть на чужие решения.
ссылка на оригинал статьи https://habr.com/ru/articles/931874/
Добавить комментарий