md-github
md-github
Небольшой MCP-сервер, который позволяет пользовательскому коннектору claude.ai выполнять точечные правки markdown в репозиториях GitHub одной учётной записи, объединяя любое количество изменений в ровно один коммит.
Claude аутентифицируется через OAuth 2.1 + DCR (этого требует форма коннектора). Секрет согласия каждого пользователя выбирает его собственный GitHub PAT и его собственную учётную запись. Ноль зависимостей во время выполнения, без базы данных, всё состояние в памяти.
Это не MCP-прокси для GitHub. Он предоставляет семь инструментов и ничего больше.
Инструменты
Инструмент | Что делает |
| Один вызов для ориентирования в репозитории: корневой |
| Каждый .md-файл с размером в байтах и SHA git-blob, опционально с планом заголовков каждого файла. Только чтение. |
| Точные байты одного файла — или нескольких файлов за один вызов — каждый со своим blob SHA и планом заголовков с диапазонами строк. Только чтение. |
| Недавние коммиты — кто автор каждого, когда и сообщение. Опциональный фильтр по пути. Только чтение. |
| Автор одного коммита, сообщение и пофайловый дифф. Только чтение. |
| Применяет упорядоченный список правок и пушит их как один коммит. Единственный инструмент, который редактирует markdown. |
| Создаёт репозиторий и наполняет его собственным |
Каждый инструмент, кроме create_repo, требует repo — простое имя ("notes") или "owner/name". Начните сессию с overview(repo); это единственный вызов, который сообщает, что там находится.
AGENT-TEMPLATE.md — это блок инструкций, который нужно вставить агенту, использующему этот коннектор, а ./connector-prompt.sh <repo> подставляет имя репозитория и копирует его в буфер обмена.
Related MCP server: brain-mcp
Много репозиториев, одно подключение
Учётная запись получает доступ к каждому репозиторию, который виден её PAT — собственному, совместному или доступному через организацию. Настраивать владельца не нужно: токен уже принадлежит одной учётной записи и уже несёт собственные права доступа, поэтому множество репозиториев, которые он видит, и есть пространство имён. Повторная настройка этого создала бы лишь второй источник истины, способный противоречить токену.
Простое repo:"notes" разрешается относительно этого видимого множества; repo:"owner/notes" пропускает поиск и обращается к репозиторию напрямую. Имя, видимое у двух владельцев, — это отказ с указанием обоих, а не догадка. При запуске ничего не перечисляется, поэтому репозиторий, созданный через create_repo, разрешается уже при следующем вызове без повторного развёртывания.
Репозитория по умолчанию нет
repo обязателен, и вызов без него — это ошибка, а не догадка. Когда за одним подключением несколько проектов, не существует «того самого» репозитория, и любой правдоподобный запасной вариант — первый попавшийся, настроенный при запуске, затронутый последним — это способ попасть правкой не в тот проект, при том что каждое сообщение по-прежнему выглядит как успех. Коммит не в тот репозиторий — также единственная здесь ошибка, которую не может поймать expect_sha, потому что защищаемый им blob находится в репозитории, на который никто не смотрит.
Ничто не выводит список репозиториев, доступных подключению — ни в результатах, ни при рукопожатии. Каждый инструмент явно называет свой репозиторий, поэтому для вызова никогда не нужен перечень, а для PAT с широкими правами он добавлял бы экран нерелевантных имён в каждый результат. Видимое множество читается из GitHub только для разрешения простого имени и никогда не печатается.
Это разрешение читает GET /user/repos, а не GET /users/:owner/repos — последний возвращает только публичные репозитории даже для вашей собственной учётной записи, поэтому приватный репозиторий заметок вообще не разрешился бы по имени. Результат кэшируется на пять минут, а при промахе перед ошибкой выполняется ещё одна выборка, так что репозиторий, созданный мгновение назад где-то ещё, всё равно разрешается.
Ничто не сужает подключение, кроме его PAT
Нет закреплённого репозитория, нет списка разрешённых репозиториев, нет префикса имени, нет переопределения ветки и нет ограничения поддеревом. Во всех ранних версиях это было; всё это удалено. Каждый из этих механизмов был второй границей рядом с собственными областями действия PAT и мог только противоречить ему, и каждый делал create_repo бессвязным — репозиторий, которого ещё не существует, не может быть в списке разрешённых, составленном при запуске. Каждый репозиторий использует свою собственную ветку по умолчанию по той же причине: одна учётная запись охватывает много репозиториев, и ветка, существующая в одном, редко существует в следующем.
PAT — это граница. Настройте его области действия в GitHub, где они реально действуют.
Каждый репозиторий документирует сам себя
Здесь намеренно нет межрепозиторного индексного файла. Каждый репозиторий документирует сам себя в собственном корневом INDEX.md — это файл, которым create_repo наполняет репозиторий и который этот сервер возвращает в контекст. Реестр в одном репозитории был бы вторым местом, где живёт истина, и он устарел бы при первом же переименовании проекта вне коннектора.
Ориентирование: overview
Сессия начинается с вызова overview(repo). Один цикл запроса-ответа возвращает корневой INDEX.md этого репозитория дословно — маршрутизатор, который говорит, какой файл отвечает на какой вопрос, — плюс все пути в репозитории с их размерами. В контекстном репозитории самого этого проекта это 152 пути и ~7k токенов, после чего модель знает, где что находится и насколько оно велико, прежде чем что-либо загружать.
Всё остальное следует из маршрутизатора: read_md({paths:[...]}) для индексных файлов папок, на которые он указывает, list_md({path_prefix, outline:true}) для сужения.
Намеренно не встраиваются: индексные файлы уровня папок. Один из них в контекстном репозитории этого проекта весит 66 КБ — встроить их все стоило бы дороже, чем прочитать файлы, которые они описывают.
Больше ничто никогда не прикрепляет индекс к результату. В ранней версии маршрутизатор добавлялся к результату каждого инструмента; индекс на 13 КБ — это ~3.5k токенов, поэтому сессия из десяти вызовов платила за одну порцию информации десять раз. overview доставляет её один раз, по запросу, и больше ничто никогда её не прикрепляет.
Индекс читается из первого из INDEX.md, index.md, README.md в корне репозитория, кэшируется на две минуты и инвалидируется любым коммитом через этот сервер — поэтому маршрутизатор, который модель только что переписала, никогда не читается устаревшим. Этот кэш и список разрешения имён — единственное, что кэширует этот сервер: ни то, ни другое никогда не является источником blob SHA, поэтому устаревшие данные не могут вызвать неверную запись. Дерево намеренно не кэшируется по этой причине.
Кто что редактировал
Внедряется собственный PAT каждого человека, поэтому GitHub записывает реального человека как автора коммита — это подлинная атрибуция git, а не что-то, синтезированное сервером. history отвечает на вопрос «кто изменил этот файл», show_commit показывает фактический дифф, а git blame работает обычным образом вне приложения.
Два ограничения, о которых стоит знать. history(path) не отслеживает переименования, поэтому коммиты, сделанные до переименования, перечисляются по старому пути — так же, как git log без --follow. А список файлов коммита разбивается GitHub на страницы по 300 файлов; инструмент сообщает, когда достиг этой границы, а не выдаёт частичный список за полный.
Чтение нескольких файлов за раз
read_md принимает paths: [...] (до 20) вместо path. Чтение пяти индексных файлов папок — это тогда один цикл запроса-ответа вместо пяти, а max_bytes становится бюджетом, общим для всего пакета и расходуемым в заданном порядке. Пакет намеренно не работает по принципу «всё или ничего»: несуществующий путь сообщает о собственной ошибке, а остальные по-прежнему возвращаются. «Всё или ничего» — свойство commit_edits, где частичный результат означал бы повреждённый репозиторий; здесь это стоило бы лишь одного цикла.
list_md принимает outline: true, чтобы показать заголовки каждого файла, не читая его. Это одно чтение на файл, поэтому при более чем 40 файлах запрос отклоняется и подсказывает, как сузить выборку.
Создание репозитория
create_repo({name, overview}) создаёт репозиторий у владельца подключения и наполняет его INDEX.md вида # <name> плюс overview. Overview задуман как документ, на который читатель попадает первым, а не как однострочное резюме, — это маршрутизатор данного репозитория.
Его два эффекта в GitHub — создание репозитория, затем его первый коммит — не могут быть одной транзакцией, поэтому они сообщаются раздельно. Если наполняющий коммит не удаётся, результат сообщает, что репозиторий существует и пуст, и называет точный вызов commit_edits, который завершит работу. Он не удаляет только что созданный репозиторий: уничтожение пространства имён ради приведения ошибки в порядок — гораздо более серьёзный сбой, чем пустой репозиторий.
Запись в репозиторий без коммитов
В совершенно новый репозиторий вообще нельзя писать через git-data API GitHub: blobs, деревья и коммиты отвечают 409 Git Repository is empty. Единственная работающая конечная точка — PUT /contents, которая создаёт ветку и начальный коммит одним запросом, — поэтому именно ею наполняет create_repo, и это единственное место, где этот сервер вызывает PUT /contents.
Он записывает ровно один файл, поэтому ровно один файл может нести пакет в пустой репозиторий. Многофайловый пакет отклоняется с инструкциями, а не разбивается на два коммита, потому что «один вызов — один коммит» — это гарантия, на которой держится вся архитектура.
POST /user/repos создаёт в учётной записи, которой принадлежит токен, а не под именем из тела запроса, — поэтому PAT, который лишь участвует в чужих репозиториях, создаёт новые в собственной учётной записи. Результат сообщает full_name, возвращённый GitHub, а не имя, которое предположил этот сервер. По имени он разрешается уже при следующем вызове.
Для создания репозитория нужно больше, чем Contents: Read and write. Классический PAT с областью repo работает; fine-grained PAT требует Administration: Read and write, вообще не может создавать репозитории в личной учётной записи (только в организации), а если он ограничен выбранными репозиториями, то всё равно не смог бы писать в новый репозиторий, — поэтому для работы с несколькими репозиториями нужны All repositories. При 403 сообщается именно это, а не общий текст про содержимое.
commit_edits принимает четыре операции:
Операция | Поля | Примечания |
|
|
|
|
| Точное совпадение байтов; должно быть уникальным, если не задан |
|
|
|
|
| Удаляет файл. |
Почему нет start_commit / end_commit
Очевидный дизайн — это область подготовки, которую вы открываете, а позже закрываете сообщением. От него отказались намеренно: он создаёт место, где готовая на вид работа может лежать неопубликованной, так что «я внёс правки и забыл закоммитить» становится возможным. Три независимых ревью дизайна пришли к одному и тому же выводу.
Вместо этого нет вообще никакой промежуточной области. commit_edits атомарен — весь пакет изменений
в множестве файлов применяется и отправляется одним вызовом, либо в GitHub не уходит ничего. Агент
накапливает свой план в собственном контексте (том единственном хранилище, которое модель читает надёжно) и расходует его за
один вызов. Каждый результат инструмента завершается постоянной строкой о том, что ничего не ожидает обработки, так что представление о наличии очереди опровергается непрерывно, а не обнаруживается позже.
Также нет таймера автоматического коммита ни при каком таймауте. Таймер простоя публикует работу, которую никто не одобрял, — отозванное удаление, незавершённую реструктуризацию. Ноль нежелательных коммитов — это проектное свойство, а не упущение. При завершении работы сервер записывает в журнал то, что отбрасывает, и не коммитит ничего.
Единственный сохраняемый элемент состояния — только при сбое: неудачный пакет удерживается 30 минут как
retry_ref, чтобы большой пакет не приходилось повторно вводить из контекста, который, возможно, уже был
сжат. Он объявляется в каждом последующем результате, очищается любым успехом и никогда
не может породить квитанцию об успехе. Он также привязан к репозиторию, для которого был создан:
повторное воспроизведение его в другом репозитории отклоняется, потому что эти правки были построены из текста, которого другой
репозиторий никогда не содержал.
Атомарность, если точно
commit_edits выполняет две фазы, и граница между ними и есть гарантия.
План — проверка, получение снимка, проверки
expect_sha, применение каждой операции к буферам в памяти. Любой сбой прерывает процесс здесь, после отправки толькоGET-запросов. Не «откат»: ни один изменяющий запрос не был отправлен. Все сбои в пакете сообщаются вместе, поэтому пакет из 12 операций с 3 дефектами стоит один ход, а не три.Выполнение — три изменяющих запроса (
POST /git/trees,POST /git/commits,PATCH /git/refs) независимо от того, сколько файлов изменено. Наблюдаем только финальныйPATCH.
Итак, после любого вызова есть ровно два наблюдаемых состояния: либо существует один коммит, либо ветка побайтно идентична прежней.
expect_sha требуется ровно там, где операция уничтожает целый файл, — delete и
write mode=overwrite. Нельзя целиком заменить или удалить файл, который вы никогда не наблюдали.
list_md возвращает полные SHA блобов, поэтому для удаления никогда не нужно читать содержимое.
Конкурентность
Содержимое повторно читается в момент коммита из единственного зафиксированного снимка, поэтому окно чтения-изменения-записи составляет
около секунды, а не длину разговора. PATCH ... force:false — это настоящий
compare-and-swap на стороне сервера; force: true не отправляется никуда. При коллизии весь план
повторно выполняется относительно новой вершины: если ничего из того, чего касается пакет, не сдвинулось, он применяется молча; если сдвинулось,
он останавливается и возвращает свежее содержимое из upstream прямо в ответе, а не затирает его.
Окружение
Var | Notes |
| Подписывает токены, которые выпускает этот сервер. |
| Собственный базовый URL этого сервиса, без завершающего слэша. |
| Зафиксирован на 3000, чтобы соответствовать сгенерированному домену Railway. |
| По умолчанию |
Одна нумерованная тройка на человека:
Var | Notes |
| Что этот человек вводит на странице согласия. Секрет и есть идентичность. |
| GitHub PAT этого человека. Используется только для его собственных запросов и составляет всю его зону доступа. |
| Необязательная метка, по умолчанию |
Это вся конфигурация на человека: секрет и PAT. Больше настраивать нечего — доступен каждый репозиторий, который видит PAT, и каждый вызов называет тот, с которым работает.
USER<N>_NAME — это ключ идентичности, а не метка: переименование кого-либо аннулирует их действующие токены, и
они должны переподключиться. Изменение их PAT вступает в силу немедленно, без переподключения.
Перенос более старого развёртывания: удалите USER<N>_REPO, USER<N>_OWNER, USER<N>_REPOS,
USER<N>_REPO_PREFIX, USER<N>_BRANCH и USER<N>_ROOT, если они у вас есть. Ни одна из них больше не читается,
а секрет плюс PAT — это вся конфигурация.
Подключение из claude.ai
Настройки → Коннекторы → Добавить пользовательский коннектор.
URL:
<PUBLIC_URL>/mcp.Оставьте идентификатор клиента и секрет пустыми.
Подключить, затем введите свой собственный
USER<N>_SECRET.
Оба человека добавляют один и тот же URL; секрет, который вводит каждый, привязывает его сессию к его собственному PAT и репозиторию.
Тесты
npm install && npm run build
npm run test:unit # 92 assertions: scanner, edit ops, byte fidelity — no network
# integration: 258 assertions against a stateful fake GitHub
USER1_NAME=alice USER1_SECRET=secret-alice USER1_PAT=pat-alice \
USER2_NAME=bob USER2_SECRET=secret-bob USER2_PAT=pat-bob \
USER3_NAME=frank USER3_SECRET=secret-frank USER3_PAT=pat-frank \
JWT_SECRET=test-jwt PUBLIC_URL=http://127.0.0.1:8787 PORT=8787 \
GITHUB_API_URL=http://127.0.0.1:8899 npm start &
npm run test:smoketests/fake-github.mjs — это фейк с сохранением состояния с настоящей реализацией git-blob-SHA, DAG коммитов,
видимостью репозиториев по токенам (собственные и с совместным доступом, так что разрешение имён что-то доказывает),
журналом запросов и внедряемыми сбоями, поэтому коммит, сделанный через сервер, можно наблюдать последующим
чтением. Именно это позволяет набору тестов проверять то, что действительно важно: что N правок порождают
ровно один коммит и ноль вызовов PUT /contents, что удаление переживает сериализацию как буквальный
"sha":null, что force:false присутствует в каждом обновлении ссылки, что неудачный пакет оставляет
ноль изменяющих запросов, что конкурентный пуш коллеги никогда не затирается, что вызов, называющий
один репозиторий, не отправляет запросы ни в какой другой, что create_repo создаёт ровно один коммит в
ровно новом репозитории и сообщает учётную запись, под которой GitHub действительно его создал, и что пакет,
удержанный после сбоя, нельзя воспроизвести в другом репозитории.
Развёртывание
railway up --service mcp-github-proxy --detachСборка выполняется через Dockerfile, намеренно. Стандартный сборщик Railway (railpack) не может собрать этот сервис, выдавая
failed to solve: secret RAILWAY_GIT_REPO_OWNER not found — его сгенерированный план объявляет
секреты сборки RAILWAY_GIT_*, которые существуют только тогда, когда исходный код сервиса — подключённый GitHub-репозиторий,
а не загрузка tarball через CLI.
Примечания
Сканер Markdown — это настоящий блочный сканер CommonMark, а не регулярное выражение
^#{1,6}. Закрывающий---front matter — это допустимое подчёркивание setext H2, поэтому наивное сканирование выдумывает фантомный заголовок, названный по последней строке YAML, и агент правил бы прямо во front matter. Заголовки внутри ограждений, отступного кода, HTML-блоков и цитат корректно не адресуемы.Побайтовая точность намеренна: файлы с CRLF сохраняют CRLF на нетронутых строках, BOM отделяется, чтобы привязанный к началу
old_stringмог совпасть, и ничего никогда не обрезается — два завершающих пробела — это жёсткий перенос строки в Markdown.read_mdвозвращает содержимое без номеров строк на полях, потому что числа рядом с текстом, который модель собирается скопировать вold_string, — это ровно то, как номер строки попадает в искомый фрагмент. Номера строк появляются только в структурах и сообщениях об ошибках — оттуда ничего не копируется.Короткий файл, возвращённый целиком, не получает структуры. Структура — это карта файла, который вы не читали; печатать её над двенадцатью строками, которые она описывает, — шум. Она появляется снова, как только файл становится достаточно длинным, чтобы листать, или окно частичное.
Ошибка 401 от GitHub показывается как текст ошибки инструмента и никогда как HTTP-401 от
/mcp. Старый прокси передавалWWW-Authenticateот GitHub, из-за чего claude.ai отправлялся повторно аутентифицироваться в GitHub и возникал цикл повторной аутентификации, в то время как настоящая проблема — мёртвый PAT — оставалась невидимой.PAT — это настоящая граница безопасности, и теперь он также единственный, кто определяет доступ: ничто в конфигурации этого сервера не сужает его. Ограничьте область действия самого PAT в GitHub. Обратите внимание на противоречие с
create_repo: ему нужен токен, который может достигать репозиториев, не существовавших, когда токен был создан, что противоположно fine-grained PAT с выбранными репозиториями.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables to interact with GitHub repositories directly from Claude, supporting actions like viewing repos, checking status, committing and pushing changes, and managing pull requests.
- AlicenseNot gradedqualityBmaintenanceConnects claude.ai to a private GitHub repo of markdown files as a personal second brain, providing guarded read and write tools for knowledge management.20MIT
- AlicenseNot gradedqualityCmaintenanceConnects Claude to GitHub repositories for querying repos, reviewing PRs, managing issues, searching code, and automating workflows.MIT
- AlicenseNot gradedqualityCmaintenanceConnects Claude directly to GitHub repositories for reading code, making changes, committing, and managing branches and pull requests.118MIT
Related MCP Connectors
Connect AI assistants to your GitHub-hosted Obsidian vault to seamlessly access, search, and analy…
Connect AI assistants to GitHub - manage repos, issues, PRs, and workflows through natural language.
Edit your Overleaf LaTeX projects from Claude and ChatGPT; every change is a real Git commit.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jjenkins2004/mcp-github-proxy'
If you have feedback or need assistance with the MCP directory API, please join our Discord server