- BrainTools - https://www.braintools.ru -

А что, если MCP вам вообще не нужен?

Команда AI for Devs [1] подготовила перевод статьи о том, почему увлечение MCP-серверами может быть избыточным. Автор показывает на практике: во многих сценариях агенты справляются куда лучше, когда работают напрямую через Bash и небольшие скрипты, без громоздких серверов, длинных описаний и лишнего контекстного шума.


После нескольких месяцев безумия вокруг «агентного» кодинга Twitter всё ещё полыхает обсуждениями MCP-серверов. Раньше я делал небольшой бенчмарк [2], чтобы понять, подходят ли для одной конкретной задачи инструменты на Bash или MCP-серверы. Коротко: оба варианта могут быть эффективными, если подойти к делу с умом.

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

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

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

Я парень простой, а люблю простые вещи. Агенты умеют запускать Bash и хорошо писать код. Bash и код легко комбинируются. Так что может быть проще, чем попросить агента просто вызывать CLI-утилиты и писать код? В этом нет ничего нового. Мы так делали с самого начала. Я лишь хочу убедить вас, что во многих ситуациях MCP-сервер вам не нужен — и даже не желателен.

Позвольте показать это на типичном сценарии использования MCP-сервера: инструментах разработчика в браузере.

Мои сценарии использования браузерных DevTools

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

  • Запустить браузер — при необходимости с моим стандартным профилем, чтобы я был уже авторизован

  • Перейти по URL — либо в активной вкладке, либо в новой

  • Выполнить JavaScript в контексте активной страницы

  • Сделать скриншот видимой области

И если под конкретный сценарий понадобятся какие-то дополнительные средства, я хочу, чтобы агент быстро сгенерировал нужный инструмент и встроил его рядом с остальными.

Проблемы распространённых браузерных DevTools для агента

Для моих задач обычно советуют Playwright MCP [3] или Chrome DevTools MCP [4]. Оба варианта неплохие, но им приходится закрывать все возможные кейсы. Playwright MCP содержит 21 инструмент и занимает 13,7 тыс. токенов (6,8% контекста Claude). Chrome DevTools MCP — 26 инструментов и 18,0 тыс. токенов (9,0%). Такой объём инструментов только запутает вашего агента, особенно если дополнить их другими MCP-серверами и встроенными средствами.

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

Принятие Bash (и кода)

Вот мой минимальный набор инструментов, как показано в README.md:

# Browser Tools

Minimal CDP tools for collaborative site exploration.

## Start Chrome

```bash
./start.js              # Fresh profile
./start.js --profile    # Copy your profile (cookies, logins)
```

Start Chrome on `:9222` with remote debugging.

## Navigate

```bash
./nav.js https://example.com
./nav.js https://example.com --new
```

Navigate current tab or open new tab.

## Evaluate JavaScript

```bash
./eval.js 'document.title'
./eval.js 'document.querySelectorAll("a").length'
```

Execute JavaScript in active tab (async context).

## Screenshot

```bash
./screenshot.js
```

Screenshot current viewport, returns temp file path.

Это всё, что я даю агенту. Набор маленький, но полностью закрывает мои сценарии. Каждый инструмент — это простой скрипт на Node.js, использующий Puppeteer Core [5]. Прочитав этот README целиком, агент понимает, какие инструменты доступны, когда их применять и как вызывать их через Bash.

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

Инструмент Start

Агенту нужно уметь запускать новый сеанс браузера. В задачах по скрапингу я часто хочу использовать свой реальный профиль Chrome, чтобы везде быть залогинен. Этот скрипт либо копирует мой профиль Chrome в временную папку через rsync (Chrome не позволяет запускать отладку на основном профиле), либо стартует с нуля:

#!/usr/bin/env node

import { spawn, execSync } from "node:child_process";
import puppeteer from "puppeteer-core";

const useProfile = process.argv[2] === "--profile";

if (process.argv[2] && process.argv[2] !== "--profile") {
    console.log("Usage: start.ts [--profile]");
    console.log("nOptions:");
    console.log("  --profile  Copy your default Chrome profile (cookies, logins)");
    console.log("nExamples:");
    console.log("  start.ts            # Start with fresh profile");
    console.log("  start.ts --profile  # Start with your Chrome profile");
    process.exit(1);
}

