Давайте будем честны: текущее состояние вызова функций (Function Calling) у LLM и экосистемы MCP (Model Context Protocol) вызывает изжогу в лучшем случае. Чтобы дать агенту возможность сделать curl или дернуть локальный скрипт, современному разработчику предлагают:

  1. Описать килобайты монструозных JSON-схем прямо в системном промпте (и спалить 15-30k токенов контекста еще до первого вопроса пользователя).

  2. Написать полноценный сетевой MCP-сервер со своим бойлерплейтом, SDK и зависимостями.

  3. Завернуть всё это в жирный Docker-контейнер, выделив 200 Мб ОЗУ ради выполнения элементарного скрипта на три строки.

И один bash-tool вместо десяти нормальных инструментов задачу не решает, сессии превращаются в нечто вроде (псевдокод):

bash("systemctl is-active my-app.service")
bash("journalctl -u my-app.service -n 50 --no-pager")
bash("ss -tunlp | grep 3000")
bash("kill -9 $(lsof -t -i:3000)")
bash("systemctl restart my-app.service")

Что повторяется три раза только за эту сессию, и это - совсем не DRY (Don’t Repeat Yourself). Интуитивно у инженера в этом моменте слегка подскакивает пульс и давление, и он хочет абстрагировать это в call_tool("check_journals_and_stats", {...})

А когда в проекте накапливается больше 30 таких абстракций - модель неизбежно ловит Lost in the Middle и начинает галлюцинировать схемами, а разработчик оплачивает тонны входящих токенов за документацию API, которую агент даже не собирался вызывать в текущей сессии.

Отдельное спасибо здесь стоит сказать маркетингу Anthropic - работает он отлично. Термины вроде MCP, agent, harness, tooling теперь на слуху у людей, которые до этого к тулингу LLM никакого отношения не имели, причём многие оперируют ими скорее на уровне потребления хайпа, чем реального понимания. Модели у Anthropic отличные, тут вопросов нет. Но стоит помнить, что всё вокруг их флагманского продукта (включая сам MCP как протокол) в первую очередь продвигает этот продукт, а развитие области вокруг него - уже вторичная задача.

Короче, я устал от натягивания совы на глобус именованием MCP как стандартизированного протокола для всего тулинга самой популярной области последних лет и сделал 🛠️ toolhub - self-hosted движок, который превращает любой консольный скрипт (Bun, Python, Bash, Go, PHP, etc) в инструмент для LLM на лету, с иерархической навигацией по дереву папок и околонулевыми требованиями к зависимостям.

Файловая система навыков вместо плоского списка

Почему контекст модели забивается? Потому что типичные MCP-прокси дают модели плоский массив из 100 тулов.

У меня подход иной - древовидная навигация:

/
├── system/
│   ├── fetch_logs
│   └── restart_service
├── database/
│   └── query_pg
└── sublime/
    └── replace-literal

Скажите мне, серьёзные разработчики, ML-щики, архитекторы, системные аналитики - не это ли было самым адекватным путём с самого начала? Хотя бы с момента, когда стало ясно, что без кеширования контекста направление продолжит быть убыточным.

Модель не знает схемы сотен инструментов заранее. На старте через hub.getSmartPrompt() в системный промпт инжектится только корневая карта дерева с краткими аннотациями назначений категорий (1–2 строки на папку). Модель стартует с базовым контрактом:

  1. listTools("/system") - посмотреть, что валяется в конкретной ветке, и подтянуть точные описания только нужных инструментов.

  2. callTool("/system/fetch_logs", { "service": "nginx" }) - вызвать конкретный инструмент с конкретными параметрами.

Результат: окно контекста чистое, модель лезет в папку только тогда, когда ей действительно нужен инструмент из этой категории. Когда она понимает, что к текущему запросу папка с ЭТИМ названием может иметь непосредственное отношение.

Важное уточнение - toolhub сознательно не работает ни с одним из стандартных интерфейсов tool calling каких-то конкретных провайдеров. Весь Re-Act loop построен на классических XML-like командах:

<hub>listTools("/")</hub>
<hub>callTool("/sublime/list-project-files", {})</hub>

Так с минимальным оверхедом на вызов мы даём возможность пользоваться тулами абсолютно любым моделям, даже если у них не встроена поддержка тулинга, или она сделана как-то странно/криво. По сути XML-обвязка (<hub>...</hub>) сопоставима или легче, чем JSON-разметка нативного tool_call у большинства провайдеров. Единственный таким образом порог входа для модели - понимание, что такое xml, json, js и древовидная навигация.

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

