Build a GitHub MCP Server

Build an MCP server that gives AI models live access to GitHub data.

Introduction

30 Second Summary

AI models are powerful, but they're locked away from live data unless you give them a way to reach it. Think about how much more useful an AI assistant would be if it could look up real GitHub repositories, check open issues, or read documentation on demand.

In this project, you will build a GitHub MCP server that connects AI models to live GitHub data. You'll register tools that any MCP-compatible AI client can call, test them interactively in a browser UI, and learn the critical error handling pattern that keeps production servers running.

What You'll Build

Picture asking an AI to check the latest issues on a repository, and watching your server fetch live data from GitHub and hand it back in a clean, readable format.

By the end of this project, you'll have:

  • A tool that fetches any public repo's key stats (stars, forks, language, topics) and returns a clean summary to the AI model.
  • A tool that browses a repo's open issues with titles, authors, labels, and links.
  • A tool that reads a repo's full README content, decoded from base64 and ready to use.
  • Secret Mission: Add a repository search tool that lets you find repos by keyword.

Are there any prerequisites?

This project assumes you're comfortable with TypeScript and Node.js. You'll need Node.js v20 or later and VS Code installed on your machine.

Before We Start

Before we write any code, let's get clear on what you're building and why it matters for how AI models interact with the real world.

Set Up Your MCP Server Project

Before your MCP server can expose GitHub data to AI models, it needs a foundation. The server uses the MCP TypeScript SDK to register tools and communicate with AI clients over a transport layer.

In this step, you'll create a new TypeScript project, install the MCP SDK, and write a minimal server that connects over stdio. Stdio is the transport that lets local AI clients (like Claude Desktop or Cursor) talk to your server directly.

In this step, get ready to:
  • Initialize a TypeScript project configured for ES modules.
  • Install the MCP SDK and supporting dependencies.
  • Create a minimal server that starts and connects over stdio.
Scaffold the project

Every Node.js project starts with a directory and a package.json. You'll create both, then configure the project for ES modules (which the MCP SDK requires).

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and initialize a new Node.js project by running these commands:
mkdir github-mcp-server
cd github-mcp-server
npm init -y

You should see output showing a new package.json was created with default values.

Command not found?

If npm is not recognized, Node.js is not installed or not on your PATH. Run node --version to check. You need v20 or later.

Help me fix my Node.js installation

  • Open the project in VS Code by running:
code .

VS Code opens with the github-mcp-server folder loaded. You should see package.json in the Explorer sidebar.

The default package.json from npm init -y needs a few changes. The MCP SDK ships as ES modules, so the project must set "type": "module". You'll also add convenience scripts for starting the server and launching the Inspector.

  • In VS Code, click on package.json in the Explorer sidebar to open it.
  • Replace the entire contents with the following:
{
  "name": "github-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts"
  }
}

What does this configuration do?

  • "type": "module" tells Node.js to treat .js and .ts files as ES modules (using import/export instead of require).
  • The start script runs your server directly from TypeScript using tsx (no compile step needed).
  • The inspect script launches the MCP Inspector (a web UI for testing your tools).
  • Save package.json.

Now install the project's dependencies. The MCP SDK provides the server framework, and Zod handles input validation for your tools.

  • Open VS Code's integrated terminal by pressing Ctrl+` (macOS and Windows).
  • Install the runtime dependencies by running:
npm install @modelcontextprotocol/sdk zod
  • Install the development dependencies by running:
npm install -D tsx typescript @types/node

You should see output confirming packages were added. Your package.json now includes dependencies and devDependencies sections.

Seeing permission errors or network issues?

Make sure you're running the terminal inside the github-mcp-server folder (check with pwd). If you see EACCES errors on macOS, avoid using sudo. Fix npm permissions instead.

Help me fix npm install errors

Next, create the TypeScript configuration file. This tells the compiler how to handle your ES module code.

  • In VS Code's Explorer sidebar, click the New File icon (next to the folder name).
  • Name the file tsconfig.json and press Enter.
  • Paste the following configuration:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

What does this configuration do?

  • "module": "Node16" and "moduleResolution": "Node16" ensure Node.js-style module resolution for ES modules.
  • "include": ["src"] tells TypeScript to only compile files inside the src/ directory.
  • Save tsconfig.json.

Now create the server file. This is where all your MCP logic will live.

  • In the Explorer sidebar, click the New Folder icon and name it src.
  • Right-click the src folder and select New File.
  • Name the file index.ts and press Enter.
  • Paste the following code into src/index.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// Create the MCP server instance
