Как создать MCP-сервер и научить ИИ работать с любым кодом и инструментами через LangGraph

от автора

Всё стремительнее на глазах формируется новый виток в развитии инструментов для работы с искусственным интеллектом: если ещё недавно внимание разработчиков было приковано к 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 дня — всё просто.

Теперь представьте: Вы задаёте нейросети вопрос:

«Дружище, подскажи, какая там погода будет в Краснодаре в ближайшие четыре дня?»

ИИ-агент сам:

  1. Извлекает из запроса нужные переменные (city = "Краснодар", days = 4)

  2. Вызывает вашу функцию

  3. Получает результат

  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, нейросеть получает не только сам код, но и полное описание: что делает функция, какие параметры принимает, что возвращает.

Как работает «мозг» ИИ-агента

Процесс принятия решений выглядит примерно так:

  1. Пользователь пишет: «Какая сейчас погода в Москве?»

  2. ИИ анализирует: «Нужна информация о погоде в конкретном городе»

  3. ИИ сканирует доступные инструменты: «У меня есть функция get_weather, которая принимает название города»

  4. ИИ принимает решение: «Это именно то, что нужно!»

  5. ИИ вызывает функцию: get_weather("Москва")

  6. ИИ получает результат и формулирует ответ пользователю

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 — удобная библиотека для генерации тестовых (фейковых) данных. Сегодня она нам пригодится при создании демонстрационных инструментов.

План действий

Вот что мы сегодня сделаем шаг за шагом:

  1. Научимся писать свои инструменты (тулзы) и подключать их напрямую к ИИ-агенту

  2. Разберёмся, как подключать готовые MCP-серверы и использовать их инструменты в своём проекте

  3. Создадим свой собственный MCP-сервер

  4. Задеплоим его в облако с помощью 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. Объединяем все наши кастомные тулзы в 1 список (если они есть)

  2. Объединяем тулзы MCP-сервера (серверов) в другой список

  3. Объединяем эти 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). Это означает, что агент:

  1. Размышляет (Reasoning) — анализирует задачу и планирует действия

  2. Действует (Acting) — выполняет нужные инструменты

  3. Наблюдает — получает результаты и корректирует план

  4. Повторяет цикл до получения финального ответа

В отличие от ручной сборки графа, 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="Ты дружелюбный ассистент, который может генерировать фейковых пользователей, \ выполнять вычисления и делиться интересными фактами.", ) 

При инициализации мы передаем:

  1. Модель (обратите внимание, без явного бинда — просто инициализация модели)

  2. Передаем список наших инструментов в параметре tools

  3. Пишем пользовательский промпт, который определяет поведение агента

Магия 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"         } 

Ключевые принципы:

  1. Модульность: мы назначаем основную функцию setup_basic_math_routes(), которая аргументом всегда принимает наш сервер — server. Далее последующая логика ничем не будет отличаться от той, которую мы рассматривали ранее.

  2. Безопасность: в calculate_basic мы ограничиваем доступные функции, чтобы предотвратить выполнение опасного кода.

  3. Подробные ответы: каждая функция возвращает структурированную информацию с пояснениями.

Геометрические вычисления

По остальным модулям приведу основные функции с комментариями:

# 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: Регистрация и создание проекта

  1. Заходим на сайт amvera.ru и регистрируемся (за регистрацию, кстати, получаем 111 рублей на внутренний баланс — достаточно для бесплатного старта)

  2. Кликаем на «Создать проект». Даем ему имя (например, «math-mcp-server») и выбираем тариф. Для тестов будет достаточно «Начальный плюс»

Шаг 2: Загрузка файлов

  1. На экране загрузки файлов выбираем удобный способ. Я выбрал «Через интерфейс». Загружаем файлы:

    • server.py

    • amvera.yml

    • requirements.txt

    • Папку routes/ со всеми модулями

    Жмем «Далее»

Шаг 3: Проверка настроек

  1. Если вы загрузили файл с настройками, то на новом экране вы увидите заполненные поля. Проверяем, чтобы все было корректно, и нажимаем «Завершить»

Шаг 4: Активация домена

  1. Проваливаемся в проект, там выбираем вкладку «Домены» и активируем бесплатное доменное имя. Не забываем передвинуть ползунок для активации!

Шаг 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-серверы создали вы! Всегда интересно посмотреть на чужие решения.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Делаем курс по LangGraph / LangChain?

0% Ну естественно!0
100% Да, можно2
0% Делай, но я уже и так все знаю0
0% Не делай0
0% Пожалуйста, нет!0

Проголосовали 2 пользователя. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/articles/931874/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *