Команда AI for Devs подготовила перевод статьи о том, почему увлечение MCP-серверами может быть избыточным. Автор показывает на практике: во многих сценариях агенты справляются куда лучше, когда работают напрямую через Bash и небольшие скрипты, без громоздких серверов, длинных описаний и лишнего контекстного шума.
После нескольких месяцев безумия вокруг «агентного» кодинга Twitter всё ещё полыхает обсуждениями MCP-серверов. Раньше я делал небольшой бенчмарк, чтобы понять, подходят ли для одной конкретной задачи инструменты на Bash или MCP-серверы. Коротко: оба варианта могут быть эффективными, если подойти к делу с умом.
К сожалению, многие из самых популярных MCP-серверов оказываются неэффективны для конкретной задачи. Им нужно закрывать все возможные случаи, а значит — предоставлять большое количество инструментов с длинными описаниями, которые ощутимо расходуют контекст.
Расширять существующий MCP-сервер тоже непросто. Можно, конечно, открыть его исходники и что-то поправить, но тогда придётся разбираться в кодовой базе вместе с вашим агентом.
Кроме того, MCP-серверы не сочетаются друг с другом. Результаты, которые возвращает MCP-сервер, должны проходить через контекст агента, чтобы попасть на диск или быть объединёнными с другими результатами.
Я парень простой, а люблю простые вещи. Агенты умеют запускать Bash и хорошо писать код. Bash и код легко комбинируются. Так что может быть проще, чем попросить агента просто вызывать CLI-утилиты и писать код? В этом нет ничего нового. Мы так делали с самого начала. Я лишь хочу убедить вас, что во многих ситуациях MCP-сервер вам не нужен — и даже не желателен.
Позвольте показать это на типичном сценарии использования MCP-сервера: инструментах разработчика в браузере.
Мои сценарии использования браузерных DevTools
Мои варианты использования сводятся к двум задачам: работать над веб-фронтендом вместе с агентом или превращать агента в маленького хитрого скрапера, чтобы вытягивать данные со всего интернета. Для этих двух задач мне нужен лишь минимальный набор инструментов:
Запустить браузер — при необходимости с моим стандартным профилем, чтобы я был уже авторизован
Перейти по URL — либо в активной вкладке, либо в новой
Выполнить JavaScript в контексте активной страницы
Сделать скриншот видимой области
И если под конкретный сценарий понадобятся какие-то дополнительные средства, я хочу, чтобы агент быстро сгенерировал нужный инструмент и встроил его рядом с остальными.
Проблемы распространённых браузерных DevTools для агента
Для моих задач обычно советуют Playwright MCP или Chrome DevTools MCP. Оба варианта неплохие, но им приходится закрывать все возможные кейсы. 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. Прочитав этот 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» или в моём маленьком проекте sitegeist.ai. Армин тоже ранее затрагивал тему того, насколько мощнее 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-сервер.
Надуманный пример
Позвольте показать, как пользоваться этим набором утилит, на слегка надуманном примере. Я решил собрать простой скрейпер 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).
В заключение
Создавать такие инструменты невероятно просто. Они дают вам всю необходимую свободу и делают вас, вашего агента и расход токенов гораздо эффективнее. Найти browser tools можно на GitHub.
Этот общий принцип применим к любым «обвязкам», в которых есть среда выполнения кода. Просто выйдите за рамки MCP — и обнаружите, что такой подход куда мощнее, чем жёсткая структура, которой приходится придерживаться в MCP.
Но вместе с большой силой приходит и большая ответственность. Вам придётся самостоятельно продумать, как вы будете организовывать, развивать и поддерживать эти инструменты. Система навыков от Anthropic — один из вариантов, хотя он хуже переносится на других агентов. Или вы можете использовать мой подход, описанный выше.
Русскоязычное сообщество про AI в разработке

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