Skip to main content
Glama

md-github

Un pequeño servidor MCP que permite a un conector personalizado de claude.ai hacer ediciones quirúrgicas de markdown en los repositorios de GitHub de una cuenta, agrupando cualquier número de cambios en exactamente un commit.

Claude se autentica con OAuth 2.1 + DCR (que el formulario del conector requiere). El secreto de consentimiento de cada persona selecciona su propio PAT de GitHub y su propia cuenta. Cero dependencias en tiempo de ejecución, sin base de datos, todo el estado en memoria.

Esto no es un paso a través de GitHub MCP. Expone siete herramientas y nada más.

Las herramientas

Herramienta

Qué hace

overview

Una llamada para orientarse en un repositorio: su INDEX.md raíz verbatim más cada ruta de archivo con su tamaño. Solo lectura.

list_md

Cada archivo .md con tamaño en bytes y SHA de blob de git, opcionalmente con el esquema de encabezados de cada archivo. Solo lectura.

read_md

Los bytes exactos de un archivo — o varios archivos en una sola llamada — cada uno con su SHA de blob y un esquema de encabezados con rangos de línea. Solo lectura.

history

Commits recientes — quién los autoró, cuándo y el mensaje. Filtro de ruta opcional. Solo lectura.

show_commit

Autor, mensaje y diff por archivo de un commit. Solo lectura.

commit_edits

Aplica una lista ordenada de ediciones y las empuja como un commit. La única herramienta que edita markdown.

create_repo

Crea un repositorio y lo siembra con su propio INDEX.md.

Toda herramienta excepto create_repo requiere repo — un nombre simple ("notes") o "owner/name". Inicia una sesión con overview(repo); es la única llamada que te dice qué hay ahí.

AGENT-TEMPLATE.md es el bloque de instrucciones para pegar en un agente que usará este conector, y ./connector-prompt.sh <repo> rellena el nombre del repositorio y lo copia al portapapeles.

Related MCP server: brain-mcp

Muchos repositorios, una conexión

Una identidad alcanza cada repositorio que su PAT pueda ver — propios, colaborados o mediante una organización. No hay propietario que configurar: un token ya pertenece a una cuenta y ya lleva su propio acceso, así que el conjunto de repositorios que puede ver es el espacio de nombres. Configurarlo de nuevo solo crearía una segunda fuente de verdad que puede discrepar con el token.

Un repo:"notes" simple se resuelve contra ese conjunto visible; repo:"owner/notes" omite la búsqueda y aborda el repositorio directamente. Un nombre visible bajo dos propietarios es una negativa que nombra a ambos, no una suposición. Nada se enumera al arrancar, así que un repositorio creado por create_repo se resuelve en la siguiente llamada sin necesidad de redesplegar.

No hay repositorio predeterminado

repo es obligatorio, y una llamada que lo omita es un error en lugar de una suposición. Con varios proyectos detrás de una conexión no existe "el" repositorio, y cualquier alternativa plausible — el primero, el configurado al arrancar, el tocado por última vez — es una forma de que una edición aterrice en el proyecto equivocado mientras cada mensaje sigue pareciendo un éxito. Un commit en el repositorio equivocado es también el único error aquí que expect_sha no puede detectar, porque el blob que protege está en un repositorio que nadie está mirando.

Nada lista los repositorios que una conexión puede alcanzar — ni en los resultados, ni en el apretón de manos. Cada herramienta nombra su repositorio explícitamente, así que nunca se necesita un roster para hacer una llamada, y para un PAT de alcance amplio pondría una pantalla llena de nombres irrelevantes en cada resultado. El conjunto visible se lee de GitHub solo para resolver un nombre simple, y nunca se imprime.

Esa resolución lee GET /user/repos, no GET /users/:owner/repos — este último devuelve solo repositorios públicos incluso para tu propia cuenta, así que un repositorio de notas privado no se resolvería por nombre en absoluto. Se cachea durante cinco minutos, y un fallo re-fetch una vez antes de fallar, así que un repositorio creado hace un momento en otro lugar aún se resuelve.

