Skip to main content
Glama
darl33

github-universal-mcp

by darl33
README.md
# github-universal-mcp

Self-hosted remote MCP server that gives any MCP client read-only tools for private GitHub repos. Runs on Cloudflare Workers. It speaks standard MCP (Streamable HTTP) and the MCP authorization spec (OAuth 2.1 with PKCE, dynamic client registration, protected resource metadata), so it works with any client that supports remote servers with OAuth, hosted or local. Towards GitHub it uses a GitHub App user token. Only allowlisted GitHub logins can finish sign-in.

Derived from [nemolize/remote-mcp-github](https://github.com/nemolize/remote-mcp-github) (MIT, see `LICENSE`). Kept: the `workers-oauth-provider` wiring and `src/workers-oauth-utils.ts` (consent page, CSRF and state binding). Changed: GitHub App flow instead of OAuth App scopes, login allowlist, stateless Streamable HTTP at `/mcp` (no Durable Object, no SSE), tool surface.

## Status

Read-only tools:

| Tool | Does |
| --- | --- |
| `github_whoami` | Returns the signed-in login. Use to confirm the connection works. |
| `github_list_repos` | Lists repos the App can see (`visibility`: all, public, private), newest first, 100 per page. |
| `github_get_repo` | Metadata for one repo: visibility, default branch, description. |
| `github_get_contents` | A file's text (truncated at 100k chars) or a directory listing. Optional `ref`. |

The GitHub App token only sees repos the App is installed on; a 404 on a repo you own usually means it isn't in the installation's repository access.

## Endpoints

| Path | Purpose |
|---|---|
| `/mcp` | MCP endpoint (Streamable HTTP, JSON responses). Requires a Bearer token. |
| `/authorize`, `/token`, `/register` | OAuth server used by MCP clients (dynamic client registration). |
| `/callback` | GitHub App redirect target. |
| `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource/mcp` | Discovery metadata. |

## Deploy

Needs Node.js 22+, a Cloudflare account and a GitHub account. Commands assume a bash shell (WSL or Fedora).

1. Set `ALLOWED_GITHUB_LOGINS` in `wrangler.jsonc` to your GitHub login, then install and log in to Cloudflare:

   ```bash
   git clone https://github.com/darl33/github-universal-mcp
   cd github-universal-mcp
   npm ci
   npx wrangler login
   npx wrangler whoami   # note your workers.dev subdomain
   ```

   The Worker URL will be `https://github-mcp.<subdomain>.workers.dev`.

2. Create the KV namespace and paste the printed `id` into `wrangler.jsonc` (`kv_namespaces[0].id`):

   ```bash
   npx wrangler kv namespace create OAUTH_KV
   ```

3. Create the GitHub App at https://github.com/settings/apps/new:

   | Field | Value |
   |---|---|
   | Homepage URL | `https://github-mcp.<subdomain>.workers.dev` |
   | Callback URL | `https://github-mcp.<subdomain>.workers.dev/callback` |
   | Expire user authorization tokens | On (tokens last 8 h, refresh tokens 6 months) |
   | Request user authorization (OAuth) during installation | Off |
   | Enable Device Flow | Off |
   | Webhook > Active | Off |
   | Repository permissions | Contents: Read, Metadata: Read, Pull requests: Read, Issues: Read |
   | Account permissions | None |
   | Where can this GitHub App be installed? | Only on this account |

   Then: generate a client secret, and use **Install App** to install it on **Only select repositories**. A GitHub App user token can only reach repos that are both installed and visible to the user, so the install list is the access boundary.

4. Set secrets and deploy:

   ```bash
   npx wrangler secret put GITHUB_CLIENT_ID       # App "Client ID" (starts with Iv)
   npx wrangler secret put GITHUB_CLIENT_SECRET
   openssl rand -hex 32 | npx wrangler secret put COOKIE_ENCRYPTION_KEY
   npm run deploy
   curl -s https://github-mcp.<subdomain>.workers.dev/.well-known/oauth-authorization-server
   ```

5. Connect a client. Point any MCP client that supports remote servers with OAuth at:

   ```
   https://github-mcp.<subdomain>.workers.dev/mcp
   ```

   Leave any OAuth client ID and secret fields empty; the client registers itself. The client opens a browser to the consent page, then GitHub; sign in with an allowlisted account. Local clients that use a `http://localhost` or `http://127.0.0.1` redirect work too, since the registration accepts loopback redirect URIs.

   Client guides: [Claude](docs/install-claude.md), [Codex](docs/install-codex.md).

   To debug without a client, use MCP Inspector (`npx @modelcontextprotocol/inspector`) with transport "Streamable HTTP" and the URL above.

## Smoke test

1. Ask the client (or Inspector) to call `github_whoami`. Expect your own login.
2. Sign-in as any other GitHub account must end on a 403 page.
3. `curl -i -X POST https://github-mcp.<subdomain>.workers.dev/mcp` must return 401 with a `WWW-Authenticate` header pointing at the resource metadata.

## Troubleshooting

- Logs: `npx wrangler tail`. If a client says sign-in succeeded but calls fail, check whether its requests to `/mcp` carry an `Authorization: Bearer` header.
- If a client shows the server as connected but never offers its tools, test with MCP Inspector first. If Inspector works, the problem is on the client side.
- Some clients refuse to add a second server with the same URL. Remove the old entry before re-adding.
- Token refresh: MCP access tokens are issued to expire 5 minutes before the GitHub token they carry, and the client's refresh also refreshes the GitHub token. A sign-in lasts about 6 months, the lifetime of GitHub's refresh token at sign-in. If a client never refreshes and instead fails after about 8 hours, it is not handling `invalid_token` / refresh tokens; reconnect it.
- A client refresh that fails with `invalid_grant` means GitHub rejected the refresh token (revoked app authorization, or two refreshes raced and the second used an already-rotated token). Sign in again.

## Security notes

- Tokens: the GitHub access and refresh tokens (rotated on every refresh) live in the grant `props`, which `workers-oauth-provider` encrypts before writing to KV. The key is derived from the MCP access token held by the client, so KV contents alone cannot be decrypted.
- Allowlist: `ALLOWED_GITHUB_LOGINS` in `wrangler.jsonc`. It matches login, which GitHub lets users rename; the numeric user id is stored in props if a stricter id check is wanted later.
- The server is never authless: `/mcp` returns 401 without a valid token issued after the allowlist check.