Skip to main content
Glama

md-github

一个轻量级 MCP 服务器,让 claude.ai 自定义连接器 能够在一个账户的各个 GitHub 仓库中执行精准的 markdown 编辑,并将任意数量的更改批量合并为恰好一次提交

Claude 通过 OAuth 2.1 + DCR 进行身份验证(这是连接器表单所要求的)。每个人的同意密钥选择各自的 GitHub PAT 和各自的账户。零运行时依赖,无数据库,所有状态都保存在内存中。

不是一个 GitHub MCP 透传。它只暴露七个工具,仅此而已。

工具

Tool

作用

overview

一次调用即可在仓库中定位:其根目录 INDEX.md 的原文,以及每个文件路径及其大小。只读。

list_md

每个 .md 文件的字节大小和 git blob SHA,可选显示每个文件的标题大纲。只读。

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 是要粘贴到将使用此连接器的 agent 处的指令块,./connector-prompt.sh <repo> 会填入仓库名称并将其复制到剪贴板。

Related MCP server: brain-mcp

多个仓库,一个连接

一个身份可以访问其 PAT 能看到的每个仓库——拥有的、协作的,或通过组织访问的。没有需要配置的 owner:令牌已经属于某个账户,并且已经携带了它自己的访问权限,所以它能看到的仓库集合就是命名空间。再次配置它只会制造第二个可能与令牌不一致的事实来源。

裸的 repo:"notes" 会针对该可见集合进行解析;repo:"owner/notes" 跳过查找,直接寻址该仓库。一个在两个 owner 下都可见的名称会返回一个同时列出两者的拒绝,而不是猜测。启动时不会枚举任何内容,因此由 create_repo 创建的仓库会在紧接着的下一次调用中解析成功,无需重新部署。

没有默认仓库

repo 是必需的,省略它的调用会报错,而不是猜测。当一个连接背后有多个项目时,不存在所谓的“那个”仓库,而每一个看似合理的回退——第一个、启动时配置的那个、最后碰过的那个——都是一种让编辑落入错误项目的方式,同时每条消息看起来仍然像成功。错误仓库的提交也是这里 expect_sha 无法捕获的唯一错误,因为它所保护的 blob 位于一个没人关注的仓库中。

没有任何东西会列出连接可以访问的仓库——结果中不会,握手中也不会。每个工具都显式地指名其仓库,因此发起调用永远不需要名册;而对于权限范围很广的 PAT,名册会把一屏不相关的名称放到每一个结果上。可见集合只用于解析裸名称时从 GitHub 读取,并且永远不会被打印出来。

该解析读取的是 GET /user/repos,而不是 GET /users/:owner/repos——后者即使对你的自有账户也只返回公开仓库,因此私有笔记仓库根本无法按名称解析。它会被缓存五分钟,未命中时会先重新获取一次再失败,所以刚刚在别处创建的仓库仍然可以解析。

除了 PAT,没有任何东西会收窄连接

没有固定仓库,没有仓库白名单,没有名称前缀,没有分支覆盖,也没有子树限制。早期版本都有这些;它们被移除了。每一个都是紧挨着 PAT 自身权限范围的第二道边界,只能与 PAT 产生分歧,而且每一个都让 create_repo 变得不自洽——一个尚不存在的仓库不可能出现在启动时写入的白名单上。每个仓库都使用自己的默认分支,原因相同:一个身份横跨多个仓库,而存在于一个仓库的分支很少会存在于下一个仓库。

PAT 就是边界。在 GitHub 中设置它的权限范围,那里才是它真正生效的地方。

每个仓库自我记录

这里刻意没有跨仓库的索引文件。每个仓库在自己的根目录 INDEX.md 中自我记录,这个文件既是 create_repo 初始化的文件,也是此服务器回填到上下文中的文件。某个仓库中的注册表会成为事实存在的第二个位置,而且一旦有人在连接器之外重命名项目,它就会立刻过期。

定位:overview

会话从调用 overview(repo) 开始。一次往返返回该仓库根目录 INDEX.md 的原文——这个路由器说明哪个文件回答哪个问题——以及仓库中每个路径及其大小。在本项目自己的上下文仓库中,这是 152 个路径和 ~7k 个 token,之后模型在获取任何内容之前就知道一切在哪里、有多大。

其他一切都遵循路由器:用 read_md({paths:[...]}) 读取它指向的文件夹索引,用 list_md({path_prefix, outline:true}) 来缩小范围。

刻意内联:文件夹级别的索引文件。在本项目的上下文仓库中,其中一个有 66 KB——把它们全部内联的代价比读取它们所描述的文件还要高。

其他任何东西都不会把索引附加到结果上。早期版本会把路由器附加到每个工具结果上;一个 13 KB 的索引是 ~3.5k 个 token,所以一个十次调用的会话为同一条信息付了十次费。overview 在被请求时只交付一次,其他任何东西都不会再附加一个。

