- BrainTools - https://www.braintools.ru -
Команда AI for Devs [1] подготовила перевод статьи о том, почему увлечение MCP-серверами может быть избыточным. Автор показывает на практике: во многих сценариях агенты справляются куда лучше, когда работают напрямую через Bash и небольшие скрипты, без громоздких серверов, длинных описаний и лишнего контекстного шума.
После нескольких месяцев безумия вокруг «агентного» кодинга Twitter всё ещё полыхает обсуждениями MCP-серверов. Раньше я делал небольшой бенчмарк [2], чтобы понять, подходят ли для одной конкретной задачи инструменты на Bash или MCP-серверы. Коротко: оба варианта могут быть эффективными, если подойти к делу с умом.
К сожалению, многие из самых популярных MCP-серверов оказываются неэффективны для конкретной задачи. Им нужно закрывать все возможные случаи, а значит — предоставлять большое количество инструментов с длинными описаниями, которые ощутимо расходуют контекст.
Расширять существующий MCP-сервер тоже непросто. Можно, конечно, открыть его исходники и что-то поправить, но тогда придётся разбираться в кодовой базе вместе с вашим агентом.
Кроме того, MCP-серверы не сочетаются друг с другом. Результаты, которые возвращает MCP-сервер, должны проходить через контекст агента, чтобы попасть на диск или быть объединёнными с другими результатами.
Я парень простой, а люблю простые вещи. Агенты умеют запускать Bash и хорошо писать код. Bash и код легко комбинируются. Так что может быть проще, чем попросить агента просто вызывать CLI-утилиты и писать код? В этом нет ничего нового. Мы так делали с самого начала. Я лишь хочу убедить вас, что во многих ситуациях MCP-сервер вам не нужен — и даже не желателен.
Позвольте показать это на типичном сценарии использования MCP-сервера: инструментах разработчика в браузере.
Мои варианты использования сводятся к двум задачам: работать над веб-фронтендом вместе с агентом или превращать агента в маленького хитрого скрапера, чтобы вытягивать данные со всего интернета. Для этих двух задач мне нужен лишь минимальный набор инструментов:
Запустить браузер — при необходимости с моим стандартным профилем, чтобы я был уже авторизован
Перейти по URL — либо в активной вкладке, либо в новой
Выполнить JavaScript в контексте активной страницы
Сделать скриншот видимой области
И если под конкретный сценарий понадобятся какие-то дополнительные средства, я хочу, чтобы агент быстро сгенерировал нужный инструмент и встроил его рядом с остальными.
Для моих задач обычно советуют Playwright MCP [3] или Chrome DevTools MCP [4]. Оба варианта неплохие, но им приходится закрывать все возможные кейсы. Playwright MCP содержит 21 инструмент и занимает 13,7 тыс. токенов (6,8% контекста Claude). Chrome DevTools MCP — 26 инструментов и 18,0 тыс. токенов (9,0%). Такой объём инструментов только запутает вашего агента, особенно если дополнить их другими MCP-серверами и встроенными средствами.
Кроме того, использование этих инструментов означает, что вы сталкиваетесь с проблемой сочетаемости: любой вывод должен пройти через контекст агента. Это можно частично обойти с помощью саб-агентов, но тогда вы тянете за собой весь набор проблем, которые идут в комплекте с саб-агентами.
Вот мой минимальный набор инструментов, как показано в 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.
Когда я начинаю сессию, где агенту нужно взаимодействовать с браузером, я просто прошу его прочитать этот файл полностью — и этого достаточно, чтобы он эффективно работал. Давайте разберём реализации инструментов, чтобы понять, насколько мало там кода.
Агенту нужно уметь запускать новый сеанс браузера. В задачах по скрапингу я часто хочу использовать свой реальный профиль 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, либо без него.
Когда браузер уже запущен, агенту нужно уметь переходить по 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();
Агенту нужно выполнять 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();
Иногда агенту нужно получить визуальное представление страницы, поэтому естественно иметь инструмент для скриншотов:
#!/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-сервера сделать это либо очень сложно, либо вообще невозможно.
И добавить новый инструмент или изменить существующий — смехотворно просто. Позвольте показать.
Когда мы с агентом придумываем способ скрапинга конкретного сайта, часто гораздо эффективнее, если я могу напрямую указать ему нужные 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-разметка, этот инструмент отлично подходит для быстрой подстройки скрапера.
Если вам сложно понять, что делает этот инструмент — не переживайте. В конце поста будет видео, где видно, как он работает. Но прежде чем перейти к нему, покажу ещё один инструмент.
Во время одного из моих недавних скрапингов мне понадобились HTTP-only cookies сайта, чтобы детерминированный скрапер мог выдавать себя за меня. Инструмент Evaluate JavaScript тут не подходит, потому что он работает в контексте страницы. Но мне понадобилась буквально минута, чтобы попросить Claude создать такой инструмент, добавить его в README — и можно было продолжать работу.

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

Вот как я всё организовал, чтобы пользоваться этим набором вместе с 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 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
Нажмите здесь для печати.