Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/afraid-cougars-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'@alfalab/core-components-mcp': minor
---

##### Новый инструмент `component_changelog`

Добавлен MCP-инструмент для работы с историей изменений компонентов. Поддерживает три режима:

- **full** — полный changelog текущей мажорной версии компонента
- **single** — запись для конкретной версии (`version`)
- **diff** — все изменения между двумя версиями (`v1`, `v2`)

Записи, содержащие только обновления зависимостей без изменений самого компонента, автоматически исключаются из выдачи.

##### Генерация данных

- Сгенерированы данные для `v50.16.0`
- Исправлен путь генерации данных

##### Реструктуризация скилл-файлов

MCP-специфичный контент вынесен из `SKILL.md` в отдельный файл `MCP.md`:

- `SKILL.md` — основной скилл: правила использования MCP-инструментов, паттерны импортов, темизация, CSS-переменные
- `MCP.md` — справочник по MCP-инструментам: когда вызывать каждый инструмент, воркфлоу, примеры

Команда `add-skill` теперь копирует все `.md`-файлы из папки `skills/`, а не только `SKILL.md` — новые справочные файлы подхватываются автоматически.

##### Обновлён SKILL.md

- Добавлено описание `component_changelog` с примерами вызовов для всех трёх режимов
- Добавлен сценарий «upgrading a component» в раздел workflow
- Добавлено правило: если компонент не найден в MCP — агент сообщает об этом явно, не fallback на общие React-паттерны
- Добавлена таблица deprecated-компонентов с заменами и инструкция по миграции: агент сообщает о deprecated-статусе и предлагает перейти на актуальный компонент
- Добавлено явное правило: для получения любых данных о компонентах всегда использовать MCP-инструменты — прямое чтение `CHANGELOG.md`, `package.json` и исходных файлов запрещено
- Добавлена заметка о переименовании peer-зависимостей в `@alfalab/core-components@49`: `@alfalab/core-config` → `@alfalab/core-components-config`, `@alfalab/stack-context` → `@alfalab/core-components-stack-context`
28 changes: 26 additions & 2 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ MCP-сервер для работы с библиотекой компонен
npx @alfalab/core-components-mcp@latest add-skill
```

Скилл будет размещён в `.agents/skills/core-components/SKILL.md` относительно директории вызова. Если скилл уже существует — он будет обновлён до актуальной версии.
Файлы будут размещены в `.agents/skills/core-components/` относительно директории вызова:

- `SKILL.md` — основной скилл: паттерны импортов, темизация, CSS-переменные, правила использования MCP
- `MCP.md` — справочник по MCP-инструментам: когда и как вызывать каждый инструмент, примеры

Если файлы уже существуют — они будут обновлены до актуальной версии.

---

Expand All @@ -68,7 +73,7 @@ yarn generate-data
Скрипт:

- ищет entry point компонентов в `packages/*/src`;
- собирает пропсы, описание и демо;
- собирает пропсы, описание, демо и changelog из корневого `CHANGELOG.md`;
- пересоздаёт каталог `src/data/v<version>`;
- обновляет `src/version.ts` по версии из `packages/root/package.json`.

Expand Down Expand Up @@ -100,3 +105,22 @@ yarn generate-data
| ----------- | -------- | ------------ | ----------------------------------------------------------- |
| `component` | `string` | да | Имя компонента (например `Button`, `Input`, `ActionButton`) |
| `name` | `string` | нет | Заголовок демо для получения кода |

---

### `component_changelog`

Возвращает записи changelog для компонента. Поддерживает три режима работы:

- **full** — без `version`, `v1` и `v2`: весь changelog текущего и предыдущего мажора (например, 50.x.x и 49.x.x).
- **single** — с `version`: запись для одной конкретной версии.
- **diff** — с `v1` и `v2`: все изменения между двумя версиями (не включая `v1`, включая `v2`). Порядок `v1`/`v2` не важен.

Данные парсятся из корневого `CHANGELOG.md` монорепозитория; записи без изменений самого компонента (например, чисто транзитивные обновления зависимостей) в данные не попадают.

| Параметр | Тип | Обязательный | Описание |
| ----------- | -------- | ------------ | ----------------------------------------------------------- |
| `component` | `string` | да | Имя компонента (например `Button`, `Input`, `ActionButton`) |
| `version` | `string` | нет | Конкретная версия для запроса, например `50.13.0` |
| `v1` | `string` | нет | Начальная версия для diff-режима, например `50.0.0` |
| `v2` | `string` | нет | Конечная версия для diff-режима, например `50.13.0` |
63 changes: 40 additions & 23 deletions packages/mcp/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,47 @@
/>
```

## Обработка deprecate компонентов
## Синхронизация версий

На данный момент из генерации исключены компоненты из deprecated-секции. В следующих итерациях нужно будет решить, что с ними делать: собирать по ним данные или нет.
Цель: MCP-сервер должен отдавать данные для той версии библиотеки, которая установлена в проекте пользователя.

### Хранение данных

- Генерировать **minor-снапшоты** вместо patch-снапшотов: папка `v50.16`, а не `v50.16.3`. При выходе `50.16.4` снапшот `v50.16` перегенерируется, а не создаётся новая папка.
- Накапливать снапшоты всего текущего мажора (все `50.x`). Снапшоты предыдущего мажора — опционально, по мере необходимости.
- Хранить без сжатия (текущий формат JSON).
- Убрать `rmSync` из `create-index-dir.mjs` — папка удаляется только если это тот же minor, иначе создаётся рядом.

```
src/data/
├── v50.12/
│ ├── button.json
│ └── ...
├── v50.15/
└── v50.16/
```

Доступные версии определяются в рантайме через `readdirSync` по паттерну `v\d+\.\d+`.

### Автодетект версии (рантайм)

При старте сервера определять версию библиотеки по следующей цепочке:

1. `node_modules/@alfalab/core-components/package.json` — читать `version` относительно `process.cwd()`
2. `package.json` проекта — смотреть в `dependencies` / `devDependencies` / `peerDependencies`
3. Fallback — последний доступный minor из `versions.json`

### Алгоритм резолюции версии

По найденной версии (например `50.16.3`) выбрать нужный снапшот:

1. Точное совпадение minor: ищем `v50.16` → если есть, берём
2. Ближайший меньший minor: ищем максимальный `50.x` где `x < 16`
3. Fallback: последний minor в `versions.json`

### Триггер генерации

Запускать `generate-data` автоматически в CI при каждом релизе библиотеки, чтобы minor-снапшот всегда был актуален на момент публикации пакета.

## Описание компонента

Expand All @@ -45,31 +83,10 @@
/>
```

## Импорты

В альфа-версии для каждого компонента было поле с информацией об импортах:

```json
{
"imports": [
{
"from": "@alfalab/core-components/accordion",
"named": ["Accordion"]
}
]
}
```

Скорее всего, будет достаточно держать эту информацию в скилле. Например, описать импорты атомов, импорт из рута и импорты desktop/mobile-версий.

## Примеры кода

На данный момент это самое узкое место в сборке данных. Все демки находятся в `docs/description.mdx`, поэтому приходится просто парсить файл и получать данные оттуда. Но сами данные местами могут быть мусорными и отдавать агенту неочищенную информацию. Нужно рассмотреть варианты, как получать примеры кода более детерминированно.

## Skill

Для более качественной работы необходимо написать скилл. В нем нужно описать, как работать с импортами, компонентами, темизацией и переменными. Также его стоит обогатить данными из текущих страниц сторибука. В итоге нужно дать максимально подробную информацию по работе с библиотекой, потому что одного MCP будет недостаточно.

## Scenario

Для каждой демки нужен сценарий использования (`success`, `error` и т. д.). По факту сценарии уже могут быть описаны, но само демо может быть комбайном из разных сценариев. Необходимо делать примеры более атомарными.
1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"copy:assets": "copyfiles -u 1 \"src/{data,skills}/**/*\" dist/",
"dev": "tsx src/index.mts",
"generate-data": "node scripts/generate-data.mjs",
"inspect": "npx @modelcontextprotocol/inspector",
"start": "node dist/index.mjs"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/scripts/create-index-dir.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { version } = rootPackageJson;

