Введение: что вы получите в конце этого гайда

Представьте, что вы открываете чат с ИИ-ассистентом и пишете: «Зайди на страницу конкурента, собери названия и цены всех товаров из каталога и сведи их в таблицу». Ассистент не отвечает «у меня нет доступа к интернету», а действительно загружает страницу, вытаскивает данные и отдаёт вам готовый результат. Именно это вы построите, пройдя гайд до конца. Связующим звеном между языковой моделью и вебом станет ваш собственный MCP-сервер, написанный на Python.

Важная оговорка: мы не будем разбирать готовый Playwright MCP и другие коробочные решения. Про них в блоге есть отдельные материалы. Здесь задача другая: написать сервер с нуля, чтобы вы понимали каждую строчку, могли добавлять свои инструменты, подключать мобильные прокси и адаптировать логику под конкретные задачи. Своё решение всегда гибче чужого.

Для кого этот гайд

  • Маркетологи и владельцы бизнеса, которым нужно быстро собирать цены, отзывы, описания товаров и контент конкурентов, не заказывая парсер у разработчика.
  • Арбитражники, которые мониторят офферы, лендинги и креативы и хотят делегировать рутину ИИ-агенту.
  • Разработчики, которые слышали про протокол MCP, но ещё не собирали свой сервер и хотят рабочий шаблон.
  • Пользователи мобильных прокси, которым важно, чтобы запросы агента уходили через их прокси, а не напрямую с домашнего IP.

Что нужно знать заранее

Гайд рассчитан на начинающих. Опыт программирования не обязателен, но пригодится понимание того, что такое командная строка и как открыть файл в текстовом редакторе. Весь код можно копировать целиком, а каждая его часть объяснена простым языком. Если вы уже пишете на Python, для вас есть отдельный блок с продвинутыми возможностями ближе к концу статьи.

Сколько времени потребуется

Планируйте 2-3 часа на первый проход. Установка инструментов займёт около 30 минут, минимальный работающий MCP-сервер появится через час, а оставшееся время уйдёт на добавление инструментов извлечения данных, подключение прокси и тестирование. Повторить всё с нуля на другом компьютере вы сможете уже за 20-30 минут.

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

Прежде чем писать код, убедитесь, что у вас есть всё необходимое. Этот раздел можно пройти за полчаса, и он избавит от половины типичных проблем на следующих этапах.

Системные требования

  • Компьютер с Windows 10/11, macOS 12 и новее или Linux (Ubuntu 22.04 и новее). Всё описанное работает на любой из этих систем, различия только в путях к файлам.
  • Минимум 4 ГБ оперативной памяти и 1 ГБ свободного места на диске.
  • Стабильный доступ в интернет.

Что установить

  1. Python 3.11 или новее. На 2026 год актуальны версии 3.12 и 3.13. Скачайте установщик с официального сайта проекта Python. На Windows в первом окне установщика обязательно поставьте галочку Add python.exe to PATH, иначе команда python не будет находиться в терминале. На macOS удобнее поставить Python через Homebrew командой brew install python. На Ubuntu выполните sudo apt install python3 python3-venv python3-pip.
  2. Текстовый редактор для кода. Рекомендуем Visual Studio Code. Он бесплатный, подсвечивает синтаксис и показывает ошибки. Подойдёт и любой другой редактор, даже Блокнот, но с VS Code вам будет удобнее.
  3. MCP-клиент, то есть приложение с ИИ-агентом, к которому вы подключите сервер. Самый простой вариант для начинающих: Claude Desktop. Также MCP поддерживают редактор Cursor, VS Code с расширением GitHub Copilot и ряд других инструментов. Установите хотя бы один из них до начала работы.
  4. Node.js 20 или новее. Он нужен не для самого сервера, а для утилиты MCP Inspector, с помощью которой мы будем отлаживать инструменты. Скачайте установщик LTS-версии с официального сайта Node.js и установите с настройками по умолчанию.

Доступы

Для раздела про прокси понадобятся данные вашего мобильного прокси: хост, порт, ��огин и пароль, а также ссылка для смены IP-адреса, если ваш тариф её поддерживает. Всё это находится в личном кабинете провайдера. Если прокси пока нет, гайд можно пройти и без него: сервер будет работать напрямую, а прокси вы добавите позже одной строкой.

Резервные копии

Мы будем править конфигурационный файл MCP-клиента. Перед этим скопируйте его в безопасное место, например на рабочий стол с пометкой «backup». Если что-то пойдёт не так, вы просто вернёте копию на место. Сам код сервера храните в отдельной папке и после каждого рабочего шага сохраняйте копию файла или делайте коммит в Git, если умеете им пользоваться.

Совет: Создайте на диске отдельную папку с коротким путём без пробелов и кириллицы, например C:/mcp-collector на Windows или ~/mcp-collector на macOS и Linux. Пробелы и русские буквы в путях регулярно ломают запуск серверов из конфигов, а вы потратите час на поиск причины.

Базовые понятия: как устроен MCP-сервер и зачем он ИИ-агенту

Прежде чем писать первую строку кода, разберёмся с терминами. Без этого инструкция будет выглядеть набором магических заклинаний, а с ним каждое действие станет логичным.

Что такое MCP

MCP (Model Context Protocol) — открытый протокол, который описывает, как языковая модель общается с внешними инструментами. До его появления каждый сервис придумывал свой способ «дать ИИ руки». MCP стандартизировал это: если вы написали сервер по протоколу, его поймёт любой совместимый клиент, будь то Claude Desktop, Cursor или ваш собственный агент. Можно сравнить MCP с USB-разъёмом: неважно, что вы подключаете, флешку или мышь, разъём один и тот же.

Клиент и сервер

В архитектуре MCP два участника. Клиент — это приложение с ИИ, которое задаёт вопросы и вызывает инструменты. MCP-сервер — это программа, которая эти инструменты предоставляет. В нашем случае сервер будет умением «ходить в интернет и доставать данные», а клиентом станет ваш ИИ-ассистент. Сервер запускается локально на вашем компьютере, и клиент общается с ним напрямую.

Инструменты, ресурсы и промпты

