Wiki-MCP-Server с распределённым графом знаний и авторизацией

от автора

3 апреля 2026 года Андрей Карпати описал реально работающую систему, где он складывает необработанные исследовательские материалы в папку, показывает их LLM, которая с нуля создает и поддерживает всю взаимосвязанную вики-систему. ИИ пишет статьи, создает обратные ссылки между связанными идеями, классифицирует концепции и постоянно обновляет всю систему по мере поступления новых материалов. Вот промпт, который всё это делает LLM Wiki gist

Его новый рабочий процесс превращает необработанные исследовательские материалы в самоподдерживающуюся вики, базу знаний без RAG, только файлы Markdown и LLM, которая выполняет функции библиотекаря. Вместо того чтобы просматривать необработанные документы по каждому запросу, как в RAG, здесь LLM считывает исходный материал один раз и компилирует его в структурированную, организованную вики-систему. Его исследовательская вики-страница по одной теме разрослась примерно до 100 статей и 400 000 слов.

Мы взяли эту идею и инфраструктурно доработали:

Аспект

Karpathy LLM Wiki

Наша реализация

Хранение

Markdown-файлы в папке

AlloyDB + pgvector (SQL)

Связи

[[wikilinks]] в тексте

Типизированные рёбра графа (11 типов)

Поиск

Нет (или grep)

Векторный + графовый гибрид

Обновление

LLM переписывает markdown

SQL-функции + LLM-классификация из БД

Доступ

Локальная папка

HTTP-сервер с авторизацией

Мультипользовательность

Нет

Роли admin и reader

Протокол

MCP (StreamableHTTP + SSE)

Классификация связей

LLM при записи

ai.generate() прямо в SQL функции

Ключевое отличие: у Карпати wiki — это заметки Obsidian, которые LLM читает и пишет. У нас wiki — это база данных с графом, к которой несколько агентов обращаются через стандартизированный протокол. Карпати решал задачу «один человек + один LLM ведут заметки». Мы решаем задачу «несколько AI-агентов + человек совместно работают с растущей базой знаний». Разные масштабы — разная инфраструктура.

С чего всё начиналось

Домашняя wiki уже давно была назрела, так как проект оказался непрост. Смысл проекта — добавить блоки Titans в стандартную Gemma 3 и посмотреть что из этого выйдет. Технология Titans описана здесь

Конечно, сначала мы реализовали подход Karpaty, добавив векторный поиск по эмбеддингам.

Потом мы добавили граф знаний с типизированными рёбрами. Выбор технологии построения графа потребовал размышлений и занял некоторое время: Как выбирали технологию построения графа Но с ИИ-кодером реализация всегда быстрее, чем выбор технологий. Раз-раз, реализовали и увидели неплохой рост recall:

Метрика

До (вектор)

После (вектор + граф)

Avg recall

46.7%

68.3%

Enrichment

92 концепции найдены графом

… И серьезный прирост на абстрактных запросах:

Запрос

Было

Стало

«Что общего у MesaNet и Titans»

0%

67%

«Альтернативы Softmax attention»

0%

67%

«Ассоциативное сканирование»

50%

100%

«Дистилляция в TTT»

67%

100%

Граф в общем не нужен на точных запросах. Но там, где человек спрашивает «как связано X и Y», граф находит путь: Линейные RNN → Ассоциативное сканирование → NLM → M3 Optimizer — три прыжка по концепциям, которые векторный поиск не найдёт. Граф – это очень круто.

И всё было хорошо, пока wiki была личным инструментом. Сервер, который это всё обслуживал, был однопользовательским. stdio-транспорт — процесс запускается, обслуживает одного клиента, умирает. Это агент OpenClaw написал себе такую локальную базу знаний, и у него можно теперь спрашивать в телеграме. Точнее у нее, ее зовут Мнемозина, и она богиня знаний. Но. Хоть и прикольно писать код в телеграме, но иногда хочется вернуться в VS Code, да и проект, ради которого всё вот это, лежит на локальной машине. А значит нужен mcp-server. Stdio-транспорт MCP подразумевает, что сервер — это дочерний процесс клиента. Клиент подключился → сервер родился → клиент отключился → сервер умер. Для CLI-утилиты это нормально, но для сервера знаний, к которому стучатся 3-4 агента одновременно — не работает. Кроме того, чтобы подключить удалённого агента, приходилось пробрасывать порт через SSH-туннель со всеми паролями и явками, или запускать отдельный SSE-прокси. Кстати, запустили такой прокси, нормально отдает наружу. Но без авторизации. Все инструменты — и читающие, и пишущие — доступны любому, кто подключился. Агент-читатель случайно дёрнет graph_classify_edge — и перепишет типы рёбер.