// Kill existing Chrome
try {
    execSync("killall 'Google Chrome'", { stdio: "ignore" });
} catch {}

// Wait a bit for processes to fully die
await new Promise((r) => setTimeout(r, 1000));

// Setup profile directory
execSync("mkdir -p ~/.cache/scraping", { stdio: "ignore" });

if (useProfile) {
    // Sync profile with rsync (much faster on subsequent runs)
    execSync(
        'rsync -a --delete "/Users/badlogic/Library/Application Support/Google/Chrome/" ~/.cache/scraping/',
        { stdio: "pipe" },
    );
}

// Start Chrome in background (detached so Node can exit)
spawn(
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
    ["--remote-debugging-port=9222", `--user-data-dir=${process.env["HOME"]}/.cache/scraping`],
    { detached: true, stdio: "ignore" },
).unref();

// Wait for Chrome to be ready by attempting to connect
let connected = false;
for (let i = 0; i < 30; i++) {
    try {
        const browser = await puppeteer.connect({
            browserURL: "http://localhost:9222",
            defaultViewport: null,
        });
        await browser.disconnect();
        connected = true;
        break;
    } catch {
        await new Promise((r) => setTimeout(r, 500));
    }
}

if (!connected) {
    console.error("✗ Failed to connect to Chrome");
    process.exit(1);
}

console.log(`✓ Chrome started on :9222${useProfile ? " with your profile" : ""}`);

Агенту нужно знать лишь одно: чтобы запустить браузер, он должен вызвать скрипт start.js через Bash — либо с параметром --profile, либо без него.

Инструмент Navigate

Когда браузер уже запущен, агенту нужно уметь переходить по URL — либо в новой вкладке, либо в активной. Ровно это и делает инструмент navigate:

#!/usr/bin/env node

import puppeteer from "puppeteer-core";

const url = process.argv[2];
const newTab = process.argv[3] === "--new";

if (!url) {
    console.log("Usage: nav.js <url> [--new]");
    console.log("nExamples:");
    console.log("  nav.js https://example.com       # Navigate current tab");
    console.log("  nav.js https://example.com --new # Open in new tab");
    process.exit(1);
}

const b = await puppeteer.connect({
    browserURL: "http://localhost:9222",
    defaultViewport: null,
});

if (newTab) {
    const p = await b.newPage();
    await p.goto(url, { waitUntil: "domcontentloaded" });
    console.log("✓ Opened:", url);
} else {
    const p = (await b.pages()).at(-1);
    await p.goto(url, { waitUntil: "domcontentloaded" });
    console.log("✓ Navigated to:", url);
}

await b.disconnect();

Инструмент Evaluate JavaScript

Агенту нужно выполнять JavaScript, чтобы читать и изменять DOM активной вкладки. Код, который он пишет, выполняется прямо в контексте страницы, поэтому ему не приходится возиться с самим Puppeteer. Всё, что ему нужно, — это писать код, используя DOM API, а с этим он прекрасно справляется:

#!/usr/bin/env node

import puppeteer from "puppeteer-core";

const code = process.argv.slice(2).join(" ");
if (!code) {
    console.log("Usage: eval.js 'code'");
    console.log("nExamples:");
    console.log('  eval.js "document.title"');
    console.log('  eval.js "document.querySelectorAll('a').length"');
    process.exit(1);
}

const b = await puppeteer.connect({
    browserURL: "http://localhost:9222",
    defaultViewport: null,
});

const p = (await b.pages()).at(-1);

if (!p) {
    console.error("✗ No active tab found");
    process.exit(1);
}

const result = await p.evaluate((c) => {
    const AsyncFunction = (async () => {}).constructor;
    return new AsyncFunction(`return (${c})`)();
}, code);

if (Array.isArray(result)) {
    for (let i = 0; i < result.length; i++) {
        if (i > 0) console.log("");
        for (const [key, value] of Object.entries(result[i])) {
            console.log(`${key}: ${value}`);
        }
    }
} else if (typeof result === "object" && result !== null) {
    for (const [key, value] of Object.entries(result)) {
        console.log(`${key}: ${value}`);
    }
} else {
    console.log(result);
}