MCP-сервер может отдавать клиенту три типа сущностей:

  • Инструменты (tools) — функции, которые модель может вызвать: «скачай страницу», «вытащи все ссылки», «смени IP прокси». Это основа нашего гайда.
  • Ресурсы (resources) — данные, которые сервер предоставляет для чтения, например содержимое файла с настройками или результат последнего сбора.
  • Промпты (prompts) — заготовленные шаблоны запросов, которые пользователь может вызвать одной командой.

Для сбора данных достаточно инструментов. Ресурсы и промпты мы затронем в продвинутом блоке.

Как модель понимает, что вызывать

Здесь есть важный нюанс. Когда клиент подключается к серверу, он запрашивает список инструментов с их названиями, описаниями и параметрами. Эти описания попадают в контекст модели. Дальше модель сама решает, какой инструмент вызвать и с какими аргументами, опираясь именно на текст описания. Поэтому описания функций в нашем коде — не формальность, а инструкция для ИИ. Чем яснее вы напишете, что делает инструмент и когда его использовать, тем точнее агент будет работать.

Транспорт: stdio и HTTP

Сервер и клиент должны как-то обмениваться сообщениями. Протокол предусматривает два основных способа. stdio — клиент сам запускает ваш скрипт как дочерний процесс и общается с ним через стандартный ввод и вывод. Это самый простой вариант для локальной работы, и с него мы начнём. Streamable HTTP — сервер работает как веб-сервис, к которому клиент подключается по адресу. Этот вариант нужен, если сервер живёт на удалённой машине или к нему подключаются несколько клиентов. Его разберём в продвинутом блоке.

⚠️ Внимание: При транспорте stdio весь стандартный вывод процесса занят служебными сообщениями протокола. Если вы напишете в коде обычный print для отладки, клиент получит мусор вместо корректного ответа и разорвёт соединение. Отладочные сообщения можно выводить только в поток ошибок stderr. Запомните это правило, оно спасёт вам много времени.

Почему сбор данных через MCP удобен

Классический парсер жёстко прописан: он умеет собирать конкретные поля с конкретного сайта. Как только вёрстка меняется, парсер ломается. Связка «ИИ-агент плюс MCP-сервер» работает иначе: сервер даёт универсальные инструменты (скачать, извлечь текст, найти элементы по селектору), а модель сама разбирается в структуре страницы и формулирует итог. Вы получаете гибкость без переписывания кода под каждый новый источник.

Шаг 1: Создаём проект и устанавливаем зависимости

Цель этапа: подготовить изолированное окружение Python и установить библиотеки, кото��ые нужны для MCP-сервера. В конце шага у вас будет папка проекта с рабочим виртуальным окружением.

Зачем нужно виртуальное окружение

Виртуальное окружение — это отдельная копия Python со своими библиотеками внутри папки проекта. Оно нужно, чтобы наш сервер не конфликтовал с другими Python-программами на компьютере, а MCP-клиент точно знал, какой интерпретатор запускать. Без него половина проблем «у меня в терминале работает, а в клиенте нет» гарантирована.

Пошаговая инструкция

  1. Откройте терминал. На Windows нажмите Win+R, введите powershell и нажмите Enter. На macOS откройте приложение Terminal через Spotlight (Cmd+Пробел, затем введите Terminal). На Linux нажмите Ctrl+Alt+T.
  2. Создайте папку проекта и перейдите в неё. На Windows выполните две команды: mkdir C:/mcp-collector, затем cd C:/mcp-collector. На macOS и Linux: mkdir ~/mcp-collector, затем cd ~/mcp-collector.
  3. Проверьте версию Python командой python --version (на macOS и Linux может потребоваться python3 --version). Вы должны увидеть строку вида Python 3.12.x. Если версия ниже 3.11 или команда не найдена, вернитесь к разделу подготовки и переустановите Python.
  4. Создайте виртуальное окружение командой python -m venv .venv. В папке проекта появится скрытая папка .venv. Это займёт 10-20 секунд.
  5. Активируйте окружение. На Windows в PowerShell: .venv/Scripts/Activate.ps1. Если PowerShell пишет, что выполнение скриптов запрещено, выполните команду Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, подтвердите буквой Y и повторите активацию. На macOS и Linux: source .venv/bin/activate. После активации в начале строки терминала появится пометка (.venv).
  6. Обновите менеджер пакетов: python -m pip install --upgrade pip.
  7. Установите библиотеки одной командой: pip install "mcp[cli]" httpx beautifulsoup4. Здесь mcp — официальный Python SDK протокола (на 2026 год актуальна ветка 1.x), httpx — современная библиотека для HTTP-запросов с поддержкой прокси, beautifulsoup4 — инструмент для разбора HTML. Установка займёт 1-2 минуты.
  8. Создайте пустой файл server.py в папке проекта. В VS Code: откройте папку через File, Open Folder, затем нажмите иконку нового файла на панели слева и введите имя.

Что означают библиотеки

  • mcp берёт на себя весь протокол: регистрацию инструментов, обмен сообщениями, описание параметров. Модуль FastMCP внутри него позволяет объявить инструмент обычной функцией с декоратором.
  • httpx скачивает страницы. В отличие от устаревшего requests, он поддерживает HTTP/2, асинхронность и удобную настройку прокси.
  • beautifulsoup4 превращает HTML в дерево, по которому легко искать элементы по тегам и CSS-селекторам.

Совет: Сразу запомните полный путь к интерпретатору внутри виртуального окружения. На Windows это C:/mcp-collector/.venv/Scripts/python.exe, на macOS и Linux — /Users/имя/mcp-collector/.venv/bin/python (или /home/имя/... на Linux). Он понадобится при подключении к клиенту. Узнать точный путь можно командой where python на Windows или which python на macOS и Linux при активированном окружении.

✅ Проверка: Выполните команду pip list. В списке должны присутствовать пакеты mcp, httpx и beautifulsoup4. Также выполните python -c "import mcp, httpx, bs4; print('ok')" — в ответ должно появиться слово ok без ошибок.

Возможные проблемы

  • Команда python не найдена. На Windows переустановите Python с галочкой Add to PATH. На macOS используйте python3 вместо python.
  • pip ругается на права доступа. Скорее всего, окружение не активировано и вы устанавливаете пакеты в системный Python. Проверьте пометку (.venv) в начале строки.
  • Ошибка сборки при установке. Обновите pip и попробуйте снова. Если не помогает, проверьте, что версия Python не ниже 3.11.

