A remote MCP server that exposes GitHub through a public Cloudflare Workers deployment.
Connect it from Claude.ai (custom connector) or the Codex CLI (codex mcp add), and the client 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.
See Setup for the exact commands. Best-effort service — no uptime or quota guarantees. Self-hosting is documented under Self-Hosting Setup for anyone who wants their own Cloudflare account, quota, or OAuth App ownership.
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 · 🟡 partial — some tools in the area still missing (see the linked issue or inline note) · gh api, local git) · ❌ absent (linked issue = tracked; unlinked = not currently tracked or structurally out of scope).
| Feature area | This server | Official mcp__github__* |
gh CLI |
|---|---|---|---|
| Repo metadata & discovery | ✅ | ✅ | ✅ |
| Issue read / triage / lifecycle (labels, assignees, comments) | 🟡 sub-issues (#161) | ✅ | ✅ |
| Code & repo search | 🟡 users / orgs / PR search (#163) | ✅ | ✅ |
| 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 | 🟡 no draft → ready-for-review (GraphQL markPullRequestReadyForReview), no lock / unlock (#191) |
✅ | ✅ |
| PR reviews & review comments (read + reply + submit + pending) | ✅ one-shot + pending lifecycle (create_pr_review, create_pending_pr_review / add_comment_to_pending_pr_review / submit_pending_pr_review / delete_pending_pr_review, add_pr_review_comment_reply) |
✅ | 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) | 🟡 run usage / log deletion / artifact download (#159) | ✅ | ✅ |
| Actions administration (secrets, variables, cache, workflow enable / disable) | 🟡 Codespaces / Dependabot secrets (#202) | ❌ | ✅ |
| Releases & tags | 🟡 read + write (list_releases, get_release, list_tags, create / update / delete release); no asset upload / download (#192) |
🟡 read only | ✅ |
| Repo admin (create / fork / delete) | ✅ create / fork / delete (create_repository, fork_repository, delete_repository) |
✅ | ✅ |
| Repository configuration edit (rename, archive, deploy keys, autolinks) | ❌ (#196) | ❌ | ✅ |
| Label definitions (create / edit / delete / clone) | ✅ | 🟡 read only | ✅ |
| Gists (list / read / create / update / delete) | ✅ read + write (list_gists, get_gist, list_gist_comments, create_gist, update_gist, delete_gist) |
✅ | ✅ |
| Security scanning (secret / code / Dependabot) | 🟡 single-alert get_* (#158) |
✅ | gh api only |
| Repository rulesets | ❌ (#195) | ❌ | ✅ |
| Copilot delegation (assign issue / request PR review) | ❌ (#162) | ✅ | ✅ (--add-assignee "@copilot" / --add-reviewer "@copilot") |
| Discussions (list / categories / get / comments) | ✅ read (list_discussions, list_discussion_categories, get_discussion, get_discussion_comments) |
✅ | ✅ (gh discussion, preview) |
| Notifications (list / mark read / thread subscription) | ❌ (#139) | ✅ | gh api only |
| Projects (v2) | ✅ read + write — project CRUD (create / edit / close / delete / copy / link / unlink), field create / delete, items (add / remove / archive / draft / field values) | ✅ | ✅ (gh project) |
| Local working-tree ops (clone, checkout, …) | ❌ — out of scope (remote-only) | ❌ | ✅ |
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 built-in
resolve_review_thread/unresolve_review_threadavoid thegh api graphqlfallback. The official MCP recently added the same tools; this is no longer a coverage differentiator, but the Markdown-serialized inline flow keeps it a smoother match for the review-response loop.
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) |
create_repository |
write | Create a repo for the authenticated user (or an org) — visibility, auto-init, gitignore/license |
fork_repository |
write | Fork owner/repo to the authenticated user (or an organization); optional default-branch-only |
delete_repository |
write | Permanently delete owner/repo (destructive; GitHub keeps a 90-day restoration window for orgs) |
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 (renders the comment_id for edit / delete) |
list_linked_branches |
read | Branches linked to an issue via the development sidebar (companion read for develop_issue) |
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 |
get_pull_request_files |
read | Files changed in a PR — status, additions/deletions, truncated patch snippet per file |
get_pull_request_status |
read | Combined merge-readiness — Actions check-runs + legacy commit statuses, in one call |
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) |
list_pr_review_thread_comments |
read | All comments (root + replies) in one review thread — surfaces reply databaseId for update/delete |
search_code |
read | Code search across GitHub |
search_users |
read | Cross-GitHub user search (type:user forced automatically; login + profile URL) |
search_orgs |
read | Cross-GitHub organization search (type:org forced automatically) |
search_pull_requests |
read | Cross-GitHub PR search (is:pr forced; renders state / draft / merged) |
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 |
get_workflow_run_logs |
read | Full run log archive download URL (zip of all jobs; short-lived URL, metadata only, no download) |
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) |
rerun_workflow_run |
write | Re-run an entire workflow run (all jobs) — new attempt; poll get_workflow_run for status |
rerun_failed_jobs |
write | Re-run only the failed jobs of a run — new attempt; poll get_workflow_run for status |
cancel_workflow_run |
write | Cancel an in-progress run (async); poll get_workflow_run until conclusion is cancelled |
trigger_workflow_dispatch |
write | Manually dispatch a workflow_dispatch workflow on a ref with optional inputs |
list_actions_secrets |
read | Actions secrets by name + timestamps (values are never returned by GitHub) |
set_actions_secret |
write | Create / update an Actions secret — encrypted client-side (sealed box); value never echoed back |
delete_actions_secret |
write | Delete an Actions secret by name |
list_actions_variables |
read | Actions variables (name, plaintext value, timestamps) — the vars context workflows read |
get_actions_variable |
read | Single Actions variable by name (value + timestamps) |
set_actions_variable |
write | Create / update an Actions variable (create-first, falls back to update on 409) |
delete_actions_variable |
write | Delete an Actions variable by name |
list_actions_caches |
read | Actions caches (ID, key, ref, size, last accessed) — find a cache to evict |
delete_actions_cache |
write | Delete caches by cache_id or by full key (optionally scoped to a ref) |
enable_workflow |
write | Re-enable a disabled workflow so its triggers fire again |
disable_workflow |
write | Disable a workflow — its triggers stop firing until re-enabled |
list_releases |
read | Releases newest first (ID, name, tag, draft / prerelease / published state, date, author) |
get_release |
read | Single release detail + notes body — by release_id, by tag, or the latest when neither given |
list_tags |
read | Git tags (name, commit SHA) — for release metadata on a tag, use get_release with the tag |
create_release |
write | Create a release (and its tag) from tag_name (+ name, body, draft, prerelease, generated notes) |
update_release |
write | Edit a release by release_id (rename, edit notes, publish a draft, toggle prerelease) |
delete_release |
write | Delete a release by release_id (leaves the git tag in place) |
list_gists |
read | Authenticated user's gists newest first (ID, description, public flag, file count, updated_at) |
get_gist |
read | Single gist detail — description, owner, public flag, per-file metadata + length-capped content |
list_gist_comments |
read | Comments on a gist (companion read for a future comment-write tool) |
create_gist |
write | Create a gist with one or more files (description, files, public flag) |
update_gist |
write | Edit a gist's description and/or add / replace / rename / delete files |
delete_gist |
write | Delete a gist by gist_id (irreversible) |
list_secret_scanning_alerts |
read | Secret-scanning alerts (number, state, secret type, resolution, date) — never the raw secret value |
list_code_scanning_alerts |
read | Code-scanning alerts (number, state, rule + severity, tool, most-recent location path:line) |
list_dependabot_alerts |
read | Dependabot alerts (number, state, package, severity, advisory GHSA + summary) |
get_secret_scanning_alert |
read | One secret-scanning alert (type, state, resolution, timestamps) — never the raw secret value |
get_code_scanning_alert |
read | One code-scanning alert (rule, severity, tool, most-recent location) |
get_dependabot_alert |
read | One Dependabot alert (package, severity, GHSA, advisory summary) |
list_projects |
read | Projects (v2) of a user / org — or the authenticated user — number, title, state, node ID (PVT_…) |
get_project |
read | Single Project (v2) detail — visibility, state, item count, field definitions + options |
list_project_items |
read | Project (v2) board items — type, title, owner/repo#N, Status, assignees; include_archived opt-in |
list_project_fields |
read | Project (v2) field definitions — name, data type, single-select option names + IDs |
add_project_item |
write | Add an existing issue / PR to a Project (v2) by its content node ID |
remove_project_item |
write | Remove an item from a Project (v2) by its item node ID (PVTI_…) |
update_project_item_field |
write | Set a Project (v2) item's field value — text / number / date / single-select option |
create_project_draft_item |
write | Add a draft item (title + optional body) to a Project (v2) without an underlying issue |
create_project |
write | Create a Project (v2) under a user / org — or the authenticated user — returns number + node ID |
update_project |
write | Edit a Project (v2) — title, short description, visibility, open/closed state (close / reopen) |
delete_project |
write | Delete a Project (v2) permanently, including draft items and field configuration (irreversible) |
copy_project |
write | Copy a Project (v2) — fields, views, workflows — to a new project (optionally with draft items) |
link_project_to_repository |
write | Link a Project (v2) to a repository (shows in the repo's Projects tab) |
unlink_project_from_repository |
write | Unlink a Project (v2) from a repository |
create_project_field |
write | Create a custom Project (v2) field — TEXT / NUMBER / DATE / SINGLE_SELECT (with option names) |
delete_project_field |
write | Delete a custom Project (v2) field by its field node ID (irreversible) |
archive_project_item |
write | Archive — or with undo: true restore — a Project (v2) item by its item node ID |
list_discussions |
read | Discussions in a repo, most recently updated first — optional category filter, answered state |
list_discussion_categories |
read | Discussion categories (name, slug, answerable flag, node ID — the category_id for filtering) |
get_discussion |
read | Single discussion detail — title, body, author, category, answered state, counts, timestamps |
get_discussion_comments |
read | Top-level comments on a discussion — author, accepted-answer marker, reply count, body preview |
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 |
update_pull_request_branch |
write | Update a PR's branch with its base's latest changes (mirrors the "Update branch" button) |
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 |
create_pr_review |
write | Submit a review (APPROVE / REQUEST_CHANGES / COMMENT) with optional body + batched inline comments |
create_pending_pr_review |
write | Open a pending (draft) review — no event, no notification; seed optional inline comments |
add_comment_to_pending_pr_review |
write | Append one inline thread to a pending review (GraphQL addPullRequestReviewThread) |
submit_pending_pr_review |
write | Submit a pending review with a verdict (APPROVE / REQUEST_CHANGES / COMMENT) — one notification |
delete_pending_pr_review |
write | Discard a pending review without submitting |
add_pr_review_comment_reply |
write | Reply to an existing PR review comment thread (by comment_id) |
update_pr_review_comment |
write | Edit a submitted inline review comment's body (by comment_id) |
delete_pr_review_comment |
write | Delete a submitted inline review comment (destructive; replies in the same thread are not affected) |
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 |
create_label |
write | Create a label definition in the repo (name + optional colour / description) |
update_label |
write | Rename a label definition and / or change its colour / description |
delete_label |
write | Delete a label definition from the repo (removes it everywhere it is applied; destructive) |
clone_labels |
write | Copy label definitions from another repo (skips existing names, or overwrite to update them) |
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 |
update_issue_comment |
write | Edit a conversation comment's body on an issue or PR (by comment_id) |
delete_issue_comment |
write | Delete a conversation comment from an issue or PR (destructive) |
pin_issue |
write | Pin an issue to the top of the repo's issue list (GraphQL; max three pinned per repo) |
unpin_issue |
write | Unpin a pinned issue (GraphQL) |
lock_issue |
write | Lock an issue / PR conversation (optional reason: off-topic / too heated / resolved / spam) |
unlock_issue |
write | Unlock a previously locked issue / PR conversation |
transfer_issue |
write | Transfer an issue to another repo of the same owner (permanent move; GraphQL) |
delete_issue |
write | Permanently delete an issue (destructive, no restoration window; requires admin; GraphQL) |
develop_issue |
write | Create a branch linked to an issue (GraphQL createLinkedBranch; optional name / base ref) |
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.
Use the public endpoint unless you specifically need to operate your own Worker.
-
Open
Claude.ai → Settings → Connectors → Add custom connector. -
Name the connector, for example
remote-mcp-github. -
Set Remote MCP server URL to:
https://remote-mcp-github.nemolize.workers.dev/sse -
Click Add and then Connect.
-
Approve the MCP authorization page.
-
Authorize the GitHub OAuth App.
After the connector is enabled, start a new Claude chat and ask something like "List my GitHub repositories by most recently updated". Claude should call list_my_repos.
For the Codex CLI:
codex mcp add remote-mcp-github --url https://remote-mcp-github.nemolize.workers.dev/mcp
codex mcp login remote-mcp-githubcodex mcp add registers the server against its streamable-HTTP transport (/mcp); codex mcp login walks through the same GitHub OAuth flow as the Claude.ai connector. Once logged in, the tools become available to Codex sessions.
The public instance uses GitHub OAuth. It does not ask you to paste a GitHub token into Claude or into this repository.
The OAuth App currently requests read:user repo delete_repo gist project:
read:useridentifies the authenticated GitHub user.repoenables private-repository reads and write tools such as issue comments, branches, pull requests, and commits.delete_repois required only bydelete_repository.gistis required only by the gist tools.projectcovers the GitHub Projects (v2) tools — reads (list_projects,get_project,list_project_items,list_project_fields) and writes (add_project_item,remove_project_item,update_project_item_field,create_project_draft_item).
The GitHub access token is encrypted in the Worker's OAuth KV storage and used server-side for GitHub API calls. See Security notes for the implementation model.
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 delete_repo gist project from GitHub. The repo portion is what enables private-repo visibility for read tools and the create/comment/branch capabilities of the write tools; delete_repo is required only by delete_repository; gist is required only by the gist tools (list_gists, get_gist, list_gist_comments, create_gist, update_gist, delete_gist); project is required only by the Projects (v2) tools — it covers both the read tools (list_projects, get_project, list_project_items, list_project_fields) and the write tools (add_project_item, remove_project_item, update_project_item_field, create_project_draft_item, create_project, update_project, delete_project, copy_project, link_project_to_repository, unlink_project_from_repository, create_project_field, delete_project_field, archive_project_item). Existing connections authorized under the previous read:project scope must re-authorize before the Projects write tools work. To run the read tools only against public repositories, change src/github-handler.ts to read:user public_repo gist read:project (the read-only read:project suffices when the write tools aren't needed).
The Actions administration tools (list_actions_secrets, set_actions_secret, delete_actions_secret, the variable tools, the cache tools, enable_workflow / disable_workflow) need no additional OAuth scope: the classic repo scope already covers the Actions secrets / variables / cache / workflow-state endpoints. What they do require is a sufficient repository permission level on the target repo — collaborator access for secrets and variables (per GitHub's REST docs: "authenticated users must have collaborator access to a repository to create, update, or read" them), and write access for cache eviction and workflow enable / disable — which is a property of your access to the repository, not of the token's scopes.
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.
- 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.
MIT