await b.disconnect();

Инструмент Screenshot

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

#!/usr/bin/env node

import { tmpdir } from "node:os";
import { join } from "node:path";
import puppeteer from "puppeteer-core";

const b = await puppeteer.connect({
    browserURL: "http://localhost:9222",
    defaultViewport: null,
});

const p = (await b.pages()).at(-1);

if (!p) {
    console.error("✗ No active tab found");
    process.exit(1);
}

const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `screenshot-${timestamp}.png`;
const filepath = join(tmpdir(), filename);

await p.screenshot({ path: filepath });

console.log(filepath);

await b.disconnect();

Этот скрипт делает снимок видимой области активной вкладки, сохраняет его в .png во временной директории и выводит путь к файлу агенту, который затем может прочитать изображение и «увидеть» его с помощью своих vision-возможностей.

Преимущества

Итак, как всё это выглядит по сравнению с MCP-серверами, которые я упоминал выше? Во-первых, я могу подгружать README только тогда, когда он действительно нужен, и не плачу за него в каждой сессии. Это очень похоже на недавно появившиеся skills от Anthropic. Только мой подход ещё более свободный и работает с любым кодовым агентом. Всё, что требуется — попросить агента прочитать README.

Небольшое отступление: многие, включая меня, использовали похожий подход ещё до появления системы skills от Anthropic. Что-то подобное можно увидеть в моём посте «Prompts are Code [6]» или в моём маленьком проекте sitegeist.ai [7]. Армин [8]тоже ранее затрагивал тему того, насколько мощнее Bash и код по сравнению с MCP. Skills от Anthropic добавляют постепенное раскрытие информации (мне это очень нравится) и делают возможности доступными для нетехнической аудитории почти во всех своих продуктах (и это мне тоже нравится).

Вернёмся к README: вместо 13–18 тысяч токенов, как в MCP-серверах выше, мой README занимает всего 225 токенов. Такая эффективность появляется благодаря тому, что модели уже умеют писать код и пользоваться Bash. Я экономлю место в контексте, полагаясь на их существующие знания.

Эти простые инструменты также легко комбинируются. Вместо того чтобы возвращённый результат попадал в контекст агента, агент может просто сохранить данные в файл и обработать их позже — самостоятельно или через код. Он также может легко связать несколько вызовов в одной Bash-команде.

Если я понимаю, что вывод инструмента расходует токены неэффективно, я просто меняю формат вывода. В зависимости от MCP-сервера сделать это либо очень сложно, либо вообще невозможно.

И добавить новый инструмент или изменить существующий — смехотворно просто. Позвольте показать.

Добавление инструмента Pick

Когда мы с агентом придумываем способ скрапинга конкретного сайта, часто гораздо эффективнее, если я могу напрямую указать ему нужные DOM-элементы — просто кликами. Чтобы сделать это максимально простым, я могу собрать небольшой «пикер». Вот что я добавляю в README:

## Pick Elements

```bash
./pick.js "Click the submit button"
```

Interactive element picker. Click to select, Cmd/Ctrl+Click for multi-select, Enter to finish.

А вот код:

#!/usr/bin/env node

import puppeteer from "puppeteer-core";

const message = process.argv.slice(2).join(" ");
if (!message) {
    console.log("Usage: pick.js 'message'");
    console.log("nExample:");
    console.log('  pick.js "Click the submit button"');
    process.exit(1);
}

const b = await puppeteer.connect({
    browserURL: "http://localhost:9222",
    defaultViewport: null,
});

const p = (await b.pages()).at(-1);

if (!p) {
    console.error("✗ No active tab found");
    process.exit(1);
}