Nada limita una conexión excepto su PAT

No hay repositorio fijado, ni lista blanca de repositorios, ni prefijo de nombre, ni anulación de rama, ni confinamiento de subárbol. Versiones anteriores tenían todos ellos; fueron eliminados. Cada uno era un segundo límite junto a los alcances del propio PAT, capaz solo de discrepar con él, y cada uno hacía incoherente create_repo — un repositorio que aún no existe no puede estar en una lista blanca escrita al arrancar. Cada repositorio usa su propia rama predeterminada, por la misma razón: una identidad abarca muchos repositorios, y una rama que existe en uno rara vez existe en el siguiente.

El PAT es el límite. Configúralo en GitHub, donde realmente tiene efecto.

Cada repositorio se documenta a sí mismo

Deliberadamente no hay archivo de índice entre repositorios. Cada repositorio se documenta a sí mismo en su propio INDEX.md raíz, que es el archivo que create_repo siembra y el archivo que este servidor alimenta de vuelta al contexto. Un registro en un repositorio sería un segundo lugar donde vive la verdad, y se volvería obsoleto la primera vez que alguien renombrara un proyecto fuera del conector.

Orientarse: overview

Una sesión comienza llamando a overview(repo). Un viaje de ida y vuelta devuelve el INDEX.md raíz de ese repositorio verbatim — el enrutador que dice qué archivo responde a qué pregunta — más cada ruta en el repositorio con su tamaño. En el repositorio de contexto de este propio proyecto eso son 152 rutas y ~7k tokens, después de lo cual el modelo sabe dónde está todo y cuánto pesa antes de obtener nada.

Todo lo demás sigue del enrutador: read_md({paths:[...]}) para los índices de carpeta a los que apunta, list_md({path_prefix, outline:true}) para acotar.

Deliberadamente no incluidos: los archivos de índice a nivel de carpeta. Uno de ellos en el repositorio de contexto de este proyecto tiene 66 KB — incluirlos todos costaría más que leer los archivos que describen.

Nada más adjunta nunca un índice a un resultado. Una versión anterior adjuntaba el enrutador a cada resultado de herramienta; un índice de 13 KB son ~3.5k tokens, así que una sesión de diez llamadas pagaba por una pieza de información diez veces. overview lo entrega una vez, cuando se pide, y nada más lo adjunta nunca.

El índice se lee del primero de INDEX.md, index.md, README.md en la raíz del repositorio, se cachea durante dos minutos y se invalida por cualquier commit a través de este servidor — así que un enrutador que el modelo acaba de reescribir nunca se lee de vuelta obsoleto. Ese caché y la lista de resolución de nombres son las únicas cosas que este servidor cachea: ninguno es nunca una fuente de SHA de blob, así que uno obsoleto no puede causar una escritura incorrecta. El árbol está deliberadamente no cacheado, por esa razón.

Quién editó qué

El PAT de cada persona se inyecta, así que GitHub registra al humano real como autor del commit — esto es atribución genuina de git, no algo que el servidor sintetice. history responde "quién cambió este archivo", show_commit muestra el diff real, y git blame funciona normalmente fuera de la aplicación.

Dos límites que vale la pena conocer. history(path) no sigue renombramientos, así que los commits anteriores a un renombramiento se listan bajo la ruta antigua — igual que git log sin --follow. Y la lista de archivos de un commit está paginada por GitHub en 300 archivos; la herramienta informa cuando ha alcanzado ese límite en lugar de presentar una lista parcial como completa.

Leer varios archivos a la vez

