Skip to main content
Glama
README.md
# github-readonly-mcp

A small, self-hosted MCP connector that gives Claude **read-only** access to
your GitHub repos — including private ones. Which repos are reachable is
determined entirely by the scope of the GitHub token you give it (see below),
not by a list in the code. It exists because:

- Cowork's cloud sandbox blocks direct calls to `api.github.com` (you'll get
  `403 GitHub access to this repository is not enabled for this session`).
- GitHub's own official remote MCP server (`api.githubcopilot.com/mcp`) needs
  a GitHub App OAuth flow that Claude's custom-connector UI doesn't support
  (it only takes an OAuth Client ID/Secret, not GitHub's flow).
- Broader third-party connectors grant read **and** write access, which is
  more than this needs.

This connector is deliberately narrow: it runs on Cloudflare Workers (free
tier is plenty), gates access behind a single shared password + OAuth 2.1,
and exposes exactly 7 read-only tools. The reachable repos are whatever the
GitHub token can read — scope the token, not the code. **There is no write
path in the code at all** — no create/update/delete/merge function exists
anywhere, so it can't modify GitHub no matter what a model asks it to do.

Tools it exposes: `list_repos`, `list_commits`, `list_branches`,
`list_pull_requests`, `list_issues`, `get_file_contents`, `search_code`.

Everything below has already been tested locally end-to-end (dynamic client
registration → password-gated authorize → token exchange → authenticated MCP
session → tools/list → tools/call, including a real 401 from GitHub with a
placeholder token and a correctly-rejected malformed repo name). What's
left is deploying it for real and connecting it to Claude.

## 1. Prerequisites

- Node.js 18+ and npm (already used to build this).
- A free Cloudflare account: https://dash.cloudflare.com/sign-up
- A GitHub **fine-grained personal access token**, read-only. **This token is
  the connector's only access boundary** — it can read exactly the repos the
  token can, so scope it deliberately:
  https://github.com/settings/personal-access-tokens/new
  - Repository access: "Only select repositories" → pick exactly the repos you
    want reachable (or "All repositories" if you truly want full read access).
  - Permissions: Contents (Read-only), Metadata (Read-only), Pull requests
    (Read-only), Issues (Read-only). Don't grant anything else.

## 2. Install dependencies

```bash
cd github-readonly-mcp
npm install
```

## 3. Log in to Cloudflare

```bash
npx wrangler login
```

This opens a browser to authorize the Wrangler CLI against your Cloudflare
account.

## 4. Create the KV namespace

`@cloudflare/workers-oauth-provider` needs a KV namespace to store OAuth
grants and token hashes (no secrets are stored in plaintext — just hashes).

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

This prints something like:

```
[[kv_namespaces]]
binding = "OAUTH_KV"
id = "abcd1234...."
```

Copy that `id` value into `wrangler.jsonc`, replacing the placeholder:

```jsonc
"kv_namespaces": [
  { "binding": "OAUTH_KV", "id": "abcd1234...." }  // <- your real id here
],
```

## 5. Set the two secrets

These are **never** written to wrangler.jsonc or committed anywhere — they
live only in Cloudflare's encrypted secret store.

```bash
npx wrangler secret put GITHUB_PAT
# paste your fine-grained GitHub PAT when prompted

npx wrangler secret put CONNECTOR_PASSWORD
# choose a real password — this is the one gate between the internet and
# your GitHub read tools, so make it a proper passphrase, not "test123"
```

## 6. (No allow-list to configure)

There is no repo allow-list in this connector: it can read any repo the
`GITHUB_PAT` grants. The token you scoped in step 1 **is** the boundary —
if you want to narrow what's reachable, narrow the token's repository access,
then rotate it (step 5 / "Updating later"). The connector still validates that
each `repo` argument is a well-formed `"owner/name"` before calling GitHub, so
malformed values can't be used to reach unintended API paths.

## 7. Deploy

```bash
npm run deploy
```

Wrangler will print a URL like `https://github-readonly-mcp.<your-subdomain>.workers.dev`.
That's your connector's base URL — save it, you'll need it in the next step.

Sanity-check it:

```bash
curl https://github-readonly-mcp.<your-subdomain>.workers.dev/health
# -> "github-readonly-mcp is running. Connect it to Claude as a custom connector."
```

## 8. Add it to Claude as a custom connector

1. In Claude, go to **Settings → Connectors → Add custom connector**.
2. Name it something like "GitHub (read-only)".
3. Remote MCP server URL: `https://github-readonly-mcp.<your-subdomain>.workers.dev/mcp`
4. Save. Claude will kick off the OAuth flow: it hits `/register` automatically,
   then opens the `/authorize` page in step 9.

## 9. Authorize it

You'll land on a small dark-themed page titled "Authorize Claude." Enter the
`CONNECTOR_PASSWORD` you set in step 5 and submit. That's the only manual
gate — after this, Claude holds a scoped OAuth token and can call the 7
tools directly.

## 10. Verify it works

Ask Claude something like "using the GitHub read-only connector, list
recent commits on jpalvarezb/bonafe" and confirm it returns real commit
data (not a 401). You can also ask it to `list_repos` to see everything the
token can reach.

## 11. Tell me once it's connected

Once steps 1–10 are done, let me know — I'll delete and recreate the
"Daily Schedule Review (cloud)" scheduled task so its connector list picks
up this new one, then we'll run one more supervised test exercising
`list_commits` / `list_pull_requests` / `list_issues` against your real repos
with real credentials, to confirm the whole chain works end-to-end.

## Updating later

Changed a tool, re-scoped the repos, or rotated the PAT? (Re-scoping the
reachable repos now means issuing a new fine-grained token and rotating it —
there's no list in the code to edit.)

```bash
npm run deploy          # code/config changes
npx wrangler secret put GITHUB_PAT   # to rotate the token
```

No need to touch the OAuth setup again — Claude's existing connection keeps
working across redeploys as long as the KV namespace and secrets stay in
place.

## Security notes

- **Read-only by construction, not by policy.** There is no function in
  `src/github.ts` (or anywhere else) that issues a POST/PATCH/PUT/DELETE to
  GitHub. A prompt injection or a misbehaving model can't make this
  connector write to GitHub — the code to do so doesn't exist.
- **The GitHub token is the access boundary.** There is no repo allow-list;
  the connector can read exactly what the `GITHUB_PAT` can. Use a fine-grained,
  read-only token scoped to just the repos you want reachable. The connector
  does validate that each `repo` argument is a well-formed `"owner/name"`
  before calling GitHub, which prevents malformed values from being smuggled
  into unintended API paths — but that is an injection guard, not access
  control. **Anyone who has the connector password can read every repo the
  token can**, so keep both the token narrow and the password strong.
- **Secrets never touch source control.** `GITHUB_PAT` and
  `CONNECTOR_PASSWORD` are Cloudflare secrets (`wrangler secret put`), not
  `vars` in wrangler.jsonc, and `.dev.vars` is gitignored.
- **One shared password, by design.** The `/authorize` gate uses a single
  password rather than a full user-account system because this connector
  has exactly one legitimate caller — you, connecting your own Claude to
  your own GitHub data. If you ever want to share it with others, that
  authorize.ts gate is the one place to revisit.

## Project layout

```
src/
  index.ts       OAuthProvider wiring: routes /authorize, /token, /register, /mcp
  authorize.ts   Password-gated consent page (the only manual step)
  mcp-agent.ts   MCP server: registers the 7 read-only tools
  github.ts      GitHub REST wrapper — GET-only, "owner/name" format-validated
  env.d.ts       Adds GITHUB_PAT/CONNECTOR_PASSWORD to wrangler's generated Env type
wrangler.jsonc   Worker config: DO binding, KV binding
```