索引从仓库根目录的 INDEX.mdindex.mdREADME.md 中的第一个读取,缓存两分钟,并且会被通过此服务器的任何提交失效——所以模型刚刚重写的路由器永远不会被读回过期版本。该缓存和名称解析列表是此服务器仅有的两样缓存:两者都永远不会成为 blob SHA 的来源,所以过期的缓存不会导致错误的写入。出于这个原因,树被刻意缓存。

谁编辑了什么

每个人自己的 PAT 会被注入,因此 GitHub 会把真实的人记录为提交作者——这是真正的 git 归属,不是服务器合成的任何东西。history 回答“谁改了这个文件”,show_commit 显示实际的差异,git blame 在应用之外也能正常工作。

有两个值得了解的局限。history(path) 不跟踪重命名,所以重命名之前的提交会列在旧路径下——与不带 --followgit log 相同。另外,提交的文件列表被 GitHub 以 300 个文件分页;工具在到达该边界时会报告,而不是把部分列表呈现为完整列表。

一次读取多个文件

read_md 接受 paths: [...](最多 20 个)而不是 path。这样读取五个文件夹索引就是一次往返而不是五次,max_bytes 成为跨批次共享的预算,按给定顺序消耗。该批次刻意不是全有或全无:不存在的路径会报告自己的错误,而其他路径仍然返回。全有或全无是 commit_edits 的属性,在那里部分结果意味着仓库损坏;而在这里它只会多花一次往返。

list_md 接受 outline: true 来显示每个文件的标题,而无需读取文件。这需要每个文件一次读取,所以超过 40 个文件会被拒绝,并说明如何缩小范围。

创建仓库

create_repo({name, overview}) 在连接的 owner 下创建仓库,并用 # <name> 加上 overview 组成 INDEX.md 来初始化它。overview 意在成为读者最先看到的文档,而不是一行摘要——它是该仓库的路由器。

它在 GitHub 上的两个效果——先创建仓库,再创建首次提交——不可能是一个事务,所以它们被分别报告。如果初始化提交失败,结果会说明仓库已存在且为空,并指出完成该工作的确切 commit_edits 调用。它不会删除刚刚创建的仓库:为了收拾一个错误而销毁一个命名空间,是远比一个空仓库更糟糕的失败。

写入没有提交的仓库

一个全新的仓库根本无法通过 GitHub 的 git-data API 写入:blob、tree 和 commit 都会返回 409 Git Repository is empty.。唯一可用的端点是 PUT /contents,它在一个请求中同时创建分支和初始提交——所以这就是 create_repo 用来初始化的方式,也是此服务器唯一调用 PUT /contents 的地方。

它只写一个文件,所以写入空仓库的批次也只能携带一个文件。多文件批次会被拒绝并附上说明,而不是拆成两次提交,因为“一次调用,一次提交”是整个设计所依赖的保证。

POST /user/repos令牌所属的账户下创建,而不是在请求体中的名称下创建——所以一个仅仅在别人的仓库上协作的 PAT 会在它自己的账户中创建新仓库。结果报告的是 GitHub 返回的 full_name,而不是此服务器假设的名称。它会在紧接着的下一次调用中按名称解析成功。

创建仓库需要的权限不止 Contents: Read and write。带有 repo 权限范围的经典 PAT 可以;细粒度 PAT 需要 Administration: Read and write,完全无法在个人账户中创建仓库(只能在组织中创建),而且如果它被限制在选定的仓库,它也无法写入新仓库——所以多仓库使用需要 All repositories。403 会准确说明这一点,而不是通用的 contents 错误消息。

commit_edits 支持四种操作:

op

Fields

Notes

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

按标题寻址一个 section,执行 replace / append / prepend / delete

delete

path, expect_sha

删除一个文件。

为什么没有 start_commit / end_commit

显而易见的设计是一个暂存区,你打开它,之后用一条消息关闭它。它被刻意否决了:它创造了一个地方,让看起来已完成的工作可以搁置而不发布,于是“我做了编辑但忘了提交”就变得可以构造出来。三次独立的设计评审得出了相同的结论。

相反,根本没有暂存区commit_edits 是原子的——跨多个文件的一整批更改,在一次调用中应用并推送,或者完全不向 GitHub 发送任何内容。代理在自己的上下文中(模型唯一能可靠读取的存储)积累计划,并在一次调用中将其消耗掉。每个工具结果都以一行常驻说明结尾,表明没有待处理的内容,因此“有东西已排队”的信念会不断被驳斥,而不是留待以后发现。

同样也没有自动提交定时器,任何超时时间下都没有。空闲定时器会发布无人批准的工作——一次撤回的删除、一次半完成的重构。零多余提交是设计使然,而非疏忽。关闭时,服务器会记录它丢弃的内容,并且不提交任何内容。

唯一保留的状态是仅限失败的:失败的批次会作为 retry_ref 保留 30 分钟,这样大批次就不必从可能已被压缩的上下文中重新输入。它会在每个后续结果中公布,任何成功都会清除它,而且它永远不会产生成功回执。它还绑定到其编写时所针对的仓库:拒绝将其重放到另一个仓库,因为这些编辑是根据另一个仓库从未包含的文本构建的。

