A step-by-step guide to deploying a remote MCP server that lets Claude (or any MCP client) manage GitHub Issues, labels, and milestones on your repositories, with GitHub OAuth authentication.
By the end of this guide, you'll have a working MCP server that Claude.ai can connect to natively — no browser extensions, no local servers, no mcp-remote proxy needed.
A Cloudflare Worker that exposes 12 MCP tools for GitHub Issues management:
- Issues: list, create, update, comment
- Labels: list, create with colors
- Milestones: list, create with due dates
- Repositories: list authorized repos with metadata
- Health check and help tools
The server authenticates users via GitHub OAuth. The same OAuth token used for authentication is reused for GitHub API calls — no separate Personal Access Token needed.
Access is restricted to a single authorized GitHub account (yours), and scoped to a configurable list of repositories.
| Requirement | Why |
|---|---|
| Node.js 18+ | To run wrangler and build the project |
| A Cloudflare account | Free tier is sufficient for Workers and KV |
| A GitHub account | Used for OAuth authentication and API access |
| wrangler CLI | npm install -g wrangler (Cloudflare's CLI) |
MCP Client (Claude.ai, MCP Inspector)
|
| HTTPS (Streamable HTTP transport)
v
┌──────────────────────────────────────────────┐
│ OAuthProvider wrapper │
│ - Intercepts /authorize, /token, /register │
│ - Validates OAuth tokens on /mcp │
├──────────────────────────────────────────────┤
│ /mcp → MCP Server │
│ - 12 GitHub Issues tools │
│ - Calls GitHub REST API with OAuth token │
├──────────────────────────────────────────────┤
│ /authorize, /callback → GitHub OAuth │
│ - Redirects to GitHub for login │
│ - Requests "repo" scope for full access │
│ - Verifies user identity (ID + username) │
│ - Stores OAuth token for API reuse │
├──────────────────────────────────────────────┤
│ KV Namespace (OAUTH_KV) │
│ - Stores OAuth tokens and sessions │
└──────────────────────────────────────────────┘
Key difference from the vault server: this Worker has no storage layer. It's a stateless proxy — every tool call makes a real-time request to the GitHub REST API using the OAuth token obtained during authentication. The GitHub token is stored in the MCP session (via completeAuthorization({ props: { accessToken } })), and tools retrieve it from the MCP context on each call.
git clone https://github.com/your-username/mcp-github-issues.git
cd mcp-github-issues
npm installnpx wrangler loginThis opens a browser window. Authorize wrangler to access your Cloudflare account.
npx wrangler kv namespace create "OAUTH_KV"Copy the returned ID and paste it into wrangler.jsonc:
No R2 bucket needed — this server doesn't store any data.
Edit the ALLOWED_REPOS variable in wrangler.jsonc to list the repositories the server can access:
"vars": {
"ALLOWED_REPOS": "your-username/repo-one,your-username/repo-two"
}This is a comma-separated list of owner/repo strings. The server will reject any API call targeting a repository not in this list. You can add or remove repos later by editing this field and redeploying.
Helper scripts are included for convenience:
# Add a repository
./scripts/add-repo.sh your-username/new-repo
# Remove a repository
./scripts/remove-repo.sh your-username/old-repo- Go to GitHub Developer Settings > OAuth Apps
- Click New OAuth App
- Fill in:
- Application name:
GitHub Issues MCP (local) - Homepage URL:
http://localhost:8787 - Authorization callback URL:
http://localhost:8787/callback
- Application name:
- Click Register application
- Copy the Client ID
- Click Generate a new client secret and copy it
Important: This OAuth App needs the repo scope (not just read:user). The scope is requested during the OAuth flow in github-handler.ts, not in the GitHub App settings. The App just needs to exist; the scope is determined at authorization time.
curl -s https://api.github.com/users/YOUR_GITHUB_USERNAME | grep '"id"'Note the numeric ID (e.g., 12345678).
Create a .dev.vars file at the project root:
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
COOKIE_ENCRYPTION_KEY=generate-with-openssl-rand-hex-32
ALLOWED_GITHUB_ID=your-github-user-id
ALLOWED_GITHUB_LOGIN=your-github-usernameGenerate the cookie encryption key:
openssl rand -hex 32npm run devThe server starts on http://localhost:8787.
npx @modelcontextprotocol/inspector@latest- Open http://localhost:5173
- Enter
http://localhost:8787/mcpas the server URL - In OAuth Settings, enable Quick OAuth Flow
- Click Connect — you'll be redirected to GitHub
- GitHub will ask for
reposcope — this is expected (the server needs to read/write issues) - After authorizing, click List Tools
You should see all 12 tools. Try:
aliveto confirm the server is runninglist_reposto see your authorized repositorieslist_issueson one of your repos
Go to GitHub Developer Settings and create a second OAuth App:
- Application name:
GitHub Issues MCP (prod) - Homepage URL:
https://your-worker-name.your-subdomain.workers.dev - Authorization callback URL:
https://your-worker-name.your-subdomain.workers.dev/callback
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY
npx wrangler secret put ALLOWED_GITHUB_ID
npx wrangler secret put ALLOWED_GITHUB_LOGINUse the production GitHub OAuth App credentials.
npm run deployYour MCP server is now live at https://your-worker-name.your-subdomain.workers.dev/mcp.
- Go to Settings > Integrations in Claude.ai
- Click Add custom integration
- Enter your Worker URL:
https://your-worker-name.your-subdomain.workers.dev/mcp - Claude will redirect you to GitHub for authentication
- Once authorized, the issue management tools appear in your conversations
Add to your Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"github-issues": {
"command": "npx",
"args": [
"mcp-remote",
"https://your-worker-name.your-subdomain.workers.dev/mcp"
]
}
}
}Restart Claude Desktop. On first use, a GitHub authorization window opens.
| Test | Action | Expected Result |
|---|---|---|
| Server responds | Call alive |
Success message |
| List repos | Call list_repos |
Shows your authorized repositories |
| List issues | Call list_issues with repo: "your-username/your-repo" |
Shows open issues |
| Create issue | Call create_issue with title and repo |
New issue created on GitHub |
| View issue | Call get_issue with issue number |
Full issue details with comments |
| Add comment | Call add_issue_comment on an issue |
Comment appears on GitHub |
| List labels | Call list_labels |
Shows repo labels with colors |
| Create label | Call create_label with name and color |
New label appears on GitHub |
Unlike the vault server (which uses GitHub OAuth purely for identity), this server reuses the GitHub OAuth token for API calls. Here's how it works:
- During the OAuth callback, the server exchanges GitHub's authorization code for an access token
- This token is stored in the MCP session via
completeAuthorization({ props: { accessToken } }) - When a tool is called, it retrieves the token from the MCP context:
getTokenFromContext(extra) - The token is used in the
Authorization: Bearerheader for all GitHub API requests
This means the OAuth token serves double duty: it proves the user's identity AND provides API access to their repositories. No separate Personal Access Token is needed.
The repo scope requested during OAuth grants full read/write access to the user's repositories. This is the minimum scope needed for issue management.
The server has two layers of access control:
In github-handler.ts, the callback verifies:
const ALLOWED_GITHUB_ID = parseInt(env.ALLOWED_GITHUB_ID || "0");
const ALLOWED_GITHUB_LOGIN = env.ALLOWED_GITHUB_LOGIN || "";
if (userData.id !== ALLOWED_GITHUB_ID || userData.login !== ALLOWED_GITHUB_LOGIN) {
// → 403 Access denied
}Both the numeric ID and the username must match. This prevents someone from creating a GitHub account with the same username (GitHub doesn't allow this, but the dual check is defense in depth).
In the tool handlers, every API call is checked against ALLOWED_REPOS:
if (!isRepoAllowed(repo, env)) {
return errorResponse("Repository not in the authorized list.");
}Even if an authenticated user tries to access a repo not in the list, the request is rejected before it reaches the GitHub API.
| Problem | Cause | Fix |
|---|---|---|
| Claude says "Failed to connect" | OAuth callback URL mismatch | Check the callback URL in your GitHub OAuth App matches your Worker URL exactly |
| 403 "Access denied" after GitHub login | GitHub ID or username mismatch | Verify ALLOWED_GITHUB_ID and ALLOWED_GITHUB_LOGIN secrets |
| "Repository not in the authorized list" | Repo not in ALLOWED_REPOS |
Add the repo to wrangler.jsonc and redeploy |
| GitHub API returns 404 | Token doesn't have access to the repo | Make sure you authorized the repo scope during OAuth; if the repo is in an org, the org may need to approve the OAuth App |
| Tools work in Inspector but not Claude.ai | Using local OAuth App for production | Create a separate GitHub OAuth App with the production callback URL |
list_issues returns empty |
No open issues or wrong repo name | Check the repo has open issues; try state: "all" to include closed |
| Resource | How to Remove |
|---|---|
| Worker | npx wrangler delete from the project folder |
| KV Namespace | Cloudflare Dashboard > Workers & Pages > KV > Delete |
| GitHub OAuth App (local) | GitHub > Settings > Developer settings > OAuth Apps > Delete |
| GitHub OAuth App (prod) | GitHub > Settings > Developer settings > OAuth Apps > Delete |