export function createIndexDir() {
const indexDir = `v${version}`;
const versionDir = path.resolve(dirname, '..', 'data', indexDir);
const versionDir = path.resolve(dirname, '..', 'src', 'data', indexDir);

rmSync(versionDir, { recursive: true, force: true });
mkdirSync(versionDir, { recursive: true });
Expand Down
7 changes: 7 additions & 0 deletions packages/mcp/scripts/generate-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { extractComponentDescription } from './extract-component-description.mjs
import { generateDemo } from './generate-demo.mjs';
import { generateDoc } from './generate-doc.mjs';
import { getComponentEntryPoints } from './get-component-entry-points.mjs';
import { parseChangelog } from './parse-changelog.mjs';

const { dirname } = import.meta;
const rootChangelogPath = path.resolve(dirname, '../../..', 'CHANGELOG.md');

function main() {
const files = getComponentEntryPoints();
Expand All @@ -23,6 +27,8 @@ function main() {

const demos = generateDemo(path.join(path.dirname(filePath), 'docs', 'description.mdx'));

const changelog = parseChangelog(rootChangelogPath, displayName);

writeFileSync(
path.resolve(versionDir, `${packageName}.json`),
JSON.stringify(
Expand All @@ -32,6 +38,7 @@ function main() {
description,
props,
demos,
changelog,
},
null,
1,
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/scripts/get-component-entry-points.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function getComponentEntryPoints() {
'packages/calendar-input/**',
'packages/date-input/**',
'packages/date-range-input/**',
'packages/date-time-input/**',
'packages/intl-phone-input/**',
'packages/loader/**',
'packages/time-input/**',
Expand Down
125 changes: 125 additions & 0 deletions packages/mcp/scripts/parse-changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { existsSync, readFileSync } from 'node:fs';

/**
* Извлекает из секции версии все блоки изменений, относящихся к указанному компоненту.
*
* Структура секции:
* ### Minor/Patch Changes
* #### [#PR](url) ← ссылка на PR
* ##### ComponentName ← имя компонента (может быть списком: "Slider, SliderInput")
* - описание изменений
*
* @param {string} versionSection Текст одной секции версии (## 50.X.X ...)
* @param {string} componentName Имя компонента (например "Button")
* @returns {string[]} Массив текстовых блоков с изменениями
*/
function extractComponentBlocks(versionSection, componentName) {
const lines = versionSection.split('\n');

let prRef = null;
let collecting = false;
let buffer = [];
const blocks = [];

const flush = () => {
const content = buffer.join('\n').trim();

if (collecting && content) {
blocks.push(prRef ? `#### ${prRef}\n\n${content}` : content);
}

buffer = [];
collecting = false;
};

for (const line of lines) {
if (line.startsWith('#### ')) {
flush();
prRef = /^#### \[#\d+\]/.test(line) ? line.slice('#### '.length) : null;
continue;
}

if (line.startsWith('##### ')) {
flush();
const heading = line.slice('##### '.length).trim();

collecting = heading.split(/\s*,\s*/).some((n) => n.trim() === componentName);

continue;
}

if (/^#{2,3} /.test(line)) {
flush();
prRef = null;
continue;
}

if (collecting && !/^<sup>/.test(line)) {
buffer.push(line);
}
}

flush();

return blocks;
}

/**
* Парсит корневой CHANGELOG.md монорепозитория и возвращает записи изменений
* для указанного компонента в рамках текущей мажорной версии.
*
* Алгоритм:
* 1. Читаем файл и определяем текущую мажорную версию по первому вхождению ## X.Y.Z.
* 2. Делим файл на секции по заголовкам вида "## X.Y.Z".
* 3. Оставляем только секции текущего и предыдущего мажоров (например 50 и 49).
* 4. В каждой секции ищем все блоки `##### ComponentName` и собираем их содержимое.
* 5. Возвращаем массив { version, description }.
*
* @param {string} changelogPath Путь до корневого CHANGELOG.md
* @param {string} componentName Отображаемое имя компонента (например "Button")
* @returns {{ version: string, description: string }[]}
*/
export function parseChangelog(changelogPath, componentName) {
if (!existsSync(changelogPath)) {
return [];
}

const content = readFileSync(changelogPath, 'utf-8');

const firstMatch = /^## (\d+)\.\d+\.\d+/m.exec(content);

if (!firstMatch) {
return [];
}

const currentMajor = parseInt(firstMatch[1], 10);
const prevMajor = currentMajor - 1;

const sections = content.split(/\n(?=## \d+\.\d+\.\d+)/);
const result = [];

for (const section of sections) {
const versionMatch = /^## (\d+\.\d+\.\d+)/.exec(section);

if (!versionMatch) {
continue;
}

const version = versionMatch[1];
const major = parseInt(version.split('.')[0], 10);

if (major !== currentMajor && major !== prevMajor) {
continue;
}

const blocks = extractComponentBlocks(section, componentName);

if (blocks.length === 0) {
continue;
}

result.push({ version, description: blocks.join('\n\n') });
}

return result;
}
19 changes: 12 additions & 7 deletions packages/mcp/src/add-skill.mts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
import path from 'path';

export function addSkill(): void {
const sourcePath = path.join(import.meta.dirname, 'skills', 'SKILL.md');
const sourceDir = path.join(import.meta.dirname, 'skills');
const targetDir = path.join(process.cwd(), '.agents', 'skills', 'core-components');
const targetPath = path.join(targetDir, 'SKILL.md');

const existed = existsSync(targetPath);

mkdirSync(targetDir, { recursive: true });
copyFileSync(sourcePath, targetPath);

console.log(existed ? `Skill updated: ${targetPath}` : `Skill added: ${targetPath}`);
const files = readdirSync(sourceDir).filter((f) => f.endsWith('.md'));

for (const file of files) {
const targetPath = path.join(targetDir, file);
const existed = existsSync(targetPath);

copyFileSync(path.join(sourceDir, file), targetPath);

console.log(existed ? `Skill updated: ${targetPath}` : `Skill added: ${targetPath}`);
}
}
Loading