Поднимать Docker-контейнер на каждый чих - это дорого по ресурсам (особенно если вы крутите агентов на скромной VPS или Raspberry Pi). toolhub использует только Bun и его возможности. Глобальная зависимость всего одна, так что я даже не стал собирать для вас бойлерплейтные Dockerfile || docker-compose.yml - нужны они будут не всем, собрать самому - 3 минуты.

Механика исполнения под капотом простая как топор:

Workspace (/tmp) ➔ Инъекция (код + input.json) ➔ Установка deps ➔ runCmd с таймаутом ➔ rm -rf

  1. Создание изолята: На лету генерируется временная папка /tmp/hub_run_<timestamp>_<hash>.

  2. Инъекция кода и аргументов: В папку кладется код и input.json, а все параметры вызова автоматически пробрасываются в ENV-переменные вида $INPUT_<KEY_NAME>. Хочешь - парси JSON в коде, хочешь - пиши bash-однострочник, который сразу читает $INPUT_HOST. Никаких обвязок.

  3. Зависимости: Если прописан installCmd, он отрабатывает в контексте папки. Bun и современные пакетные менеджеры за счет глобального кэша хоста подтягивают модули за миллисекунды; для Python-скриптов аналогично используются локальные wheel-кэши либо глобальное окружение хоста.

  4. Запуск процесса: Выполняется runCmd с жестким ограничением timeoutMs. Результат считывается из output.json или stdout.

  5. Очистка: Папка воркспейса сразу же аннигилируется через rm -rf.

Если скрипт выполняется в консоли - он автоматически становится навыком ИИ. Без написания оберток на протоколе MCP.

А что с MCP?

Я не против MCP как идеи, я против его кривых реализаций. В toolhub нативная поддержка MCP реализована в двух режимах:

  1. Stateless (Stdio): Классика. Спавнится процесс -> отправляется запрос -> профит -> процесс убивается. Подходит для разовых утилит, т.е. для 90% того что есть на рынке. Так же для удалённых MCP в том числе.

  2. Stateful Pool (Persistent PID): Самое вкусное. Если ваш MCP-сервер держит тяжелый контекст (Puppeteer с авторизованной сессией браузера, SSH-соединение, пул подключений к БД) - toolhub держит процесс в памяти. Повторные вызовы отрабатывают за 10–30 мс без затрат на холодный старт. Простой регулируется через TTL (по дефолту 5 минут), после чего память освобождается. Повторное использование агентом до истечения TTL - продлевает его жизнедеятельность.

Если MCP-инструмент из внешнего сервера вам понравился, но вам хочется затюнить его описание под свою модель, навесить строгую output-schema или прописать few-shots примеры (что классический MCP напрямую не поддерживает) - кнопка MCP Promote в админке в 1 клик переносит его метаданные и схемы вызова напрямую в базу toolhub, позволяя докручивать промпт и валидацию инструмента прямо из UI.

«А если я сижу в Claude Code / Cursor и хочу дергать тулы оттуда?»

Сам я не фанат завязки на конкретные закрытые проприетарные CLI, но если вы плотно сидите на Claude Code, Cursor или Cline - концептуально вам ничего не мешает использовать toolhub как единый бэкенд навыков.

Для этого достаточно написать микроскопический Stdio-мост на 40 строк (toolhub-mcp-bridge), который будет транслировать агентские вызовы Клода в REST API вашего локального toolhub. Прописали один сервер в конфиг - и Клодоподобные на лету видят всю вашу древовидную экосистему скриптов.

Перчатка брошена: кто первый напишет и оформит красивый toolhub-mcp-bridge в виде отдельного пакета/PR - с удовольствием добавлю в официальный README и закреплю авторство!

Бесконечная Федеративность

Узлы toolhub умеют каскадироваться.

Вы можете поднять локальный toolhub на рабочем ноутбуке, подключить к нему Remote-узлом toolhub вашего домашнего сервера, к которому подключен toolhub инфраструктуры дева.

Для агента это выглядит как единый путь: <hub>callTool("/office/home/server/reboot", {})</hub>