// Inject pick() helper into current page
await p.evaluate(() => {
    if (!window.pick) {
        window.pick = async (message) => {
            if (!message) {
                throw new Error("pick() requires a message parameter");
            }
            return new Promise((resolve) => {
                const selections = [];
                const selectedElements = new Set();

                const overlay = document.createElement("div");
                overlay.style.cssText =
                    "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";

                const highlight = document.createElement("div");
                highlight.style.cssText =
                    "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
                overlay.appendChild(highlight);

                const banner = document.createElement("div");
                banner.style.cssText =
                    "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";

                const updateBanner = () => {
                    banner.textContent = `${message} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
                };
                updateBanner();

                document.body.append(banner, overlay);

                const cleanup = () => {
                    document.removeEventListener("mousemove", onMove, true);
                    document.removeEventListener("click", onClick, true);
                    document.removeEventListener("keydown", onKey, true);
                    overlay.remove();
                    banner.remove();
                    selectedElements.forEach((el) => {
                        el.style.outline = "";
                    });
                };

                const onMove = (e) => {
                    const el = document.elementFromPoint(e.clientX, e.clientY);
                    if (!el || overlay.contains(el) || banner.contains(el)) return;
                    const r = el.getBoundingClientRect();
                    highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${r.top}px;left:${r.left}px;width:${r.width}px;height:${r.height}px`;
                };

                const buildElementInfo = (el) => {
                    const parents = [];
                    let current = el.parentElement;
                    while (current && current !== document.body) {
                        const parentInfo = current.tagName.toLowerCase();
                        const id = current.id ? `#${current.id}` : "";
                        const cls = current.className
                            ? `.${current.className.trim().split(/s+/).join(".")}`
                            : "";
                        parents.push(parentInfo + id + cls);
                        current = current.parentElement;
                    }

                    return {
                        tag: el.tagName.toLowerCase(),
                        id: el.id || null,
                        class: el.className || null,
                        text: el.textContent?.trim().slice(0, 200) || null,
                        html: el.outerHTML.slice(0, 500),
                        parents: parents.join(" > "),
                    };
                };

                const onClick = (e) => {
                    if (banner.contains(e.target)) return;
                    e.preventDefault();
                    e.stopPropagation();
                    const el = document.elementFromPoint(e.clientX, e.clientY);
                    if (!el || overlay.contains(el) || banner.contains(el)) return;

                    if (e.metaKey || e.ctrlKey) {
                        if (!selectedElements.has(el)) {
                            selectedElements.add(el);
                            el.style.outline = "3px solid #10b981";
                            selections.push(buildElementInfo(el));
                            updateBanner();
                        }
                    } else {
                        cleanup();
                        const info = buildElementInfo(el);
                        resolve(selections.length > 0 ? selections : info);
                    }
                };

                const onKey = (e) => {
                    if (e.key === "Escape") {
                        e.preventDefault();
                        cleanup();
                        resolve(null);
                    } else if (e.key === "Enter" && selections.length > 0) {
                        e.preventDefault();
                        cleanup();
                        resolve(selections);
                    }
                };

                document.addEventListener("mousemove", onMove, true);
                document.addEventListener("click", onClick, true);
                document.addEventListener("keydown", onKey, true);
            });
        };
    }
});

const result = await p.evaluate((msg) => window.pick(msg), message);

if (Array.isArray(result)) {
    for (let i = 0; i < result.length; i++) {
        if (i > 0) console.log("");
        for (const [key, value] of Object.entries(result[i])) {
            console.log(`${key}: ${value}`);
        }
    }
} else if (typeof result === "object" && result !== null) {
    for (const [key, value] of Object.entries(result)) {
        console.log(`${key}: ${value}`);
    }
} else {
    console.log(result);
}

await b.disconnect();

Когда мне проще кликнуть по нескольким DOM-элементам, чем объяснять агенту структуру DOM, я просто прошу его использовать инструмент pick. Это невероятно эффективно и позволяет собирать скраперы буквально за пару минут. А если на сайте изменилась DOM-разметка, этот инструмент отлично подходит для быстрой подстройки скрапера.

Если вам сложно понять, что делает этот инструмент — не переживайте. В конце поста будет видео, где видно, как он работает. Но прежде чем перейти к нему, покажу ещё один инструмент.

Добавление инструмента Cookies

Во время одного из моих недавних скрапингов мне понадобились HTTP-only cookies сайта, чтобы детерминированный скрапер мог выдавать себя за меня. Инструмент Evaluate JavaScript тут не подходит, потому что он работает в контексте страницы. Но мне понадобилась буквально минута, чтобы попросить Claude создать такой инструмент, добавить его в README — и можно было продолжать работу.