Что изменилось в v3

StreamableHTTP + SSE в одном процессе

Мы перешли на двухтранспортную архитектуру в рамках одного Express-приложения:

POST/GET/DELETE /mcp        → StreamableHTTP (новый протокол MCP 2025-11-25)GET /mcp/sse + POST /mcp/messages → SSE (legacy, протокол 2024-11-05)

StreamableHTTP — это свежий стандарт MCP transport. Один endpoint /mcp, на нем три HTTP-метода. Клиент может открыть долговременное соединение, сервер держит сессию в памяти через InMemoryEventStore. Старые клиенты, которые умеют только SSE, подключаются через /mcp/sse — и это тоже работает.

Зачем оставлять два транспорта? Не все MCP-клиенты уже перешли на StreamableHTTP. SSE-legacy гарантирует, что ничего не сломается. Зачем это в нашем камерном проекте? А чтобы было.

Ролевая модель: admin и reader

Два уровня доступа:

Роль

Инструменты

Что может

admin

Все read + graph_upsert_node, graph_classify_edge, graph_dedup_edges

Чтение + мутации графа

reader

wiki_search, wiki_read, wiki_graph, wiki_tags, wiki_backlinks, wiki_graph_*

Только чтение

Роль определяется при подключении — через Basic Auth. Admin знает пароль админа, reader знает пароль читателя. Инструменты, недоступные роли, просто не регистрируются в MCP-сервере этой сессии. Ограничение здесь на уровне регистрации инструментов, а не runtime-проверки. MCP-клиент получает список доступных инструментов при initialize. Если инструмента нет в списке — он просто не появится в UI, не попадёт в tool_choice модели, не будет случайно вызван.

Кстати интересно получилось: мы дали другому агенту OpenClaw админский доступ, и он сказал, что инструменты изменения wiki он видит, но использовать не имеет права, потому что он простой кодер. То есть, дословно:

Сейчас у меня в подключены 15 инструментов через reader. Если я не прокинул write-инструменты — скорее всего это политика tools.profile: "coding", которая фильтрует мутационные MCP-инструменты.

Разделение по ролям решается на сервере так:

function buildServer(role) {  const server = new McpServer({ name: 'titans-wiki', version: '3.0.0' });  registerReadTools(server);  if (role === 'admin') registerAdminTools(server);  return server;}

При новом подключении — создаётся инстанс McpServer с нужным набором инструментов, подключается к выбранному транспорту. Всё.

Архитектура v3

                    ┌─────────────────────┐                    │   Express + CORS    │                    │   Port 8000         │                    └──────────┬──────────┘                               │                    ┌──────────▼──────────┐                    │  Auth Middleware    │                    │  Basic / Bearer     │                    └──────────┬──────────┘                               │               ┌───────────────┼───────────────┐               │                               │    ┌──────────▼──────────┐         ┌──────────▼──────────┐    │  StreamableHTTP     │         │  SSE (legacy)       │    │  /mcp               │         │  /mcp/sse           │    │  POST/GET/DELETE    │         │  + /mcp/messages    │    └──────────┬──────────┘         └──────────┬──────────┘               │                               │               └───────────────┬───────────────┘                               │                    ┌──────────▼──────────┐                    │  buildServer(role)  │                    │  ┌─ registerRead    │                    │  └─ registerAdmin   │  (только admin)                    └──────────┬──────────┘                               │                    ┌──────────▼──────────┐                    │  AlloyDB + pgvector │                    │  graph_nodes (72)   │                    │  graph_edges (215)  │                    │  wiki_pages         │                    └─────────────────────┘

LLM из SQL: ai.generate() в AlloyDB

Один из самых изящных трюков — классификация рёбер прямо из базы данных:

CREATE OR REPLACE FUNCTION graph_classify_edge(  _source_label TEXT,  _target_label TEXT,  _context TEXT DEFAULT NULL) ...  _result := ai.generate(_prompt);...

AlloyDB Omni с версии 1.5.2 поддерживает google_ml_integration — можно вызвать Gemini прямо из plpgsql. Один пакетный прогон — и 205 нетипизированных рёбер получили конкретные типы (depends_on, develops, based_on). Стоимость: $0.01 на весь прогон.

