No description
  • Go 99.6%
  • Dockerfile 0.4%
Find a file
Ymnuk ba742bc7f5
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
Fix CI
2026-06-03 16:12:13 +03:00
cmd fix: динамическое добавление проиндексированных файлов с чанками в поиск 2026-06-03 16:08:25 +03:00
docs Добавление метрик мониторинга 2026-06-03 12:54:35 +03:00
internal fix: динамическое добавление проиндексированных файлов с чанками в поиск 2026-06-03 16:08:25 +03:00
.dockerignore fix: обработка рекурсивных директорий с файлами 2026-06-03 15:44:21 +03:00
.env.example Сохранение кэша в режиме local 2026-06-03 09:31:05 +03:00
.gitignore Добавление исключений при сборки 2026-06-03 12:01:43 +03:00
.golangci.yml Обработка документов 2026-06-02 08:49:58 +03:00
.woodpecker.yml Fix CI 2026-06-03 16:12:13 +03:00
docker-compose.yml Документация 2026-06-02 11:01:38 +03:00
Dockerfile Fix CI 2026-06-03 16:12:13 +03:00
go.mod Добавление метрик мониторинга 2026-06-03 12:54:35 +03:00
go.sum Добавление метрик мониторинга 2026-06-03 12:54:35 +03:00
LICENSE first commit 2026-06-01 20:05:12 +03:00
README.md Fix CI 2026-06-03 16:12:13 +03:00

mcp-doc-indexer

MCP-сервер для индексации и семантического поиска по документации в форматах Markdown (.md, .mdx) и plain text (.txt).

Два режима работы:

  • local — однопользовательский: сервирует ./docs через MCP (stdio или HTTP)
  • shared — многопользовательский multi-tenant: SQLite, Lazy Load, fsnotify, раздельные эндпоинты Control и Data Plane

Протокол — JSON-RPC 2.0 (MCP) на всех эндпоинтах. REST не используется.


Быстрый старт (local mode)

Требования

  • Go 1.22+
  • Embedding-сервер (совместимый с OpenAI API /v1/embeddings)

Запуск

# из исходников (локально)
go run ./cmd/mcp-doc-indexer

# из репозитория (Go 1.21+)
go run git.ymnuktech.ru/ymnuk/mcp-doc-indexer/cmd/mcp-doc-indexer@latest

# установка как утилиты
go install git.ymnuktech.ru/ymnuk/mcp-doc-indexer/cmd/mcp-doc-indexer@latest
mcp-doc-indexer

# сборка и запуск
go build -o mcp-doc-indexer ./cmd/mcp-doc-indexer
./mcp-doc-indexer

# HTTP вместо stdio
./mcp-doc-indexer --transport http

# с кастомным embedding-сервером
./mcp-doc-indexer --embedding-url http://localhost:11434/v1/embeddings

Пример MCP-запроса (stdio)

{"method":"tools/call","params":{"name":"search_docs","arguments":{"query":"как настроить CI"}}}

Подключение к MCP-агентам (Cline, Roo, Claude Desktop)

cline_mcp_settings.json / mcp.json:

{
  "mcpServers": {
    "doc-indexer": {
      "command": "/path/to/mcp-doc-indexer",
      "args": ["--mode", "local", "--transport", "stdio"],
      "env": {
        "EMBEDDING_URL": "http://localhost:8000"
      }
    }
  }
}

Для разработки — через go run:

{
  "mcpServers": {
    "doc-indexer": {
      "command": "go",
      "args": ["run", "./cmd/mcp-doc-indexer", "--mode", "local", "--transport", "stdio"]
    }
  }
}

Архитектура

                         ┌──────────────────────────────────┐
                         │         HTTP Router              │
                         │  (http.ServeMux)                 │
                         │                                  │
                         │  /mcp ────────────────────────────│───▶ Control Server
                         │  /mcp/{session_id} ──────────────│───▶ Session Server
                         └──────────────────────────────────┘
                                     │
                    ┌────────────────┼──────────────────┐
                    ▼                                    ▼
       ┌─────────────────────────┐       ┌─────────────────────────┐
       │ Control MCP Server      │       │ Session MCP Server      │
       │ (один на весь сервер)   │       │ (свой для каждой        │
       │                         │       │  зарегистрированной     │
       │ • register              │       │  сессии)                │
       │ • unregister            │       │                         │
       │ • list                  │       │ • search_docs           │
       └─────────────────────────┘       │ • get_content           │
                                         └─────────────────────────┘
                                                    │
                                                    ▼
                                         ┌─────────────────────┐
                                         │  session.Session    │
                                         │  • Load / Unload    │
                                         │  • Search           │
                                         │  • GetContent       │
                                         │  • State Machine    │
                                         │  • fsnotify         │
                                         └─────────────────────┘

