Project 16: GitHub MCP Integration - PR Workflow Automation
Project 16: GitHub MCP Integration - PR Workflow Automation
Project Overview
| Attribute | Value |
|---|---|
| File | P16-github-mcp-integration.md |
| Main Programming Language | TypeScript |
| Alternative Programming Languages | Python, Go |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | 3. The “Service & Support” Model |
| Difficulty | Level 3: Advanced |
| Knowledge Area | MCP / GitHub API / DevOps |
| Software or Tool | MCP SDK, GitHub API, gh CLI |
| Main Book | “Software Engineering at Google” by Winters et al. |
| Time Estimate | 2 weeks |
| Prerequisites | Project 15 completed, GitHub API familiarity, OAuth understanding |
What You Will Build
An MCP server that wraps the GitHub API for PR workflows: create PRs, list open PRs, add reviewers, respond to comments, merge PRs. Includes OAuth authentication and rate limiting.
This project shows how MCP servers can wrap existing APIs to make them accessible to Claude. You will learn about authentication, pagination, and real-world API integration.
Real World Outcome
You: Create a PR for my current branch
Claude: [Invokes mcp__github__create_pr]
I have created PR #142:
Title: feat(auth): Add OAuth2 support
URL: https://github.com/you/repo/pull/142
Description: Added OAuth2 authentication with refresh token support
Labels: enhancement, needs-review
Reviewers: @alice, @bob (auto-assigned based on CODEOWNERS)
Status: Ready for review
You: What comments are on the PR?
Claude: [Invokes mcp__github__list_pr_comments]
Comments on PR #142:
@alice (2 hours ago):
> The token refresh logic looks good, but can we add a test
> for the edge case when the refresh token expires?
@bob (1 hour ago):
> +1 on Alice's comment. Also, should we log token refresh events?
Would you like me to respond to these comments or make changes?
The Core Question You Are Answering
“How do I wrap an existing API (like GitHub) as an MCP server so Claude can interact with it naturally?”
Many developers already have tools they love. MCP lets you keep using those tools through Claude, creating a natural language interface to existing workflows. This pattern applies to any REST API you want to expose.
MCP API Wrapper Architecture
+------------------------------------------------------------------+
| CLAUDE CODE |
| |
| "Create a PR for my current branch" |
| | |
| v |
| +------------------------------------------------------------+ |
| | MCP CLIENT | |
| +------------------------------------------------------------+ |
| | |
+--------------------------|------------------------------------------+
| stdio (JSON-RPC)
v
+------------------------------------------------------------------+
| GITHUB MCP SERVER |
| |
| +--------------------+ +--------------------+ |
| | Tool: create_pr | | Tool: list_prs | |
| | Tool: merge_pr | | Tool: add_reviewer | |
| | Tool: list_comments| | Tool: respond | |
| +--------------------+ +--------------------+ |
| | |
| +--------------------+ +--------------------+ |
| | Auth Handler | | Rate Limiter | |
| | (Token mgmt) | | (Quota tracking) | |
| +--------------------+ +--------------------+ |
| | |
+--------------------------|------------------------------------------+
| HTTPS
v
+------------------------------------------------------------------+
| GITHUB REST API |
| api.github.com |
+------------------------------------------------------------------+
GitHub API Rate Limiting
+---------------------------------------------------------------+
| RATE LIMIT FLOW |
+---------------------------------------------------------------+
| |
| Request 1 Request 2 Request 3 |
| | | | |
| v v v |
| +---------+ +---------+ +---------+ |
| | Check | | Check | | Check | |
| | Quota | | Quota | | Quota | |
| +---------+ +---------+ +---------+ |
| | | | |
| Remaining: 4999 Remaining: 4998 Remaining: 0 |
| | | | |
| v v v |
| +----------+ +----------+ +----------+ |
| | PROCEED | | PROCEED | | WAIT FOR | |
| | | | | | RESET | |
| +----------+ +----------+ +----------+ |
| | |
| Reset-At: 2025-12-22T10:00:00Z |
| | |
| v |
| Wait 45 sec |
| | |
| v |
| RETRY |
| |
+---------------------------------------------------------------+
Response Headers:
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4998
X-RateLimit-Reset: 1734865200
Concepts You Must Understand First
Stop and research these before coding:
1. GitHub API
| Aspect | Details | Reference | |——–|———|———–| | REST vs GraphQL | When to use each | docs.github.com/en/rest | | Authentication | PAT, OAuth Apps, GitHub Apps | GitHub authentication docs | | Rate limits | 5000 req/hour for authenticated | API rate limit docs | | Pagination | Link headers, cursor-based | REST pagination guide |
2. MCP Authentication Patterns
+--------------------------------------------------+
| AUTHENTICATION FLOW |
+--------------------------------------------------+
| |
| 1. Environment Variable |
| GITHUB_TOKEN=ghp_xxxx... |
| | |
| v |
| 2. MCP Server reads on startup |
| const token = process.env.GITHUB_TOKEN |
| | |
| v |
| 3. Server includes in all requests |
| Authorization: Bearer ghp_xxxx... |
| |
+--------------------------------------------------+
3. Complex Tool Schemas
- Nested objects in inputSchema
- Optional vs required parameters
- Array parameters for batch operations
- Reference: JSON Schema specification
Questions to Guide Your Design
Before implementing, think through these:
1. What Operations to Support?
| Category | Operations | Complexity |
|---|---|---|
| PRs | create, list, merge, close, request_review | Medium-High |
| Comments | list, create, respond, resolve | Medium |
| Issues | list, create, close, label | Medium |
| Repos | list, get_info | Low |
2. Authentication Strategy
| Method | Pros | Cons | Use When |
|---|---|---|---|
| Personal Access Token | Simple setup | Per-user, manual | Local development |
| OAuth App | User-friendly | Complex flow | Multi-user apps |
| GitHub CLI auth | Reuse existing | Dependency on gh | When gh is available |
3. Error Handling Scenarios
| Error | Response Code | Your Action |
|---|---|---|
| Rate limit exceeded | 403 | Return remaining/reset time |
| Not found | 404 | Clear error message |
| Permission denied | 403 | Explain required permissions |
| Network failure | - | Retry with backoff |
| Invalid input | 422 | Surface validation errors |
Thinking Exercise
Design the PR Creation Tool
{
"name": "create_pr",
"description": "Create a pull request from a branch",
"inputSchema": {
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository in format owner/repo"
},
"head": {
"type": "string",
"description": "Branch containing changes"
},
"base": {
"type": "string",
"description": "Branch to merge into",
"default": "main"
},
"title": {
"type": "string",
"description": "PR title"
},
"body": {
"type": "string",
"description": "PR description in markdown"
},
"draft": {
"type": "boolean",
"description": "Create as draft PR",
"default": false
},
"reviewers": {
"type": "array",
"items": {"type": "string"},
"description": "GitHub usernames to request review from"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Labels to apply to the PR"
}
},
"required": ["repo", "head", "title"]
}
}
Questions to consider:
- Should you auto-detect
repofrom the current git directory? - How do you handle branch names with slashes (e.g.,
feature/auth)? - What if the user does not specify reviewers: should you use CODEOWNERS?
- Should you auto-generate the PR body from commit messages?
The Interview Questions They Will Ask
- “How would you build a natural language interface to the GitHub API?”
- Discuss MCP tool design, parameter mapping, and response formatting.
- “How do you handle API rate limits in a user-facing tool?”
- Explain exponential backoff, remaining/reset headers, and user notification.
- “What is the security model for API tokens in CLI tools?”
- Discuss environment variables, secure storage, and credential rotation.
- “How do you design tool schemas for complex operations?”
- Explain JSON Schema, optional parameters, and sensible defaults.
- “How would you test an MCP server that depends on external APIs?”
- Discuss mocking, integration tests, and recorded responses.
Hints in Layers
Hint 1: Start with gh CLI
The gh CLI is already installed on most developer machines. Shell out to it for a simpler initial implementation:
import { execSync } from "child_process";
const result = execSync(`gh pr create --title "${title}" --body "${body}"`, {
encoding: "utf-8",
cwd: repoPath
});
Hint 2: Use Environment Variables
Set GITHUB_TOKEN and read it in your server. Never hardcode tokens:
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error("GITHUB_TOKEN environment variable required");
}
Hint 3: Track Rate Limit Headers
GitHub returns rate limit info in every response. Surface it to Claude:
const remaining = response.headers.get("X-RateLimit-Remaining");
const reset = response.headers.get("X-RateLimit-Reset");
return {
data: result,
rateLimit: {
remaining: parseInt(remaining),
resetsAt: new Date(parseInt(reset) * 1000)
}
};
Hint 4: Handle Pagination
For list operations, accept pagination parameters and indicate if there are more results:
{
name: "list_prs",
inputSchema: {
type: "object",
properties: {
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"] },
page: { type: "number", default: 1 },
per_page: { type: "number", default: 30, maximum: 100 }
}
}
}
Books That Will Help
| Topic | Book | Chapter | Why It Helps |
|---|---|---|---|
| API Design | “Building Microservices” by Newman | Ch. 4 | Service communication patterns |
| OAuth Patterns | “OAuth 2.0 Simplified” by Parecki | All | Token-based authentication |
| Rate Limiting | “Designing Data-Intensive Applications” | Ch. 4 | Distributed system patterns |
| Git Workflows | “Software Engineering at Google” | Ch. 16 | Code review practices |
Implementation Skeleton
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { execSync } from "child_process";
const server = new Server(
{ name: "github-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Rate limit tracking
let rateLimitRemaining = 5000;
let rateLimitReset = new Date();
server.setRequestHandler("tools/list", async () => {
return {
tools: [
{
name: "create_pr",
description: "Create a pull request",
inputSchema: {
type: "object",
properties: {
repo: { type: "string", description: "owner/repo format" },
head: { type: "string", description: "Source branch" },
base: { type: "string", description: "Target branch", default: "main" },
title: { type: "string", description: "PR title" },
body: { type: "string", description: "PR description" },
draft: { type: "boolean", default: false },
reviewers: { type: "array", items: { type: "string" } }
},
required: ["repo", "head", "title"]
}
},
{
name: "list_prs",
description: "List pull requests",
inputSchema: {
type: "object",
properties: {
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" }
},
required: ["repo"]
}
},
{
name: "list_pr_comments",
description: "List comments on a pull request",
inputSchema: {
type: "object",
properties: {
repo: { type: "string" },
pr_number: { type: "number" }
},
required: ["repo", "pr_number"]
}
},
{
name: "merge_pr",
description: "Merge a pull request",
inputSchema: {
type: "object",
properties: {
repo: { type: "string" },
pr_number: { type: "number" },
merge_method: {
type: "string",
enum: ["merge", "squash", "rebase"],
default: "squash"
}
},
required: ["repo", "pr_number"]
}
}
]
};
});
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "create_pr") {
const { repo, head, base = "main", title, body, draft, reviewers } = args;
let cmd = `gh pr create --repo ${repo} --head ${head} --base ${base}`;
cmd += ` --title ${JSON.stringify(title)}`;
if (body) cmd += ` --body ${JSON.stringify(body)}`;
if (draft) cmd += " --draft";
if (reviewers?.length) cmd += ` --reviewer ${reviewers.join(",")}`;
const result = execSync(cmd, { encoding: "utf-8" });
return {
content: [{
type: "text",
text: `PR created successfully:\n${result}`
}]
};
}
if (name === "list_prs") {
const { repo, state = "open" } = args;
const cmd = `gh pr list --repo ${repo} --state ${state} --json number,title,author,createdAt,url`;
const result = JSON.parse(execSync(cmd, { encoding: "utf-8" }));
if (result.length === 0) {
return { content: [{ type: "text", text: `No ${state} PRs found.` }] };
}
let output = `Found ${result.length} ${state} PR(s):\n\n`;
for (const pr of result) {
output += `#${pr.number}: ${pr.title}\n`;
output += ` Author: ${pr.author.login}\n`;
output += ` Created: ${pr.createdAt}\n`;
output += ` URL: ${pr.url}\n\n`;
}
return { content: [{ type: "text", text: output }] };
}
if (name === "list_pr_comments") {
const { repo, pr_number } = args;
const cmd = `gh pr view ${pr_number} --repo ${repo} --json comments`;
const result = JSON.parse(execSync(cmd, { encoding: "utf-8" }));
if (result.comments.length === 0) {
return { content: [{ type: "text", text: "No comments on this PR." }] };
}
let output = `Comments on PR #${pr_number}:\n\n`;
for (const comment of result.comments) {
output += `@${comment.author.login} (${comment.createdAt}):\n`;
output += `> ${comment.body.split("\n").join("\n> ")}\n\n`;
}
return { content: [{ type: "text", text: output }] };
}
if (name === "merge_pr") {
const { repo, pr_number, merge_method = "squash" } = args;
const cmd = `gh pr merge ${pr_number} --repo ${repo} --${merge_method}`;
execSync(cmd, { encoding: "utf-8" });
return {
content: [{
type: "text",
text: `PR #${pr_number} merged successfully using ${merge_method} method.`
}]
};
}
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error.message}`
}],
isError: true
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitHub MCP server running on stdio");
}
main().catch(console.error);
Learning Milestones
| Milestone | What It Proves | Verification |
|---|---|---|
| Basic PR operations work | You understand API wrapping | Create a test PR |
| Authentication is seamless | You understand token management | Works with GITHUB_TOKEN |
| Rate limits are handled | Production-ready implementation | Display remaining quota |
| Pagination works | Handle large result sets | List 100+ PRs |
Core Challenges Mapped to Concepts
| Challenge | Concept | Book Reference |
|---|---|---|
| OAuth authentication flow | Token management | “OAuth 2.0 Simplified” |
| Handling pagination | API design patterns | “Building Microservices” Ch. 4 |
| Rate limit handling | Resilient services | “Designing Data-Intensive Applications” |
| Complex tool schemas | Nested objects and arrays | JSON Schema Specification |
Extension Ideas
Once the basic server works, consider these enhancements:
- Add PR diff viewing to show changed files
- Implement comment threading for responding to specific comments
- Add CI status checking to verify checks pass before merge
- Implement auto-assign using CODEOWNERS file parsing
- Add PR templates support for consistent descriptions
Common Pitfalls
- Not handling rate limits - Always check remaining quota and wait if needed
- Forgetting to escape shell arguments - Use proper quoting or libraries
- Not handling pagination - Large repos have many PRs
- Exposing tokens in error messages - Sanitize error output
- Not handling network failures - Add retry logic with exponential backoff
Success Criteria
You have completed this project when:
- Your MCP server authenticates with GitHub via GITHUB_TOKEN
- Claude can create pull requests with title, body, and reviewers
- Claude can list open PRs for any repository
- Claude can view comments on a specific PR
- Claude can merge PRs using different strategies
- Rate limit information is tracked and surfaced
- Errors are handled gracefully with helpful messages
- The server works with any GitHub repository you have access to