Трудности, с которыми столкнулись

  1. Направленность рёбер. MesaNet → Conjugate Gradient Solver — это depends_on? uses? used_in? Пришлось разделить uses (A использует B) и used_in (B применяется в A), плюс depends_on (A не может без B). Двукратный прогон классификации.

  2. Дубли рёбер. После LLM-классификации одна пара страниц имела два ребра: depends_on и mentions. Функция graph_dedup_edges() оставляет ребро с максимальным весом.

  3. authored_by наоборот. Gemini иногда ставила person → paper вместо paper → person. Правило: authored_by только для конкретных работ, не для обзорных тем.

  4. Версия AlloyDB. Для ai.generate() нужен google_ml_integration ≥ 1.5.2. У нас была 1.4.3. Обновление контейнера с 16.8.0 до 16.11.0 с перепривязкой volume — аккуратная операция с данными на борту.

Многопользовательский сценарий: как это работает сейчас

У нас два OpenClaw-агента:

  • Мнемозина (основной) — подключается с ролью admin, может всё: искать, читать, обновлять граф, классифицировать рёбра.

  • Полифем (второй агент) — подключается с ролью reader, только читает. graph_upsert_node и graph_classify_edge у него просто не появляются в списке инструментов.

  • gemini-cli на локальной машине – reader

  • Cline в VS-Code – reader

Набор инструментов v3

Read-инструменты (доступны всем):

Инструмент

Назначение

wiki_read

Чтение содержимого страницы

wiki_graph

Связи-[[wikilinks]] из страницы

wiki_tags

Страницы по тегу / все теги

wiki_backlinks

Обратные ссылки на страницу

wiki_graph_neighbors

BFS-обход от ноды

wiki_graph_path

Кратчайший путь между концептами

wiki_graph_context

Гибридный retrieval (вектор + граф)

wiki_graph_edges

Все рёбра определённого типа

wiki_graph_stats

Статистика графа + сироты

wiki_graph_contradictions

Поиск противоречий

Admin-инструменты (только admin):

Инструмент

Назначение

graph_upsert_node

Создать/обновить ноду

graph_classify_edge

LLM-классификация типа ребра

graph_dedup_edges

Удаление дублей, селф-лупов, разворот directed-ошибок

Чему мы научились

  1. Граф + вектор > вектор. На абстрактных запросах гибридный retrieval выигрывает у чистого векторного поиска. На точных — не проигрывает. Нет причин не использовать граф, если данные позволяют.

  2. Типы рёбер — это не украшение, а архитектура. 11 типов с весами (depends_on = 0.95, mentions = 0.3) позволяют ранжировать hop-результаты осмысленно, а не просто «найдено на расстоянии 2».

  3. Авторизация на уровне регистрации инструментов надёжнее runtime-проверок. Если инструмент не зарегистрирован — клиент его не видит. Модель его не вызывает. Пользователь его не ждёт.

  4. LLM из SQL — killer feature для энтерпрайз-графов. Нет нужды в промежуточном слое: вызов Gemini прямо из plpgsql через ai.generate() классифицирует рёбра. Кстати, там можно и эмбеддинги вычислять для строки, чтобы на SQL передавать текст параметром, а не вектор, но иногда этот вектор нужно повторно использовать на клиенте

  5. Два транспорта — таков миграционный путь. StreamableHTTP — будущее MCP. SSE — настоящее. Поддержка обоих в одном процессе означает, что клиенты переходят на новый протокол в своём темпе.

Что еще накрутили:

  • Графовый reasoning: не просто находить путь, но и объяснять его. «MesaNet связан с Titans через 3 хопа, потому что оба используют Surrogate Memory, которая основана на Fast Weight Programmers».

  • Авто-обнаружение противоречий: при добавлении нового источника проверять, не противоречит ли он существующим утверждениям.

Что еще интересно попробовать:

  • Версионирование рёбер: отслеживать, когда связь была создана и почему — для растущей wiki это становится критичным.

  • WebSocket-транспорт: для real-time уведомлений об изменениях графа.


Стек: Node.js, Express, AlloyDB Omni, pgvector, MCP SDK, Vertex AI (text-embedding-004), Gemini 2.5 Flash Lite (edge classification)

P.S. Кому интересно почитать про Titans, Miras, MesaNet и прочие технологии Test-Time Training (TTT), напишите, я дам подключение к этому MCP, он не опубликован на публичных хабах. А всякий научпоп – в канале @veriga_pro_AI

ссылка на оригинал статьи https://habr.com/ru/articles/1044200/