@aimuzov/thinks-mcp
v0.3.0
Published
MCP server that writes, replies and rephrases in your own voice, learned from a Telegram export. Hands the calling model a measured style profile and your real messages — no API keys, no generation of its own.
Maintainers
Readme
thinks-mcp
MCP-сервер, который пишет, отвечает и переформулирует текст твоим голосом — выученным по выгрузке Telegram и истории твоих репозиториев.
Сервер ничего не генерирует сам и не ходит ни в какие API. Он отдаёт вызывающей модели бриф: измеренный стиль-профиль, настоящие твои сообщения, подобранные под конкретный запрос, числовые ограничения и формат ответа. Пишет по этому брифу сама модель — Claude Code, Claude Desktop, что угодно.
Сервер говорит по-русски: описания инструментов, брифы, профиль и CLI — всё на русском, а стилевые пробы (канцелярит, стоп-слова, плейсхолдеры при чистке) рассчитаны на русскоязычный архив. Английский понимают поиск и кодовые регистры, где встречаются оба языка.
Как это работает
Две фазы, разделённые файлом SQLite.
Сборка (руками, редко): выгрузка → фильтрация → чистка персональных данных → склейка сообщений в ходы → метрики → индексы.
Выдача (MCP-сервер): запрос инструмента → BM25-поиск по архиву → бриф.
Единица корпуса — не сообщение, а ход: серия сообщений подряд в пределах 90 секунд. Так и выглядит живая переписка: заметная доля сообщений идёт очередью, мысль разбивается на несколько коротких реплик вместо абзаца. Индексируй сервер отдельные сообщения — он учил бы обратному.
Установка
Нужен Node 24 или новее: корпус и поиск построены на node:sqlite с FTS5,
который стабилен начиная с этой версии.
Через mise
mise use -g npm:@aimuzov/thinks-mcpЛокально, из исходников
pnpm i && pnpm build && npm pack && npm i -g ./aimuzov-thinks-mcp-*.tgzУчти: npm i -g ставит бинарник в ту версию Node, которая активна в этот
момент. Если mise переключит версию, thinks-mcp пропадёт из PATH — поэтому
в конфиге MCP-хоста лучше запускать через mise exec, а не полагаться на голое
имя команды.
Сборка чат-корпуса
Выгрузи архив в Telegram: Settings → Advanced → Export Telegram data, формат JSON, снять галочки со всех медиа (нужен только текст). Затем:
thinks-mcp build ~/Downloads/Telegram\ Desktop/DataExport/result.jsonПорядок величины: несколько сотен тысяч сообщений собираются примерно за 15
секунд. Выгрузка парсится целиком в память, и пик примерно вшестеро больше
файла — на экспорте в 400 МБ это около 2.5 ГБ. Если Node не хватит кучи,
подними её: NODE_OPTIONS=--max-old-space-size=8192 thinks-mcp build ...
Где что лежит и собрано ли:
thinks-mcp where
thinks-mcp profileСборка корпуса кода
Второй, независимый корпус — комментарии из твоих репозиториев. Нужен для того, для чего чат-корпус не годится: писать комментарии в коде.
THINKS_CODE_EMAILS="me@personal,me@work" thinks-mcp code ~/Projects/*/ ~/work/repoАвторство определяется через git blame: блок попадает в корпус, только если
больше половины его строк написаны с указанных адресов. Чужие комментарии,
строки-разделители, закомментированный код и директивы инструментов
(eslint-disable, shellcheck source=) отсеиваются.
Даёт два регистра: code — инлайн, jsdoc — докблоки. Замеряются они
раздельно, потому что это разные жанры: инлайн обычно однострочный, а докблок
начинается с итоговой фразы и продолжается.
Повторная сборка переиспользует результаты git blame для файлов, которые не
менялись — ключ по blob-хешу. На десятке репозиториев это разница между
полуминутой и парой секунд.
Два корпуса живут в одной базе и не мешают друг другу: build пересобирает
только чат-регистры, code — только кодовые.
Корпус хранится в ~/.config/thinks-mcp/style.db — рядом с настройками, а не
рядом с кодом. Иначе при обновлении пакета он потерялся бы вместе со старой
версией.
Подключение
Скопируй .mcp.json.example в .mcp.json:
{
"mcpServers": {
"thinks": {
"command": "/opt/homebrew/bin/mise",
"args": ["exec", "npm:@aimuzov/thinks-mcp", "--", "thinks-mcp"]
}
}
}Если ставил из исходников через npm i -g — запускай через ту версию Node,
в которую лёг бинарник:
{
"mcpServers": {
"thinks": {
"command": "/opt/homebrew/bin/mise",
"args": ["x", "node@24", "--", "thinks-mcp"]
}
}
}Сервер стартует и без собранного корпуса и объясняет, что делать, — машина, где архив ещё не импортирован, получит рабочий сервер, а не упавший.
Инструменты
| Инструмент | Что делает |
|---|---|
| write_as_me | бриф для текста с нуля по заданию |
| reply_as_me | бриф для ответа на входящее сообщение |
| rephrase_as_me | бриф для переписывания готового текста |
| check_as_me | детерминированная оценка 0–100 и список отклонений |
| find_my_messages | поиск по архиву: «как я обычно отказываю» |
У каждого есть параметр register: dm — личка, group — групповой чат,
longform — длинный авторский текст, code — инлайн-комментарий, jsdoc —
докблок. Стиль в них разный, поэтому профиль, ограничения и поиск разведены по
регистрам, а check_as_me для кода проверяет своё: ширину строки, маркеры,
воду вместо факта, типы в фигурных скобках.
Для комментариев в коде брать только code и jsdoc. Чат-регистры замерены по
переписке — короткие реплики, разговорные формы, эмодзи — и в коде дают чужой
голос.
У write_as_me и find_my_messages есть ещё lang (ru/en) — осмысленно
для кода, где используются оба языка.
Остальные параметры:
examples(4–40, по умолчанию 18) у трёх инструментов-брифов — сколько примеров из архива подложить в бриф;length(short/normal/long) уwrite_as_me— относительно обычного для регистра объёма;hintуreply_as_me— что должно быть сказано в ответе;limit,yearFromиmatchIncomingуfind_my_messages; последний ищет по сообщениям собеседников, а не по твоим.
У check_as_me есть необязательный параметр code — строки, над которыми стоит
комментарий. С ним проверка ловит и пересказ кода. Ограничение честное: она
сравнивает слова, поэтому русский комментарий над английским кодом оценить не
может и промолчит.
Ресурсы: style://profile и style://profile/{register} — профиль как
markdown. Промпты: as-me, reply-as-me и comment-as-me.
Рабочий цикл, который задают промпты: получить бриф → написать →
check_as_me → переписать по замечаниям, пока оценка не станет высокой.
Чего стоит пример ответа
reply_as_me строит примеры из пар, а пары бывают двух сортов.
Сообщение с reply_to_message_id — это факт: автор сам выбрал, на что
отвечает. Всё остальное выведено из порядка сообщений, то есть «что написали
перед этим». Такая догадка ломается там, где переписка обычнее всего: тебе
пишут «Извини», ответ совсем про другое, и пара учит модель отвечать не по теме.
Поэтому пары с явной цитатой ранжируются выше выведенных, а выведенная пара, на которую автор потратил больше получаса, — ещё ниже. У каждого примера подписано, какой он.
В некоторых регистрах пар почти нет: выгрузка Telegram для супергруппы почти не содержит сообщений собеседников. Там бриф прямо это говорит, а не выдаёт подходящие по теме ходы за ответы.
Свежесть
За десять лет переписки привычки заметно смещаются — пунктуация, длина реплики, ритм. Профиль, усреднённый по всему архиву, не описывает ни сегодняшнего человека, ни его же десятилетней давности. Поэтому:
- профиль и ограничения считаются по последним годам (
THINKS_RECENT_YEARS), а цифры за всё время показываются справочно; - выдача поиска взвешивается по году — свежий пример при прочих равных выигрывает. Вес подобран так, чтобы десятилетие возраста стоило примерно треть типичного разброса BM25 в выдаче: свежесть влияет, но нерелевантное новое не обгоняет релевантное старое.
Знаки препинания
Кавычки-ёлочки, длинное и короткое тире, лапки — типографика печатной книги, и
модель ставит её по умолчанию. Ставит ли её владелец архива, решает замер: доля
сообщений с этим знаком считается отдельно для чата и для каждого кодового
жанра. Ниже 2% знак попадает в профиль как чужой, check_as_me за него
штрафует, а бриф просит вместо тире дефис и простые кавычки. Порог здесь свой,
не общий для антипаттернов: дефис в 1% сообщений уже выдаёт текст, а вот
канцелярское слово при такой доле — ещё нет.
Двойной дефис измеряется, но никогда не штрафуется. Это не чужой знак, а ASCII-замена тире, и она разная по регистрам: в переписке её нет, в комментариях она может быть нормой. Там, где её доля от 1%, бриф прямо просит писать тире двумя дефисами.
Отдельная беда — комментарии, написанные с ассистентом. git blame считает их
твоими, а знаки в них его: в моём корпусе за 2026 год доля ёлочек в докблоках
подскочила с нуля до 6%. Год, начиная с которого комментарии писались уже не
вручную, задаётся в THINKS_CODE_HANDWRITTEN_UNTIL. Всё, что позже, остаётся
в индексе и в поиске, но в замер знаков не идёт. Если после отсечки в жанре
осталось меньше 200 строк, знаки для него не считаются вовсе: уверенный ноль на
двадцати строках хуже, чем честное отсутствие цифры.
Так же замеряются многоточие одним знаком … и стрелка →.
Некоторые знаки порогом не решить. Автозамена или раскладка могли годами ставить
— в рукописные комментарии, и архив считает это привычкой.
THINKS_NEVER_MARKS -- единственное правило, которое объявляется, а не
замеряется: —:--,…:...,→:=>,«:",»:" перечисляет знаки и то, что пишешь
вместо них. Объявленный знак штрафуется во всех регистрах при любой замеренной
доле, замечание цитирует его с соседними словами и называет замену, а бриф
просит писать замену.
Приватность
Это архив личной переписки, поэтому:
- выгрузка и собранный индекс не коммитятся никогда — каталог с данными лежит вообще вне репозитория;
- телефоны, почта и номера карт вырезаются по разметке Telegram — выгрузка гарантирует, что разбиение на сущности покрывает текст сообщения целиком, — плюс страховочные регулярки для того, что Telegram не разметил;
- чаты, отправители и репозитории хранятся под псевдонимами, настоящие имена в базу не попадают;
- фамилии вырезаются из текста сообщений, имена — нет: имя никого не идентифицирует, а без них примеры выглядели бы как документ с вымарками;
- из стиль-профиля имена собственные исключаются отдельно.
Исключить чаты целиком: THINKS_CHAT_STOPLIST="Чат один,Чат два".
Команды
thinks-mcp build <dump.json> # собрать чат-корпус
thinks-mcp code <репозитории> # собрать корпус комментариев
thinks-mcp profile jsdoc # профиль по регистру
thinks-mcp where # где лежит индекс
thinks-mcp holdout --answers # слепая проверка качества
thinks-mcp serve # то же, что без аргументов
thinks-mcp --helpВ самом репозитории:
mise run check # типы, форматирование, тесты
pnpm test
pnpm buildholdout — слепая проверка качества: при сборке 20 реальных пар
«входящее → ответ» откладываются и не попадают в индекс. Сначала смотришь
только входящие, отвечаешь через reply_as_me, потом сверяешь с тем, что было
отвечено на самом деле.
Переменные окружения
| Переменная | По умолчанию | Зачем |
|---|---|---|
| THINKS_DATA_DIR | $XDG_CONFIG_HOME/thinks-mcp или ~/.config/thinks-mcp | каталог с индексом |
| THINKS_DUMP | <data-dir>/dump.json | выгрузка, если не передана аргументом |
| THINKS_DB | <data-dir>/style.db | путь к файлу индекса |
| THINKS_OWNER_ID | автоопределение | если автоопределение ошиблось |
| THINKS_CHAT_STOPLIST | пусто | чаты через запятую, которые не индексируются |
| THINKS_CODE_EMAILS | git config --global user.email | git-адреса автора через запятую |
| THINKS_RECENT_YEARS | 3 | окно «как я пишу сейчас» для профиля |
| THINKS_CODE_HANDWRITTEN_UNTIL | нет | год, с которого комментарии писались не вручную: замер знаков их не берёт |
| THINKS_NEVER_MARKS | пусто | знаки, которые не набираешь, и замена, например —:--,…:...,→:=>,«:",»:" |
| THINKS_BURST_WINDOW | 90 | окно склейки сообщений в ход, секунды |
| THINKS_LONGFORM_MIN | 300 | порог longform-регистра, символы |
| THINKS_HOLDOUT | 20 | сколько пар отложить на слепую проверку |
Зависимости
@modelcontextprotocol/sdk и zod — и всё. Полнотекстовый поиск — FTS5 из
встроенного в Node node:sqlite, стеммеры русского и английского написаны
здесь же (src/search/stem.ts), потому что FTS5 токенизирует оба алфавита, но
не знает морфологии.
Лицензия
MIT
