Skip to main content
Glama

md-github

Небольшой MCP-сервер, который позволяет пользовательскому коннектору claude.ai выполнять точечные правки markdown в репозиториях GitHub одной учётной записи, объединяя любое количество изменений в ровно один коммит.

Claude аутентифицируется через OAuth 2.1 + DCR (этого требует форма коннектора). Секрет согласия каждого пользователя выбирает его собственный GitHub PAT и его собственную учётную запись. Ноль зависимостей во время выполнения, без базы данных, всё состояние в памяти.

Это не MCP-прокси для GitHub. Он предоставляет семь инструментов и ничего больше.

Инструменты

Инструмент

Что делает

overview

Один вызов для ориентирования в репозитории: корневой INDEX.md дословно плюс все пути к файлам с их размерами. Только чтение.

list_md

Каждый .md-файл с размером в байтах и SHA git-blob, опционально с планом заголовков каждого файла. Только чтение.

read_md

Точные байты одного файла — или нескольких файлов за один вызов — каждый со своим blob SHA и планом заголовков с диапазонами строк. Только чтение.

history

Недавние коммиты — кто автор каждого, когда и сообщение. Опциональный фильтр по пути. Только чтение.

show_commit

Автор одного коммита, сообщение и пофайловый дифф. Только чтение.

commit_edits

Применяет упорядоченный список правок и пушит их как один коммит. Единственный инструмент, который редактирует markdown.

create_repo

Создаёт репозиторий и наполняет его собственным INDEX.md.

Каждый инструмент, кроме 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 принимает четыре операции:

Операция

Поля

Примечания

write

path, content, mode

create (по умолчанию), overwrite (требует expect_sha), append.

str_replace

path, old_string, new_string, replace_all

Точное совпадение байтов; должно быть уникальным, если не задан replace_all.

edit_section

path, heading, mode, content

replace / append / prepend / delete — раздел, адресуемый по его заголовку.

delete

path, expect_sha

Удаляет файл.

Почему нет 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

JWT_SECRET

Подписывает токены, которые выпускает этот сервер.

PUBLIC_URL

Собственный базовый URL этого сервиса, без завершающего слэша.

PORT

Зафиксирован на 3000, чтобы соответствовать сгенерированному домену Railway.

GITHUB_API_URL

По умолчанию https://api.github.com. Тестовый шов.

Одна нумерованная тройка на человека:

Var

Notes

USER<N>_SECRET

Что этот человек вводит на странице согласия. Секрет и есть идентичность.

USER<N>_PAT

GitHub PAT этого человека. Используется только для его собственных запросов и составляет всю его зону доступа.

USER<N>_NAME

Необязательная метка, по умолчанию user<N>. Становится полем sub токена.

Это вся конфигурация на человека: секрет и 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

  1. Настройки → Коннекторы → Добавить пользовательский коннектор.

  2. URL: <PUBLIC_URL>/mcp.

  3. Оставьте идентификатор клиента и секрет пустыми.

  4. Подключить, затем введите свой собственный 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:smoke

tests/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 с выбранными репозиториями.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables to interact with GitHub repositories directly from Claude, supporting actions like viewing repos, checking status, committing and pushing changes, and managing pull requests.
  • A
    license
    Not graded
    quality
    C
    maintenance
    Connects Claude to GitHub repositories for querying repos, reviewing PRs, managing issues, searching code, and automating workflows.
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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