А что, если MCP вам вообще не нужен? - 1

Это несравнимо проще, чем править, тестировать и отлаживать существующий MCP-сервер.

Надуманный пример

Позвольте показать, как пользоваться этим набором утилит, на слегка надуманном примере. Я решил собрать простой скрейпер Hacker News: я просто выбираю элементы DOM для агента, а на их основе он уже способен написать минимальный скрейпер на Node.js. Вот как это выглядит в работе. Несколько фрагментов я ускорил — Клод вёл себя медленно, как обычно.

В реальных задачах скрейпинга всё будет заметно сложнее. Да и для такого простого сайта, как Hacker News, нет смысла делать всё именно так. Но общий принцип понятен.

Итоговое количество токенов:

А что, если MCP вам вообще не нужен? - 2

Как сделать это многоразовым для разных агентов

Вот как я всё организовал, чтобы пользоваться этим набором вместе с Claude Code и другими агентами. В домашнем каталоге у меня есть папка agent-tools. В неё я клонирую репозитории отдельных инструментов — например, репозиторий browser tools, о котором шла речь выше. Затем я настраиваю alias:

alias cl="PATH=$PATH:/Users/badlogic/agent-tools/browser-tools:<other-tool-dirs> && claude --dangerously-skip-permissions"

Так все скрипты становятся доступны в сессиях Клода, но при этом не захламляют мою обычную среду. Я также добавляю к каждому скрипту префикс с полным именем инструмента, например browser-tools-start.js, чтобы исключить конфликты имён. Кроме того, я дописываю в README одну короткую фразу о том, что все скрипты доступны глобально. Благодаря этому агенту не нужно менять рабочий каталог, чтобы вызвать скрипт инструмента — это экономит немного токенов и снижает вероятность того, что агент запутается из-за постоянных переключений директорий.

Наконец, я добавляю каталог с инструментами агента как рабочий для Claude Code через команду /add-dir, чтобы можно было использовать @README.md для ссылки на README конкретного инструмента и подгружать его в контекст агента. Мне это нравится больше, чем автодетект навыков от Anthropic — на практике он работает не очень надёжно. Плюс это снова экономит немного токенов: Claude Code вставляет frontmatter всех найденных навыков в системный промпт (или в первое пользовательское сообщение — уже не помню, см. https://cchistory.mariozechner.at [9]).

В заключение

Создавать такие инструменты невероятно просто. Они дают вам всю необходимую свободу и делают вас, вашего агента и расход токенов гораздо эффективнее. Найти browser tools можно на GitHub [10].

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

Но вместе с большой силой приходит и большая ответственность. Вам придётся самостоятельно продумать, как вы будете организовывать, развивать и поддерживать эти инструменты. Система навыков от Anthropic — один из вариантов, хотя он хуже переносится на других агентов. Или вы можете использовать мой подход, описанный выше.

Русскоязычное сообщество про AI в разработке

А что, если MCP вам вообще не нужен? - 3

Друзья! Эту статью подготовила команда ТГК «AI for Devs [1]» — канала, где мы рассказываем про AI-ассистентов, плагины для IDE, делимся практическими кейсами и свежими новостями из мира ИИ. Подписывайтесь [1], чтобы быть в курсе и ничего не упустить!

Автор: python_leader

Источник [11]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/22292

URLs in this post:

[1] AI for Devs: https://t.me/+PKwWx13UceRkYzZi

[2] небольшой бенчмарк: https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/

[3] Playwright MCP: https://github.com/microsoft/playwright-mcp

[4] Chrome DevTools MCP: https://github.com/ChromeDevTools/chrome-devtools-mcp

[5] Puppeteer Core: https://pptr.dev/

[6] Prompts are Code: https://mariozechner.at/posts/2025-06-02-prompts-are-code/

[7] sitegeist.ai: http://sitegeist.ai

[8] Армин : https://lucumr.pocoo.org/2025/8/18/code-mcps/

[9] https://cchistory.mariozechner.at: https://cchistory.mariozechner.at

[10] GitHub: https://github.com/badlogic/browser-tools

[11] Источник: https://habr.com/ru/articles/968110/?utm_campaign=968110&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100