Режимы

local shared
Путь ./docs (жёстко) --path-root + путь из register
БД SQLite (./docs/docs.db) SQLite (--db-path)
Lazy Load Нет (всегда Loaded) Да (Loaded ↔ Unloaded)
fsnotify Да Per-session
Сессии Одна, ID="local" Множественные
Control (register/unreg/list) Недоступны Доступны на POST /mcp
Data (search_docs/get_content) Доступны на POST /mcp Доступны на POST /mcp/{session_id}
Транспорт stdio (default) или http Только http

Инструменты MCP

Инструмент local shared Эндпоинт (shared) Описание
register да POST /mcp Создать сессию для директории
unregister да POST /mcp Удалить сессию
list да POST /mcp Список активных сессий
search_docs да да POST /mcp/{session_id} Семантический поиск
get_content да да POST /mcp/{session_id} Получить полный текст чанка

register

Создаёт новую сессию, индексирует файлы (.md, .mdx, .txt) в указанной директории и создаёт независимый MCP-сервер для Data Plane по адресу POST /mcp/{session_id}. Путь задаётся относительно --path-root. Синхронная операция — ответ только после завершения индексации.

  • Где доступен: только shared, POST /mcp
  • Вход: path (string, обяз.) — путь относительно path-root; description (string, опц.) — описание сессии
  • Выход: session_id (string) — UUID сессии; path (string) — абсолютный путь; chunks_count (int) — число проиндексированных чанков
  • Особенности: если директория не существует — создаётся (os.MkdirAll); выход за --path-root через .. запрещён; директория резолвится через filepath.Join + filepath.Clean с проверкой префикса; после регистрации клиент может подключиться к POST /mcp/{session_id} для поиска

unregister

Удаляет сессию: останавливает fsnotify-наблюдение, удаляет соответствующий Session MCP Server (эндпоинт /mcp/{session_id} перестаёт отвечать), сбрасывает dirty-данные в SQLite, чистит RAM.

  • Где доступен: только shared, POST /mcp
  • Вход: session_id (string, обяз.)
  • Выход: text: "unregistered"
  • Ошибка: -32000 если сессия не найдена

list

Возвращает метрики по всем зарегистрированным сессиям.

  • Где доступен: только shared, POST /mcp
  • Вход: не требуется
  • Выход: массив sessions с полями: session_id, path, description, state (Loaded/Unloaded), chunks_count, last_access (Unix timestamp)

search_docs

Семантический поиск по проиндексированным документам. Использует dot-product между эмбеддингом запроса и эмбеддингами чанков. В local mode при первом запуске индексирует все файлы и сохраняет результат в SQLite; при повторных запусках загружает данные из кеша (проверка mtime). В shared mode при первом запросе к сессии в состоянии Unloaded выполняет Lazy Load (автоматическая загрузка из SQLite + проверка изменений файлов по mtime).

  • Где доступен: local (POST /mcp) и shared (POST /mcp/{session_id})
  • Вход: query (string, обяз.) — текст запроса; limit (int, опц., default 10) — максимум результатов; min_score (float, опц., default 0.4) — минимальный порог схожести
  • Выход: массив результатов с полями: text (string) — превью чанка; score (float) — косинусная близость; file (string) — путь к исходному файлу; section_path (string, опц.) — иерархия заголовков; chunk_id (string) — ID для get_content
  • Особенности: в shared mode сессия идентифицируется через URL path (/mcp/{session_id}); в local mode используется единственная сессия; если min_score == 0, применяется значение --min-score из флагов

get_content

Возвращает полный текст фрагмента документа по его ID. Источник данных: RAM (если сессия Loaded) → SQLite (если Unloaded).

  • Где доступен: local (POST /mcp) и shared (POST /mcp/{session_id})
  • Вход: chunk_id (string, обяз.)
  • Выход: text (string) — полный текст чанка; line_start/line_end (int) — строки в исходном файле; char_start/char_end (int) — смещения в символах