read_md toma paths: [...] (hasta 20) en lugar de path. Leer cinco índices de carpeta es entonces un viaje de ida y vuelta en lugar de cinco, y max_bytes se convierte en un presupuesto compartido entre el lote y gastado en el orden dado. El lote está deliberadamente no todo-o-nada: una ruta que no existe informa su propio error mientras los demás aún devuelven. Todo-o-nada es una propiedad de commit_edits, donde un resultado parcial sería un repositorio corrupto; aquí solo costaría un viaje de ida y vuelta.

list_md toma outline: true para mostrar los encabezados de cada archivo sin leerlo. Eso es una lectura por archivo, así que se rechaza por encima de 40 archivos y dice cómo acotar.

Crear un repositorio

create_repo({name, overview}) crea el repositorio bajo el propietario de la conexión y lo siembra con un INDEX.md de # <name> más el overview. Se pretende que el overview sea el documento que un lector encuentra primero, no un resumen de una línea — es el enrutador de ese repositorio.

Sus dos efectos en GitHub — el repositorio, luego su primer commit — no pueden ser una transacción, así que se informan por separado. Si el commit de siembra falla, el resultado dice que el repositorio existe y está vacío, y nombra la llamada exacta de commit_edits que termina el trabajo. No elimina el repositorio que acaba de crear: destruir un espacio de nombres para arreglar un error es un fallo mucho peor que un repositorio vacío.

Escribir en un repositorio que no tiene commits

Un repositorio recién creado no se puede escribir a través de la API de git-data de GitHub en absoluto: blobs, árboles y commits responden todos 409 Git Repository is empty. El único endpoint que funciona es PUT /contents, que crea la rama y el commit inicial en una sola solicitud — así que eso es con lo que siembra create_repo, y es el único lugar donde este servidor llama a PUT /contents.

Escribe exactamente un archivo, así que exactamente un archivo es lo que un lote en un repositorio vacío puede llevar. Un lote de varios archivos se rechaza con instrucciones en lugar de dividirse en dos commits, porque "una llamada, un commit" es la garantía en la que se basa todo el diseño.

POST /user/repos crea bajo la cuenta a la que pertenece el token, no bajo un nombre en el cuerpo de la solicitud — así que un PAT que solo colabora en repositorios de otros crea nuevos en su propia cuenta. El resultado informa el full_name que GitHub devolvió en lugar de un nombre que este servidor asumió. Se resuelve por nombre en la siguiente llamada.

Crear un repositorio necesita más que Contents: Read and write. Un PAT clásico con el alcance repo funciona; un PAT de grano fino necesita Administration: Read and write, no puede crear repositorios en una cuenta personal en absoluto (solo en una organización), y si está limitado a repositorios seleccionados no podría escribir en el nuevo repositorio de todos modos — así que el uso multi-repositorio quiere All repositories. Un 403 dice exactamente esto en lugar del mensaje genérico de contents.

commit_edits toma cuatro operaciones:

op

Campos

Notas

write

path, content, mode

create (predeterminado), overwrite (necesita expect_sha), append.

str_replace

path, old_string, new_string, replace_all

Coincidencia exacta de bytes; debe ser única a menos que replace_all.

edit_section

path, heading, mode, content

replace / append / prepend / delete una sección abordada por su encabezado.

delete

path, expect_sha

Elimina un archivo.

Por qué no hay start_commit / end_commit

El diseño obvio es un área de preparación que abres y luego cierras con un mensaje. Fue rechazado deliberadamente: crea un lugar donde trabajo que parece terminado puede quedarse sin publicar, así que "hice las ediciones y olvidé commitear" se vuelve construible. Tres revisiones de diseño independientes convergieron en la misma conclusión.

En su lugar, no hay ninguna área de preparación (staging) en absoluto. commit_edits es atómico: un lote completo de cambios en muchos archivos, aplicado y enviado en una sola llamada, o no se envía nada a GitHub. El agente acumula su plan en su propio contexto (el único almacén que un modelo lee de forma fiable) y lo gasta en una sola llamada. Cada resultado de herramienta termina con una línea fija que dice que no hay nada pendiente, de modo que la creencia de que algo está en cola se refuta continuamente en lugar de dejarse para descubrirla más tarde.

