Skip to content

Latest commit

 

History

History
334 lines (241 loc) · 12.1 KB

File metadata and controls

334 lines (241 loc) · 12.1 KB

Deploy Your Own GitHub Issues MCP Server on Cloudflare Workers

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.

What You'll Build

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.

Prerequisites

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)

Architecture Overview

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.


Step 1: Clone and Install

git clone https://github.com/your-username/mcp-github-issues.git
cd mcp-github-issues
npm install

Step 2: Authenticate with Cloudflare

npx wrangler login

This opens a browser window. Authorize wrangler to access your Cloudflare account.

Step 3: Create the KV Namespace

npx wrangler kv namespace create "OAUTH_KV"

Copy the returned ID and paste it into wrangler.jsonc:

"kv_namespaces": [
  {
    "binding": "OAUTH_KV",
    "id": "abc123def456..."  // ← paste your ID here
  }
]

No R2 bucket needed — this server doesn't store any data.

Step 4: Configure Authorized Repositories

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

Step 5: Create a GitHub OAuth App

5a: Local Development OAuth App

  1. Go to GitHub Developer Settings > OAuth Apps
  2. Click New OAuth App
  3. Fill in:
    • Application name: GitHub Issues MCP (local)
    • Homepage URL: http://localhost:8787
    • Authorization callback URL: http://localhost:8787/callback
  4. Click Register application
  5. Copy the Client ID
  6. 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.

5b: Find Your GitHub User ID

curl -s https://api.github.com/users/YOUR_GITHUB_USERNAME | grep '"id"'

Note the numeric ID (e.g., 12345678).

Step 6: Configure Local Development

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-username

Generate the cookie encryption key:

openssl rand -hex 32

Step 7: Test Locally

npm run dev

The server starts on http://localhost:8787.

Test with MCP Inspector

npx @modelcontextprotocol/inspector@latest
  1. Open http://localhost:5173
  2. Enter http://localhost:8787/mcp as the server URL
  3. In OAuth Settings, enable Quick OAuth Flow
  4. Click Connect — you'll be redirected to GitHub
  5. GitHub will ask for repo scope — this is expected (the server needs to read/write issues)
  6. After authorizing, click List Tools

You should see all 12 tools. Try:

  • alive to confirm the server is running
  • list_repos to see your authorized repositories
  • list_issues on one of your repos

Step 8: Deploy to Production

8a: Create a Production GitHub OAuth App

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

8b: Set Production Secrets

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_LOGIN

Use the production GitHub OAuth App credentials.

8c: Deploy

npm run deploy

Your MCP server is now live at https://your-worker-name.your-subdomain.workers.dev/mcp.

Step 9: Connect to Claude

Claude.ai (Web)

  1. Go to Settings > Integrations in Claude.ai
  2. Click Add custom integration
  3. Enter your Worker URL: https://your-worker-name.your-subdomain.workers.dev/mcp
  4. Claude will redirect you to GitHub for authentication
  5. Once authorized, the issue management tools appear in your conversations

Claude Desktop

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.

Step 10: Verify Everything Works

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

How the OAuth Token Is Reused

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:

  1. During the OAuth callback, the server exchanges GitHub's authorization code for an access token
  2. This token is stored in the MCP session via completeAuthorization({ props: { accessToken } })
  3. When a tool is called, it retrieves the token from the MCP context: getTokenFromContext(extra)
  4. The token is used in the Authorization: Bearer header 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.


Understanding the Scope Guard

The server has two layers of access control:

Layer 1: User Identity

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).

Layer 2: Repository Scope

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.


Troubleshooting

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

Uninstall

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

Further Reading