What You Will Build
By the end of this tutorial, you will have a working MCP server that fetches live GitHub user data. Claude Code will call it directly from your terminal. You will type "look up the GitHub profile for torvalds" and Claude Code will invoke your server, hit the GitHub API, and return structured results.
MCP (Model Context Protocol) is an open standard that lets AI agents access external tools and data through a unified interface. Instead of hardcoding API calls into prompts, you build a server that exposes tools. Any MCP-compatible client (Claude Code, Claude Desktop, Cursor, and dozens more) can discover and call those tools automatically.
This tutorial builds something real. Not a hello-world echo server. A tool that queries a live API and returns useful data.
What you need:
- Node.js 18+ and npm
- Claude Code installed and authenticated
- A terminal
- 20 minutes
Why Build an MCP Server?
You have probably already used MCP servers without thinking about it. Claude Code's built-in file reading, web search, and bash execution are all MCP tools under the hood.
Building your own server means you can give Claude Code access to anything: your company's internal API, a database, a monitoring dashboard, a deployment pipeline. The protocol handles discovery, schema validation, and transport. You write the logic.
Three reasons to build instead of just configure:
- Custom data sources. Connect Claude Code to APIs that no existing MCP server covers.
- Controlled access. Your server decides what data to expose and how to format it. You set the boundaries.
- Reusability. One server works across every MCP client. Build once, use everywhere.
For a broader view of where MCP fits in the AI CLI tools ecosystem, that guide covers the landscape.
Step 1: Scaffold the Project
Create a new directory and initialize it:
mkdir github-mcp-server
cd github-mcp-server
npm init -y
Install the MCP SDK and Zod for schema validation:
npm install @modelcontextprotocol/sdk zod@3
npm install -D @types/node typescript
Update package.json to enable ES modules and add a build script:
{
"type": "module",
"bin": {
"github-mcp": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Create the source directory:
mkdir src
Your project structure:
github-mcp-server/
├── package.json
├── tsconfig.json
└── src/
└── index.ts # We will write this next
Step 2: Write the Server
Create src/index.ts. This is the entire server. Every line is explained inline.
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.
// The name and version appear when clients discover this server.
const server = new McpServer({
name: "github-lookup",
version: "1.0.0",
});
// Define a TypeScript interface for the GitHub API response.
// We only pick the fields we care about.
interface GitHubUser {
login: string;
name: string | null;
bio: string | null;
public_repos: number;
followers: number;
following: number;
html_url: string;
created_at: string;
}
// Register a tool called "get_github_user".
// The first argument is the tool name.
// The second argument is metadata: description + input schema.
// The third argument is the handler function.
server.registerTool(
"get_github_user",
{
description: "Look up a GitHub user profile by username",
inputSchema: {
username: z
.string()
.min(1)
.describe("GitHub username (e.g. torvalds, sindresorhus)"),
},
},
async ({ username }) => {
const url = `https://api.github.com/users/${encodeURIComponent(username)}`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "github-mcp-server/1.0",
Accept: "application/vnd.github.v3+json",
},
});
if (!response.ok) {
return {
content: [
{
type: "text" as const,
text: `GitHub API returned ${response.status}: user "${username}" not found or rate-limited.`,
},
],
};
}
const user = (await response.json()) as GitHubUser;
const summary = [
`**${user.name || user.login}** (@${user.login})`,
user.bio ? `Bio: ${user.bio}` : null,
`Public repos: ${user.public_repos}`,
`Followers: ${user.followers} | Following: ${user.following}`,
`Profile: ${user.html_url}`,
`Account created: ${user.created_at.split("T")[0]}`,
]
.filter(Boolean)
.join("\n");
return {
content: [{ type: "text" as const, text: summary }],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `Failed to reach GitHub API: ${error instanceof Error ? error.message : "unknown error"}`,
},
],
};
}
},
);
// Connect the server to stdio transport.
// Claude Code communicates with MCP servers over stdin/stdout.
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitHub MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
Key details:
server.registerTooltakes a name, config object (withdescriptionandinputSchema), and an async handler. TheinputSchemauses Zod objects directly. The SDK validates input automatically before your handler runs.- Return format is always
{ content: [{ type: "text", text: "..." }] }. This is the MCP standard response shape. console.errorinstead ofconsole.log. Stdio-based MCP servers use stdout for JSON-RPC messages. Anyconsole.logcall corrupts the protocol. Always useconsole.errorfor debug output.
Step 3: Build and Test Locally
Build the TypeScript:
npm run build
You should see a build/index.js file. Test that it starts without errors:
node build/index.js
The process should hang (waiting for stdin input). That is correct. Press Ctrl+C to stop it.
For interactive testing before connecting to Claude Code, use the MCP Inspector:
npx @modelcontextprotocol/inspector node build/index.js
This opens a web UI at http://localhost:6274. You can select the get_github_user tool, enter a username, and see the raw response. The Inspector is the fastest way to verify your tool works before integrating it with any client.
Step 4: Connect to Claude Code
Claude Code reads MCP server configurations from .mcp.json at your project root. Create this file in your working project (not inside the MCP server directory, but in the project where you use Claude Code):
{
"mcpServers": {
"github-lookup": {
"command": "node",
"args": ["/absolute/path/to/github-mcp-server/build/index.js"]
}
}
}
Replace /absolute/path/to/ with the real path. You can find it by running pwd inside your github-mcp-server directory.
Alternatively, add it via the Claude Code CLI so it is available across all your projects:
claude mcp add github-lookup --scope user -- node /absolute/path/to/github-mcp-server/build/index.js
Restart Claude Code after adding the configuration. Run /mcp inside Claude Code to verify the server is connected. You should see github-lookup listed with a green status.
Step 5: Use It
Open Claude Code in any project where the server is configured and type:
Look up the GitHub profile for torvalds
Claude Code will:
- Recognize that
get_github_useris the right tool for this request. - Ask for your approval to call it (first time only, unless you have auto-approve enabled).
- Pass
{"username": "torvalds"}to your server. - Display the formatted result.
Try a few more:
What does the GitHub user sindresorhus work on?
Compare the GitHub profiles of gaearon and dan-abramov
Claude Code reads the tool description and input schema to decide when and how to call it. You did not write any prompt engineering. The MCP protocol handled the wiring.
Troubleshooting
"Server not found" in /mcp output.
The path in .mcp.json is wrong. Use an absolute path. Run node /your/path/build/index.js manually to verify it starts.
Server starts but tools do not appear.
Run npm run build again. Claude Code runs the compiled JavaScript, not the TypeScript source. If you edited src/index.ts, you must rebuild.
"Rate limit exceeded" from GitHub.
The GitHub API allows 60 unauthenticated requests per hour. For higher limits, add a GITHUB_TOKEN environment variable and include it as a Bearer token in the fetch headers. You can pass environment variables to MCP servers in .mcp.json:
{
"mcpServers": {
"github-lookup": {
"command": "node",
"args": ["/absolute/path/to/github-mcp-server/build/index.js"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here"
}
}
}
}
Server crashes silently.
Check stderr output. Run the server manually: node build/index.js 2>&1 and look for error messages. Common causes: missing node_modules (run npm install), wrong Node.js version (need 18+), or syntax errors in the build output.
JSON parse errors in Claude Code.
You have a console.log somewhere in your server code. Replace every console.log with console.error. Stdout is reserved for MCP protocol messages.
Developing MCP Servers: The Multi-Pane Workflow
When you are actively building an MCP server, you are juggling three things simultaneously: editing src/index.ts, running npm run build and restarting the server, and testing with Claude Code or the MCP Inspector. Switching between these with Ctrl+Tab or Alt+Tab is slow and breaks your focus.
The productive setup is three terminal panes visible at once. Editor on the left, build/server logs in the top right, Claude Code or Inspector in the bottom right. Save the file, glance at the build output, test the tool. No window switching. No lost context.
Next Steps
You have a working MCP server. Here is where to go from here.
Add more tools. Call server.registerTool again with a different name and schema. A single server can expose dozens of tools. Consider adding get_github_repo (repository details), list_github_repos (list user repos), or search_github_code (search across repositories).
Add resources. MCP servers can also expose resources (read-only data like files or API responses) and prompts (pre-written templates). The SDK supports all three. See the MCP specification for details.
Publish and share. If your server is useful to others, publish it on npm. Add a clear README with the .mcp.json configuration snippet. The MCP ecosystem is growing fast, and the best way to contribute is to build servers for APIs that do not have one yet.
Explore the ecosystem. Browse existing MCP servers at the MCP servers repository for inspiration. If you are building tools for your AI CLI workflow, MCP servers are the composable building blocks that make agents genuinely useful.
For deeper Claude Code configuration patterns, including CLAUDE.md best practices and advanced agent skills, those guides pick up where this tutorial leaves off.
Ready to streamline your terminal workflow?
Multi-terminal drag-and-drop layout, workspace Git sync, built-in AI integration, AST code analysis — all in one app.