Tampoco hay temporizador de auto-commit, con ningún tiempo de espera. Un temporizador de inactividad publicaría trabajo que nadie aprobó: una eliminación retractada, una reestructuración a medio terminar. Cero commits no deseados es una propiedad diseñada, no una omisión. Al apagarse, el servidor registra lo que descarta y no confirma nada.

La única pieza de estado retenido es solo para fallos: un lote que falla se retiene durante 30 minutos como retry_ref para que un lote grande no tenga que reescribirse desde un contexto que quizá ya se haya compactado. Se anuncia en cada resultado posterior, se limpia con cualquier éxito y nunca puede producir un recibo de éxito. También está vinculado al repositorio contra el que se creó: reproducirlo en otro repositorio se rechaza, porque esas ediciones se construyeron a partir de texto que el otro repositorio nunca ha contenido.

Atomicidad, con precisión

commit_edits ejecuta dos fases y el límite es la garantía.

  • Plan — validación, obtención de instantánea, comprobaciones de expect_sha, aplicación de cada operación a los búferes en memoria. Cualquier fallo aborta aquí habiendo emitido solo GETs. No es "revertido": nunca se envió ninguna solicitud mutativa. Todos los fallos de un lote se notifican juntos, de modo que un lote de 12 operaciones con 3 defectos cuesta un turno en lugar de tres.

  • Ejecución — tres solicitudes mutativas (POST /git/trees, POST /git/commits, PATCH /git/refs) independientemente de cuántos archivos cambien. Solo el PATCH final es observable.

Así que después de cualquier llamada hay exactamente dos estados observables: existe un commit, o la rama es byte-idéntica a antes.

expect_sha es obligatorio exactamente donde una operación destruye un archivo completo: delete y write mode=overwrite. No puedes reemplazar o eliminar por completo un archivo que nunca observaste. list_md devuelve los SHA de blob completos, por lo que una eliminación nunca necesita una lectura de contenido.

Concurrencia

El contenido se vuelve a leer en el momento del commit desde una única instantánea fijada, por lo que la ventana de lectura-modificación-escritura es de aproximadamente un segundo en lugar de la duración de una conversación. PATCH ... force:false es un auténtico comparar-y-canje (compare-and-swap) del lado del servidor; force: true nunca se envía a ninguna parte. Ante una colisión, todo el plan se vuelve a ejecutar contra la nueva cabeza: si nada de lo que toca el lote se ha movido, aterriza silenciosamente; si algo se ha movido, se detiene y devuelve el contenido ascendente nuevo en línea en lugar de sobrescribirlo.

Entorno

Var

Notas

JWT_SECRET

Firma los tokens que emite este servidor.

PUBLIC_URL

La URL base propia de este servicio, sin barra final.

PORT

Fijado a 3000 para coincidir con el dominio de Railway generado.

GITHUB_API_URL

Por defecto https://api.github.com. El punto de prueba.

Un trío numerado por persona:

Var

Notas

USER<N>_SECRET

Lo que esa persona escribe en la página de consentimiento. La identidad es el secreto.

USER<N>_PAT

El PAT de GitHub de esa persona. Se usa solo para sus propias solicitudes y para todo su alcance.

USER<N>_NAME

Etiqueta opcional, por defecto user<N>. Se convierte en el sub del token.

Esa es toda la configuración por persona: un secreto y un PAT. No hay nada más que establecer: todos los repositorios que el PAT puede ver son accesibles, y cada llamada nombra aquel sobre el que actúa.

USER<N>_NAME es una clave de identidad, no una etiqueta: renombrar a alguien invalida sus tokens activos y debe reconectarse. Cambiar su PAT surte efecto de inmediato sin reconexión.

Migración de un despliegue antiguo: elimina USER<N>_REPO, USER<N>_OWNER, USER<N>_REPOS, USER<N>_REPO_PREFIX, USER<N>_BRANCH y USER<N>_ROOT si tienes alguno. Ya no se leen, y un secreto más un PAT es toda la configuración.