HubSDK берет на себя рекурсивную нормализацию путей, проксирование авторизации и прозрачное туннелирование запросов. А для защиты от дурака и циклических ссылок (когда Сервер А случайно ссылается на Сервер Б, ссылающийся на А) каждый запрос маркируется цепочкой заголовков x-hub-nodes с отслеживанием node_id каждого узла и жестким лимитом глубины (maxHops, по умолчанию задан разумный предел).

Шеринг навыков и встроенный Time-Machine

Любой инструмент или целую ветку дерева со всеми подпапками, раннерами и схемами можно экспортировать в один переносимый JSON-манифест .toolpack. Написал крутую связку для деплоя или работы с базой - выгрузил в 1 клик, закинул коллеге, он нажал «Import», и навыки сразу появились в его агенте. Так же с отдельными тулами.

А чтобы не бояться экспериментировать с промптами и кодом инструментов прямо в админке, toolhub ведет автоматический журнал ревизий (ToolVersion). Каждое сохранение создает неизменяемый снимок кода, схем и описания с возможностью отката к любой предыдущей версии в один клик.

Бенчмарки и производительность: а не медленно ли?

Главная претензия теоретиков к древовидной навигации: «Это же лишний RTT! Зачем делать listTools, если можно отправить всё сразу?»

Воспроизводимый бенчмарк-сьют - код лежит прямо в репозитории в папке basic-toolpacks/benchmark/. Замеры проводились на локальном тестовом стенде (VM в Proxmox, 12 vCPU Intel i5-14500 cpu_type: host, 32 GB RAM, Debian 12, NVMe SSD).

Методология: 100 последовательных итераций на каждый сценарий с расчетом p50, p90, p99 перцентилей + стресс-тест на параллельный спавн воркспейсов:

Сценарий / Нагрузка

Среда / Раннер

Cold Start

Avg Latency

p50

p90

p99

Throughput

listTools("/") (Корень)

Navigation Engine

5.5 ms

1.0 ms

0.8 ms

1.7 ms

2.6 ms

~990 req/s

listTools("/bench") (Ветка)

Navigation Engine

1.7 ms

1.5 ms

1.4 ms

2.1 ms

3.6 ms

~680 req/s

Bash System Telemetry

OS Spawn (/bin/sh)

33.7 ms

13.4 ms

12.7 ms

20.3 ms

27.1 ms

~75 req/s

Bun CPU Cruncher (50k iters)

Bun Runtime

30.9 ms

25.8 ms

25.2 ms

30.1 ms

38.0 ms

~39 req/s

Bun Heavy Disk I/O (25 files batch)

FS Workspace I/O

31.7 ms

34.3 ms

26.2 ms

66.7 ms

129.6 ms

~29 req/s

Python 3 SHA256 (500 hashes)

Python 3 Process

69.5 ms

64.8 ms

41.6 ms

129.1 ms

177.0 ms

~15 req/s

Важно про железо: замеры выше сделаны на мощной VM (12 vCPU/32GB) - это потолок производительности runtime’а, а не то, что вы получите на Raspberry Pi или бюджетной VPS. На слабом железе (1 vCPU/512MB) абсолютные цифры будут выше в разы, но относительная разница между навигацией по дереву (~1 мс) и полноценным спавном процесса (10-60+ мс) сохранится - узкое место всегда сам раннер, а не Navigation Engine.

Параллельный стресс-тест на изоляцию воркспейсов:

15 одновременных вызовов тулов отработали параллельно суммарно за 138 мс (Ошибок: 0, 100% изоляция ФС во временных директориях).

Что значат эти цифры?

  1. Задержка навигации по дереву каталогов составляет 1.0–1.5 мс. Замеры отражают чистый overhead самого toolhub Runtime. На фоне сетевого latency и генерации токенов LLM (500–2000 мс) оверхед toolhub - это абсолютная погрешность.

  2. Полный жизненный цикл одноразового bash-инструмента (генерация воркспейса /tmp/hub_run_*, запись input.json, спавн процесса, чтение output.json и атомарный rm -rf) занимает всего 13 миллисекунд.

Про базу данных: По умолчанию из коробки используется легковесная встроенная SQLite - чтобы проект поднимался без поднятия отдельных сервисов СУБД. Но благодаря тому, что весь бэкенд построен на Prisma ORM, при желании вынести ToolHub в высоконагруженный корпоративный кластер переезд на PostgreSQL, MySQL или CockroachDB сводится грубо говоря к изменению одной строчки провайдера в schema.prisma.