Шаг 2: Пишем минимальный MCP-сервер с первым инструментом

Цель этапа: написать работающий MCP-сервер с одним инструментом, который скачивает страницу по адресу и возвращает её HTML. Это фундамент, на который мы будем наращивать функции.

Код сервера

Откройте файл server.py и вставьте в него следующий код целиком:

import sys
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP('web-collector')

HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
}

def log(message: str) -> None:
print(message, file=sys.stderr)

@mcp.tool()
def fetch_page(url: str, max_chars: int = 20000) -> str:
'''Скачивает страницу по указанному URL и возвращает её HTML-код.
Используй, когда нужно посмотреть исходную разметку страницы.
Параметр max_chars ограничивает длину ответа, чтобы не переполнять контекст.'''
log(f'fetch_page: {url}')
with httpx.Client(headers=HEADERS, timeout=20.0, follow_redirects=True) as client:
response = client.get(url)
response.raise_for_status()
return response.text[:max_chars]

if __name__ == '__main__':
mcp.run()

Разбор кода строка за строкой

  1. FastMCP('web-collector') создаёт объект сервера с именем web-collector. Это имя клиент покажет в списке подключённых серверов.
  2. HEADERS — заголовки, которые мы отправляем сайтам. Многие сайты отдают неполный контент или ошибку, если запрос приходит без привычного браузерного User-Agent. Заголовок Accept-Language подсказывает, что нам нужен русскоязычный вариант страницы.
  3. Функция log пишет сообщения в stderr. Именно так, а не через обычный print, потому что stdout занят протоколом. Эти сообщения вы увидите в логах клиента и в MCP Inspector.
  4. @mcp.tool() — декоратор, который превращает обычную функцию в инструмент MCP. SDK автоматически читает имя функции, типы параметров и докстринг и формирует описание для модели. Значение по умолчанию max_chars = 20000 означает, что параметр необязательный.
  5. Докстринг в тройных кавычках — это то, что прочитает ИИ. Здесь мы объясняем, что делает инструмент и когда его применять. Пишите такие описания подробно и на том языке, на котором общаетесь с агентом.
  6. httpx.Client с параметром follow_redirects=True автоматически проходит по переадресациям, а timeout=20.0 не даёт запросу висеть бесконечно.
  7. raise_for_status() выбрасывает ошибку, если сайт вернул код 4xx или 5xx. SDK перехватит её и вернёт клиенту понятное сообщение об ошибке вместо тишины.
  8. mcp.run() запускает сервер с транспортом stdio по умолчанию. Он будет ждать команд от клиента.

Первая проверка через MCP Inspector

Запустить server.py напрямую бесполезно: он будет ждать сообщений от клиента и ничего не покажет. Для проверки используем MCP Inspector — веб-интерфейс, который имитирует клиента и позволяет вызывать инструменты руками.

  1. Убедитесь, что виртуальное окружение активировано и вы находитесь в папке проекта.
  2. Выполните команду mcp dev server.py. Эта команда входит в состав установленного пакета mcp с расширением cli. При первом запуске она скачает Inspector через npx, это займёт около минуты.
  3. В терминале появится адрес вида http://localhost:6274 и, в новых версиях, токен доступа. Откройте адрес в браузере (часто он открывается сам).
  4. В левой панели Inspector проверьте, что выбран транспорт STDIO, команда — python, аргументы — server.py. Нажмите кнопку Connect.
  5. Индикатор состояния станет зелёным с подписью Connected. Перейдите на вкладку Tools в верхнем меню и нажмите List Tools.
  6. В списке появится инструмент fetch_page с описанием из докстринга и двумя параметрами. Кликните по нему.
  7. В поле url введите https://example.com, поле max_chars оставьте пустым или введите 5000. Нажмите Run Tool.
  8. Справа появится результат: HTML-код страницы, начинающийся с тега doctype. Внизу, во вкладке с логами сервера, вы увидите строку fetch_page: https://example.com.

✅ Проверка: Inspector показывает статус Connected, в списке Tools есть fetch_page, вызов с адресом example.com возвращает HTML без ошибок. Если всё так, ваш первый MCP-сервер работает.

Возможные проблемы

  • mcp dev пишет, что npx не найден. Не установлен Node.js. Установите его и перезапустите терминал.
  • Inspector открылся, но Connect выдаёт ошибку. Проверьте, что в поле команды указан python из активированного окружения. Можно вписать полный путь к python.exe внутри .venv.
  • Ошибка SyntaxError при подключении. Код скопирован с потерей отступов. В Python отступы обязательны: тело функций сдвигается на четыре пробела. Проверьте файл в редакторе.
  • Инструмент возвращает ошибку 403. Сайт не принял запрос. Для example.com такого не будет, а для реальных сайтов вернёмся к этому в шаге про прокси.

Шаг 3: Подключаем MCP-сервер к ИИ-клиенту

Цель этапа: зарегистрировать сервер в настройках ИИ-клиента, чтобы агент видел ваш инструмент и мог вызывать его из обычного чата. Разберём подключение к Claude Desktop как к самому распространённому варианту и коротко покажем альтернативы.

Подключение к Claude Desktop

  1. Откройте Claude Desktop. Зайдите в настройки: на Windows через меню в левом верхнем углу, пункт Settings; на macOS через меню Claude, пункт Settings.
  2. Перейдите на вкладку Developer и нажмите кнопку Edit Config. Откроется папка с файлом claude_desktop_config.json. Если файла нет, клиент создаст его.
  3. Сделайте резервную копию этого файла, скопировав его на рабочий стол.
  4. Откройте файл в VS Code или другом редакторе. Если файл пустой, вставьте содержимое целиком. Если в нём уже есть другие серверы, добавьте свой блок внутрь объекта mcpServers через запятую.
{
"mcpServers": {
"web-collector": {
"command": "C:/mcp-collector/.venv/Scripts/python.exe",
"args": ["C:/mcp-collector/server.py"]
}
}
}