State Machine (shared mode)

[Start] → register() → StateLoaded (RAM + fsnotify)
                          │
                  (IDLE_TIMEOUT)
                          ▼
                      StateUnloaded (RAM cleared, fsnotify stopped)
                          │
                   (incoming search_docs)
                          ▼
                  Lazy Load → mtime-diff sync → StateLoaded

Конфигурация

CLI-флаги

Флаг По умолчанию Описание
--mode local local или shared
--transport stdio stdio или http
--port 8080 HTTP порт
--host 0.0.0.0 HTTP адрес
--path-root ./projects Корень для путей сессий (shared)
--db-path ./projects/projects.db Файл SQLite (shared/local). Для local по умолчанию ./docs/docs.db
--idle-timeout 30m Таймаут простоя сессии (shared)
--embedding-url http://embedding:8000 URL embedding-сервера
--embedding-model default Модель эмбеддингов
--min-score 0.4 Порог схожести по умолчанию (0.11.0)
--version Показать версию

Все флаги также поддерживаются через переменные окружения (например, MODE=shared).

Docker

# Сборка
docker build --build-arg VERSION=v0.1.0 -t mcp-doc-indexer .

# Запуск (local mode, HTTP)
docker run -p 8080:8080 \
  -v ./docs:/docs:ro \
  mcp-doc-indexer -- --transport http

# Запуск (shared mode)
docker run -p 8080:8080 \
  -v ./projects:/projects \
  mcp-doc-indexer -- \
    --mode shared \
    --transport http

Docker Compose

# Запуск с embedding-сервером
docker compose up

# с кастомной директорией docs
DOCS_DIR=/path/to/docs docker compose up

Результаты тестирования

На технической документации из 9 Markdown-файлов.

Сравнение размера чанка (maxChunkSize)

Параметры: дробление по всем заголовкам h1h6, модель default.

Метрика 600 символов 1400 символов
Количество чанков 87 70
Средний размер чанка ~280 chars 311 chars
Максимальный score 0.494 0.590
Минимальный score 0.276 0.360

Сравнение дробления по заголовкам

Параметры: maxChunkSize = 1400, модель default.

Запрос Все h дробят h3+ мержатся
Архитектура MCP сервера 0.590 0.445
CI настройка и сборка 0.494 0.408
Семантический поиск 0.443 0.454
Как запустить тесты 0.439 0.424
Конфигурация Docker 0.401 0.363
Лицензия проекта 0.298 0.353

Вывод: дробление по всем заголовкам даёт более точные результаты (5 из 6 запросов). Каждый раздел документирует одну тему — отдельный чанк даёт чистый семантический сигнал. Оптимальная конфигурация: maxChunkSize = 1400 + дробление по всем заголовкам.

SQLite-кеш: холодный старт vs загрузка из БД

На той же документации (72 чанка, embedding-сервер llm-simple-api).

Запуск Время Результаты поиска
1-й (cold start) ~4 мин 6/6 запросов
2-й (из SQLite) 4 мс Идентичны

При повторном запуске сервер загружает 72 чанка из SQLite за миллисекунды, проверяет mtime файлов и пропускает полную индексацию. Эмбеддинги не пересчитываются — результаты поиска идентичны.

Ускорение: ~60000× (4 мин → 4 мс).

Сбросить кеш: demo --reset или удалить ./docs/docs.db.


Разработка

Сборка

go build -o mcp-doc-indexer ./cmd/mcp-doc-indexer

Версионирование

go build -ldflags "-X git.ymnuktech.ru/ymnuk/mcp-doc-indexer/internal/version.Version=v0.1.0" \
  -o mcp-doc-indexer ./cmd/mcp-doc-indexer

Тесты

# unit + integration (без внешних зависимостей)
go test -race ./...

# покрытие
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

Мониторинг

При HTTP-транспорте доступны два эндпоинта:

  • GET /health — JSON со статусом сервера, режимом, uptime, количеством сессий и чанков
  • GET /metrics — Prometheus-метрики в формате text/plain

Метрики (mcp_doc_indexer_search_total, mcp_doc_indexer_search_duration_seconds, mcp_doc_indexer_sessions_active и др.) автоматически регистрируются в глобальном реестре Prometheus.


Лицензия

MIT