Когда дерево НЕ даёт выигрыша (или даже мешает) - честный список:

Ситуация

Почему

Мало тулов (до ~10)

Схема и так маленькая, а лишний listTools-запрос - это чистый оверхед без экономии

Агент трогает почти все категории за сессию

Если он всё равно облазит 50 из 60 тулов - дерево платит почти как плоский список, но ещё и с лишним RTT сверху

Задача всегда одна и та же, тул известен заранее

Навигация не нужна, если и так понятно, что вызывать

Слабая модель с плохим tool-use

Двухшаговая схема (сначала посмотреть, потом вызвать) требует дисциплины; модель послабее может путать пути или забывать зайти в папку - тут надёжность плоского списка важнее экономии токенов

Сервис с очень высокой параллельностью на одном тулсете

Нишевый кейс, не основной сценарий toolhub - подробно можно обсудить отдельно, если кому-то актуально

Итого: чем шире набор тулов и чем уже реальное использование за сессию - тем сильнее дерево выигрывает. toolhub целится ровно в этот кейс.

Быстрый запуск

# 1. Клонируем
git clone https://github.com/Talos-Popcorn/toolhub.git
cd toolhub

# 2. Установка зависимостей
bun install

# 3. Синхронизация схемы БД Prisma
bun run db:push

# 4. Интерактивный seed+install
# (Выбор языка системного промпта EN/RU/ZH, настройка паролей админки и агента)
bun run db:seed

# Или в тихом режиме для CI/Docker:
# bun run prisma/seed.ts --lang=ru --admin-pass=admin --agent-pass=123

# 5. Запуск в дев режиме
bun run dev

Панель управления доступна по адресу http://localhost:5173/admin/ (или порт 3000 в прод-сборке). Swagger/OpenAPI документация: http://localhost:3000/docs (в дев-режиме). Дальше - импортируем какие-нибудь тулпаки из папки basic-toolpacks и пробуем запускать их в Playground.

Интеграция в любой JS/TS код занимает пару строк:

import { HubSDK } from './SDK/JS/sdk';

const hub = new HubSDK('http://localhost:3000', '123');

async function runAgentLoop(userQuery: string) {
  const systemPrompt = await hub.getSmartPrompt();
  
  const messages = [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userQuery }
  ];

  while (true) {
    const aiResponse = await llm.generate(messages);
    const action = await hub.processAgentResponse(aiResponse);

    if (!action.called) {
      console.log('Ответ агента:', aiResponse);
      break;
    }

    messages.push({ role: 'assistant', content: aiResponse });
    messages.push({ 
      role: 'user', 
      content: `HUB_RESULT: ${JSON.stringify(action.result)}` 
    });
  }
}

Кстати, для тех, кто пробовал мой прошлый проект 🧪lab (статья, репо) (serverless-клиент для LLM без бэкенда) - поддержка 🛠️ toolhub уже встроена и прекрасно работает. Теперь можно цеплять локальные тулы прямо из браузерного интерфейса. Смотрим, как работает:

Запуск из Лабы
Запуск из Лабы
Древовидная структура
Древовидная структура
Плейграунд
Плейграунд

Лицензия и Open Source

Проект полностью открыт, распространяется под AGPL-3.0. Первичная задача - выкинуть из стека лишний оверхед и дать нормальный инструмент для быстрой автоматизации без переплат за токены.

Буду рад конструктивной критике, issue и пулл-реквестам.