На macOS и Linux замените пути на свои, например /Users/ivan/mcp-collector/.venv/bin/python и /Users/ivan/mcp-collector/server.py. Обратите внимание: даже на Windows пути записаны через прямые слеши. Так проще, потому что обратные слеши в JSON нужно удваивать, а прямые Windows понимает без проблем.

  1. Сохраните файл. Убедитесь, что в нём нет лишних запятых после последнего элемента и все скобки закрыты. Одна лишняя запятая делает JSON невалидным, и клиент молча проигнорирует конфиг.
  2. Полностью закройте Claude Desktop и запустите снова. На Windows недостаточно закрыть окно: кликните правой кнопкой по значку в системном трее и выберите Quit. Клиент читает конфиг только при старте.
  3. После запуска откройте новый чат. Под полем ввода найдите иконку инструментов (значок ползунков или разъёма). Кликните по ней: в списке должен быть сервер web-collector с одним инструментом fetch_page.
  4. Напишите в чат: «Скачай страницу https://example.com с помощью fetch_page и скажи, какой заголовок у этой страницы». Клиент запросит разрешение на вызов инструмента. Нажмите Allow или Allow for this chat.
  5. Через пару секунд агент ответит, что заголовок страницы — Example Domain. Он сделал реальный запрос через ваш сервер.

Подключение к Cursor и VS Code

В Cursor откройте Settings, раздел MCP, нажмите Add new global MCP server. Откроется файл mcp.json с точно такой же структурой, как у Claude Desktop. Вставьте тот же блок и сохраните. В VS Code с Copilot создайте в корне рабочей папки файл .vscode/mcp.json, где вместо ключа mcpServers используется ключ servers, а внутри — те же command и args. После сохранения над блоком сервера появится кнопка Start. Во всех клиентах принцип одинаков: указать команду запуска интерпретатора и путь к скрипту.

Совет: Указывайте в command именно python из виртуального окружения, а не просто слово python. Клиент запускает процесс со своим набором переменных окружения, и системная команда python может оказаться другой версией без установленных библиотек. Полный путь исключает эту проблему раз и навсегда.

✅ Проверка: В интерфейсе клиента виден сервер web-collector, агент по запросу вызывает fetch_page и корректно пересказывает содержимое страницы example.com. В логах клиента (в Claude Desktop это папка logs рядом с конфигом, файл mcp-server-web-collector.log) видна строка fetch_page: https://example.com.

Возможные проблемы

  • Сервер не появился в списке. Проверьте JSON на валидность: вставьте содержимое в любой онлайн-валидатор JSON или откройте в VS Code, он подчеркнёт ошибки. Убедитесь, что клиент перезапущен полностью.
  • Рядом с сервером красный индикатор ошибки. Откройте лог-файл. Чаще всего там ModuleNotFoundError: указан не тот python. Проверьте путь в command.
  • Агент говорит, что не может получить доступ к интернету. Он не увидел инструмент. Убедитесь, что инструменты включены в панели ползунков, и попросите явно: «используй инструмент fetch_page».
  • Ошибка spawn ENOENT. Путь к python или к server.py указан с ошибкой. Скопируйте путь из проводника и замените обратные слеши прямыми.

Шаг 4: Добавляем инструменты извлечения данных

Цель этапа: научить сервер отдавать не сырой HTML, а полезные данные: чистый текст, список ссылок и элементы по CSS-селектору. После этого агент сможет собирать структурированную информацию, не тратя контекст на разметку.

Почему одного fetch_page мало

HTML реальной страницы весит сотни килобайт, а большая часть — скрипты, стили и служебная разметка. Если каждый раз отдавать модели всё подряд, она быстро упрётся в лимит контекста, а вы заплатите за лишние токены. Правильная стратегия: сервер делает грубую очистку и структурирование, а модель работает уже с компактными данными. Поэтому мы добавим три специализированных инструмента.

Обновлённый код

Замените содержимое server.py на расширенную версию. Функция fetch_page осталась, но общая логика скачивания вынесена в отдельную функцию _get_html, которую используют все инструменты.

import sys
from urllib.parse import urljoin
import httpx
from bs4 import BeautifulSoup
from mcp.server.fastmcp import FastMCP

mcp = FastMCP('web-collector')

HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
}

def log(message: str) -> None:
print(message, file=sys.stderr)

def _get_html(url: str) -> str:
log(f'GET {url}')
with httpx.Client(headers=HEADERS, timeout=20.0, follow_redirects=True) as client:
response = client.get(url)
response.raise_for_status()
return response.text

def _clean(text: str) -> str:
return ' '.join(text.split())

@mcp.tool()
def fetch_page(url: str, max_chars: int = 20000) -> str:
'''Возвращает сырой HTML страницы. Используй только когда нужна именно разметка,
например чтобы подобрать CSS-селектор. Для чтения содержимого используй extract_text.'''
return _get_html(url)[:max_chars]

@mcp.tool()
def extract_text(url: str, max_chars: int = 15000) -> str:
'''Возвращает чистый текст страницы без скриптов, стилей и разметки.
Лучший выбор, когда нужно прочитать статью, описание товара или отзывы.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
for tag in soup(['script', 'style', 'noscript', 'svg', 'header', 'footer', 'nav']):
tag.decompose()
title = _clean(soup.title.get_text()) if soup.title else ''
body = _clean(soup.get_text(' '))
return f'Заголовок: {title}. Текст: {body}'[:max_chars]

@mcp.tool()
def extract_links(url: str, limit: int = 100, contains: str = '') -> list[dict]:
'''Возвращает список ссылок со страницы: текст ссылки и полный адр��с.
Параметр contains фильтрует ссылки, в адресе которых есть указанная подстрока,
например /product/ или /catalog/.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
result = []
seen = set()
for a in soup.find_all('a', href=True):
full = urljoin(url, a['href'])
if full in seen or (contains and contains not in full):
continue
seen.add(full)
result.append({'text': _clean(a.get_text())[:120], 'url': full})
if len(result) >= limit:
break
return result

