@daisforge/ui-mcp
v0.2.6
Published
MCP server for @daisforge/ui — component discovery, props and examples for coding agents
Readme
@daisforge/ui-mcp
MCP-сервер для @daisforge/ui — даёт кодовому агенту точные пропсы, типы, категории (wrapper/composition/standalone/form) и примеры для 260+ компонентов библиотеки, а не только для ~40 задокументированных в Storybook. description/category/keywords для выбора компонента под задачу заполнены у всех 243 неошибочных записей индекса (T2), не только у тех, что описаны в Storybook.
Быстрый старт
Внутри монорепо dais/ui сервер уже зарегистрирован в корневом .mcp.json — ничего дополнительно делать не нужно.
Для потребителей (npm install @daisforge/ui @daisforge/ui-mcp):
{
"mcpServers": {
"daisforge-ui": {
"command": "node",
"args": ["node_modules/@daisforge/ui-mcp/dist/server.js"]
}
}
}Инструменты
list_components({ type?, category?, scope?, role?, limit?, offset? })— ответ{ items, shown, total, hasMore?, truncationNotice? }. По умолчанию отдаёт толькоrole: "primary"(~177 из 243 компонентов) — слоты вродеDrawerDFHeaderи служебные примитивы вродеCanvasRectскрыты;role: "all"снимает фильтр. Каждый элемент несётdescription/category(100% покрытие, см. T2 вTASKS.MD);keywordsв этом компактном списке не дублируются — они участвуют в скорингеsearch_components, а не в беглом обзоре. Безlimitсписок сам бюджетируется под лимит ответа: с полными description/category у всех 177 primary-компонентов за один вызов без фильтров помещается ~120 из них — это ожидаемо (не баг),truncationNoticeобъясняет, чем добрать остаток (фильтры илиlimit/offset)search_components({ query })— ищет и по компонентам, и по фичам (TableCanvas/Filtering и т.п.); скоритhint,keywords(2-4 синонима задачи на ru/en на компонент) иdescription—keywordsвесят наравне сhint(+20);role: "internal"понижается штрафом, но не исключается. Результаты по компонентам несутsupersededBy/chooseWhen, если заполнены — выбор между конкурирующими компонентами происходит именно здесь (T9)get_component({ name })— компактная карточка, несётdescription/category/keywords. Уpart/internal-записей —parentComponent(куда идти за деталями); у владельца папки —relatedExports(какие части/внутренние примитивы у него есть, раз их не видно вlist_componentsпо умолчанию); у записей, дублирующих compound-часть родителя (compoundPartOf), пропсы не повторяются — карточка сразу отсылает кget_component_props({ name: parent, part }).minimalUsage— синтетический<Component requiredProp={...} />из обязательных пропсов, есть у 100% записей всегда;exampleTitles— заголовки настоящих примеров (может быть[], это честное «примеров нет», не повод зватьget_component_examples). Уlegacy: true—supersededBy(что брать вместо); у компонентов из конкурирующих групп (Table/TableCanvas,Select/FormSelect,Popover/PopoverBetaи т.п., 34 компонента) —chooseWhen, одна строка, при каких условиях брать именно этот (T9)get_component_props({ name, part? })— полные пропсы (собственные + унаследованные от атома). Пропсы отсортированы:required→ с описанием → без описания →deprecatedв конец, по алфавиту внутри яруса (T7) — при обрезке по бюджету теряется наименее нужное, а не случайное подмножество. Описания, дословно совпадающие с JSDoc React DOM-типизации того же имени, не попадают в индекс (T7). У части пропсов естьdefault— значение по умолчанию, извлечённое из деструктуризации параметра render-функции или тега@defaultв JSDoc (T6); поле опционально, отсутствует, если дефолт не резолвленget_component_examples({ name, title? })— только настоящие примеры, до 4 суммарно, из трёх источников:full-code/args-onlyиз curated Storybook-стори,usage— реальное JSX-вхождение, найденное грепом по остальным пакетам монорепо,vendor— curated-пример из вендоренного снэпшота@salutejs/sdds-finaiс переписанным на@daisforge/uiимпортом, в том числе из "чужого" файла снэпшота — например, примерыAccordionItemнаходятся внутриAccordion.json(T3); отсортированыfull-code → usage → vendor → args-only. УusageестьsourceFile(путь относительно корня монорепо)list_features({ component }),get_feature({ component, feature }),get_feature_examples({ component, feature, title? })list_categories()— категории каталога с количеством компонентов по типам; значенияcategoryотсюда подходят как фильтр дляlist_components(они же продублированы прямо в описанииlist_components, чтобы фильтр можно было применить без этого вызова)get_installation_guide()— установка пакета, подключение стилей и токенов, использование компонентов и иконок; на него ссылаетсяdataVersionNotice, когда@daisforge/uiв проекте не установлен
Одиннадцать тулов. Ресурсов (resources/list/resources/read) сервер не объявляет вовсе: list_categories/get_installation_guide были переведены в ресурсы в T8 (аргументов не принимают, контент не зависит от вызова) и возвращены в тулы в T16 — ресурс агент может прочитать, только зная его URI заранее, а список ресурсов, в отличие от списка тулов, сам в контекст агента не приходит. В транскриптах A/B-замера (40 прогонов с MCP, 279 вызовов инструментов) не было ни одного чтения ресурса.
Ручное тестирование тулов (MCP Inspector)
Чтобы точечно повызывать любой тул (get_component, search_components и т.д.) через UI, без настройки клиента:
npm run mcp:inspectКоманда собирает сервер (tsc) и поднимает @modelcontextprotocol/inspector (через npx, ничего не ставится в зависимости проекта) — откроется браузер со списком тулов, формой аргументов под каждый и живым JSON-ответом. Сервер запускается как stdio-процесс (node ./dist/server.js), сеть наружу не используется.
Тот же скрипт есть и в корне монорепо: npm run mcp:inspect (пересобирает индекс перед запуском).
Разработка
npx nx typecheck @daisforge/ui-mcp # tsc --noEmit
npx nx lint @daisforge/ui-mcp # eslint (тот же конфиг, что и у остальных пакетов монорепо)
npx nx build @daisforge/ui-mcp # tsc -p tsconfig.lib.json → dist/ts-morph — devDependency (нужна только индексеру на этапе сборки индекса, не публикуется в рантайме MCP-сервера); соответствующее исключение для import/no-extraneous-dependencies настроено в .eslintrc.json только для src/indexer/**.
Сборка индекса
npm run mcp:build-indexПишет packages/mcp-server/data/component-index.json (запасной индекс, вшивается в публикуемый @daisforge/ui-mcp) и, если уже собран dist/packages/ui-kit — также dist/packages/ui-kit/mcp-data/component-index.json (уезжает вместе с публикацией @daisforge/ui). Автоматически запускается после npm run build (см. корневой postbuild).
Курированная мета — обязательный вход, а не опция
_docs/meta/components-meta.json лежит в .gitignore (это build-артефакт npm run meta), поэтому на свежем клоне и в CI его нет. Без него из индекса молча исчезает всё, что приходит только оттуда: features (77 записей, 46 из них у одного TableCanvas), guides.installation, curated-описания/keywords/chooseWhen ui-kit компонентов и все примеры типа full-code. Ровно такой выпотрошенный индекс уехал в npm 0.1.1 — у потребителя list_features отвечал 0, а поиск по фичам не находил ничего.
Поэтому:
mcp:build-indexи корневойpostbuildсами вызываютnpm run metaперед сборкой индекса;- индексер падает на старте, если меты всё-таки нет (
assertMetaAvailableвsrc/indexer/loadMeta.ts); осознанно собрать неполный индекс —MCP_INDEX_ALLOW_MISSING_META=1; - перед записью индекс проходит проверку наполненности (
src/validate/checkCompleteness.ts): при обвале любого источника (features, full-code примеры, curated-доки, типы, compound-части, гайд по установке) файл не записывается, прогон падает. Та же проверка идёт первым шагом вmcp:validate-index.
Резолв индекса — три уровня
- workspace — если рядом физически лежит
packages/ui-kit(мы внутри монорепо), сервер читает свежесобранный локальный индекс. - installed — версия
@daisforge/ui, установленная у потребителя, уже содержитmcp-data/component-index.json. Побеждает, только если этот индекс не деградировал: пустойfeaturesили отсутствиеguides.installationозначают, что при сборке не сгенерировали курированную мету, — тогда сервер откатывается наbundledи объясняет это вdataVersionNotice. Так вышло с@daisforge/[email protected]— первой версией, куда индекс вообще начал класться: релизный шаг собирал его мимоnpm run meta, и обновление библиотеки до неё ухудшало данные MCP (до 1.14.0 работал полноценныйbundled, с 1.14.0 включался заведомо худшийinstalled). - bundled — запасной индекс, вшитый в сам
@daisforge/ui-mcp. Нужен для версий@daisforge/ui, опубликованных до появления mcp-data, для деградировавшегоinstalled(см. выше) и для случая, когда библиотека вообще не установлена. В этом случае в ответах присутствуетdataVersionNotice/libNotInstalled.
Всё работает полностью оффлайн — сервер никогда не обращается по сети.
Атомарный слой (@salutejs/sdds-finai)
Пропсы атомарных компонентов подмешиваются в inheritedProps[] из закоммиченного статического снэпшота (vendor/atomic-mcp-data/), а не через живой MCP атомарной команды — так наш сервер не зависит от установленного @salutejs/sdds-mcp и работает полностью оффлайн.
Обновление снэпшота — одной командой, без чужого чекаута:
npm run mcp:vendor-atomic:check # сухой прогон: что приедет, ничего не пишется
npm run mcp:vendor-atomic # обновить снэпшот
npm run mcp:vendor-refresh # то же + следом собрать документацию (см. ниже)Данные тянутся с https://plasma.sberdevices.ru/mcp/<lib>/<version>/ — того же CDN, поверх которого работает официальный @salutejs/sdds-mcp (в их npm-пакете лежит только dist/, данных нет; MANIFEST_BASE_URL в их dist/config.js указывает ровно сюда). В рантайме нашего MCP-сервера сеть по-прежнему не используется: снэпшот закоммичен, загрузка — только на этапе обновления.
Версия берётся не latest, а та, что реально установлена (node_modules/@salutejs/sdds-finai/package.json), чтобы индекс не обещал пропсы, которых у потребителя нет. Флаги:
| Флаг | Назначение |
| --- | --- |
| --version <x.y.z\|latest> | явная версия вместо установленной |
| --on-missing keep\|latest\|fail | что делать, если версии нет на CDN (по умолчанию keep — оставить текущий снэпшот и предупредить) |
| --dry-run | показать дифф, ничего не записывать |
| --force | разрешить даунгрейд (по умолчанию снэпшот нельзя перезаписать более старой версией) |
| --lib, --base-url | другая дизайн-система / другое зеркало CDN |
| --source <dir> | прежний офлайн-режим: взять локальный каталог mcpData вместо сети |
Что важно знать (подробно — ARCHITECTURE.md §1.6b):
- Опубликованы не все версии.
0.355.0/0.351.0на CDN нет вовсе, и запрос несуществующей версии отвечает 301 на главную страницу сайта, которая отдаёт HTML с кодом 200 — поэтому скрипт не следует редиректам и проверяет форму каждого файла до записи. Пропущенная версия не валит релиз: срабатывает--on-missing=keep. - Бандл доков может быть новее одноимённого npm-пакета. На
0.360.0снэпшот принёс пропсы, которых в типах установленного@salutejs/[email protected]нет (Sheet.snapPoints,Modal.focusTrapSelectors,DatePicker.onChangeVisibleDate,Popover.stretchHeight,Skeleton.animationDuration) —mcp:validate-indexпоказывает их как +8 диагностик. Пин по версии нужен, но полной гарантии не даёт. - Со страницы доков берётся только первая таблица пропсов — 14 таблиц (
AccordionItem,CalendarBase,Col,DatePickerRange,ListItem,NotificationsProvider,SegmentItem,RectSkeleton/TextSkeleton,TabItem/TabsControllerи др.) в бандл не попадают вовсе, и перевендоринг этого не чинит — это дефект генератора атомарной команды. vendor/atomic-mcp-data/provenance.json(пишется скриптом, не приходит с CDN) фиксирует, откуда и когда взяты данные:resolvedVersion,installedLibVersion,sourceUrl,fetchedAt, счётчики по секциям и sha256 каждого файла.- Помимо
components/иbeta/снэпшот теперь несёт гайды (form,utils,tokens,how-to-icons,intro,FAQ,next,react_17,mcp) — они складываются в снэпшот, но в индекс пока не попадают; секции берутся из самого манифеста, поэтому новая приедет сама.
Документация атомарки (vendor/atomic-docs/)
В бандле на CDN прозы нет: их генератор режет summary компонента по 300 символов — медиана 44 символа, у 34 из 74 компонентов остаётся голое имя. Текст берётся отдельно, из первого скрипта их же пайплайна (generate-index), где страница лежит целиком:
npm run mcp:harvest-docs:check # сухой прогон
npm run mcp:harvest-docs # собрать документациюСкрипт делает sparse-чекаут тега @salutejs/sdds-finai@<установленная версия> (только каталог доков — 17 МБ), ставит пять парсер-пакетов в отдельную папку, прогоняет их generateIndex и раскладывает результат в vendor/atomic-docs/<slug>.json ({name, slug, category, url, content}). Весь цикл — ~10 секунд, 87 страниц, 706 685 символов; медиана текста на компонент вырастает с 44 до 5 902 символов.
Сборка докусауруса не нужна: раскрытие <PropsTable> у них опционально (fs.existsSync), а пропсы у нас и так из ts-morph и с CDN. Недоступен GitHub или нет тега под версию — ::warning:: и каталог остаётся прежним (--on-missing=keep), релиз не падает. Флаги: --version, --ref, --repo, --on-missing keep|fail, --dry-run.
Обратная операция — склеить каталог в один файл ровно того формата, что отдаёт их generateIndex ([{pageContent, metadata}], готово к заливке в векторную базу):
npm run mcp:export-docs # → packages/mcp-server/.probe/atomic-docs-index.json
npm run mcp:export-docs -- --out /путь.json # свой путьПо умолчанию пишет в .probe/ (в .gitignore), чтобы в репозитории не лежала вторая копия тех же данных. Сверено с оригинальным выводом их скрипта: metadata совпадает 87/87, контент 83/87 — расхождение ровно на 4 страницы, которые менялись между тегом версии и master.
Пока это только сырьё: в индекс проза не попадает и тулами не отдаётся — медиана 5.9k символов на компонент не помещается в карточку get_component при бюджете ответа 25k. Как отдавать (отдельный тул или клип со ссылкой на url) — отдельная задача.
Курированные данные каталога (description/category/keywords)
Индексер сам находит компоненты, пропсы и типы из исходников, но description/category/keywords — курированный контент, у которого два независимых редактируемых источника (оба читаются на этапе mcp:build-index, сами по себе НЕ индексер):
generators/meta-info/config/meta-config.json(полеcomponents) — для компонентов с собственным кодом вui-kit(wrapper/composition/standalone с реальной логикой поверх атома или самостоятельной реализацией). Этот файл — вход генератораnpm run meta(generators/meta-info/generate-meta.js), который пишет_docs/meta/components-meta.json._docs/meta/components-meta.json— build-артефакт, полностью перезаписывается при каждомnpm run meta— редактировать его руками нельзя, правки делаются только вmeta-config.json.mergeMeta.tsчитает уже сгенерированный_docs/meta/components-meta.json.packages/mcp-server/vendor/atomic-curated-meta.json— для компонентов без единой строчки собственного кода: чистых реэкспортов атомов@salutejs/sdds-finai(export { X } from '@salutejs/sdds-finai'). У них нет ни JSDoc, ни своей Storybook-страницы, поэтомуgenerators/meta-infoих не видит в принципе (резолвит по папке вpackages/storybook/src/stories/<Name>/, которой у реэкспортов нет). Читается напрямую черезmergeAtomicCuratedMeta.ts, идёт сразу заmergeMetaв пайплайнеbuildIndex.ts— независимый источник, не проходит через генераторnpm run meta.
Оба источника — "curated wins": непустое значение перекрывает то, что нашёл индексер сам (JSDoc-описание, эвристику category).
A/B-замер: помогает ли индекс
npm run mcp:validate-index проверяет, что индекс не врёт. Проверка того, что он помогает, живёт в ab/ — одни и те же 20 задач на генерацию кода решаются агентом с MCP и без, в обстановке настоящего потребителя (песочница вне репозитория с опубликованным @daisforge/ui из npm, без исходников библиотеки).
npm run mcp:ab:consumer # собрать стенд потребителя
npm run mcp:ab:drift # карта расхождений «индекс ↔ установленный пакет»
npm run mcp:ab # прогон + метрики (.probe/ab/report.md)
npm run mcp:ab:app # собрать из ответов работающее приложение + vite build
npm run mcp:ab:selfcheck # поверка самого скорера, без запуска агентаПоследний замер (20 задач × 3 руки, sonnet): вызовов инструментов до первой строчки кода — 24.3 без MCP против 7.1 с MCP, доля ответов, проходящих tsc, — 60% против 85%. Разбор, оговорки и разрез по сложности задач — ARCHITECTURE.md §9, подробности харнесса — ab/README.md.
Публикация
@daisforge/ui-mcp версионируется и публикуется независимо от @daisforge/ui — не через Lerna (пакет не входит в npm workspaces и не виден lerna version), а через отдельный workflow .github/workflows/publish-mcp-server.yml.
Запуск — вручную, из Actions UI или через gh:
gh workflow run publish-mcp-server.yml -f bump=patch -f dry_run=true # проверочный прогон, ничего не публикует
gh workflow run publish-mcp-server.yml -f bump=patch -f dry_run=false # реальный релизbump — patch/minor/major. Workflow сам поднимает версию в package.json (npm version), коммитит и тегирует (ui-mcp-vX.Y.Z — отдельный неймспейс от тегов ui-kit, vX.Y.Z), затем публикует в тот же registry.npmjs.org, что и @daisforge/ui.
Workflow объявлен и как workflow_call — задел на будущее, если релиз ui-mcp решат триггерить автоматически из publish-npm.yml вместо ручного запуска.
Зеркальный (white-label) пакет
Тем же прогоном публикуется второй, зеркальный пакет: тот же собранный dist/ и тот же индекс, но под другим именем и с другим именем связанной библиотеки. Отдельного репозитория и отдельной ветки для этого нет — исходники в src/ про зеркало ничего не знают.
Работает это так: scripts/build-alias-package.mjs копирует dist/ и data/component-index.json во временный каталог вне рабочего дерева (ALIAS_OUT_DIR, по умолчанию системный tmp), прогоняет по ним таблицу текстовых замен, собирает свои package.json и README.md, и npm publish делается уже оттуда. Основной пакет при этом не меняется ни на байт.
Имена берутся из секретов репозитория — в коде их нет:
| Секрет | Что это |
| --- | --- |
| ALT_MCP_PACKAGE_NAME | имя зеркального npm-пакета |
| ALT_LIB_PACKAGE_NAME | имя связанной библиотеки — подставляется вместо @daisforge/ui |
| ALT_NPM_TOKEN | токен с правами publish в новый scope |
Локально зеркало собирается той же командой, что и в CI:
npm run build --prefix packages/mcp-server # dist/ должен быть свежим
ALT_MCP_PACKAGE_NAME=@acme/ds-mcp ALT_LIB_PACKAGE_NAME=@acme/ds \
npm run mcp:alias:stage --prefix packages/mcp-serverПуть к собранному зеркалу скрипт печатает последней строкой; переопределяется через ALIAS_OUT_DIR.
Что стоит знать, если придётся это чинить:
- Staging обязан лежать вне рабочего дерева git.
npm publishищет.gitвверх от каталога пакета и, найдя, добавляет в манифест реестра полеgitHeadс SHA текущего HEAD (@npmcli/package-json/lib/normalize.js). У основного пакета это поле есть —npm view @daisforge/ui-mcp gitHeadего показывает. Одинаковый SHA у двух пакетов связал бы зеркало с основным намертво, поэтому скрипт сам проверяет, что каталог не внутри репозитория, и падает, если это не так. В тарболеgitHeadне лежит — только в метаданных реестра, из-за чегоnpm packэту утечку не покажет. - Порядок замен в
REPLACEMENTSзначим: длинные токены идут первыми, иначе@daisforge/ui-mcpпревратится в<ALT_LIB>-mcp. Список закрытый, но в конце сборки работает гард — рекурсивный греп staging на/dais/i. Появится в артефактах новая форма бренда (новое имя стори-папки, новая ссылка в комментарии) — сборка зеркала упадёт с указанием файла и строки, и в таблицу нужно добавить правило. Молча бренд не утечёт. - Зеркало публикуется первым, основной пакет вторым. Если падает зеркало (новый scope, отдельный токен, своя история версий), job обрывается до публикации
@daisforge/ui-mcpи доgit push— ничего не опубликовано, ничего не запушено, повтор чистый. - Версия у зеркала та же, что у основного пакета (лок-степ). Если истории версий в двух scope разойдутся,
npm publishупадёт сEPUBLISHCONFLICT. dist/indexerиdist/validateв зеркало не едут: они нужны только внутри монорепо и тянутts-morph(devDependency) — из тарбола запуститься не могут в принципе. Изdistтакже вычищаются.map(ссылаются на../src/*, которого в тарболе нет).@salutejs/*не переименовывается — это реальные импорты атомов в vendor-примерах, потребителю они нужны рабочими.- Если секреты не заданы или
publish_alias=false, шаги зеркала пропускаются с warning — обычный релиз@daisforge/ui-mcpот этого не страдает.
Известные ограничения v1
- Иконки, токены, миксины, утилиты не проиндексированы — сознательная граница v1 (см. план). Следующий шаг.
- Три пары внутренних sub-компонентов с одинаковым именем в разных папках (
TableFilterSelectListItem,ContainerStyled,Canvas— есть и вTable/TableCanvas/TableGlide) — при коллизии имён в индекс попадает только последняя обработанная запись. Не влияет на публичные компоненты, только на внутренние helper-подкомпоненты таблиц. - Поиск (
search_components) — простой substring-скоринг, без embeddings/NLP. Хорошо работает на прямых терминах и на курированныхhint, слабее — на общих словах, которые встречаются во многих фичах одновременно (например "ячеек" — общее слово почти для всех табличных фич). atomicDataMissing: trueу 31 записи из 247 (было 34 до перевендоринга на0.360.0: уDivider,ModalDFDividerиWidgetDividerстраница в доках появилась;ToolbarDividerостался — егоatomicBaseназывается так же, как он сам, а страницы с таким именем нет) — вендорных пропсов нет по двум причинам: у 11 таблица в доках атомарной команды есть, но не первая на своей странице, и снэпшот её теряет (CalendarBase*,DatePickerRange,NotificationsProvider,SegmentItem,TextSkeleton,TabItem,TabsController); у остальных таблицы нет в доках вовсе (compound-частиDrawerHeader/DrawerContent/DrawerFooter,SegmentIconItem,IconTabItem,ToastProvider,Typography+ 13 атомов типографики). Пропсы у всех 31 есть — из собственного резолва ts-morph, причём с описаниями иrequired, которых вендор не несёт вовсе. Сборка не падает, диагностика индексера помечает каждую дыру её причиной. Подробности —ARCHITECTURE.md§1.6b.
