Я знаю, что ты думал в прошлый дейлик
Aw sheets, here we go again
-
Утро среды.
-
Вы медленно открываете meet/slack/rocket/etc и нажимаете на кнопку «
»
-
Имена людей в групповом звонке вам давно известны. Слова, произносимые людьми вам тоже, кажется, известны. Но что-то было вами забыто, что-то очень важное, что будет так нужно вспомнить в тот момент, как очередь доберется по вашу душу.
-
Через окно солнце щекочет экран монитора, заставляя вас отклоняться то вправо, то влево, дабы увидеть символы на мониторе. На крутом подоконнике журчат жирные голуби, а над вами сверлит до боли {любимый} сосед.
-
И вот внезапно подходит ваша очередь, и, как гром среди ясного неба, звучит неожиданное: «{HeroName}, что нам расскажешь сегодня?».
-
Вы, пытаясь выцарапать из чертогов полусонного разума, наконец вытаскиваете из головы обрывки кода, складывая их в рваные фразы, пытаясь раздуть важность сказанного обилием уточнений и ещё более длинным списком уточнений тех самых уточнений. Ещё пару десятков минут обсуждаете надуманные проблемы с менеджерами.
-
Очередь переходит к следующим участникам беседы.
-
Фоновым процессом вы продолжаете слушать других участников карнавала. Все кастуемые заклинания других пользователей группового чата моментально стираются вашей памятью.
-
Наконец звук в наушниках затихает, и вы снова отправляетесь выполнять рабочие квесты.
-
Повторите снова.
Если быть честным, муторные обсуждения вещей, которые вполне можно было бы оговорить письменно, нехило выбивают из фокуса — независимо от времени суток. Несмотря на исследования, в которых утверждается, что дейлики полезны, большую часть времени, проведённого на дейликах, я бы не отнёс к чему-то действительно ценному или стоящему траты времени.
Посему я долго думал, как можно скрестить полезное, хайповое и приятное — и пришёл к созданию собственного MCP-сервера для самодокументирующихся пушей в Git, с помощью которых можно составлять дайджесты и использовать RAG.
Идея
Долгое время я наблюдаю, как ИИ несётся вперед — несётся отбирать у меня работу по 300к/nanosec и вручать талончик на уютную должность на заводе. Несмотря на это зрелище, я всё же не отказываюсь от благ, которые даёт мне ИИ, хоть моя психика и сопротивляется этому изо всех сил.
Пару недель назад я установил себе Cursor. До этого всё время пользовался продуктами JetBrains.
Поюзав Cursor, я параллельно начал погружаться в MCP: читать документацию, тестировать написанные сообществом серверы.
В один из дней, когда глаза уже выжгло от бесконечных [fix]
в [commit_messages]
, и последний дейлик был позади, я решил — пора писать свой MCP-сервер. Такой, который бы прибил созвоны, задушил бессмысленные названия коммитов и превратил всё это в приятный для чтения фид, понятный как менеджерам, так и разработчикам.
Чтобы внедрить практику пушей через агента в команде, нужно, чтобы execution prompt был максимально приближен к «полевым условиям» (или просто — к привычным командам и контексту).
Какие ещё должны быть преимущества у данного решения?
-
Проверяется, есть ли уже
.git
; если нет — скрипт выполняетgit init
. -
Генерация сообщений коммитов с помощью LLM исходя из самого сообщения комита (в дальнейшем планируется добавить генерацию сообщения из данных внутри diff, или diffsumary, в случае если комит слишком большой).
-
Автоматическое определение текущей ветки, автоматически создавать
master
илиmain
, если их нет, и ставит upstream‑связь. -
Вместо набора ручных команд (
git init
,git config
,git add
,git commit
,git remote add
,git push
) достаточно написать одну команду агенту:
git push main [сообщение коммита] имя_репозитория
Такой подход не будет сильно корёжить разработчика — ну, почти.
Хотя в глаза сразу бросается странное: имя_репозитория
.
Неужели каждый раз нужно указывать полное название репозитория, в который я хочу запушить?
К сожалению, AI не знает, откуда его вызывают, в каком окружении он работает и кто именно инициирует вызов — если эта информация явно не передана в запросе. Это сделано специально — из соображений безопасности, универсальности и контроля доступа. Поэтому в конфиге вы указываете абсолютный путь (или пути), где находятся все ваши репозитории.
Для отладки есть отдельная тулза — list_repositories
. С её помощью можно в чате с AI-агентом получить, а точнее отебажить список всех локально доступных репозиториев.
После пуша хочется куда-то сохранять информацию о коммите. Например, можно поднять PostgreSQL, прикрутить pgvector
, накатить индексы и сделать семантический поиск по сущностям коммитов. В целом, решение рабочее. Но стоит помнить: ягода pg не для этого росла. В таких задачах куда разумнее использовать специализированные векторные базы — они производительнее, лучше масштабируются, и уже из коробки поддерживают все необходимые ML-модули. И так далее по списку.
В статьях про векторные БД чаще всего встречается ChromaDB, реже — Weaviate, Pinecone или Qdrant (возможно, мне просто знания букв не хватает для чтения). Из спортивного интереса, солидарности со «слабыми» и потому что функционала Weaviate из коробки достаточно для MVP, я выбрал именно её. На её основе мы реализуем RAG — и для тех, кто хочет знать, кто вчера уронил продакшен, и для автоматической генерации дайджестов по коммитам прошедшего дня.
В завершение подключим уведомления в Slack: так вся команда будет в курсе всех твоих «[fix]».

