Designing the GitHub MCP Server Architecture
In this chapter, we build a complete GitHub MCP server from scratch. This is the kind of project you would put on your resume or publish as an open-source package. By the end, you will have a server that lets AI manage repositories, issues, pull requests, and code — all through MCP.
What We Are Building
┌─────────────────────────────────────────────────┐
│ GitHub MCP Server │
│ │
│ Tools: │
│ ├── list-repos List user repositories │
│ ├── search-repos Search GitHub repos │
│ ├── create-issue Create a new issue │
│ ├── list-issues List and filter issues │
│ ├── create-pull-request Create a PR │
│ ├── list-pull-requests List and filter PRs │
│ ├── search-code Search code in repos │
│ └── get-file-contents Read file from repo │
│ │
│ Resources: │
│ ├── github://user/profile User profile │
│ ├── github://repos/{owner}/{repo}/readme │
│ └── github://repos/{owner}/{repo}/stats │
│ │
│ Prompts: │
│ ├── repo-analysis Analyze a repository │
│ └── issue-triage Triage open issues │
└─────────────────────────────────────────────────┘
Project Setup
mkdir github-mcp-server && cd github-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
GitHub API Client
// src/github.ts
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
console.error("Error: GITHUB_TOKEN environment variable is required");
process.exit(1);
}
const BASE_URL = "https://api.github.com";
export async function githubApi(path: string, options: RequestInit = {}) {
const url = `${BASE_URL}${path}`;
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`,
Accept: "application/vnd.github.v3+json",
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(`GitHub API error ${response.status}: ${error.message || response.statusText}`);
}
return response.json();
}
// Rate limit helper
export async function checkRateLimit() {
const data = await githubApi("/rate_limit");
return {
remaining: data.rate.remaining,
limit: data.rate.limit,
resetAt: new Date(data.rate.reset * 1000).toISOString(),
};
}
Server Skeleton
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "github-mcp-server",
version: "1.0.0",
});
// We will add tools, resources, and prompts in the next lessons
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitHub MCP Server running");
Configuration for Claude Desktop
{
"mcpServers": {
"github": {
"command": "node",
"args": ["/path/to/github-mcp-server/build/index.js"],
"env": {
"GITHUB_TOKEN": "ghp_your_personal_access_token"
}
}
}
}
Architecture Decisions
Why a Personal Access Token? For simplicity. In the enterprise chapter (Chapter 7), we will upgrade to OAuth 2.1 for multi-user scenarios.
Why not use Octokit? We use raw fetch to keep dependencies minimal and teach the underlying API. In production, you could swap in Octokit for convenience.
Error strategy: Every tool validates inputs, checks rate limits, and returns clear error messages. The AI should always know what went wrong and what to do about it.
Key Takeaway
A well-architected MCP server starts with a clear plan: what tools, resources, and prompts will it expose? How will it authenticate? How will it handle errors? This design-first approach ensures your server is coherent, maintainable, and ready for production.