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}\n\nВопрос: ${query}`, }, ], }); console.log("Ответ LLM:", completion.choices[0].message?.content);
Итог
По итогу на выходе несложная имплементация для mcp-сервера, позволяющая генерировать документацию и получать данные в человекочитаемом виде менеджерам-проектов и разработчикам.
В ближайших планах:
-
Добавить cron‑задачи для автоматического формирования и рассылки дайджестов по ключевым изменениям.
-
Реализовать гибкий tool‑интерфейс агента для запросов в Weaviate (поддержка различных фильтров, векторных и keyword‑поисков).
-
Ввести возможность пользовательской настройки промптов для LLM и расширить логику обработки diff‑патчей (например, конкатенация нескольких коммитов в один отчёт).
ссылка на оригинал статьи https://habr.com/ru/articles/903802/
Добавить комментарий