How to Build Your First MCP Server
Model Context Protocol (MCP) servers let you extend Claude with custom tools. You'll build one in TypeScript that actually works, register tools properly, and connect it to Claude Code. This tutorial covers the core protocol mechanics and gets you writing functional server code.
Setting up the foundation
Start with a new Node.js project and install the MCP SDK:
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
Create a basic TypeScript configuration:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
}
}
The MCP SDK handles the protocol layer. Your server implements specific tools and resources that Claude can call.
Understanding the protocol basics
MCP uses JSON-RPC 2.0 for communication. Your server announces capabilities during initialization, then responds to tool calls. The client (Claude Code) discovers what your server can do through the tools/list method.
Three core concepts matter: tools (functions Claude can call), resources (data your server manages), and prompts (templates for common requests). Start with tools since they're the most useful.
Tools receive arguments from Claude and return results. Think of them as API endpoints Claude can hit when solving problems.
Building your first tool
Create src/server.ts with a simple calculator tool:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "calculator-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
Register the calculator tool:
server.setRequestHandler("tools/list", async () => {
return {
tools: [
{
name: "calculate",
description: "Performs basic arithmetic operations",
inputSchema: {
type: "object",
properties: {
expression: {
type: "string",
description: "Math expression to evaluate (e.g., '2 + 2')"
}
},
required: ["expression"]
}
}
]
};
});
The input schema tells Claude what arguments your tool expects. Be specific about types and requirements.
Handle the actual tool calls:
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
if (name === "calculate") {
try {
// Simple eval for demo - don't do this in production
const result = eval(args.expression);
return {
content: [
{
type: "text",
text: `Result: ${result}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`
}
]
};
}
}
throw new Error(`Unknown tool: ${name}`);
});
Never use eval() in real servers. This example keeps things simple, but parse expressions safely in production code.
Starting the server
Add the transport layer and start listening:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Add a start script to package.json:
{
"scripts": {
"start": "npx tsc && node dist/server.js"
}
}
Your server communicates over stdio, which Claude Code expects for MCP connections.
Connecting to Claude Code
Claude Code discovers MCP servers through configuration files. On macOS, edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"calculator": {
"command": "node",
"args": ["/path/to/your/project/dist/server.js"]
}
}
}
Use absolute paths. Restart Claude Code after changing the configuration.
When Claude needs to do math, it can now call your calculator tool. The conversation might look like: "What's 15 * 23?" and Claude will invoke your calculate tool with the expression "15 * 23".
Adding more useful tools
Real MCP servers do more than math. Build tools that read files, make API calls, or process data. Here's a file reader tool:
{
name: "read_file",
description: "Reads content from a file",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "File path to read"
}
},
required: ["path"]
}
}
Tools should be focused and single-purpose. Don't create one tool that does everything. Claude works better with specific tools it can combine.
Error handling patterns
MCP tools fail in predictable ways. Handle errors gracefully:
server.setRequestHandler("tools/call", async (request) => {
try {
// Tool logic here
} catch (error) {
return {
content: [
{
type: "text",
text: `Tool failed: ${error.message}`
}
],
isError: true
};
}
});
The isError flag tells Claude the operation failed. Return helpful error messages that explain what went wrong and how to fix it.
Testing your server
Test servers outside Claude Code first. Create a simple test script:
import { spawn } from "child_process";
const server = spawn("node", ["dist/server.js"]);
// Send initialization request
const initRequest = {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {}
};
server.stdin.write(JSON.stringify(initRequest) + "\n");
Debug protocol issues by logging requests and responses. The MCP SDK includes logging utilities for development.
Building MCP servers opens up Claude's capabilities in specific domains. Skills vs MCP explains when to choose each approach. Start with focused tools that solve real problems, then expand based on what users actually need.
Your calculator server handles basic math, but production servers might integrate with databases, external APIs, or file systems. The protocol stays the same while the tools grow more sophisticated.