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