A Claude.ai-ready remote MCP server that exposes GitHub as a custom connector, deployed to Cloudflare Workers.
Connect this server in Claude.ai → Settings → Connectors → Add custom connector and Claude can list/search/inspect your repositories, read files, fetch PR diffs, and (with repo scope) create issues, comment, and branch — all under the user's own GitHub identity via standard OAuth 2.1 / PKCE.
A public instance maintained by the author is deployed at
https://remote-mcp-github.nemolize.workers.dev. It is offered on a best-effort basis with no uptime or quota guarantees; self-host (the steps below) for anything you depend on.
Several GitHub MCP servers exist, alongside GitHub's own gh CLI. This server's niche: responses are curated and bounded for an LLM consumer — each tool returns the fields that matter and truncates large payloads (diffs, file contents) at the boundary, so a single response can't overrun the model's context window — and every call runs under the user's own GitHub OAuth identity. (Output is serialized as Markdown rather than JSON; that is a deliberate format trade-off, not a strict win — see Differentiators below.)
The table below compares coverage by feature area against the two most common alternatives. It stays deliberately coarse-grained — the tool table is the source of truth for which tools exist right now, and gh's coverage is documented in the GitHub CLI manual. Legend: ✅ first-class · gh api, local git) · ❌ absent.
| Feature area | This server | Official mcp__github__* |
gh CLI |
|---|---|---|---|
| Repo metadata & discovery | ✅ | ✅ | ✅ |
| Issue read / triage / lifecycle (labels, assignees, comments) | ✅ | ✅ | ✅ |
| Code & repo search | ✅ | ✅ | ✅ |
| File content read + remote commit (single & multi-file) | ✅ | ✅ | gh api only |
| Branch list / create / delete | ✅ | ✅ | gh api / local git |
| PR open / diff / request reviewers | ✅ | ✅ | ✅ |
| PR read detail / merge / update lifecycle | ✅ | ✅ | ✅ |
| PR reviews & review comments (read + reply) | list_pr_reviews + threads); reply not yet |
✅ | gh api for inline) |
| PR review thread resolve / unresolve | ✅ | ❌ | gh api graphql only |
| Commit history (list / show / compare) | ✅ | ✅ | gh api only |
| Workflow / Actions (CI status, logs, rerun) | ✅ | ✅ | |
| Releases & tags | ❌ | ✅ | ✅ |
| Repo admin (create / fork / delete) | ❌ | ✅ | ✅ |
| Security scanning (secret / code / Dependabot) | ❌ | ✅ | gh api only |
| Local working-tree ops (clone, checkout, …) | ❌ — out of scope (remote-only) | ❌ | ✅ |
Also outside this server's scope today: notifications, Copilot delegation, gists, and projects — reach for the official server or gh for those.
Differentiators — where a focused server earns its place next to the official one:
- Context-bounded, curated output. Each tool returns the fields that matter and truncates large payloads (diffs, file contents) at the boundary, so the model spends fewer tokens per call and a single response never overruns the context window. Output is serialized as Markdown — a deliberate format trade-off: denser and closer to how the model consumes the result, at the cost of the unambiguous structure raw JSON gives a programmatic caller. The official server returns JSON.
- PR review thread resolve / unresolve (#39, shipped). The official MCP still cannot resolve a review thread, so its users fall back to
gh api graphql; this server exposes it directly vialist_pr_review_threads(discover thread IDs) +resolve_review_thread/unresolve_review_thread. This is the gap that motivated the table, and closing it is the clearest differentiator.
All tools respond in Markdown (not raw JSON) so the model can read them efficiently, and large payloads (diff, file content) are truncated at the boundary.
| Tool | Kind | Purpose |
|---|---|---|
list_my_repos |
read | Authenticated user's repositories, with visibility / sort options |
get_repo |
read | Repo metadata (default branch, visibility, flags, stars, language, timestamps) |
get_authenticated_user |
read | Identity bound to the current OAuth token (login, profile, repo counts) |
search_repositories |
read | Cross-GitHub repo search (uses GitHub search qualifiers like org:, user:<login>, stars:>N) |
search_issues |
read | Issue / PR search inside a specific repo |
get_issue |
read | Single issue / PR detail (title, body, state, labels, assignees, milestone) |
list_issue_comments |
read | Conversation comments on an issue or PR |
list_labels |
read | Labels defined in the repo (companion read for add_labels / update_issue) |
get_file_content |
read | Raw file contents at a path + ref (directory listings supported) |
list_commits |
read | Commit history (git log) filtered by branch / path / author / date window |
get_commit |
read | Single commit detail — message, author, parents, per-file stats, diff |
compare_commits |
read | Diff between two refs (ahead / behind counts, merge base, per-file stats, diff) |
get_pr_diff |
read | Unified diff for a pull request |
get_pull_request |
read | Full PR detail — state, mergeable state, head/base SHAs, reviewers, commit/diff counts |
list_pr_reviews |
read | Submitted reviews — state (APPROVED / CHANGES_REQUESTED / …), reviewer, summary body, submitted_at |
list_pr_review_threads |
read | PR review threads with node IDs (PRRT_…) + resolved state (companion read for resolve/unresolve) |
search_code |
read | Code search across GitHub |
list_workflow_runs |
read | Recent Actions workflow runs filtered by workflow / branch / event / status |
get_workflow_run |
read | Single run detail — status / conclusion, event, actor, head branch / SHA, attempt, timestamps |
list_workflow_run_jobs |
read | Jobs of a run with per-step status — the "what failed?" lookup |
list_workflows |
read | Workflows defined in the repo (ID, name, state, path) — discover pipelines / find a workflow_id |
get_job_logs |
read | Plain-text logs of a single job, tail-truncated — the "why did it fail?" lookup after the job list |
list_workflow_run_artifacts |
read | Artifacts produced by a run (ID, name, size, expiry) — the "what did the build produce?" lookup |
get_artifact |
read | Single artifact metadata + archive download URL (zip; metadata only, no download / unzip) |
list_branches |
read | List branches in a repo (name, head SHA, protected flag) |
create_branch |
write | Branch from a base (or the repo's default) |
delete_branch |
write | Delete a branch (default branch refused) |
commit_file |
write | Create or update a single file on a branch in one commit |
commit_files |
write | Create or update multiple files on a branch in one commit (Tree API, per-file mode / encoding) |
delete_file |
write | Delete a single file on a branch in one commit (auto-SHA lookup like commit_file) |
create_pull_request |
write | Open a PR (same-repo head by default; cross_repo_head for fork PRs) |
update_pull_request |
write | Edit a PR's title / body / state (close / reopen) / base branch |
merge_pull_request |
write | Merge a PR (merge / squash / rebase; optional commit message and sha concurrency guard) |
request_pr_review |
write | Request reviewers (users and/or teams) on a PR |
resolve_review_thread |
write | Mark a PR review thread resolved (GraphQL; thread node ID) |
unresolve_review_thread |
write | Re-open a resolved PR review thread (GraphQL; thread node ID) |
create_issue |
write | Title + body + labels + assignees |
update_issue |
write | Edit title / body / state / labels / assignees / milestone (labels and assignees replace) |
add_labels |
write | Append labels to an issue or PR without restating the existing set |
remove_label |
write | Remove a single label from an issue or PR |
add_assignees |
write | Append assignees to an issue or PR without restating the existing set |
remove_assignees |
write | Remove specific assignees from an issue or PR |
add_comment |
write | Comment on an issue or PR |
Both /mcp (Streamable HTTP) and /sse endpoints are exposed; Claude.ai currently uses /sse.
Each tool call logs the GitHub rate-limit headers ([github-ratelimit] remaining/limit, resets at …) to wrangler tail so quota exhaustion is observable. Every successful write also emits a structured audit line (e.g. [github-audit] {"tool":"commit_file","owner":"o","repo":"r","branch":"main","path":"x.ts"}) to the Workers log, giving per-call accountability for LLM-mediated mutations. Both go to the Workers log only and never appear in the tool responses returned to the model.
commit_files reads the branch head and writes it back as a new ref; a concurrent push to the same branch in that window fails with a 422 and is surfaced to the caller to retry (no automatic retry). Its inline content (utf-8) path is bound by the Tree API's ~1 MB per-file cap; pass encoding: "base64" to upload larger files via the Blob API instead. See the inline comments in src/tools/files.ts for detail.
- A Cloudflare account with the
wranglerCLI authenticated (pnpm dlx wrangler@latest login) - Permission to create GitHub OAuth Apps
- Node.js 22+ and
pnpm
git clone https://github.com/<your-owner>/remote-mcp-github.git
cd remote-mcp-github
pnpm installCreate two OAuth Apps at https://github.com/settings/applications/new — one for local dev, one for production. Use the values below; the production URLs use the *.workers.dev host you'll be deploying to.
| Purpose | Homepage URL | Authorization callback URL |
|---|---|---|
| Dev | http://localhost:8788 |
http://localhost:8788/callback |
| Prod | https://remote-mcp-github.<your-subdomain>.workers.dev |
https://remote-mcp-github.<your-subdomain>.workers.dev/callback |
After creation, generate a Client Secret for each app and keep both Client ID + Secret pairs handy.
pnpm dlx wrangler@latest kv namespace create OAUTH_KVCopy the resulting id value and paste it into wrangler.jsonc under kv_namespaces[0].id. This namespace stores encrypted OAuth grants.
Copy the example and fill in the dev OAuth App credentials:
cp .dev.vars.example .dev.vars
# edit .dev.varsGenerate the cookie encryption key with openssl rand -hex 32.
.dev.vars is git-ignored.
Alternatively, export the three keys as environment variables (e.g. via your
shell or a secret manager) instead of writing .dev.vars — wrangler dev
reads the required secrets from process.env as well.
Push the prod OAuth App credentials and a fresh cookie key to Cloudflare:
pnpm dlx wrangler@latest secret put GITHUB_CLIENT_ID
pnpm dlx wrangler@latest secret put GITHUB_CLIENT_SECRET
pnpm dlx wrangler@latest secret put COOKIE_ENCRYPTION_KEY # openssl rand -hex 32pnpm devThe server listens on http://localhost:8788. Validate it with MCP Inspector:
DANGEROUSLY_OMIT_AUTH=true pnpm dlx @modelcontextprotocol/inspectorOpen the printed URL, choose Transport Type: SSE, set URL: http://localhost:8788/sse, switch Connection Type: Direct, and click Connect. The first request triggers the OAuth dance via the dev GitHub OAuth App.
pnpm deployVerify:
curl https://remote-mcp-github.<your-subdomain>.workers.dev/.well-known/oauth-authorization-server- Open
Claude.ai → Settings → Connectors → Add custom connector - Name: anything (e.g.
remote-mcp-github) - Remote MCP server URL:
https://remote-mcp-github.<your-subdomain>.workers.dev/sse - Add → Connect → approve on the MCP authorize page → authorize on GitHub → done
In a new chat, prompt Claude with something like "List my GitHub repositories by most recently updated" — Claude will pick list_my_repos and call it.
pnpm lint # eslint + tsc --noEmit + prettier --check, in parallel
pnpm fix # eslint --fix && prettier --writeCI runs each sub-check (lint:eslint, lint:typecheck, lint:prettier) as a separate matrix job for clearer status reporting; locally, pnpm lint is the one-shot equivalent and pnpm fix auto-resolves formatting and any autofixable ESLint findings before opening a PR.
Tests run with Vitest via @cloudflare/vitest-pool-workers, so they execute inside the real Workers runtime (workerd) backed by Miniflare — not Node.
pnpm test # one-shot run
pnpm test:watch # watch modeCross-cutting tests live under top-level test/. Tests that exercise a single module can also be co-located as *.test.ts next to the source. CI runs pnpm test as a dedicated Test job on every PR.
The suite includes test/mcp-e2e.test.js, a transport-level E2E that drives /register → /authorize → /token and exercises /mcp initialize + tools/list against the real OAuth provider. To avoid a GitHub round-trip in CI, the test pool swaps in test/_fixtures/fake-github-handler.ts via the buildOAuthProvider factory in src/index.ts; tool execution against real GitHub is covered separately by the manual harness below.
scripts/e2e/oauth-e2e.mjs drives the full OAuth 2.1 + PKCE handshake against a locally-running server and then exercises a few read tools over the Streamable HTTP MCP transport, asserting on the rendered Markdown. It is manual — approving the GitHub consent in a browser window is the one non-scripted step — and is deliberately not wired into CI.
pnpm dev # in one shell
pnpm run e2e:oauth # in another; prints an authorize URL (open it manually), then waits for ?code=...Defaults assert against nemolize/remote-mcp-github so the maintainer can run it with no setup. Forks override via env:
EXPECTED_OWNER=acme EXPECTED_REPO=widget EXPECTED_LOGIN=acmebot pnpm run e2e:oauthOther knobs: MCP_BASE (default http://localhost:8788), CALLBACK_PORT (default 9876), TIMEOUT_MS (default 300000).
The server requests read:user repo from GitHub. The repo portion is what enables private-repo visibility for read tools and the create/comment/branch capabilities of the write tools. To run the read tools only against public repositories, change src/github-handler.ts to read:user public_repo.
src/
├── index.ts # OAuthProvider + MyMCP class wiring
├── tools.ts # GitHub tools + helpers (truncation, rate-limit log)
├── github-handler.ts # OAuth redirect handler (scope set here)
├── workers-oauth-utils.ts
└── utils.ts
wrangler.jsonc # Cloudflare Workers config; KV id goes here
.dev.vars.example # Template for the dev secrets
Two prefixed axes: type:* mirror the repo's conventional-commit types (feat / fix / chore / docs); area:* map to a responsibility area — a src/tools/*.ts module or a planned tool surface — and are created on demand when the first issue touching that area is filed. GitHub's standard workflow labels (good first issue, help wanted, duplicate, invalid, question, wontfix) are kept as-is.
- Tokens are encrypted at rest in the
OAUTH_KVnamespace usingCOOKIE_ENCRYPTION_KEY. Rotate the key (and re-deploy) to invalidate all active grants. - The Worker is the OAuth server for Claude.ai (and any other MCP client) and the OAuth client for GitHub. The GitHub access token never leaves the Worker — it sits in
this.props.accessTokeninside the Durable Object instance, used by Octokit per request. - All tool calls go through a
wrapTool()boundary that converts thrown errors into{ isError: true, content: [{ type: "text", text: "Error: …" }] }so the model sees the failure mode rather than the connection dropping. The error text is forwarded verbatim; Octokit already redacts the Authorization header, so tokens do not leak, though other fields are not sanitised (defence-in-depth, not done today). - Write-tool payloads carry input-size caps (file content, commit/PR/issue/comment text, per-commit file count, and the aggregate content size of a multi-file commit — see
src/tools/common.ts) as defence-in-depth, so a runaway model can't burn Worker CPU/memory with a multi-megabyte payload well under the platform's 100 MiB request limit. Oversized input is rejected with a descriptive error (per-field caps at schema validation; the aggregate-commit cap in thecommit_fileshandler before any API call). - This is still a small server. Audit before exposing to untrusted users; consider tightening CORS, limiting allowed origins, or restricting
ALLOWED_USERNAMESfor sensitive write tools.
MIT