FADQ (Frequently Asked Dушные Questions)
  1. Запускать такое на рабочей тачке - опасно! Да, исполнение скриптов прямо в ОС хоста без изоляции несет риски. То, что по дефолту toolhub работает без Docker - это фича для экономии ресурсов на слабых VPS и Raspberry Pi. Заворачивать сам toolhub в Docker, LXC-контейнер или отдельную VM - это задача оператора под конкретный контур безопасности. Тебе никто этого не запрещает!

  2. Зачем мне toolhub, если есть LangChain / LlamaIndex / AutoGen? Затем, что это тяжелые фреймворки с горой абстракций, жестко завязанные на код вашего приложения. 🛠️ toolhub - это самостоятельный standalone-сервис. Вы можете менять языки программирования, переключать LLM-провайдеров или переписывать агентов с нуля - ваши инструменты остаются централизованными, версионируемыми и доступными по единому REST/SDK API.

  3. «Древовидная навигация через listTools - это же лишние RTT (Round Trip Time)! Модель тратит лишние итерации вместо того, чтобы сразу вызвать тул!» Арифметика простая: listTools("/") уходит в системный промпт через getSmartPrompt() и кэшируется точно так же, как и плоский список схем. А вот когда агент дальше идёт в конкретную папку - listTools("/database") - это уже вызов внутри диалога, и вот тут дерево начинает реально экономить, потому что в контекст попадают схемы только нужной ветки, а не всех 60 тулов разом. Проверялось на синтетических тулпаках для типичного использования toolhub - один агент, личное пользование, разработка - дерево дешевле плоского списка на 60-85% уже начиная с 30+ тулов, и разрыв растёт вместе с их числом. Подробнее - выше!

  4. «Зачем писать бэкенд на Bun/TS? Почему не на Rust/Go, если мы боремся за производительность?» Потому что Bun дает идеальный баланс: стартует за миллисекунды, мгновенно исполняет JS/TS без транспиляции и не требует компиляции под каждую платформу. Если тебе нужен Rust - пиши тул на Rust, компилируй в бинарник и вызывай его через ToolHub как runCmd: "./my_rust_binary". ToolHub - ЯП-агностичен, а оверхед на запуск тулов благодаря Fastify минимален.

  5. «Что произойдет, если 50 агентов одновременно запустят скрипты с pip install? Все упадет от гонки зависимостей?» Каждый вызов исполняется в абсолютном изоляте /tmp/hub_run_<timestamp>_<hash>. Папки и файлы не пересекаются в файловой системе. Если тебе нужен глобальный кэш тяжелых пакетов - используй глобальный pip-кэш на хосте или нативные бинарные раннеры. Безусловно, важно понимать, что всё зависит от раннера, тула, и прямоты твоих рук.

  6. «А если скрипт зависнет в бесконечном цикле или пожрет всю память?» Для этого у каждого тула есть жестко заданный timeoutMs. По истечении таймаута процесс жестко гасится через SIGKILL, воркспейс подчищается через rm -rf, а агент получает нормальную ошибку с таймаутом, а не висит намертво.

  7. «Интеграция с Sublime Text в 2026 году?! Кто вообще пользуется Sublime, когда есть Cursor / VS Code?» Всё просто - я это сделал для себя. Мне важна скорость в 2 миллисекунды на diff, а не поедание 4 Гб ОЗУ Электроном ради подсветки пары строк. Поддержка Sublime сделана для тех, кто ценит реактивность. Хочешь под VS Code/Cursor/Windsurf/etc - SDK открыт, пиши .toolpack+bridge, PRы приветствуются. Лучшие тулпаки и тулы готов курировать, тестировать и добавлять в репо.

  8. Почему XML-теги через обычный текст? Потому что нативный Function Calling жестко привязывает к конкретным вендорам, часто ломает стриминг и плохо работает на открытых/локальных моделях. XML-формат универсален для любых сеток. В SDK toolhub встроена базовая санитаризация (игнорирование тегов внутри markdown-блоков с кодом, исправление базовых огрехов JSON вроде одинарных кавычек), но общая культура промптинга и экранирование в UI - это, как и всегда, ответственность архитектуры вашего приложения.

  9. Как это - нет докера? Что значит это фича? Это недоработка! toolhub рассчитан на Local-First / Trusted Node сценарии (когда агент крутится на твоем компе и выполняет ТВОИ скрипты). А если нужен Untrusted режим - докерфайл пишется за 3 секунды любой LLM. Сильно нужен готовый + композ = жду в issues и PR.

  10. А без LLM этим вообще можно пользоваться? Да! 🛠️ toolhub - это, по сути, сверхлегкий self-hosted FaaS (Function-as-a-Service) с древовидным REST API. Если тебе нужно из Node.js бэкенда прозрачно вызывать Python-скрипты, дергать bash-автоматизацию по вебхукам или централизованно хранить утилиты с логами и версионированием - ты просто используешь REST/SDK напрямую из обычного классического кода без всяких нейросетей.

  11. Три коммита в гитхабе? Четыре дня от роду? И ты предлагаешь мне этим пользоваться? Да так-то нет. Разработку своих проектов я веду локально в Gitea, и делюсь только тем, что действительно заслуживает шейринга. Самому проекту около 5 месяцев, а локальный репо насчитывает на данную секунду 57 коммитов. То же касается и 🧪lab и всего что я ещё захочу выложить.