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

Minimal build: one tool, `github_whoami`, to prove a client can connect and call tools before adding more.

## 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.
- `whoami` failing with 401 about 8 hours after connecting is expected in this build: token refresh is the next step. Reconnect the server in your client as a workaround.

## Security notes

- Tokens: the GitHub access and refresh tokens 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.