@mcp.tool()
def select_elements(url: str, css_selector: str, limit: int = 50) -> list[str]:
'''Находит на странице элементы по CSS-селектору и возвращает их текст.
Примеры селекторов: h2, .price, div.product-card, table tr.
Используй, когда нужны конкретные повторяющиеся блоки: цены, названия, строки таблицы.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
elements = soup.select(css_selector)[:limit]
return [_clean(el.get_text(' ')) for el in elements]

if __name__ == '__main__':
mcp.run()

Что делает каждый инструмент

  1. extract_text удаляет из документа скрипты, стили, шапку, подвал и меню, а оставшийся текст склеивает в одну строку с одиночными пробелами. Функция _clean через split и join убирает лишние переносы и табуляции. В начало ответа добавляется заголовок страницы, чтобы агент сразу понимал, что перед ним.
  2. extract_links собирает все теги a, превращает относительные адреса в абсолютные с помощью urljoin, убирает дубли через множество seen и позволяет отфильтровать ссылки по подстроке. Так агент за один вызов получает, например, все карточки товаров из каталога.
  3. select_elements — самый мощный инструмент. Он принимает CSS-селектор и возвращает текст найденных элементов. Агент может сначала посмотреть кусок HTML через fetch_page, понять, что цены лежат в классе price, а затем вызвать select_elements с селектором .price.

Обратите внимание на докстринги: мы явно подсказываем модели, какой инструмент выбрать в какой ситуации. Это заметно улучшает качество работы агента.

Как проверить

  1. Запустите mcp dev server.py и подключитесь в Inspector. В списке Tools теперь четыре инструмента.
  2. Вызовите extract_links с url любого новостного сайта или каталога и параметром contains равным части адреса раздела. Результат — список объектов с полями text и url.
  3. Вызовите select_elements с тем же адресом и селектором h2. Вы получите список заголовков.
  4. Перезапустите Claude Desktop (конфиг менять не нужно, изменился только код) и попросите: «Собери с главной страницы такого-то сайта все заголовки h2 и ссылки, которые ведут в раздел новостей, и оформи таблицей».

Совет: Если не знаете, какой селектор нужен, откройте страницу в браузере, нажмите F12, выберите инструмент выделения элемента (иконка со стрелкой в левом верхнем углу панели) и кликните по нужному блоку. В коде вы увидите его класс. Селектор с точкой и именем класса, например .product-title, обычно работает. Более того, можно просто попросить агента: «загрузи HTML и сам подбери селектор для цен».

✅ Проверка: Все четыре инструмента видны в Inspector и клиенте, extract_text возвращает читаемый текст без тегов, extract_links возвращает список с абсолютными адресами, select_elements по селектору h2 возвращает заголовки.

Возможные проблемы

  • select_elements возвращает пустой список. Либо селектор неверный, либо контент подгружается JavaScript уже после загрузки страницы. Проверьте через fetch_page: если в HTML нет нужных данных, сайт рендерит их на клиенте. Для таких сайтов нужен браузерный движок, это уже тема отдельной статьи.
  • extract_text выдаёт кракозябры. Сайт отдаёт нестандартную кодировку. Добавьте после response.raise_for_status() строку response.encoding = response.charset_encoding or 'utf-8'.
  • Ответ обрезается. Увеличьте max_chars в вызове или попросите агента запрашивать страницу частями через несколько селекторов.

Шаг 5: Подключаем мобильные прокси и ротацию IP

Цель этапа: направить все запросы MCP-сервера через мобильный прокси, добавить инструмент смены IP и проверки текущего адреса. После этого агент будет работать от лица мобильного оператора, а не с вашего домашнего или офисного IP.

Зачем сборщику данных мобильный прокси

Когда вы собираете данные с одного IP-адреса, сайты видят десятки одинаковых запросов подряд и начинают отдавать капчу, урезанный контент или ошибку 429 «слишком много запросов». Мобильный прокси решает несколько задач одновременно. Во-первых, адрес принадлежит реальному мобильному оператору, а такие адреса делят между собой тысячи абонентов, поэтому сайты относятся к ним лояльнее. Во-вторых, вы можете менять IP по ссылке или по таймеру, распределяя нагрузку. В-третьих, вы отделяете рабочую активность агента от своих личных сессий. Для маркетолога это ещё и способ увидеть сайт так, как его видит мобильный пользователь конкретного региона.

⚠️ Внимание: Прокси — инструмент для стабильной и корректной работы сборщика, а не для нарушения правил. Собирайте только публично доступные данные, соблюдайте условия использования сайтов и файл robots.txt, не создавайте избыточную нагрузку и не собирайте персональные данные без законных оснований. Ответственность за использование инструмента лежит на вас.

Пошаговая инструкция

  1. Откройте личный кабинет вашего провайдера мобильных прокси и найдите данные подключения: хост, порт, логин, пароль. Обычно они собраны в одну строку вида login:password@host:port. Там же скопируйте ссылку смены IP, если она есть.
  2. В файле server.py добавьте в начало, после остальных импортов, строку import os. Затем ниже блока HEADERS добавьте настройки:
PROXY_URL = os.environ.get('MOBILE_PROXY_URL', '')
ROTATE_URL = os.environ.get('PROXY_ROTATE_URL', '')

def _client() -> httpx.Client:
kwargs = {'headers': HEADERS, 'timeout': 30.0, 'follow_redirects': True}
if PROXY_URL:
kwargs['proxy'] = PROXY_URL
return httpx.Client(**kwargs)
  1. Замените в функции _get_html строку с httpx.Client на вызов _client(). Теперь она выглядит так: with _client() as client. Все инструменты автоматически пойдут через прокси.
  2. Добавьте два новых инструмента перед строкой if __name__:
@mcp.tool()
def current_ip() -> str:
'''Показывает IP-адрес, с которого сервер сейчас выходит в интернет.
Используй, чтобы убедиться, что прокси подключён, или после смены IP.'''
with _client() as client:
return client.get('https://api.ipify.org').text.strip()

@mcp.tool()
def rotate_ip() -> str:
'''Запрашивает смену IP-адреса мобильного прокси через ссылку из личного кабинета.
Вызывай, если сайт начал отдавать ошибки 429 или капчу. После вызова подожди 5-10 секунд.'''
if not ROTATE_URL:
return 'Ссылка смены IP не настроена в переменной PROXY_ROTATE_URL'
response = httpx.get(ROTATE_URL, timeout=15.0)
log(f'rotate_ip: status {response.status_code}')
return f'Запрос смены IP отправлен, ответ прокси-сервиса: {response.status_code}'
  1. Передайте данные прокси через переменные окружения в конфиге клиента. Мы специально не вписываем логин и пароль в код, чтобы случайно не отправить их куда-нибудь вместе с файлом. Откройте claude_desktop_config.json и дополните блок сервера секцией env:
{
"mcpServers": {
"web-collector": {
"command": "C:/mcp-collector/.venv/Scripts/python.exe",
"args": ["C:/mcp-collector/server.py"],
"env": {
"MOBILE_PROXY_URL": "http://login:password@proxy-host:port",
"PROXY_ROTATE_URL": "https://ссылка-смены-ip-из-кабинета"
}
}
}
}
  1. Подставьте реальные значения вместо login, password, proxy-host и port. Если провайдер выдаёт прокси по протоколу SOCKS5, замените http:// на socks5:// и установите дополнительный пакет командой pip install httpx[socks].
  2. Сохраните конфиг, полностью перезапустите клиент.
  3. Попросите агента: «Вызови current_ip и скажи, какой у нас адрес. Затем вызови rotate_ip, подожди десять секунд и снова проверь IP». Адреса должны различаться.

Проверка через Inspector с прокси

Inspector тоже умеет передавать переменные окружения. В левой панели раскройте раздел Environment Variables, добавьте MOBILE_PROXY_URL и PROXY_ROTATE_URL с вашими значениями, подключитесь и вызовите current_ip. Ответ должен совпадать с IP, который показывает личный кабинет провайдера.

Совет: Не вызывайте rotate_ip перед каждым запросом. У большинства провайдеров смена IP занимает несколько секунд, и слишком частые запросы могут упереться в лимит на смену. Разумная стратегия: менять адрес каждые 30-100 запросов или только при получении ошибок 429 и 403. Можно зашить эту логику прямо в _get_html, что мы сделаем в следующем шаге.

✅ Проверка: Инструмент current_ip возвращает адрес прокси, а не ваш домашний. После rotate_ip и паузы адрес меняется. Инструменты extract_text и extract_links продолжают работать, а в логах видны строки GET с адресами страниц.

Возможные проблемы

  • Ошибка 407 Proxy Authentication Required. Неверный логин или пароль, либо в них есть спецсимволы. Символы вроде @ или : в пароле нужно кодировать: @ заменить на %40, : на %3A.
  • Ошибка ConnectTimeout. Неверный хост или порт, либо ваш IP не добавлен в список разрешённых в кабинете провайдера, если у тарифа есть такая привязка.
  • current_ip показывает ваш собственный адрес. Переменная окружения не дошла до сервера. Проверьте написание MOBILE_PROXY_URL в конфиге и убедитесь, что клиент перезапущен.
  • rotate_ip возвращает статус 429 или сообщение о лимите. Вы меняете IP чаще, чем разрешает тариф. Увеличьте интервал.

Шаг 6: Делаем сервер надёжным: повторы, задержки, кэш и лимиты

Цель этапа: превратить учебный пример в инструмент, который не падает от первой ошибки сети, не бомбит сайты запросами и не переполняет контекст модели. Это последний обязательный шаг перед полноценным использованием.

Что добавляем и зачем

  • Автоматические повторы. Сетевые ошибки случаются. Вместо того чтобы сразу возвращать агенту ошибку, попробуем запрос ещё два раза с паузой.
  • Автосмена IP при блокировке. Если сайт ответил 429 или 403, а ссылка ротации настроена, сервер сам сменит адрес и повторит запрос.
  • Задержка между запросами. Вежливый сборщик не отправляет десятки запросов в секунду. Пауза в одну-две секунды снижает нагрузку на сайт и риск блокировки.
  • Кэш. Агент часто запрашивает одну страницу несколько раз разными инструментами. Кэш в памяти на несколько минут избавит от повторных загрузок.
  • Лимит размера. Не будем скачивать страницы тяжелее нескольких мегабайт.

Код

Добавьте в начало файла import time, а функцию _get_html замените на эту:

CACHE: dict[str, tuple[float, str]] = {}
CACHE_TTL = 300
REQUEST_DELAY = 1.5
MAX_BYTES = 3_000_000
_last_request = 0.0

def _get_html(url: str) -> str:
global _last_request
now = time.time()
cached = CACHE.get(url)
if cached and now - cached[0] < CACHE_TTL:
log(f'cache hit: {url}')
return cached[1]
last_error = None
for attempt in range(3):
wait = REQUEST_DELAY - (time.time() - _last_request)
if wait > 0:
time.sleep(wait)
try:
with _client() as client:
_last_request = time.time()
response = client.get(url)
if response.status_code in (403, 429) and ROTATE_URL:
log(f'status {response.status_code}, rotating ip')
httpx.get(ROTATE_URL, timeout=15.0)
time.sleep(8)
continue
response.raise_for_status()
if len(response.content) > MAX_BYTES:
raise ValueError(f'Страница слишком большая: {len(response.content)} байт')
html = response.text
CACHE[url] = (time.time(), html)
return html
except httpx.HTTPError as error:
last_error = error
log(f'attempt {attempt + 1} failed: {error}')
time.sleep(2 * (attempt + 1))
raise RuntimeError(f'Не удалось загрузить {url} после 3 попыток: {last_error}')

Как это работает

  1. Словарь CACHE хранит для каждого адреса время загрузки и HTML. Если страница запрошена меньше пяти минут назад, возвращаем сохранённую копию, не делая запрос.
  2. Перед каждым запросом считаем, сколько прошло с предыдущего, и при необходимости досыпаем паузу до REQUEST_DELAY секунд.
  3. Цикл из трёх попыток. При ответе 403 или 429 с настроенной ротацией сервер меняет IP, ждёт восемь секунд и пробует снова. При сетевых ошибках ждёт две, четыре, шесть секунд между попытками.
  4. Если страница больше трёх мегабайт, считаем это ошибкой: такие документы всё равно не влезут в контекст.
  5. После трёх неудач выбрасываем понятную ошибку с адресом и причиной. Агент получит её текстом и сможет сообщить вам или попробовать другой путь.

Также рекомендуем добавить инструмент для очистки кэша, чтобы агент мог принудительно перезагрузить страницу:

@mcp.tool()
def clear_cache() -> str:
'''Очищает кэш загруженных страниц. Вызывай, если нужно получить свежую версию страницы.'''
count = len(CACHE)
CACHE.clear()
return f'Кэш очищен, удалено записей: {count}'

Совет: Значения REQUEST_DELAY и CACHE_TTL стоит вынести в переменные окружения по аналогии с прокси, чтобы менять их без правки кода. Для мониторинга цен подойдёт задержка в две-три секунды и кэш в одну минуту, для сбора статей — задержка в секунду и кэш на час.

✅ Проверка: Вызовите extract_text для одной страницы дважды подряд. Во второй раз в логах появится строка cache hit, а ответ придёт мгновенно. Укажите несуществующий домен — через несколько секунд агент получит сообщение «Не удалось загрузить... после 3 попыток», а не зависнет.

Возможные проблемы

  • NameError: ROTATE_URL не определён. Функция _get_html объявлена выше блока с настройками прокси. Переставьте настройки PROXY_URL и ROTATE_URL выше по файлу.
  • Агент жалуется на медленную работу. Это нормально: задержки и ротация IP занимают время. Если торопитесь, уменьшите REQUEST_DELAY до 0.5, но помните о риске блокировок.
  • Память растёт. Кэш хранит все страницы за сессию. Для долгих сессий добавьте очистку записей старше TTL при каждом вызове или ограничьте размер словаря.

Проверка результата: чек-лист готового MCP-сервера

Пройдите по чек-листу и отметьте каждый пункт. Если все они выполняются, ваш MCP-сервер для сбора данных готов к реальной работе.

Что должно работать

  • Команда mcp dev server.py запускается без ошибок, Inspector подключается и показывает статус Connected.
  • В списке инструментов присутствуют fetch_page, extract_text, extract_links, select_elements, current_ip, rotate_ip и clear_cache.
  • Сервер web-collector отображается в панели инструментов ИИ-клиента без индикатора ошибки.
  • Агент по запросу в свободной форме сам выбирает подходящий инструмент и вызывает его.
  • current_ip показывает адрес мобильного прокси, а после rotate_ip адрес меняется.
  • Повторный запрос той же страницы отдаётся из кэша.
  • Ошибочный адрес приводит к понятному сообщению об ошибке, а не к зависанию.

Комплексный тест

  1. Выберите публичный сайт с каталогом или лентой статей, данные с которого разрешено использовать.
  2. Попросите агента: «Открой главную страницу сайта, найди ссылки на раздел каталога, зайди в первые пять карточек, собери название и цену и оформи таблицей с колонками Название, Цена, Ссылка».
  3. Наблюдайте за цепочкой вызовов: агент должен вызвать extract_links с фильтром, затем несколько раз select_elements или extract_text, а в конце сформировать таблицу.
  4. Проверьте несколько строк вручную, открыв карточки в браузере. Данные должны совпадать.

Показатели успеха

Сбор пяти карточек занимает не больше 30-40 секунд с учётом задержек. В логах клиента нет ошибок уровня traceback. Агент не переспрашивает, каким инструментом воспользоваться, а действует сам. Если всё так, поздравляем: вы собрали собственный MCP-сервер и подключили ИИ-агента к вебу.

Типичные ошибки при создании MCP-сервера и их решения

Здесь собраны проблемы, с которыми сталкивается почти каждый на первом проходе. Формат: проблема, причина, решение.

1. Сервер подключается в Inspector, но не работает в клиенте

Причина: в конфиге клиента указан системный python без установленных библиотек или неверный путь к файлу. Решение: пропишите полный путь к python внутри .venv и полный путь к server.py, используйте прямые слеши, перезапустите клиент полностью.

2. Клиент разрывает соединение сразу после запуска

Причина: в коде остался обычный print без file=sys.stderr, и служебный поток stdout засорился. Решение: замените все print на функцию log. Проверьте также, что библиотеки не пишут в stdout: например, некоторые прогресс-бары делают это по умолчанию.

3. Агент не вызывает инструменты и отвечает из своих знаний

Причина: описания инструментов слишком короткие или расплывчатые, и модель не понимает, когда их применять. Решение: расширьте докстринги, добавьте фразы «используй, когда...» и примеры. В первых запросах явно называйте инструмент.

4. Ошибка 403 при загрузке реальных сайтов

Причина: сайт не принимает запросы без браузерных заголовков или с подозрительного IP. Решение: проверьте, что HEADERS передаются, обновите User-Agent до актуальной версии браузера, подключите мобильный прокси и убедитесь, что ротация работает.

5. Пустой результат select_elements при верном селекторе

Причина: данные подгружаются JavaScript после загрузки страницы, и в исходном HTML их нет. Решение: проверьте через fetch_page. Если данных нет, попробуйте найти внутренний API сайта во вкладке Network браузера: часто карточки приходят в виде JSON по отдельному адресу, который можно запрашивать напрямую тем же extract_text.

6. Ошибка 407 или ConnectTimeout при работе через прокси

Причина: неверные учётные данные, незакодированные спецсимволы в пароле или неверный порт. Решение: скопируйте строку подключения из кабинета заново, закодируйте спецсимволы, проверьте протокол http или socks5.

7. JSON-конфиг не применяется

Причина: лишняя запятая, отсутствующая кавычка или обратные слеши в путях. Решение: проверьте файл в валидаторе, замените обратные слеши прямыми, убедитесь, что после последнего элемента нет запятой.

8. Сервер работает, но данные приходят в неправильной кодировке

Причина: сайт не указывает кодировку в заголовках. Решение: задайте response.encoding явно или используйте атрибут response.content с ручным декодированием через decode('utf-8', errors='ignore').

Дополнительные возможности: блок для продвинутых

Базовый сервер готов. Если вы уверенно пишете на Python и хотите большего, вот направления развития, каждое из которых можно реализовать за вечер.

Удалённый сервер через Streamable HTTP

Чтобы сервер работал на отдельной машине или к нему подключались несколько клиентов, замените последнюю строку на mcp.run(transport='streamable-http'). По умолчанию сервер поднимется на порту 8000, а адрес подключения будет http://адрес-машины:8000/mcp. В конфиге клиента вместо command и args укажите ключ url с этим адресом. При таком режиме можно писать в stdout, но лучше сохранить привычку логировать в stderr. Обязательно закройте порт от внешнего мира и добавьте проверку токена в заголовке, если сервер доступен не только из локальной сети.

Ресурсы и промпты

Ресурс с текущими настройками поможет агенту понимать контекст работы:

@mcp.resource('collector://settings')
def settings() -> str:
'''Текущие настройки сборщика.'''
return f'proxy: {"on" if PROXY_URL else "off"}, delay: {REQUEST_DELAY}, cache ttl: {CACHE_TTL}'

Промпт задаёт готовый сценарий, который пользователь вызывает одной командой:

@mcp.prompt()
def price_monitor(url: str) -> str:
'''Сценарий мониторинга цен в каталоге.'''
return f'Открой {url}, собери ссылки на карточки товаров, зайди в каждую, вытащи название и цену и составь таблицу. Если увидишь ошибку 429, вызови rotate_ip и продолжи.'

Сохранение результатов в файл

Добавьте инструмент save_csv, который принимает список словарей и путь к файлу и записывает данные через модуль csv. Агент сможет не только собирать, но и складывать результаты в таблицу, которую вы откроете в Excel. Ограничьте путь сохранения одной папкой, чтобы агент не мог писать куда угодно на диске.

Асинхронность и параллельный сбор

FastMCP поддерживает асинхронные функции: объявите инструмент через async def и используйте httpx.AsyncClient. Тогда инструмент fetch_many сможет загружать десять страниц одновременно через asyncio.gather. Не забудьте про семафор, ограничивающий число параллельных запросов, и про то, что задержка между запросами при параллельности должна считаться иначе.

Несколько прокси и умная ротация

Если у вас несколько мобильных прокси под разные регионы, храните их в переменной окружения списком через запятую и добавьте инструменту параметр region. Сервер будет выбирать прокси по региону, а агент сможет сравнивать цены, которые сайт показывает пользователям из разных городов. Это одна из самых востребованных задач у маркетологов и арбитражников.

Упаковка в Docker

Для запуска на сервере соберите образ на базе python:3.12-slim, скопируйте server.py и файл зависимостей, установите пакеты и укажите точку входа с HTTP-транспортом. Переменные прокси передавайте при запуске контейнера, а не зашивайте в образ.

⚠️ Внимание: Никогда не публикуйте код с логинами, паролями и ссылками ротации в открытых репозиториях. Держите их только в переменных окружения или в файле .env, добавленном в .gitignore. Утечка ссылки смены IP позволит посторонним управлять вашим прокси.

FAQ: частые вопросы по созданию MCP-сервера

Можно ли написать MCP-сервер не на Python?

Да. Официальные SDK есть для TypeScript, Java, Kotlin, C# и других языков. Принципы одинаковы: объявить инструменты с описаниями и запустить транспорт. Python выбран в гайде за простоту и богатый набор библиотек для работы с HTML.

Нужен ли платный тариф ИИ-клиента для работы с MCP?

Claude Desktop поддерживает локальные MCP-серверы и на бесплатном плане, но с лимитами на количество сообщений. Cursor и VS Code также позволяют подключать серверы. Проверяйте актуальные условия у конкретного клиента.

Обязательно ли использовать прокси?

Нет, сервер работает и напрямую. Прокси нужен, когда объём запросов заметный, сайты чувствительны к частоте обращений или вам важно видеть контент из конкретного региона и с мобильного IP.

Как понять, что запросы действительно идут через прокси?

Вызовите инструмент current_ip и сравните адрес с тем, что показывает личный кабинет провайдера. Дополнительно можно попросить агента загрузить страницу сервиса определения IP через extract_text.

Сколько инструментов можно добавить на один сервер?

Технически ограничений почти нет, но каждое описание занимает место в контексте модели. Практика показывает, что 5-15 хорошо описанных инструментов работают лучше, чем 50 мелких. Группируйте близкие функции параметрами.

Как обновлять сервер без перезапуска клиента?

При stdio-транспорте клиент запускает процесс при старте, поэтому изменения кода подхватятся только после перезапуска клиента. В режиме разработки удобнее проверять правки через Inspector, а клиент перезапускать по завершении.

Что делать, если сайт отдаёт данные только после выполнения JavaScript?

Наш сервер работает с исходным HTML и такие данные не увидит. Варианты: найти внутренний API сайта во вкладке Network браузера или подключить браузерный движок. Второй путь описан в отдельных материалах блога, здесь мы его сознательно не касаемся.

Как ограничить агента, чтобы он не ходил на нежелательные сайты?

Добавьте в _get_html проверку домена по белому или чёрному списку из переменной окружения и возвращайте понятную ошибку для запрещённых адресов. Это надёжнее, чем полагаться на инструкции в чате.

Можно ли использовать один MCP-сервер из нескольких клиентов одновременно?

При stdio каждый клиент запускает свою копию процесса, и это нормально: они не мешают друг другу, но и кэш у них раздельный. Для общего кэша и единого прокси переходите на HTTP-транспорт из продвинутого блока.

Заключение: что вы сделали и куда двигаться дальше

Давайте подведём итог. Вы подготовили окружение Python и установили официальный SDK протокола. Написали MCP-сервер с нуля и разобрались, как модель понимает инструменты через их описания. Подключили сервер к ИИ-клиенту и увидели, как агент сам загружает страницы. Добавили инструменты извлечения текста, ссылок и элементов по селекторам. Направили трафик через мобильный прокси с ротацией IP. Наконец, сделали сервер устойчивым: повторы, задержки, кэш и лимиты. Это уже не учебный пример, а рабочий инструмент для ежедневных задач.

Что делать дальше? Начните использовать сервер в реальных сценариях: мониторинг цен конкурентов, сбор отзывов, проверка лендингов, анализ контента в нише. По ходу вы поймёте, каких инструментов не хватает именно вам, и добавите их по образцу существующих. Каждый новый инструмент — это функция с понятным описанием, ничего сложнее.

Следующий уровень — продвинутый блок: удалённый сервер по HTTP, параллельный сбор, работа с несколькими прокси по регионам и сохранение результатов в таблицы. А когда упрётесь в сайты с динамическим контентом, загляните в смежные статьи блога про браузерную автоматизацию. Главное вы уже сделали: ваш ИИ-агент вышел в веб через собственный MCP-сервер, и вы полностью контролируете, как он это делает.