const server = new McpServer({
  name: 'github-server',
  version: '1.0.0'
});

// Connect using stdio transport and log startup to stderr
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('GitHub MCP Server running on stdio');

What does this code do?

  • McpServer is the core class from the SDK. It manages tool registration and handles incoming requests from AI clients.
  • StdioServerTransport connects the server to standard input/output. AI clients send JSON-RPC messages over stdout, so your logs go to stderr instead.
  • The z import from Zod is not used yet, but you'll need it in the next step to define input schemas for your tools.
  • console.error logs to stderr because stdout is reserved for the MCP protocol. Using console.log would corrupt the JSON-RPC stream.
  • Save src/index.ts.

✔️ Awesome, I've got everything!

Great. Make sure you've saved all three files (package.json, tsconfig.json, src/index.ts) before moving on.

ⓧ I'd like to double check the full code

Here's what package.json should look like after install:

{
  "name": "github-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "inspect": "npx @modelcontextprotocol/inspector npx tsx src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "zod": "^3.25.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.7.0",
    "typescript": "^5.7.0"
  }
}

Here's tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

And src/index.ts:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({
  name: 'github-server',
  version: '1.0.0'
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error('GitHub MCP Server running on stdio');
Verify the server starts

Your project structure is in place. Time to prove it actually runs.

Before you run this, what do you think will happen? The server connects over stdio, which means it will sit waiting for input from an AI client. How will you know it started successfully?

  • Start the server by running this command in your VS Code terminal:
npx tsx src/index.ts

You should see GitHub MCP Server running on stdio printed to your terminal. The server is now running and waiting for MCP messages on stdin.

  • Press Ctrl+C to stop the server.

Why does the server hang after printing the message?

The server uses stdio transport. It keeps the process alive, listening on stdin for JSON-RPC messages from an AI client. Without a client connected (like the MCP Inspector), there's nothing sending messages, so it just waits.

Don't see the startup message?

If you see a module resolution error, make sure "type": "module" is in your package.json. The MCP SDK uses ES module imports that fail without it.

Check that the import paths in src/index.ts end with .js (e.g., @modelcontextprotocol/sdk/server/mcp.js). This is required for Node16 module resolution.

Help me debug my MCP server startup error

Your MCP server is alive. It starts, connects to the stdio transport, and waits for instructions. Next up, you'll give it something useful to do by registering your first tool and testing it live in the MCP Inspector.

Build Your First Tool and Test It Live

Your server starts, connects to the transport, and waits for instructions. But right now it has nothing useful to offer an AI model.

A tool is how an AI model calls into your server. It registers a function with a name, a description, and a typed input schema. When the model needs information, it picks the right tool and sends a request. In this step, you'll build a tool that fetches GitHub repository info and test it end-to-end in the MCP Inspector.

In this step, get ready to:
  • Create a reusable GitHub API helper function.
  • Register your first MCP tool (get_repo_info).
  • Test the tool live in the MCP Inspector web UI.
Create a GitHub API helper

Every tool you build will need to call the GitHub REST API. A shared helper function keeps the fetch logic in one place so each tool stays focused on its own job.

  • In VS Code, open src/index.ts.
  • Find the line import { z } from 'zod'; and add the following code below it:
const GITHUB_API = 'https://api.github.com';
const USER_AGENT = 'github-mcp-server/1.0';

// Reusable helper for all GitHub API requests
async function githubRequest(path: string): Promise<any> {
  const url = `${GITHUB_API}${path}`;
  const response = await fetch(url, {
    headers: {
      'Accept': 'application/vnd.github+json',
      'User-Agent': USER_AGENT,
      'X-GitHub-Api-Version': '2022-11-28'
    }
  });

  if (!response.ok) {
    throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

What does this code do?

GITHUB_API and USER_AGENT are constants shared across all tools. The githubRequest function takes an API path (like /repos/facebook/react), attaches the required headers, and returns the parsed JSON response.

If GitHub returns an error status (like 404 for a repo that doesn't exist), the function throws an error rather than returning bad data. Your tools will handle that thrown error later.

  • Save src/index.ts.
  • Confirm the server still compiles by running:
npx tsx src/index.ts

You should see GitHub MCP Server running on stdio printed in the terminal. Press Ctrl+C to stop the server.

Seeing a compile error?

  • Check that the new code sits between the import lines at the top and the const server = new McpServer(...) block below.
  • Make sure the function signature uses a colon for the type annotation: path: string.
  • help me fix a compile error in my githubRequest function
Register the get_repo_info tool

Now you'll register a tool that uses the helper. The MCP SDK's registerTool method takes a name, a configuration object (description + typed input schema), and a handler function that runs when the tool is called.

  • In src/index.ts, find the line const transport = new StdioServerTransport(); and add the following code above it. This is the first half of the tool registration:
server.registerTool(
  'get_repo_info',
  {
    // Description shown to AI models when choosing tools
    description: 'Get information about a GitHub repository including stars, forks, and description',
    // Zod schemas validate inputs before the handler runs
    inputSchema: {
      owner: z.string().describe('Repository owner (username or organization)'),
      repo: z.string().describe('Repository name')
    }
  },
  async ({ owner, repo }) => {
    try {
      const data = await githubRequest(`/repos/${owner}/${repo}`);
  • Add this code directly below, completing the handler:
      // Format key fields as readable text for the AI model
      const info = [
        `Repository: ${data.full_name}`,
        `Description: ${data.description || 'No description'}`,
        `Language: ${data.language || 'Not specified'}`,
        `Stars: ${data.stargazers_count}`,
        `Forks: ${data.forks_count}`,
        `Open Issues: ${data.open_issues_count}`,
        `Created: ${data.created_at}`,
        `Last Updated: ${data.updated_at}`,
        `URL: ${data.html_url}`,
        `Topics: ${data.topics?.join(', ') || 'None'}`,
        `License: ${data.license?.name || 'Not specified'}`
      ].join('\n');

      return { content: [{ type: 'text', text: info }] };
    } catch (error) {
      // Return error to AI model without crashing the server
      return {
        content: [{ type: 'text', text: `Error fetching repository: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

What does this tool registration do?

  • The description tells an AI model what this tool is for, so it can decide when to call it.
  • The inputSchema uses Zod to define and validate the expected inputs. If an AI model passes the wrong types, the SDK rejects the call before your handler even runs.
  • The handler fetches data, formats it as a plain-text summary, and returns it inside a content array with type: 'text'. This is the standard MCP response format.
  • The try/catch block catches errors and returns them with isError: true. This signals failure to the AI model without crashing the server process.
  • Save src/index.ts.

✔️ Awesome, I've got everything!

Great. Confirm you've saved the file and move on to testing.

ⓧ I'd like to double check the full code

Scroll through src/index.ts and confirm the following sections appear in this order:

  • Three import statements at the top (McpServer, StdioServerTransport, z).
  • GITHUB_API and USER_AGENT constants.
  • The githubRequest async function.
  • The const server = new McpServer(...) block.
  • The server.registerTool('get_repo_info', ...) call.
  • The const transport, await server.connect(transport), and console.error(...) lines at the bottom.
Test with the MCP Inspector

The MCP Inspector is a local web UI that spawns your server as a subprocess, connects over stdio, and lets you call tools interactively. It is the fastest way to verify your tools work before connecting a real AI client.

  • Launch the Inspector by running:
npx @modelcontextprotocol/inspector npx tsx src/index.ts

What does this command do?

This command starts the MCP Inspector and passes npx tsx src/index.ts as the server command. The Inspector spawns your server as a child process, connects to it over stdio, and opens a web UI at http://localhost:6274.

  • If npm asks to install @modelcontextprotocol/inspector, confirm with y.
  • Your browser should open automatically. If it does not, open http://localhost:6274 manually.
  • In the Inspector UI, click Connect in the left pane.
  • Click the Tools tab.
  • Select get_repo_info from the tool list.
  • Enter facebook in the owner field.
  • Enter react in the repo field.

Before you click Run, what do you expect to see? Think about what the tool does with these two inputs.

  • Click Run Tool.

You should see a formatted summary of the React repository. It includes the repo name, description, star count, fork count, language, and other metadata pulled live from GitHub's API.

Not seeing results in the Inspector?

  • Make sure you clicked Connect before selecting a tool. The Inspector needs an active connection to list tools.
  • If you see no tools listed, the server may have failed to start. Check the terminal where you ran the Inspector command for error messages.
  • If you see a network error in the response, your machine may be offline or GitHub could be rate-limiting you (60 requests per hour for unauthenticated calls).
  • help me troubleshoot the MCP Inspector not showing my tool

You just completed the full MCP loop: tool registration, input validation, execution, and response. Next up, you'll add a second tool and discover what happens when things go wrong.

Add the Issues Tool and Hit the Wall

You know the registration pattern works. You proved it with get_repo_info in the last step.

Now you'll add a second tool that lists open issues for any repository. But when you test it with bad input, something unexpected will happen.

In this step, get ready to:
  • Register a list_issues tool with an optional pagination parameter.
  • Discover what happens when an unhandled error hits a tool handler.
Register the list_issues tool

This tool takes an owner, a repo name, and an optional page size. It fetches the open issues from the GitHub API and formats each one with its number, title, author, and labels.

  • In src/index.ts, find the closing ); of the get_repo_info registration. Add the following code below it to register the list_issues tool:
server.registerTool(
  'list_issues',
  {
    description: 'List open issues for a GitHub repository',
    inputSchema: {
      owner: z.string().describe('Repository owner (username or organization)'),
      repo: z.string().describe('Repository name'),
      per_page: z.number().optional().default(5).describe('Number of issues to return (max 30)')
    }
  },
  async ({ owner, repo, per_page }) => {
    // Cap results at 30 maximum
    const limit = Math.min(per_page, 30);
    const issues = await githubRequest(
      `/repos/${owner}/${repo}/issues?per_page=${limit}&state=open`
    );

    // Handle empty results
    if (issues.length === 0) {
      return {
        content: [{ type: 'text', text: `No open issues found for ${owner}/${repo}.` }]
      };
    }

What does this code do?

  • per_page is optional with a default of 5. The AI model can request more results, but Math.min(per_page, 30) caps it at 30.
  • The API call includes state=open so only currently-open issues are returned.
  • If the repo has zero open issues, the tool returns a clear message instead of empty formatting.
  • Directly below the code you just added, paste the formatting and return logic to complete the handler:
    // Format each issue with key details
    const formatted = issues
      .map((issue: any) =>
        [
          `#${issue.number}: ${issue.title}`,
          `  Author: ${issue.user?.login || 'Unknown'}`,
          `  Created: ${issue.created_at}`,
          `  Labels: ${issue.labels?.map((l: any) => l.name).join(', ') || 'None'}`,
          `  URL: ${issue.html_url}`
        ].join('\n')
      )
      .join('\n\n');

    return {
      content: [{ type: 'text', text: `Open issues for ${owner}/${repo}:\n\n${formatted}` }]
    };
  }
);

What does the formatting do?

Each issue is formatted into a multi-line block showing its number, title, who opened it, when, what labels it has, and a direct link. The double newline between issues makes the output easy for an AI model to parse.

  • Save src/index.ts.
  • Launch the Inspector by running this command in your terminal:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
  • In the Inspector browser UI, click Connect.
  • Open the Tools tab.
  • Select list_issues from the tool list.
  • Enter facebook for owner and react for repo.
  • Click Run Tool.

You should see a formatted list of open issues with numbers, titles, authors, dates, labels, and URLs.

Not seeing any issues listed?

  • Check that your two code chunks are pasted in order with no gap between the if (issues.length === 0) block and the const formatted line. They form one continuous function body.
  • Make sure the new server.registerTool( call is placed after the closing ); of get_repo_info and before the const transport line.
  • Confirm you clicked Connect in the Inspector before trying to run a tool.

Help me debug my list_issues tool

Test with an invalid repo

Your tool works with a real repository. Now try giving it a repo that does not exist.

Before you run this, what do you think will happen if you call list_issues with a completely fake repository name? Will the tool return an error message, or will something else happen?

  • In the Inspector, keep list_issues selected.
  • Change owner to nonexistent and repo to fake-repo-xyz.
  • Click Run Tool.

The server process crashes. The Inspector shows a disconnection error. You cannot call any more tools without restarting.

What just happened?

The GitHub API returned a 404 for the non-existent repo. Your githubRequest helper threw an error because of the if (!response.ok) check. But the list_issues handler has no try/catch to catch that throw.

An unhandled exception inside a tool handler corrupts the stdio JSON-RPC stream and kills the entire server process. Every tool becomes unreachable, not just the one that failed.

Notice that get_repo_info already has a try/catch that returns isError: true on failure. That pattern is what keeps the server alive. The next step fixes list_issues with the same approach.

✔️ Awesome, I've got everything!

Great. Confirm you've saved src/index.ts with the complete list_issues tool registration.

ⓧ I'd like to double check the full code

Your src/index.ts should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (McpServer, StdioServerTransport, z).
  • GITHUB_API and USER_AGENT constants.
  • githubRequest async function with fetch and error check.
  • McpServer instance (server).
  • get_repo_info tool registration with try/catch and isError: true.
  • list_issues tool registration (no try/catch).
  • StdioServerTransport connection and console.error startup log.

You've hit the wall. A single unhandled error in one tool handler brings down the entire server. In the next step, you'll fix this with the isError: true pattern and make your server resilient.

Make Your Server Resilient with Error Handling

In the last step, you watched your server crash the moment list_issues received an invalid repo name. The Inspector disconnected, and any AI model relying on your server would have lost its connection mid-conversation.

The MCP SDK provides a specific pattern for signaling errors without crashing: return isError: true in the tool response. This tells the AI model the call failed while keeping the server alive for the next request. In this step, you'll apply that pattern to list_issues and build a new get_readme tool that handles errors from the start.

In this step, get ready to:
  • Add error handling to the list_issues tool so it survives bad input.
  • Build a get_readme tool that decodes repository README content.
  • Test that all three tools handle invalid inputs gracefully without disconnecting.
Add try/catch with isError to list_issues

Your get_repo_info tool already handles errors gracefully with a try/catch block. Now you'll apply the same pattern to list_issues.

  • In src/index.ts, find the list_issues handler. Look for this line:
  async ({ owner, repo, per_page }) => {
    const limit = Math.min(per_page, 30);
  • Add try { on a new line directly after the opening => { brace. Indent all existing handler code one level inside the try block.
  • After the closing brace of the existing return statement (the one that returns formatted issues), add this catch block:
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error fetching issues: ${(error as Error).message}` }],
        isError: true
      };
    }

What does isError: true do?

When a tool handler throws an unhandled exception, it corrupts the stdio JSON-RPC stream and kills the entire server process. By catching the error and returning isError: true instead, you signal to the AI model that the call failed without breaking the connection.

The AI model receives the error message as text content and can decide what to do next. Your server stays alive for the next request.

  • Save src/index.ts.
  • Restart the MCP Inspector by running:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
  • In the Inspector browser UI, click Connect.
  • Open the Tools tab and select list_issues.
  • Enter nonexistent for owner and fake-repo-xyz for repo.
  • Click Run.

Instead of the server crashing and the Inspector disconnecting, you should see a clean error message like "Error fetching issues: GitHub API error: 404 Not Found". The connection stays alive.

Still seeing the server crash?

Make sure your try { appears directly after the => { opening brace, and that ALL existing handler code (including the const limit line) is inside the try block.

Check that the catch block's closing } sits inside the arrow function, not outside it. If braces are mismatched, TypeScript will throw a syntax error on startup.

Help me fix my try/catch in list_issues

Add the get_readme tool with error handling built in

GitHub's README endpoint returns the file content as base64-encoded text. Your new tool will decode it into readable content that an AI model can actually use.

  • In src/index.ts, find the line that says const transport = new StdioServerTransport(); and add the following tool registration above it:
server.registerTool(
  'get_readme',
  {
    description: 'Get the README content of a GitHub repository',
    inputSchema: {
      owner: z.string().describe('Repository owner (username or organization)'),
      repo: z.string().describe('Repository name')
    }
  },
  async ({ owner, repo }) => {
    try {
      // Fetch README metadata (content is base64-encoded)
      const data = await githubRequest(`/repos/${owner}/${repo}/readme`);
      // Decode base64 content into readable UTF-8 text
      const content = Buffer.from(data.content, 'base64').toString('utf-8');

      return { content: [{ type: 'text', text: content }] };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `Error fetching README: ${(error as Error).message}` }],
        isError: true
      };
    }
  }
);

What does this code do?

  • The tool calls GitHub's /repos/{owner}/{repo}/readme endpoint, which returns the README file's content as a base64-encoded string.
  • Buffer.from(data.content, 'base64').toString('utf-8') decodes the base64 string back into plain text that an AI model can read and reason about.
  • The try/catch with isError: true is built in from the start, so repos without a README file return a clean error instead of crashing.
  • Save src/index.ts.

Seeing a syntax error on save?

Make sure the new server.registerTool( block appears ABOVE the const transport line, not below it. The server must register all tools before connecting to the transport.

Help me debug a syntax error in my get_readme tool

Test error resilience across all tools

With both edits in place, you can now prove that your server handles any failure gracefully. A resilient server keeps running no matter what inputs the AI model sends.

  • Stop the previous Inspector session (close the terminal or press Ctrl+C).
  • Restart the Inspector by running:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
  • In the Inspector browser UI, click Connect.
  • Open the Tools tab.

Before you run the next test, think about what you expect to happen. If you call get_repo_info with owner nonexistent and repo fake-repo-xyz, will the server crash or return an error message?

  • Select get_repo_info, enter nonexistent for owner and fake-repo-xyz for repo, then click Run.

You see an error message like "Error fetching repository: GitHub API error: 404 Not Found" instead of a crash. The server stays connected.

  • Without restarting, select get_readme, enter facebook for owner and react for repo, then click Run.

You should see the full decoded README content for the React repository. The fact that you can call another tool immediately after an error proves the server stayed connected throughout.

  • Try get_readme with nonexistent / fake-repo-xyz to confirm it also returns a clean error message without crashing.

Why is this pattern so important?

In production, an AI model might call your tools dozens of times in a conversation. If one bad input kills the server, the model loses access to ALL your tools for the rest of that session. The isError: true pattern is the MCP equivalent of returning an HTTP error code instead of crashing a web server.

Server crashing on the invalid input test?

Double-check that you saved src/index.ts after adding the try/catch to list_issues. The Inspector spawns a fresh server process, so unsaved changes won't take effect.

If only one tool crashes but others work, the issue is in that specific tool's try/catch structure. Confirm the catch block wraps the entire handler body, not just part of it.

Help me debug my MCP server error handling

✔️ Awesome, I've got everything!

Great. Double check you've saved src/index.ts and that all three tools responded in the Inspector.

ⓧ I'd like to double check the full code

Your src/index.ts is now longer than 30 lines, so scroll through and confirm each section is present in this order:

  • Imports (McpServer, StdioServerTransport, z) at the top.
  • Constants GITHUB_API and USER_AGENT.
  • The githubRequest helper function.
  • McpServer instance creation.
  • get_repo_info tool with try/catch and isError: true.
  • list_issues tool with try/catch and isError: true (the one you just fixed).
  • get_readme tool with try/catch and isError: true (the one you just added).
  • StdioServerTransport connection and console.error startup message at the bottom.

Your server is now production-ready. It handles bad input gracefully, exposes three useful tools, and keeps its connection alive no matter what an AI model throws at it. Next up, you'll tackle a bonus challenge in the Secret Mission.

Secret mission

Add Repository Search

Your server can look up specific repos, but what if the AI model needs to discover repos it doesn't know about yet? Add a search tool that queries GitHub's Search API by keyword and returns the top results sorted by stars.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine. There are no cloud resources, no paid services, and no ongoing costs.

Resources you used:

  • The github-mcp-server project directory on your Desktop (includes source code, node_modules, and config files).

Keep everything running

No action needed. Choose this if you plan to connect your server to an MCP client like Claude Desktop, Cursor, or VS Code Copilot.

  • Your project stays in the github-mcp-server folder on your Desktop, ready to run anytime with npx tsx src/index.ts.
  • The server only uses resources while actively running. It consumes no CPU or memory when stopped.

Pause - I'll come back to this later

Since this project has no running processes or cloud services, there is nothing to pause. The project sits idle on your filesystem until you run it again.

  • If you have the MCP Inspector still running in a terminal, press Ctrl+C to stop it.
  • That is all. The project folder takes up minimal disk space and is ready whenever you return.

Delete - I don't want to use this again

Remove the entire project directory to free up disk space and delete all source code, dependencies, and config files.

  • If you have any terminal processes running (the server or Inspector), press Ctrl+C to stop them.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/github-mcp-server

On Windows, run this instead:

rmdir /s /q "$HOME\Desktop\github-mcp-server"
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir "$HOME\Desktop" (Windows). You should no longer see github-mcp-server in the output.

Nice Work!

Nice Work!

Nice work! You just built a fully functional MCP server that gives AI models live access to GitHub data.

You've learned how to:

  • Built a working MCP server from scratch with three registered tools (get_repo_info, list_issues, get_readme) using Zod input validation.
  • Implemented the isError: true pattern that prevents unhandled exceptions from crashing MCP servers in production.
  • Tested your tools interactively using the MCP Inspector web UI, connecting to the live GitHub REST API and formatting responses for AI consumption.
  • Secret Mission: Added a repository search tool handling URL encoding, wrapped response formats, and stricter rate limits.

Ready to quiz yourself?