Conectar desde claude.ai

  1. Ajustes → Conectores → Añadir conector personalizado.

  2. URL: <PUBLIC_URL>/mcp.

  3. Deja el ID de cliente y el secreto vacíos.

  4. Conectar y luego escribe tu propio USER<N>_SECRET.

Ambas personas añaden la misma URL; el secreto que cada una escribe vincula su sesión a su propio PAT y repositorio.

Pruebas

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 es un simulacro con estado con una implementación real de SHA de blob git, un DAG de commits, visibilidad de repositorios por token (propios más colaborados, de modo que la resolución de nombres demuestra algo), un registro de solicitudes y fallos inyectables, de modo que un commit realizado a través del servidor es observable mediante una lectura posterior. Es lo que permite a la suite afirmar las cosas que realmente importan: que N ediciones producen exactamente un commit y cero llamadas a PUT /contents, que una eliminación sobrevive a la serialización como un "sha":null literal, que force:false aparece en cada actualización de ref, que un lote fallido deja cero solicitudes mutativas, que el push concurrente de un colega nunca se sobrescribe, que una llamada que nombra un repositorio no emite solicitudes a ningún otro, que create_repo produce exactamente un commit en exactamente el repositorio nuevo y notifica la cuenta bajo la que GitHub realmente lo creó, y que un lote retenido tras un fallo no puede reproducirse en un repositorio diferente.

Despliegue

railway up --service mcp-github-proxy --detach

Se compila mediante el Dockerfile, deliberadamente. El constructor por defecto de Railway (railpack) falla con este servicio con failed to solve: secret RAILWAY_GIT_REPO_OWNER not found — su plan generado declara los secretos de compilación RAILWAY_GIT_*, que solo existen cuando la fuente del servicio es un repositorio de GitHub conectado, no una subida de tarball por CLI.

Notas

  • El escáner de markdown es un escáner de bloques CommonMark real, no una regex ^#{1,6}. El --- de cierre del front matter es un subrayado H2 setext legal, por lo que un escaneo ingenuo inventa un encabezado fantasma con el nombre de la última línea YAML y un agente editaría directamente dentro del front matter. Los encabezados dentro de cercas, código indentado, bloques HTML y blockquotes no son direccionables correctamente.

  • La fidelidad de bytes es deliberada: los archivos CRLF conservan CRLF en las líneas no tocadas, un BOM se separa para que un old_string anclado al inicio pueda coincidir, y nada se recorta nunca: dos espacios finales son un salto de línea duro de markdown.

  • read_md devuelve contenido sin márgenes de números de línea, porque números junto al texto que el modelo está a punto de copiar en old_string es exactamente cómo un margen termina en la aguja. Los números de línea aparecen solo en esquemas y mensajes de error: lugares de los que no se copia nada.

  • Un archivo corto devuelto completo no recibe esquema. Un esquema es un mapa de un archivo que no has leído; imprimir uno sobre las doce líneas que describe es ruido. Reaparece en el momento en que el archivo es lo bastante largo para paginarse, o la ventana es parcial.

  • Un 401 de GitHub se muestra como texto de error de herramienta y nunca como un HTTP 401 desde /mcp. El antiguo proxy reenviaba el WWW-Authenticate de GitHub, lo que enviaba a claude.ai a reautenticarse contra GitHub y producía un bucle de reautenticación mientras el problema real —un PAT muerto— permanecía invisible.

  • El PAT es la verdadera frontera de seguridad, y ahora también es la única que decide el alcance: nada en la configuración de este servidor lo reduce. Limita el propio PAT en GitHub. Ten en cuenta la tensión con create_repo: quiere un token que pueda alcanzar repositorios que no existían cuando se creó el token, que es lo contrario de un PAT de grano fino de repositorios seleccionados.

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