PDF · Free download
Keep this guide as a PDF
MCP Server Builder's Guide, as one file you can read offline and share with your team.
Sign in with Google or GitHub to download.
What Is MCP and Why Should You Care
Model Context Protocol (MCP) is an open standard created by Anthropic that defines how AI models connect to external tools and data sources. Think of it as USB for AI — a universal plug that lets any model talk to any tool through a standard interface.
Before MCP, every AI tool integration was bespoke. Want Claude to query your database? Write custom code. Want it to call your API? Write more custom code. Want GPT-4o to use the same tools? Rewrite everything for a different format.
MCP eliminates this. You build one server, and it works with Claude Code, Claude Desktop, VS Code extensions, and any other MCP-compatible client. The ecosystem is growing fast — as of April 2026, there are thousands of MCP servers on registries like Smithery and Glama, covering databases, APIs, productivity tools, and more.
MCP Architecture
Understanding the architecture is essential before you build. There are three roles in the MCP ecosystem:
Client
The application that hosts the AI model and initiates connections. Examples: Claude Code, Claude Desktop, VS Code with Claude extension. The client manages the model and routes tool calls to the appropriate server.
Server
A lightweight program that exposes capabilities to the client. Your MCP server is this piece. It can expose three types of capabilities:
- Tools — Functions the model can call (like API endpoints). Example:
get_weather,query_database,send_email. - Resources — Data the model can read (like files or records). Example: a database schema, a configuration file, a list of users.
- Prompts — Reusable prompt templates that the model can invoke. Example: a pre-built "summarize this document" prompt with specific formatting instructions.
Transport
How the client and server communicate. Two options:
| Transport | How It Works | When to Use |
|---|---|---|
| stdio | Client spawns the server as a subprocess, communicates via stdin/stdout | Local tools, CLI integrations, most common |
| SSE (Server-Sent Events) | Client connects to a running HTTP server | Remote servers, shared infrastructure, web deployments |
For most use cases, stdio is the right choice. It is simpler, requires no networking, and works out of the box with Claude Code.
Building Your First MCP Server
We will build a weather MCP server from scratch using TypeScript and the official SDK. By the end, you will have a working server that Claude can use to check the weather in any city.
Prerequisites
- Node.js 20+
- npm or pnpm
- A code editor
- (Optional) An API key from a weather service like OpenWeatherMap
Step 1: Initialize the Project
mkdir mcp-weather-server
cd mcp-weather-server
npm init -y
Install dependencies:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Set up TypeScript:
npx tsc --init
Update tsconfig.json to use modern settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Update package.json:
{
"name": "mcp-weather-server",
"version": "1.0.0",
"type": "module",
"bin": {
"mcp-weather-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts"
}
}
Step 2: Create the Server
Create src/index.ts:
#!/usr/bin/env node
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: "weather",
version: "1.0.0",
});
This is the skeleton. The McpServer class handles all the protocol details — you just add tools, resources, and prompts.
Step 3: Add a Tool
Tools are functions the model can call. Each tool has a name, description, input schema (validated with Zod), and a handler function.
// Add the get_weather tool
server.tool(
"get_weather",
"Get current weather for a city. Returns temperature, conditions, humidity, and wind speed.",
{
city: z.string().describe("City name (e.g., 'London', 'New York', 'Tokyo')"),
units: z
.enum(["celsius", "fahrenheit"])
.default("celsius")
.describe("Temperature units"),
},
async ({ city, units }) => {
try {
const weather = await fetchWeather(city, units);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(weather, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: `Error fetching weather for ${city}: ${message}`,
},
],
isError: true,
};
}
}
);
Now implement the weather fetching function. For this example, we use a free API:
interface WeatherData {
city: string;
temperature: number;
units: string;
conditions: string;
humidity: number;
wind_speed_kmh: number;
}
async function fetchWeather(
city: string,
units: "celsius" | "fahrenheit"
): Promise<WeatherData> {
// Using Open-Meteo (free, no API key required)
// First, geocode the city name
const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`;
const geoResponse = await fetch(geoUrl);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
throw new Error(`City not found: ${city}`);
}
const { latitude, longitude, name } = geoData.results[0];
// Fetch weather data
const tempUnit = units === "fahrenheit" ? "fahrenheit" : "celsius";
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&temperature_unit=${tempUnit}`;
const weatherResponse = await fetch(weatherUrl);
const weatherData = await weatherResponse.json();
const current = weatherData.current;
return {
city: name,
temperature: current.temperature_2m,
units,
conditions: weatherCodeToString(current.weather_code),
humidity: current.relative_humidity_2m,
wind_speed_kmh: current.wind_speed_10m,
};
}
function weatherCodeToString(code: number): string {
const codes: Record<number, string> = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
71: "Slight snow",
73: "Moderate snow",
75: "Heavy snow",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
};
return codes[code] || "Unknown";
}
Step 4: Add a Resource
Resources provide data that Claude can read. Let us add a resource that lists supported cities (as an example of static data):
server.resource(
"supported-cities",
"weather://cities",
async (uri) => {
const cities = [
"London", "New York", "Tokyo", "Paris", "Berlin",
"Sydney", "Toronto", "Mumbai", "Singapore", "Dubai",
];
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({ cities, note: "Any city worldwide is supported via geocoding." }),
},
],
};
}
);
Step 5: Add a Prompt
Prompts are reusable templates. Let us add one for weather reports:
server.prompt(
"weather-report",
"Generate a formatted weather report for a city",
{ city: z.string().describe("City to report on") },
({ city }) => ({
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Use the get_weather tool to fetch the current weather for ${city}, then format it as a brief, readable weather report suitable for a morning briefing. Include temperature, conditions, and a recommendation for what to wear.`,
},
},
],
})
);
Step 6: Start the Server
Add the startup code at the bottom of src/index.ts:
// Start the server with stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Note: Use console.error for logging because console.log writes to stdout, which is reserved for MCP protocol messages.
Testing Your Server Locally
Method 1: MCP Inspector
The MCP Inspector is a visual tool for testing servers:
npx @modelcontextprotocol/inspector tsx src/index.ts
This opens a browser UI where you can call tools, read resources, and test prompts interactively.
Method 2: Connect to Claude Code
Add your server to Claude Code's configuration. Edit ~/.claude.json:
{
"mcpServers": {
"weather": {
"command": "tsx",
"args": ["/absolute/path/to/mcp-weather-server/src/index.ts"]
}
}
}
Restart Claude Code and test:
> What is the weather in Tokyo right now?
Claude will call your get_weather tool and format the response.
Method 3: Automated Tests
Create src/index.test.ts (using your preferred test framework):
import { describe, it, expect } from "vitest";
describe("fetchWeather", () => {
it("returns weather data for a valid city", async () => {
const result = await fetchWeather("London", "celsius");
expect(result.city).toBe("London");
expect(typeof result.temperature).toBe("number");
expect(typeof result.humidity).toBe("number");
});
it("throws for an invalid city", async () => {
await expect(fetchWeather("Xyzzyville123", "celsius")).rejects.toThrow(
"City not found"
);
});
});
Publishing to npm
Once your server is tested and working, publish it so others can use it.
Step 1: Build
npm run build
Step 2: Add Shebang and Set Permissions
Ensure dist/index.js starts with #!/usr/bin/env node and is executable:
chmod +x dist/index.js
Step 3: Configure package.json for Publishing
{
"name": "mcp-weather-server",
"version": "1.0.0",
"description": "MCP server for weather data using Open-Meteo API",
"type": "module",
"bin": {
"mcp-weather-server": "./dist/index.js"
},
"files": ["dist"],
"keywords": ["mcp", "weather", "claude", "model-context-protocol"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-username/mcp-weather-server"
}
}
Step 4: Publish
npm login
npm publish
Users can now install and use your server:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "mcp-weather-server"]
}
}
}
Registering on Smithery and Glama
Publishing to npm makes your server installable. Registering on Smithery and Glama makes it discoverable.
Smithery
- Go to smithery.ai
- Click "Submit Server"
- Provide your npm package name, description, and a README
- Smithery will auto-detect your tools and generate documentation
- Users can install directly from Smithery's UI
Glama
- Go to glama.ai/mcp/servers
- Submit your GitHub repository URL
- Glama indexes your server's tools and makes them searchable
- Users discover your server when searching for relevant capabilities
Both registries are free and significantly increase your server's visibility.
Best Practices for MCP Server Development
1. Validate All Inputs with Zod
Never trust the model to send valid parameters. Zod schemas catch malformed inputs before they hit your logic.
// Good — validated with constraints
{
city: z.string().min(1).max(100).describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}
// Bad — no validation
{
city: z.string(), // Could be empty, could be 10,000 characters
}
2. Return Structured Errors
When something goes wrong, return an error response with a clear message. Do not throw unhandled exceptions — the model needs to understand what went wrong to recover.
return {
content: [{ type: "text", text: `Error: City "${city}" not found. Check spelling.` }],
isError: true,
};
3. Log to stderr, Not stdout
In stdio transport, stdout is the MCP protocol channel. Any non-protocol output on stdout will corrupt communication. Always use console.error for logging.
4. Keep Tools Focused
One tool should do one thing. Do not create a do_everything tool with 20 parameters. Multiple focused tools are easier for the model to use correctly.
// Good — focused tools
server.tool("get_current_weather", ...);
server.tool("get_forecast", ...);
server.tool("get_weather_alerts", ...);
// Bad — one tool that does too much
server.tool("weather", ...); // 15 parameters, unclear behavior
5. Add Descriptions Everywhere
The model reads tool and parameter descriptions to decide what to call and how. Good descriptions mean more accurate tool usage.
server.tool(
"get_weather",
// This description is what the model reads to decide when to use this tool
"Get current weather conditions for any city worldwide. Returns temperature, humidity, wind speed, and a human-readable conditions description. Uses Open-Meteo (free, no API key). Supports both Celsius and Fahrenheit.",
{
city: z.string().describe("City name in English (e.g., 'London', 'New York', 'Tokyo')"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius").describe("Temperature unit for the response"),
},
handler
);
6. Handle Rate Limits and Timeouts
External APIs fail. Network requests time out. Build resilience into your tools.
async function fetchWithTimeout(url: string, timeoutMs: number = 5000): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return response;
} finally {
clearTimeout(timeout);
}
}
What to Build Next
Now that you know the fundamentals, here are MCP server ideas sorted by complexity:
| Complexity | Idea | Tools to Expose |
|---|---|---|
| Beginner | Bookmark manager | add_bookmark, search_bookmarks, list_tags |
| Beginner | Pomodoro timer | start_timer, get_status, log_session |
| Intermediate | Git analytics | commit_frequency, contributor_stats, file_churn |
| Intermediate | Local file search | search_files, read_file, get_metadata |
| Advanced | Database explorer | list_tables, query, describe_schema, explain_query |
| Advanced | CI/CD dashboard | get_pipeline_status, trigger_build, get_test_results |
The MCP ecosystem is still early. Every useful server you build fills a gap that thousands of developers are looking for. Build what you need, publish it, and the community will find it.