Files
factory/lib/projects-meta-mcp/docs/superpowers/specs/2026-04-29-projects-meta-mcp-design.md
vitya ca669d96e1 feat(lib): bundle MCP servers from .common/lib into factory/lib/
Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for:
- projects-meta-mcp v2.25.0 (TypeScript/Node)
- wiki-graph v0.3.1 (TypeScript/Node)
- interns-mcp v0.3.3 (Python/FastMCP)

.gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc)
bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:17:29 +03:00

270 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# projects-meta-mcp — Design
**Date:** 2026-04-29
**Status:** Approved (брейнсторм завершён, спека к ревью)
**Owner:** vitya
## Цель
Дать Claude (и любому MCP-клиенту) единый, дешёвый и offline-устойчивый доступ к двум видам мета-информации, разбросанным по множеству проектов и нескольких машин:
1. **Статусы задач** — свод `.tasks/STATUS.md` всех проектов в Gitea.
2. **Общие знания** — поиск и чтение страниц общей вики (`projects-wiki`), отфильтрованных по домену текущего проекта.
## Не цели (явный YAGNI)
- ❌ Удалённый MCP-процесс на Synology (HTTP/SSE-транспорт). Откладывается до момента, когда «truly always-on» станет реальной болью.
- ❌ Карантин `_pending/` для projects-wiki. Решение по промоушену принимается в моменте.
- ❌ Sparse-checkout всех проектов на каждую машину. API + кэш проще.
- ❌ Серверный агрегатор / git-hook на стороне Gitea. Клиентский sync проще.
- ❌ Embedding/семантический поиск projects-wiki. Достаточно поиска по тексту/тегам.
- ❌ Управление зависимостями между проектами, GANTT, deadlines. Только статусы.
## Архитектура
```
┌──────────────────┐
│ Gitea (Synology)│
│ - все проекты │
│ - projects-wiki │
│ - projects-meta-│
│ mcp (этот код)│
└────────┬─────────┘
│ pull / API
┌────────────────── каждая машина ─────────────────┐
│ │
│ ~/projects/.wiki/ ← git clone │
│ ~/.cache/projects-mcp/ ← JSON-кэш STATUS │
│ │
│ sync-script (cron / руками): │
│ - git pull в projects-wiki │
│ - Gitea API → собрать STATUS.md → JSON-кэш │
│ - offline → no-op, кэш остаётся │
│ │
│ mcp-server (локальный, stdio): │
│ Tools: │
│ tasks.aggregate() │
│ tasks.search(q) │
│ knowledge.search(q, domain?) │
│ knowledge.get(slug) │
│ knowledge.suggest_promote() │
└──────────────────────────────────────────────────┘
```
**Принцип:** MCP в hot path читает только локальные файлы. Сетевые вызовы — только в `sync-script`.
## Компоненты
### 1. `projects-wiki` — отдельный git-репозиторий на Gitea
**Имя репо:** `projects-wiki`. Клонируется на каждую машину под `~/projects/.wiki/`.
**Структура:**
```
~/projects/.wiki/
├── CLAUDE.md ← правила промоушена + список доменов
├── README.md
├── cross/ ← кросс-доменные знания (git, vscode, claude-code, общие гочи)
├── node/ ← Node.js / TypeScript / npm / yarn
├── embedded/ ← микроконтроллеры, PlatformIO, STM32, Arduino
├── web/ ← фронтенд, браузер, HTTP, Nginx
└── ... ← новые домены добавляются по мере появления проектов
```
**Frontmatter каждой страницы:**
```yaml
---
title: Windows yarn requires exec()
domain: node # обязательное: node | embedded | web | cross | ...
shared_at: 2026-04-29 # дата промоушена
applies_to: [windows] # опц. — платформенные ограничения
source_project: npm-mcp # опц. — откуда промоутили
---
```
Тело — стандартный concept: **Why** + **Когда / Как чинить**.
**`projects-wiki/CLAUDE.md`** содержит:
- Полный список валидных доменов и что в каждый кладётся.
- Эвристику промоушена (см. ниже).
- Чёрный список (имена проектов, бизнес-доменов — никогда не упоминаются в shared).
### 2. `sync-script`
Bash или Node-скрипт. На каждой машине, запускается cron'ом / руками / git-хуком.
**Шаги:**
1. **Pull projects-wiki:**
```
git -C ~/projects/.wiki pull --quiet
```
Offline → silently fail (exit 0), кэш не трогается.
2. **Скачать список репозиториев пользователя через Gitea API:**
```
GET https://git.kzntsv.site/api/v1/users/OpeItcLoc03/repos?limit=50
```
С токеном (в `~/.config/projects-mcp/auth.toml`).
3. **Параллельно (limit 10) скачать `STATUS.md` каждого:**
```
GET https://git.kzntsv.site/api/v1/repos/OpeItcLoc03/{repo}/raw/.tasks/STATUS.md
```
404 → у проекта нет тасок, пропустить. Сетевая ошибка → пропустить, оставить старое значение в кэше.
4. **Собрать в `~/.cache/projects-mcp/tasks.json`** атомарно (write to `tasks.json.tmp`, rename):
```json
{
"synced_at": "2026-04-29T12:00:00Z",
"synced_from": "https://git.kzntsv.site",
"machine": "vitya-desktop",
"projects": [
{
"name": "npm-mcp",
"default_branch": "main",
"fetched_at": "2026-04-29T12:00:01Z",
"active_tasks": [
{"slug": "wiki-self-ingest", "status": "active", "next": "..."}
],
"all_tasks_count": 5,
"raw": "<полный STATUS.md>"
}
],
"errors": [{"project": "books", "reason": "network"}]
}
```
5. **Exit 0 даже при частичных ошибках.** Логи в `~/.cache/projects-mcp/sync.log`.
### 3. `mcp-server`
Stateless Node-процесс, stdio-транспорт MCP. Читает только локальные файлы. Не делает сетевых вызовов.
#### Tool surface
| Tool | Аргументы | Возвращает |
|------|-----------|------------|
| `tasks.aggregate()` | `(filter?: { status?, project? })` | Список активных задач по всем проектам. Только мета (slug, project, status, next-action), не полный текст. |
| `tasks.search(query)` | `query: string` | Поиск по `STATUS.md`-кэшу (substring + per-task title). |
| `tasks.get(project)` | `project: string` | Полный `STATUS.md` одного проекта. |
| `knowledge.search(query, opts?)` | `query: string, opts?: { domain?: string\|"all", limit?: number }` | До 10 заголовков + 1-строчное описание. По умолчанию фильтр зависит от detected domain текущего проекта (см. ниже). |
| `knowledge.get(slug)` | `slug: string` (например `node/windows-yarn-exec`) | Полная страница (frontmatter + тело). |
| `knowledge.suggest_promote()` | `()` | Возвращает кандидатов на промоушен из текущего проекта. См. логику ниже. |
| `meta.status()` | `()` | Возвращает диагностику: когда был последний sync, кэш-stale-возраст, сколько проектов, сколько shared-страниц. |
#### Two-step паттерн
`search` всегда возвращает только индекс (заголовок + 1 строка). `get` возвращает полный текст. Минимизирует токены — модель сама решает что углублять.
#### Detect domain текущего проекта
При запуске MCP получает `cwd` от клиента. Алгоритм:
1. Если в `cwd` есть `package.json` → `node`.
2. Если есть `platformio.ini` / `*.ino` / `CMakeLists.txt` с упоминанием `arm-none-eabi` или embedded SDK → `embedded`.
3. Если есть `next.config.*` / `vite.config.*` / `index.html` без `package.json` (статика) → `web`.
4. Иначе → `unknown`.
Логика фильтрации `knowledge.search`:
- Detected domain — конкретный (`node` / `embedded` / `web`) → показываем `domain == detected || domain == "cross"`.
- Detected domain `unknown` → фильтра нет (видны все домены).
- Опциональный аргумент `opts.domain` пользователя/Claude переопределяет: `"all"` снимает фильтр явно, конкретное имя меняет таргет.
`cross` в frontmatter — это **тег страницы** (универсальное знание). Detected `unknown` — это **режим поиска** (фильтра нет). Не путать.
Логика выносится в отдельный модуль `domain-detector` (тестируется юнит-тестами).
#### `knowledge.suggest_promote()`
Логика:
1. Прочесть все `concepts/*.md` в `<cwd>/.wiki/concepts/`.
2. Для каждого применить эвристику:
- **Включить как кандидата**, если страница: упоминает платформу/тулинг (Windows, yarn, Node, MCP, git, Docker), не упоминает доменные имена из чёрного списка `projects-wiki/CLAUDE.md`.
- **Усилить сигнал**, если в `projects-wiki` есть страница с близким заголовком в другом домене (= знание подтверждено повторно).
3. Вернуть список кандидатов: `{ slug, current_path, suggested_domain, confidence, reason }`.
4. Решение принимает пользователь (Claude в чате предлагает строкой `_Кандидат на shared: ... Промоутить?_`, ждёт `y`/`n`).
Промоушен ≠ автомат. Tool только **возвращает кандидатов**, ничего не пишет.
### 4. Bootstrap
На новой машине (пример Windows + Git Bash; на других ОС адаптировать пути):
```bash
# один раз:
GITEA=https://git.kzntsv.site
OWNER=OpeItcLoc03
git clone $GITEA/$OWNER/projects-meta-mcp ~/.local/projects-meta-mcp
cd ~/.local/projects-meta-mcp && npm install && npm run build
git clone $GITEA/$OWNER/projects-wiki ~/projects/.wiki
mkdir -p ~/.config/projects-mcp
cp ~/.local/projects-meta-mcp/auth.toml.example ~/.config/projects-mcp/auth.toml
# отредактировать auth.toml — заполнить gitea_token
node ~/.local/projects-meta-mcp/dist/sync.js # первый sync
# зарегистрировать MCP в claude code config (см. ниже)
```
**Формат `auth.toml`:**
```toml
gitea_url = "https://git.kzntsv.site"
gitea_user = "OpeItcLoc03"
gitea_token = "..." # personal access token, scope: read:repository
```
**Регистрация в Claude Code** (`~/.claude.json` или per-project) — пути в Windows-формате с прямыми слэшами:
```json
{
"mcpServers": {
"projects-meta": {
"command": "node",
"args": ["C:/Users/vitya/.local/projects-meta-mcp/dist/server.js"]
}
}
}
```
## Поведение в offline
| Ситуация | Что работает | Что не работает |
|----------|--------------|------------------|
| Сеть есть, синк свежий | Всё | — |
| Сеть есть, sync не запускался N часов | Всё (кэш отдаёт старое) | Свежесть других машин |
| Сеть пропала | `tasks.*`, `knowledge.search/get` — всё работает | `knowledge.suggest_promote` работает; `git push` после промоушена откладывается |
| Кэш отсутствует (новая машина без bootstrap) | Только `meta.status()` (отдаёт diagnostic с пустым кэшем) | Всё остальное |
`meta.status()` всегда возвращает возраст кэша, чтобы Claude мог предупредить пользователя «данные старше 24 часов».
## Авторизация
- Gitea API token: scope `read:repository` (и `write:repository` для пуша projects-wiki).
- Хранится в `~/.config/projects-mcp/auth.toml` (gitignore'd).
- MCP-сервер сам не читает токен — только `sync-script`. MCP оперирует уже скачанными локальными файлами.
## Тестирование
- **Unit:** `domain-detector`, парсер `STATUS.md`, эвристика промоушена.
- **Integration:** sync-script против локального Gitea (Docker fixture). Проверка: rate limit, 404 на отсутствующий `STATUS.md`, частичный отказ.
- **MCP smoke test:** запустить сервер, дёрнуть каждый tool через MCP-клиент-fixture.
## Открытые вопросы (не блокеры)
1. **Cron vs git-hook vs руками** — определится при первом использовании. Стартовать с «руками + npm script», добавить cron когда станет лень.
2. **Уведомление о stale-кэше** — порог в часах? Стартовать с 24, корректировать.
3. **Multi-user** — пока однопользовательский (`OpeItcLoc03`). Если кто-то ещё начнёт пользоваться — переделать `users/{owner}/repos` в параметр из `auth.toml`.
4. **`projects-wiki` GC** — раз в полгода аудит, какие страницы не использовались. Будет отдельный tool `meta.audit_shared(months)` в v2.
## Дальнейшие шаги
1. Создать репозиторий `projects-meta-mcp` на Gitea, запушить этот скелет.
2. Создать репозиторий `projects-wiki` на Gitea с минимальной структурой (`CLAUDE.md`, `cross/`, `node/`).
3. Перейти к фазе writing-plans: расписать пошаговый план реализации (sync-script сначала, MCP-сервер вторым, эвристика промоушена третьим).