MCP server starts…
Для начала разберемся, что такое MCP сервера.
MCP‑сервер — это сервис, функционально аналогичный API, предоставляющий агенту возможность взаимодействовать с внешними системами: файловой системой, поисковыми сервисами, мессенджерами и даже устройствами «умного дома» (Home Assistant).
Полный список серверов от сообщества можно найти здесь.
Ключевые понятия MCP‑системы:
-
GET‑запросы представлены в виде ресурсов для чтения данных;
-
POST, PUT, DELETE оформляются как инструменты (tools) для модификации состояния системы;
-
промпты в контексте MCP выступают в роли описания интерфейса клиента, аналогичного спецификации Swagger.
Для того чтобы начать создавать свой первый mcp-клиент или mcp-сервер можно использовать SDK, доступные для python, node, c#, java, kotlin. Так как я умею в ноду, и умею лучше, чем в python, то буду писать сервер на стеке node + typescript.
Устанавливаем зависимости и переходим к базе.
Для тех, кто не хочет читать, вот ссылка на сам mcp-сервер:
https://www.npmjs.com/package/@golddeity/gitdigester
Weaviate
Инициализируем клиент для weaviate.
import weaviate, { dataType, WeaviateClient, vectorizer, tokenization } from "weaviate-client";
const weaviateClient: WeaviateClient = await weaviate.connectToWeaviateCloud(
weaviateUrl,
{
authCredentials: new weaviate.ApiKey(config.weaviateKey || ""),
}
);
Если по соображениям безопасности вы не хотите использовать облачный Weaviate, можно развернуть образ базы и модели для эмбедингов с помощью docker compose локально. В данном случае я добавил модуль generative-mistral для RAG, так как mistral не требует пополнения счёта и можно погонять её локально бесплатно.
weaviate:
command:
- --host
- 0.0.0.0
- --port
- '8080'
- --scheme
- http
image: cr.weaviate.io/semitechnologies/weaviate:1.30.0
depends_on:
- t2v-transformers
ports:
- 8082:8080
- 50051:50051
volumes:
- weaviate_data:/var/lib/weaviate
restart: on-failure:0
environment:
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
ENABLE_API_BASED_MODULES: 'true'
ENABLE_MODULES: 'text2vec-transformers,generative-mistral'
MISTRAL_APIKEY: 'kQkzna77beFXrs0q4nrrF997LACYWAGk'
TRANSFORMERS_INFERENCE_API: http://t2v-transformers:8080 # Set the inference API endpoint
CLUSTER_HOSTNAME: 'node1'
t2v-transformers: # Set the name of the inference container
image: cr.weaviate.io/semitechnologies/transformers-inference:sentence-transformers-paraphrase-multilingual-MiniLM-L12-v2
environment:
ENABLE_CUDA: 0 # Set to 1 to enable
Сетапим схе:
const setUpWeaviate = async () => {
await weaviateClient.collections.create({
name: "commits",
vectorizers: vectorizer.text2VecOpenAI(),
properties: [
{
name: "commitMessage",
dataType: dataType.TEXT,
tokenization: tokenization.LOWERCASE,
},
{
name: "commitHash",
dataType: dataType.TEXT,
},
{
name: "commitDate",
dataType: dataType.DATE,
},
{
name: "commitAuthor",
dataType: dataType.TEXT,
},
{
name: "commitBranch",
dataType: dataType.TEXT,
},
{
name: "commitDiff",
dataType: dataType.TEXT,
},
],
});
}
GIT
Для git будем использовать библиотеку simple GIT. Это более надёжное решение, чем работать с гитом через спавнеры процессов. Внутри библиотеки уже реализованы все обработки ошибок, методы возвращают результаты выполнения в структурированном виде, что избежать парсинга stdout, stderr.
Вначале обновляем URL удаленного репозитория в зависимости от параметра аутентификации указанного в параметрах конфига клиентом MCP сервера.
git = simpleGit(gitOptions);
const remotes = await git.remote(['get-url', 'origin']);
if (!remotes) return;
const remoteUrl = remotes.trim();
let newUrl = '';
Пользователь должен иметь возможность указать через аргументы тип аутентификации и путь до ключей.
"--git-auth-method=ssh","--git-ssh-key=/home/{your_pc}/.ssh/id_rsa",
if (config.gitAuthMethod === 'ssh' && remoteUrl.includes('https://')) {
try {
const httpsUrl = new URL(remoteUrl);
const host = httpsUrl.hostname;
const path = httpsUrl.pathname.replace(/^//, '');
newUrl = `git@${host}:${path}`;
console.error(`Converting HTTPS URL to SSH: ${newUrl}`);
await git.remote(['set-url', 'origin', newUrl]);
} catch (error) {
console.error(`Error converting HTTPS to SSH URL: ${error}`);
}
} else if (config.gitAuthMethod.startsWith('https') && remoteUrl.startsWith('git@')) {
try {
const sshMatch = remoteUrl.match(/git@([^:]+):(.+)/);
if (sshMatch) {
const [, host, path] = sshMatch;
newUrl = `https://${host}/${path}`;
if (config.gitAuthMethod === 'https-token' && config.gitUsername && config.gitToken) {
const urlObj = new URL(newUrl);
urlObj.username = config.gitUsername;
urlObj.password = config.gitToken;
newUrl = urlObj.toString();
}
console.error(`Converting SSH URL to HTTPS: ${newUrl}`);
await git.remote(['set-url', 'origin', newUrl]);
}
} catch (error) {
console.error(`Error converting SSH URL to HTTPS: ${error}`);
}
} else if (config.gitAuthMethod === 'https-token' && remoteUrl.includes('https://') &&
config.gitUsername && config.gitToken) {
try {
const urlObj = new URL(remoteUrl);
if (urlObj.username !== config.gitUsername || urlObj.password !== config.gitToken) {
urlObj.username = config.gitUsername;
urlObj.password = config.gitToken;
newUrl = urlObj.toString();
console.error(`Updating HTTPS URL with credentials`);
await git.remote(['set-url', 'origin', newUrl]);
}
} catch (error) {
console.error(`Error updating HTTPS URL: ${error}`);
}
-
С помощью
git.remote(['set-url', 'origin', newUrl])
обновляется URL удалённого репозитория. -
Если в конфигурации аутентификация выбранна как
'ssh'
, но текущий URL имеет форматhttps://...
, тогда нужно сконвертировать его в SSH-формат. -
Создаётся объект
URL
для удобного извлеченияhostname
(имени хоста) иpathname
(пути). -
Удаляется ведущий слэш из
pathname
. -
Формируется новый URL вида:
git@host:path
. -
Выводится сообщение в консоль (через
console.error
для логирования при использовании modelcontextprotocol/inspector, но об этом позже). -
Если выбран режим HTTPS (или его разновидность) и текущий URL имеет формат SSH (git@…), необходимо выполнить обратное преобразование.
-
С помощью регулярного выражения извлекаются хост и путь.
-
Формируется новый URL в формате https://host/path.
-
Дополнительная проверка: если режим аутентификации — https-token и заданы имя пользователя и токен, то эти креденшелы добавляются в URL через объект URL (устанавливая username и password).
-
Логику сетапа данных для аутентификации опустим, там ничего интересного нет.
Tools
Инициализируем инструменты для агента. В первому аргументе указывает название инструмента, во втором примерный промпт, чтобы AI агент определил в какой момент ему дергать тот или иной инструмент. С помощью zod и метода describe мы подсказываем агенту как вычленять параметры из промпта.
server.tool(
"git_push_origin",
"Process git commit and push operations with enhanced commit messages",
{
branchName: z.string().describe("Branch name"),
commitData: z.string().describe("Commit message"),
repositoryName: z.string().optional().describe("Repository name (optional)"),
currentDirectory: z.string().optional().describe("Current working directory of the user (optional)"),
},
async ({ branchName, commitData, repositoryName, currentDirectory }) => {
Далее мы автоматически извлекаем URL репозитория, выбираем текст последнего коммит‑сообщения и прогоняем его через DeepSeek — чтобы получить минималистичный, но ёмкий результат без лишних затрат. В будущем мы добавим две гибкие настройки: возможность передавать собственный промпт для LLM и флаг, указывающий, нужно ли включать diff
при расширении сообщения коммита. Однако для нашего MVP MCP текущего набора функций более чем достаточно.
const repoUrl = await git.remote(['get-url', 'origin']) || '';
let userName = '', userEmail = '';
let latestCommit = '';
try {
userName = (await git.raw(['config', 'user.name'])) || '';
userEmail = (await git.raw(['config', 'user.email'])) || '';
} catch (error) {
console.error("Error getting git user info:", error);
userName = "Unknown";
userEmail = "unknown@example.com";
}
const completion = await openai.chat.completions.create({
model: "deepseek-chat",
messages: [
{
role: "system",
content: "Вы — ассистент, который расширяет и улучшает сообщения коммитов. Сделайте их более описательными и профессиональными, сохраняя исходный замысел. Переводите сообщение коммита на русский язык. Делайте это максимально коротко и понятно."
},
{
role: "user",
content: `Пожалуйста, расширьте и улучшите это сообщение коммита: "${commitData}"`
}
],
});
const enhancedCommitMessage = completion.choices[0]?.message?.content || commitData;
await git.add('.');
const commitResult = await git.commit(enhancedCommitMessage);
console.error(`Commit result:`, commitResult);
const gitDiff = await git.diff(['HEAD~1', 'HEAD']);
При попытке запушить изменения скрипт проходит через несколько шагов:
-
Определение текущей ветки
Сначала мы получаем название текущей ветки (currentBranch
) и сравниваем его с целевой (branchName
). Если они совпадают, ничего делать не нужно — просто пушим изменения. -
Проверка и переключение на целевую ветку
Если мы находимся не в той ветке, в которую хотим запушить:-
Выводим в консоль предупреждение о несоответствии веток.
-
Определяем хеш последнего коммита (
latestCommit = await git.revparse(['HEAD'])
). -
С помощью
git branch <branchName> --contains <latestCommit>
проверяем, есть ли этот коммит уже в целевой ветке:-
Если да, сообщаем об этом и выходим.
-
Если нет — готовимся к cherry‑pick’у.
-
-
-
Создание или переключение на ветку
-
Пытаемся выполнить
git.checkout(branchName)
. -
Если ветка не существует, Git автоматически создаст её (в режиме
checkout
без флага-b
) — или можно явно добавить-b
, чтобы было понятнее.
-
-
Чери-пик последнего коммита: Если во время применения патча возникает конфликт или пустой коммит, мы ловим ошибку и разбираем
errorMessage
. -
Обработка ошибок cherry‑pick’а
-
Пустой коммит (
nothing to commit
,previous cherry-pick is now empty
,cherry-pick is already started
):-
Пытаемся
git cherry-pick --skip
. -
Если и это не удалось — делаем
git cherry-pick --abort
.
-
-
Любая другая ошибка: сразу
git cherry-pick --abort
.
На каждом шаге выводим в консоль подробный лог — что пошло не так и какие команды выполнились.
-
-
Возврат на изначальную ветку
После успешного или прерванного cherry‑pick’а скрипт обязательно переключается обратно наcurrentBranch
(или на сохранённыйoriginalBranch
), чтобы не оставлять пользователя в незнакомом контексте. -
Сохраняем в weavite данные:
const commitsCollection = weaviateClient.collections.get("Commits");
const uuid = await commitsCollection.data.insert({
commitMessage: enhancedCommitMessage,
commitDate: new Date().toISOString(),
commitHash: latestCommit,
commitAuthor: userName,
commitEmail: userEmail,
commitBranch: branchName,
commitDiff: gitDiff,
});
8. Отправляем вебхук в slack.
if (config.slackWebhook) {
try {
await axios.post(config.slackWebhook, {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "New Git Commit"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Repository:*n${repoUrl.trim()}`
},
{
type: "mrkdwn",
text: `*Branch:*n${branchName}`
}
]
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Author:*n${userName.trim()} <${userEmail.trim()}>`
}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Original message:*n${commitData}`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Enhanced message:*n${enhancedCommitMessage}`
}
}
]
});
9.Возвращаем ответ агенту. Для ответов важен формат, поэтому именно в таком виде.
content: [
{
type: "text",
text: [
`
Commit processed successfully!`,
`Branch: ${branchName}`,
`Original message: ${commitData}`,
`Enhanced message: ${enhancedCommitMessage}`,
config.weaviateKey ? "Data saved to Weaviate." : "Weaviate integration skipped.",
config.slackWebhook ? "Notification sent to Slack." : "Slack notification skipped.",
`Note: Manual 'git push origin ${branchName}' is required to complete the operation.`
].join("n")
}
]
RAG
После наполнения базы достаточным количеством информации о комитах, мы можем начать отправлять query и получать выжимку с помощью любой генеративной модели.
Получим объект коллекции.
const commitsCollection = weaviateClient
.collections
.get("Commits");
Выполним векторный поиск по тексту запроса.
const searchRes = await commitsCollection.query
.nearText([query], {
limit: 5,
returnProperties: ["commitMessage", "commitAuthor", "commitDate"],
returnMetadata: ["distance"],
})
.do();
Формируем контекст для модели и отправляем запрос на генерацию текста на основе полученных данных.
const commits = searchRes.data.Get.Commits; //
const context = commits
.map((c: any) => `• [${c.commitDate}] ${c.commitAuthor}: ${c.commitMessage}`)
.join("n");
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "Вы — ассистент, который отвечает на вопросы на основе истории коммитов.",
},
{
role: "user",
content: `Ниже список последних коммитов:n${context}nnВопрос: ${query}`,
},
],
});
console.log("Ответ LLM:", completion.choices[0].message?.content);
Итог
По итогу на выходе несложная имплементация для mcp-сервера, позволяющая генерировать документацию и получать данные в человекочитаемом виде менеджерам-проектов и разработчикам.
В ближайших планах:
-
Добавить cron‑задачи для автоматического формирования и рассылки дайджестов по ключевым изменениям.
-
Реализовать гибкий tool‑интерфейс агента для запросов в Weaviate (поддержка различных фильтров, векторных и keyword‑поисков).
-
Ввести возможность пользовательской настройки промптов для LLM и расширить логику обработки diff‑патчей (например, конкатенация нескольких коммитов в один отчёт).
Автор: olddeity