原子性,精确地说

commit_edits 运行两个阶段,而两个阶段之间的边界就是保证。

  • 计划——验证、快照获取、expect_sha 检查、将每个操作应用到内存缓冲区。任何失败都会在此中止,且只发出过 GET 请求。不是“已回滚”:从未发送过任何变更请求。批次中的所有失败会一起报告,因此一个 12 个操作、3 个缺陷的批次只花费一轮,而不是三轮。

  • 执行——三个变更请求(POST /git/treesPOST /git/commitsPATCH /git/refs),无论更改了多少文件。只有最终的 PATCH 是可观察的。

因此,在任何调用之后,恰好有两种可观察状态:要么存在一个提交,要么分支与之前逐字节相同。

expect_sha 恰好是在操作销毁整个文件的地方必需的——deletewrite mode=overwrite。你不能整体替换或删除你从未观察过的文件。list_md 返回完整的 blob SHA,因此删除永远不需要读取内容。

并发

内容在提交时从单个固定的快照重新读取,因此读-修改-写窗口大约为一秒,而不是一次对话的长度。PATCH ... force:false 是真正的服务器端比较并交换;force: true 永远不会发送到任何地方。发生冲突时,整个计划会针对新的 head 重新运行:如果批次触及的内容没有移动,它会静默落地;如果确实有移动,它会停止并内联返回最新的上游内容,而不是覆盖。

环境

变量

说明

JWT_SECRET

为此服务器颁发的令牌签名。

PUBLIC_URL

此服务自身的基础 URL,无尾部斜杠。

PORT

固定为 3000,以匹配生成的 Railway 域名。

GITHUB_API_URL

默认为 https://api.github.com。测试接缝。

每人一组编号三元组:

变量

说明

USER<N>_SECRET

该人在同意页面上输入的内容。身份就是秘密。

USER<N>_PAT

该人的 GitHub PAT。仅用于他们自己的请求,也是他们全部的可达范围。

USER<N>_NAME

可选标签,默认为 user<N>。成为令牌的 sub

这就是完整的按人配置:一个秘密和一个 PAT。没有其他需要设置的内容——PAT 能看到的每个仓库都是可达的,每次调用都会指明它作用于哪个仓库。

USER<N>_NAME 是身份键,而不是标签:重命名某人会使他们的活动令牌失效,他们必须重新连接。更改他们的 PAT 会立即生效,无需重新连接。

迁移旧部署:如果你有 USER<N>_REPOUSER<N>_OWNERUSER<N>_REPOSUSER<N>_REPO_PREFIXUSER<N>_BRANCHUSER<N>_ROOT,请删除它们。它们都不再被读取,一个秘密加一个 PAT 就是全部配置。

从 claude.ai 连接

  1. 设置 → 连接器 → 添加自定义连接器

  2. URL<PUBLIC_URL>/mcp

  3. 将客户端 ID 和密钥留空。

  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":nullforce:false 出现在每次 ref 更新上,失败的批次留下零个变更请求,同事的并发推送永远不会被覆盖,指定一个仓库的调用不会向其他仓库发出请求,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 仓库时才存在,而不是 CLI tarball 上传。

备注

  • markdown 扫描器是真正的 CommonMark 块扫描器,而不是 ^#{1,6} 正则表达式。front matter 的结束 --- 是合法的 setext H2 下划线,因此天真的扫描会凭空产生一个以最后一行 YAML 命名的幻影标题,代理会直接编辑到 front matter 中。围栏内、缩进代码、HTML 块和块引用中的标题正确地不可寻址。

  • 字节保真是刻意的:CRLF 文件在未触及的行上保持 CRLF,BOM 会被分离出来,以便以开头锚定的 old_string 能够匹配,而且任何内容都不会被修剪——两个尾随空格是 markdown 的硬换行。

  • read_md 返回的内容不带行号边栏,因为模型即将复制到 old_string 的文本旁边的数字,正是边栏最终出现在搜索词中的方式。行号只出现在大纲和错误消息中——这些地方不会复制任何内容。

  • 完整返回的短文件没有大纲。大纲是你尚未阅读的文件的地图;在它所描述的十二行上方打印一份大纲是噪音。当文件长到需要分页浏览,或窗口是部分的时候,它就会重新出现。

  • GitHub 401 会以工具错误文本的形式呈现,而绝不会以来自 /mcp 的 HTTP 401 呈现。旧的代理转发了 GitHub 的 WWW-Authenticate,这使 claude.ai 去 GitHub 重新认证,并产生了重新认证循环,而真正的问题——失效的 PAT——仍然不可见。

  • PAT 是真正的安全边界,现在它也是唯一决定可达范围的边界:此服务器的配置中没有任何内容会缩小它。在 GitHub 中为 PAT 本身设置范围。注意与 create_repo 的紧张关系:它需要一个能够访问令牌创建时尚不存在的仓库的令牌,这与选定仓库的细粒度 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