How to build an MCP server with Claude Code — practical guide
MCP (Model Context Protocol) lets you give Claude access to custom tools, APIs, and data sources. Here's how to build your own MCP server in TypeScript and connect it to Claude Code.
By CB Patel
Model Context Protocol (MCP) is an open standard, originally created by Anthropic, for connecting AI models to tools and data sources. When Claude has an MCP server connected, it can call functions — query a database, fetch an API, read a file system — as part of a conversation. This guide shows you how to build one from scratch.
What MCP actually does
Without tools, Claude's knowledge is static — it knows what was in its training data plus what you paste into the conversation. With an MCP server, Claude can call your own live functions. Examples:
- A "weather" MCP server: Claude can fetch current weather for any city
- A "database" MCP server: Claude can query your Postgres database by natural language
- A "GitHub" MCP server: Claude can list PRs, read issues, create branches
- A "Figma" MCP server: Claude can read component names and export CSS
The pattern is always the same: you define tools (functions with typed inputs), your MCP server exposes them, and Claude decides when to call them during a conversation.
What you need
- Node.js 20 or later, TypeScript
- Claude Code (or Claude Desktop for a GUI-based workflow)
- The MCP TypeScript SDK's server package: @modelcontextprotocol/server
Step 1 — Scaffold the MCP server
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node tsx
Install a skill to help, before starting Claude Code:
curl -fsSL --create-dirs -o .claude/skills/nodejs-express-api/SKILL.md https://claudeskil.com/skills/nodejs-express-api/SKILL.md
Start Claude Code:
claude
Then ask Claude Code to scaffold:
create an MCP server in TypeScript. the entry point is src/index.ts. it should use the McpServer class from @modelcontextprotocol/server and StdioServerTransport for communication (this makes it work with Claude Code and Claude Desktop). register one tool to start: get_weather, which accepts a city string and returns a mock weather object with temperature and condition. define its input schema using zod.
Step 2 — Understand the generated structure
The agent will produce something like this:
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0" });
server.registerTool(
"get_weather",
{
description: "Get the current weather for a city",
inputSchema: z.object({ city: z.string() }),
},
async ({ city }) => ({
content: [{ type: "text", text: JSON.stringify({ city, temperature: 22, condition: "sunny" }) }],
})
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Key things to understand:
- StdioServerTransport means communication happens over stdin/stdout — Claude Code spawns your server as a subprocess, so never write logs to stdout (use console.error, not console.log)
- Each tool is registered with server.registerTool: a name, a description, an input schema, and a handler
- The zod schema defines the tool's inputs, which Claude sees when it lists your server's tools
- The content array in the return can have multiple items (text, images, embedded resources)
Step 3 — Add a real tool (GitHub PRs example)
replace the mock weather tool with a real GitHub tool called list_open_prs. it should accept owner (string) and repo (string), call the GitHub REST API with a personal access token from the GITHUB_TOKEN environment variable, and return the open PR titles, numbers, and URLs as a formatted string.
Claude Code will add the API call with fetch. Review that it:
- Handles HTTP errors (non-2xx responses)
- Reads GITHUB_TOKEN from process.env, not hardcodes it
- Returns meaningful error messages when the token is missing or invalid
Step 4 — Connect it to Claude Code
Build your server first:
npx tsc
Then register it with Claude Code. Everything after -- is the command that starts your server:
claude mcp add --transport stdio --env GITHUB_TOKEN=ghp_your_token_here my-server -- node /absolute/path/to/my-mcp-server/dist/index.js
That adds the server for you in the current project (the default local scope, stored in ~/.claude.json). To share it with your team instead, add --scope project, which writes a .mcp.json file to the project root. Keep the token out of that file by referencing an environment variable:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}
Run /mcp inside Claude Code (or claude mcp list in your terminal) to check that the server is connected, then test it:
list open PRs for vercel/next.js
Claude Code will call your list_open_prs tool, get the result, and incorporate it into its response. You'll see the tool call in the output.
Step 5 — Add more tools
add a second tool: create_issue. it should accept owner, repo, title, and body strings. create a GitHub issue via the API using the GITHUB_TOKEN. return the new issue URL on success.
As your server grows, consider:
-
Grouping related tools. A GitHub server should have all GitHub tools. Don't mix GitHub and database tools in one server — separate servers are easier to debug.
-
Descriptive tool descriptions. Claude uses the tool description (in your server's tool list) to decide when to call it. "Lists the open pull requests for a GitHub repository" is better than "list PRs". Be specific.
-
Error handling that's meaningful. Claude reads your error messages. "GitHub API error: 404 Not Found (repository does not exist or is private)" is better than "Error: 404".
Real-world MCP server ideas
Once you understand the pattern, the possibilities are significant:
Database explorer: expose list_tables, query_table, describe_schema as tools. Now Claude can explore your database by asking questions in plain English.
Deployment helper: expose get_deployments, rollback_deployment, get_logs as tools. Claude can diagnose production issues by reading real logs.
Project management: expose list_tasks, update_task_status, add_comment for Linear or Jira. Claude can triage your backlog during a planning conversation.
Internal documentation: expose search_docs, get_doc_by_id tools over your Notion or Confluence API. Claude can answer questions grounded in your actual internal docs.
Browse MCP-related skills at claudeskil.com/category/agent — including mcp-builder, which scaffolds a Model Context Protocol server in TypeScript or Python with tools, resources, and prompts wired in.