# 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` в `/.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-сервер вторым, эвристика промоушена третьим).