Day 11 — What MCP Is: Build a Claude Tool That Reads Your GitHub
MCP is the newest concept in this course and the least understood in mainstream tech.
If you can explain MCP in an interview, you are ahead of most candidates including seniors. The spec was released by Anthropic in late 2024 and adoption is growing rapidly.
By the end of this post you will have a working MCP server that connects Claude Desktop to your GitHub repositories. You will be able to ask Claude to explain your own projects, find the best project for a specific job, and generate interview-ready explanations.
Time required: 60 minutes
Cost: Free (Claude Desktop is free, GitHub API is free)
What you need: Claude Desktop installed, GitHub account, Node.js 18+
Before You Start — Check These
Check Claude Desktop is installed: Download from claude.ai/download. Sign in with your Anthropic account.
Check Node.js version:
node --version
You need v18 or higher. Download from nodejs.org if needed.
Check npm:
npm --version
Should show a version number.
Check Python (for the Python version):
python --version
Need 3.10 or higher.
What is MCP?
Before MCP, every AI tool had its own way to connect to external services. Want Claude to read your Google Drive? Custom integration. Want it to query your database? Another custom integration. Each one was bespoke and could not be reused.
MCP (Model Context Protocol) is the standardised way for AI models to connect to tools and data.
Think of it like USB-C. Before USB-C, every device had its own charger. After USB-C, one standard cable works everywhere.
MCP is the USB-C for AI tools.
Before MCP:
Claude ←(custom)→ GitHub
Claude ←(custom)→ Database
Claude ←(custom)→ Notion
After MCP:
Claude ←(MCP)→ GitHub MCP Server
Claude ←(MCP)→ Database MCP Server
Claude ←(MCP)→ Notion MCP Server
Same Claude. Same protocol. Different servers.
3 Things an MCP Server Can Expose
Resources — Data Claude can read Example: Your GitHub repositories, a database table, files on your computer
Tools — Functions Claude can call Example: Create a file, run a query, send a message
Prompts — Pre-built prompt templates Example: "Review this code for security issues"
How MCP Works
Claude Desktop (MCP Client)
↕ MCP protocol over stdin/stdout
Your MCP Server (running on your computer)
↕ HTTPS
GitHub API → returns repo data, file contents, READMEs
The MCP server is a bridge. Claude asks for data using the protocol. Your server fetches it from GitHub. Claude uses it to answer your question. All with a standard interface.
Claude Desktop discovers your server from a config file. When you start Claude Desktop, it automatically starts your MCP server too.
Project Setup
Step 1: Create Project Folder
mkdir github-mcp-server
cd github-mcp-server
Step 2: Initialise Node Project
npm init -y
This creates package.json.
Step 3: Install Dependencies
npm install @modelcontextprotocol/sdk axios dotenv
npm install --save-dev @types/node typescript ts-node
What these do:
@modelcontextprotocol/sdk— the official MCP SDK from Anthropicaxios— to call the GitHub APIdotenv— to load your GitHub tokentypescript+ts-node— to write TypeScript (optional but recommended)
Step 4: Get a GitHub Personal Access Token
- Go to github.com → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click "Generate new token (classic)"
- Give it a name: "MCP Server"
- Select scopes: check
repo(full control of private repositories) - Click Generate
- Copy the token (starts with
ghp_)
Keep this secret — it gives access to your repos.
Step 5: Create .env File
# Mac/Linux
touch .env
# Windows
type nul > .env
Add:
GITHUB_TOKEN=ghp_your_token_here
GITHUB_USERNAME=your_github_username
Step 6: Create .gitignore
.env
node_modules/
dist/
Project File Structure
github-mcp-server/
├── .env ← GitHub token (never commit)
├── .gitignore
├── package.json
├── tsconfig.json
└── src/
└── server.ts ← the MCP server
Create tsconfig.json
# Mac/Linux
touch tsconfig.json
# Windows
type nul > tsconfig.json
Add this content:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Create the MCP Server
Create the folder and file:
mkdir src
On Mac/Linux:
touch src/server.ts
On Windows:
type nul > src\server.ts
Now paste the complete server code into src/server.ts:
// src/server.ts
// GitHub MCP Server
// Lets Claude read your GitHub repositories and explain your projects
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import * as dotenv from "dotenv";
dotenv.config();
const GITHUB_TOKEN = process.env.GITHUB_TOKEN!;
const GITHUB_USERNAME = process.env.GITHUB_USERNAME!;
if (!GITHUB_TOKEN || !GITHUB_USERNAME) {
console.error("ERROR: Set GITHUB_TOKEN and GITHUB_USERNAME in .env file");
process.exit(1);
}
// GitHub API helper
const github = axios.create({
baseURL: "https://api.github.com",
headers: {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: "application/vnd.github.v3+json",
},
});
// Create the MCP server
const server = new Server(
{
name: "github-mcp-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// ── RESOURCES — Data Claude can read ─────────────────────────────────────────
// List all repositories as resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const response = await github.get(`/users/${GITHUB_USERNAME}/repos`, {
params: { sort: "updated", per_page: 20 },
});
const repos = response.data;
return {
resources: repos.map((repo: any) => ({
uri: `github://repo/${repo.name}`,
name: repo.name,
description: repo.description || "No description",
mimeType: "text/plain",
})),
};
});
// Read a specific repository
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri as string;
const repoName = uri.replace("github://repo/", "");
// Get repository info
const repoResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}`);
const repo = repoResponse.data;
// Get repository contents (root files)
const contentsResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}/contents`);
const contents = contentsResponse.data;
let result = `Repository: ${repo.name}\n`;
result += `Description: ${repo.description || "None"}\n`;
result += `Language: ${repo.language || "Unknown"}\n`;
result += `Stars: ${repo.stargazers_count}\n`;
result += `Created: ${repo.created_at?.split("T")[0]}\n`;
result += `Last updated: ${repo.updated_at?.split("T")[0]}\n\n`;
result += `Files:\n`;
// List files
for (const item of contents) {
result += `- ${item.name} (${item.type})\n`;
}
// Try to read README
const readmeFile = contents.find((f: any) =>
f.name.toLowerCase().startsWith("readme")
);
if (readmeFile?.download_url) {
const readmeResponse = await axios.get(readmeFile.download_url);
result += `\n=== README ===\n${readmeResponse.data.slice(0, 2000)}`;
}
// Try to read the main code file (first .py, .js, .ts, .java file)
const codeFile = contents.find((f: any) =>
[".py", ".js", ".ts", ".java", ".cpp", ".go"].some(
(ext) => f.name.endsWith(ext)
)
);
if (codeFile?.download_url) {
const codeResponse = await axios.get(codeFile.download_url);
result += `\n=== ${codeFile.name} ===\n${codeResponse.data.slice(0, 3000)}`;
}
return {
contents: [{ uri, mimeType: "text/plain", text: result }],
};
});
// ── TOOLS — Functions Claude can call ────────────────────────────────────────
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "list_repos",
description: "List all GitHub repositories for the user",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "get_repo_details",
description: "Get details and code from a specific repository",
inputSchema: {
type: "object",
properties: {
repo_name: {
type: "string",
description: "Name of the GitHub repository",
},
},
required: ["repo_name"],
},
},
{
name: "find_best_project_for_job",
description: "Find which repository best matches a job description",
inputSchema: {
type: "object",
properties: {
job_description: {
type: "string",
description: "The job description text or required skills",
},
},
required: ["job_description"],
},
},
{
name: "generate_interview_explanation",
description: "Generate an interview-ready explanation of a project",
inputSchema: {
type: "object",
properties: {
repo_name: {
type: "string",
description: "Repository to explain",
},
audience: {
type: "string",
description: "Who to explain to: 'technical', 'hr', or 'non-technical'",
},
},
required: ["repo_name"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "list_repos") {
const response = await github.get(`/users/${GITHUB_USERNAME}/repos`, {
params: { sort: "updated", per_page: 20 },
});
const repos = response.data.map((r: any) => ({
name: r.name,
description: r.description || "No description",
language: r.language || "Unknown",
stars: r.stargazers_count,
updated: r.updated_at?.split("T")[0],
}));
return {
content: [
{
type: "text",
text: `Found ${repos.length} repositories:\n\n${
repos.map((r: any, i: number) =>
`${i+1}. ${r.name} (${r.language})\n ${r.description}\n Last updated: ${r.updated}`
).join("\n\n")
}`,
},
],
};
}
if (name === "get_repo_details") {
const repoName = (args as any).repo_name;
const resource = await server.requestHandlers.get(
"resources/read"
);
// Fetch directly
const repoResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}`);
const contentsResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}/contents`);
const repo = repoResponse.data;
const contents = contentsResponse.data;
let details = `Repository: ${repo.name}\n`;
details += `Language: ${repo.language}\n`;
details += `Description: ${repo.description || "None"}\n\n`;
details += `Files: ${contents.map((f: any) => f.name).join(", ")}\n\n`;
const readmeFile = contents.find((f: any) =>
f.name.toLowerCase().startsWith("readme")
);
if (readmeFile?.download_url) {
const readme = await axios.get(readmeFile.download_url);
details += `README:\n${readme.data.slice(0, 1500)}\n`;
}
return { content: [{ type: "text", text: details }] };
}
if (name === "find_best_project_for_job") {
const jd = (args as any).job_description;
const reposResponse = await github.get(`/users/${GITHUB_USERNAME}/repos`, {
params: { sort: "updated", per_page: 20 },
});
const repoList = reposResponse.data.map((r: any) =>
`${r.name} (${r.language || "Unknown"}): ${r.description || "No description"}`
).join("\n");
return {
content: [
{
type: "text",
text: `Job requirements: ${jd}\n\nYour repositories:\n${repoList}\n\n[Based on this, Claude will identify the best match and explain why]`,
},
],
};
}
if (name === "generate_interview_explanation") {
const repoName = (args as any).repo_name;
const audience = (args as any).audience || "technical";
const repoResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}`);
const contentsResponse = await github.get(`/repos/${GITHUB_USERNAME}/${repoName}/contents`);
const repo = repoResponse.data;
const contents = contentsResponse.data;
let codeContext = `Repository: ${repo.name}\n`;
codeContext += `Language: ${repo.language}\n`;
codeContext += `Description: ${repo.description || "None"}\n`;
codeContext += `Files: ${contents.map((f: any) => f.name).join(", ")}\n\n`;
// Read README for context
const readme = contents.find((f: any) =>
f.name.toLowerCase().startsWith("readme")
);
if (readme?.download_url) {
const r = await axios.get(readme.download_url);
codeContext += `README:\n${r.data.slice(0, 1000)}\n`;
}
return {
content: [
{
type: "text",
text: `${codeContext}\n\n[Claude will generate a ${audience}-level explanation of this project for an interview]`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// ── Start the Server ──────────────────────────────────────────────────────────
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitHub MCP Server running");
}
main().catch(console.error);
Add Scripts to package.json
Open package.json and replace the scripts section:
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "ts-node --esm src/server.ts"
}
}
Also add "type": "module" to the top level of package.json:
{
"type": "module",
"name": "github-mcp-server",
...
}
Build the Server
npm run build
This compiles TypeScript to JavaScript in the dist/ folder.
Expected output:
(no output means success)
If you see TypeScript errors, check that you pasted the code exactly.
Connect to Claude Desktop
Find the Claude Desktop config file:
Mac:
open ~/Library/Application\ Support/Claude/
The file is claude_desktop_config.json. If it does not exist, create it.
Windows: Open File Explorer → paste in address bar:
%APPDATA%\Claude\
The file is claude_desktop_config.json. If it does not exist, create it.
Open claude_desktop_config.json and add:
{
"mcpServers": {
"github": {
"command": "node",
"args": ["/FULL/PATH/TO/github-mcp-server/dist/server.js"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here",
"GITHUB_USERNAME": "your_github_username"
}
}
}
}
Important: Replace /FULL/PATH/TO/github-mcp-server/ with the actual full path to your project.
Find your full path:
# Mac/Linux — run this in your project folder
pwd
# Windows — run this in your project folder
cd
For example, if pwd shows /Users/lakshmi/projects/github-mcp-server, then the config is:
"args": ["/Users/lakshmi/projects/github-mcp-server/dist/server.js"]
Restart Claude Desktop
Mac: Quit Claude (Cmd+Q) and reopen it
Windows: Close Claude from the taskbar and reopen it
When Claude Desktop starts, it automatically launches your MCP server.
How to verify it is connected:
Click the hammer icon (🔨) in Claude's input area. You should see your tools listed:
- list_repos
- get_repo_details
- find_best_project_for_job
- generate_interview_explanation
If you do not see the hammer icon, your server is not connected. Check the path in the config file.
Try It
Type these prompts in Claude Desktop:
"Show me all my GitHub repositories"
Claude calls list_repos and shows your repos.
"Tell me about my [repo-name] project"
Claude calls get_repo_details and reads the code and README.
"Which of my projects is best for a role requiring Python and machine learning?"
Claude calls find_best_project_for_job with your JD and recommends the best match.
"Generate an interview explanation of my [repo-name] project for a technical interviewer"
Claude calls generate_interview_explanation, reads the code, and writes a detailed technical explanation you can memorise.
Common Errors and Fixes
Hammer icon does not appear in Claude Desktop
- Check the path in
claude_desktop_config.jsonis correct and uses forward slashes - Check that
npm run buildsucceeded (dist/server.js exists) - Quit Claude completely and restart
ENOENT: no such file or directory
The path in your config is wrong. Run pwd in your project folder to get the exact path.
Error: Cannot find module
Run npm run build again. The dist folder may be missing.
GitHub API rate limit exceeded
The free GitHub API allows 5000 requests per hour. This should be more than enough. If you hit it, wait an hour.
Authentication failed
Your GitHub token may have expired or has insufficient permissions. Generate a new token with repo scope.
Break It — Learn by Experimenting
Experiment 1 — See MCP Communication
MCP communicates via stdin/stdout. To see what is actually being sent, run the server directly:
node dist/server.js
Then in another terminal, send a raw MCP message:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/server.js
What do you see? The raw JSON response listing your tools.
What you learned: MCP is just JSON over stdin/stdout. The protocol is not magic — it is a standard message format that any program can implement.
Press Ctrl+C to stop.
Experiment 2 — Add a New Tool
Add this tool to the ListToolsRequestSchema handler (inside the tools array):
{
name: "get_commit_history",
description: "Get the last 5 commits for a repository",
inputSchema: {
type: "object",
properties: {
repo_name: {
type: "string",
description: "Repository name",
},
},
required: ["repo_name"],
},
},
Add this handler inside CallToolRequestSchema:
if (name === "get_commit_history") {
const repoName = (args as any).repo_name;
const response = await github.get(
`/repos/${GITHUB_USERNAME}/${repoName}/commits`,
{ params: { per_page: 5 } }
);
const commits = response.data.map((c: any) => ({
message: c.commit.message.split("\n")[0],
date: c.commit.author.date?.split("T")[0],
author: c.commit.author.name,
}));
return {
content: [{
type: "text",
text: commits.map((c: any, i: number) =>
`${i+1}. ${c.date} — ${c.message} (${c.author})`
).join("\n"),
}],
};
}
Run npm run build. Restart Claude Desktop. Ask: "Show me recent commits for my [repo-name] project."
What you learned: Adding a new capability to Claude takes about 15 lines of code. This is the power of MCP.
Experiment 3 — Break the Config
Open claude_desktop_config.json and change the server name to a path that does not exist:
"args": ["/wrong/path/server.js"]
Restart Claude Desktop.
What happens? The hammer icon disappears. Claude has no GitHub tools.
What you learned: The MCP server must be running for Claude to access it. If the server crashes or the path is wrong, Claude loses the capability entirely.
Fix the path and restart Claude Desktop.
The Challenge
Goal: Add a tool called create_portfolio_description that:
- Reads a repository's details
- Returns a formatted description ready to paste into resumeportfolio.in
- Includes: project title, tech stack, 2-sentence description, and 3 bullet points of what you built
Format to return:
Project Title: [name]
Tech Stack: [languages and tools]
Description: [2 sentences]
Key points:
• [what you built 1]
• [what you built 2]
• [what you built 3]
The tricky part: this tool should format the output perfectly so you can copy-paste it directly into your portfolio.
What to Push to GitHub
# Make sure dist/ is in .gitignore
echo "dist/" >> .gitignore
git init
git add .
git commit -m "Day 10: GitHub MCP Server"
git remote add origin https://github.com/yourusername/github-mcp-server.git
git push -u origin main
Your repo:
github-mcp-server/
├── .gitignore ← includes .env and dist/
├── README.md
├── package.json
├── tsconfig.json
└── src/
└── server.ts
Note: Do NOT commit the dist/ folder or .env file.
What to Write in Your Portfolio
Project title: GitHub MCP Server for Claude Desktop
Description (copy this): Built an MCP (Model Context Protocol) server that extends Claude Desktop with custom GitHub tools. The server exposes 4 tools via the MCP standard: listing repositories, reading code and READMEs, finding the best project match for a job description, and generating interview-ready project explanations. Implemented using TypeScript and the official Anthropic MCP SDK with the stdio transport layer.
Tech stack: TypeScript, Node.js, Anthropic MCP SDK, GitHub REST API
How to Explain This in an Interview
"I built an MCP server — that is Model Context Protocol, Anthropic's standard for connecting AI models to external tools. My server extends Claude Desktop with GitHub capabilities. I implemented 4 tools using the official TypeScript SDK: one that lists repositories, one that reads code and READMEs from a specific repo, one that matches repos to job descriptions, and one that generates interview-ready project explanations. The server communicates with Claude Desktop via stdio transport, which is just JSON messages over stdin and stdout. The key insight is that MCP is not magic — it is a standard protocol where the AI specifies which function to call, your code executes it, and returns the result."
If asked follow-ups:
- "Why MCP instead of just building a custom integration?" → MCP is a standard. Any AI that supports MCP can use my server — not just Claude. That is the point of a protocol.
- "What is stdio transport?" → The server and client communicate by reading and writing to standard input/output. Simple and works everywhere without networking setup.
- "What resources vs tools?" → Resources are data Claude can read passively (like files). Tools are functions Claude can actively call to do things.
Key Terms
| Term | Meaning |
|---|---|
| MCP | Model Context Protocol — standard for connecting AI to tools |
| MCP Server | Program that exposes resources and tools to an AI |
| MCP Client | The AI app (Claude Desktop) that uses the server |
| Resource | Data the AI can read |
| Tool | Function the AI can call |
| stdio transport | Communication via standard input/output streams |
| Tool discovery | How Claude finds out which tools are available |
Looking Back at Days 8, 9, 10
You have now built three real AI projects:
| Day | Project | What It Shows |
|---|---|---|
| 8 | RAG Q&A Bot | How to ground AI in real documents |
| 9 | Job Search Agent | How AI can take autonomous action |
| 10 | GitHub MCP Server | How to extend AI with custom tools |
These three patterns — RAG, Agents, MCP — are the foundation of almost every AI product being built right now. You understand them at the code level, not just conceptually.
That is what makes you different from a student who just knows how to use ChatGPT.
Tomorrow: Day 11 — Fine-tuning vs prompting. Most engineers waste money training custom models when a better prompt would do the same job. We will learn when each approach is right.
Day 11 of 15 — AI Survival Kit for Engineers