MCP Claude Anthropic GitHub project AI tools

Day 11 — What MCP Is: Build a Claude Tool That Reads Your GitHub

Model Context Protocol explained clearly. Build a real MCP server that lets Claude read your GitHub, explain your projects, and generate interview answers about your code.

13 May 2026 21 min read

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 Anthropic
  • axios — to call the GitHub API
  • dotenv — to load your GitHub token
  • typescript + ts-node — to write TypeScript (optional but recommended)

Step 4: Get a GitHub Personal Access Token

  1. Go to github.com → Settings → Developer settings → Personal access tokens → Tokens (classic)
  2. Click "Generate new token (classic)"
  3. Give it a name: "MCP Server"
  4. Select scopes: check repo (full control of private repositories)
  5. Click Generate
  6. 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

  1. Check the path in claude_desktop_config.json is correct and uses forward slashes
  2. Check that npm run build succeeded (dist/server.js exists)
  3. 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:

  1. Reads a repository's details
  2. Returns a formatted description ready to paste into resumeportfolio.in
  3. 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

Ready to stand out?

Your portfolio is 60 seconds away.

Upload your resume. AI builds your portfolio. Share it everywhere.

Build Free Portfolio

Free forever · No credit card · 60 seconds