The true power of the Model Context Protocol is its extensibility. In this comprehensive tutorial, we'll build a complete MCP server that allows an AI model to interact with a weather API, demonstrating all the key concepts you need to create your own custom tools.
1. Prerequisites
Before we begin, make sure you have the following installed:
- Node.js 18+: Download from nodejs.org
- npm or yarn: Comes with Node.js
- TypeScript knowledge: Basic familiarity with TypeScript syntax
- A code editor: VS Code recommended for TypeScript support
Verify Your Setup
node --version # Should be 18.0.0 or higher
npm --version # Should be 8.0.0 or higher2. Project Setup
Let's create a new directory and initialize our project with all the necessary dependencies:
# Create project directory
mkdir weather-mcp-server
cd weather-mcp-server
# Initialize Node.js project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk zod
# Install dev dependencies
npm install -D typescript @types/node tsx
# Initialize TypeScript
npx tsc --initConfigure TypeScript
Update your tsconfig.json with these settings:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"]
}Update package.json
Add these scripts and set the module type:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts"
}
}3. Basic Server Structure
Create src/index.ts with the basic server boilerplate:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Initialize the server with metadata
const server = new Server(
{
name: "weather-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Tool and resource handlers will go here...
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
}
main().catch(console.error);Understanding the Structure
- Server metadata: The name and version identify your server to clients
- Capabilities: Declare what features your server supports (tools, resources, prompts)
- StdioServerTransport: Handles communication over standard input/output
4. Defining Tools
Tools are functions that the AI can call. We need to tell the AI what tools are available and what arguments they accept. Add this handler:
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
// Define input schemas using Zod
const GetWeatherSchema = z.object({
city: z.string().describe("The name of the city"),
units: z.enum(["celsius", "fahrenheit"]).optional()
.describe("Temperature units (default: celsius)"),
});
const GetForecastSchema = z.object({
city: z.string().describe("The name of the city"),
days: z.number().min(1).max(7).describe("Number of days (1-7)"),
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_weather",
description: "Get current weather conditions for a city including temperature, humidity, and conditions",
inputSchema: zodToJsonSchema(GetWeatherSchema),
},
{
name: "get_forecast",
description: "Get weather forecast for the next several days",
inputSchema: zodToJsonSchema(GetForecastSchema),
},
],
};
});Tool Definition Best Practices
- Use clear, descriptive names (snake_case is conventional)
- Write detailed descriptions—the AI uses these to decide when to call your tool
- Use Zod for type-safe schema definitions
- Include descriptions for each parameter
5. Handling Tool Execution
Now implement the logic that runs when the AI calls your tools:
// Simulated weather data (in production, call a real API)
async function fetchWeather(city: string, units: string = "celsius") {
// Simulate API call
const temp = Math.round(Math.random() * 30 + 5);
const conditions = ["Sunny", "Cloudy", "Rainy", "Partly Cloudy"][
Math.floor(Math.random() * 4)
];
return {
city,
temperature: units === "fahrenheit" ? Math.round(temp * 9/5 + 32) : temp,
units: units === "fahrenheit" ? "°F" : "°C",
conditions,
humidity: Math.round(Math.random() * 60 + 30),
wind: Math.round(Math.random() * 20 + 5),
};
}
async function fetchForecast(city: string, days: number) {
const forecast = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
forecast.push({
date: date.toISOString().split("T")[0],
high: Math.round(Math.random() * 15 + 20),
low: Math.round(Math.random() * 10 + 10),
conditions: ["Sunny", "Cloudy", "Rainy"][Math.floor(Math.random() * 3)],
});
}
return { city, forecast };
}
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "get_weather": {
const { city, units } = GetWeatherSchema.parse(args);
const weather = await fetchWeather(city, units);
return {
content: [
{
type: "text",
text: `Current weather in ${weather.city}:
• Temperature: ${weather.temperature}${weather.units}
• Conditions: ${weather.conditions}
• Humidity: ${weather.humidity}%
• Wind: ${weather.wind} km/h`,
},
],
};
}
case "get_forecast": {
const { city, days } = GetForecastSchema.parse(args);
const data = await fetchForecast(city, days);
const forecastText = data.forecast
.map(day => `${day.date}: ${day.conditions}, ${day.high}°C / ${day.low}°C`)
.join("\n");
return {
content: [
{
type: "text",
text: `${days}-day forecast for ${city}:\n${forecastText}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});6. Adding Resources
Resources allow your server to expose data that the AI can read. Let's add a resource that shows supported cities:
const SUPPORTED_CITIES = [
"New York", "London", "Tokyo", "Paris", "Sydney",
"Berlin", "Toronto", "Singapore", "Dubai", "Mumbai"
];
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "weather://supported-cities",
name: "Supported Cities",
description: "List of cities with weather data available",
mimeType: "application/json",
},
{
uri: "weather://api-status",
name: "API Status",
description: "Current status of the weather API",
mimeType: "application/json",
},
],
};
});
// Handle resource reads
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "weather://supported-cities":
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify({ cities: SUPPORTED_CITIES }, null, 2),
},
],
};
case "weather://api-status":
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify({
status: "operational",
lastUpdated: new Date().toISOString(),
requestsRemaining: 1000,
}, null, 2),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});7. Error Handling
Proper error handling is crucial for a good user experience:
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
// In your tool handler, wrap operations in try-catch
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "get_weather": {
const parsed = GetWeatherSchema.safeParse(args);
if (!parsed.success) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid parameters: ${parsed.error.message}`
);
}
const { city, units } = parsed.data;
if (!SUPPORTED_CITIES.includes(city)) {
throw new McpError(
ErrorCode.InvalidParams,
`City "${city}" is not supported. Use the supported-cities resource to see available cities.`
);
}
const weather = await fetchWeather(city, units);
// ... return result
}
// ... other cases
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Unexpected error: ${error instanceof Error ? error.message : "Unknown"}`
);
}
});Logging Best Practice
Since MCP uses stdio for communication, use console.error() for logging instead of console.log(). This writes to stderr and won't interfere with the JSON-RPC protocol.
8. Testing Your Server
Use the MCP Inspector to test your server interactively:
npm run inspectThis opens a web interface where you can:
- See all available tools and resources
- Test tool invocations with custom arguments
- View raw JSON-RPC messages
- Debug errors in real-time
Pro Tip: The Inspector
Always test with the Inspector before connecting to Claude. It's much easier to debug issues in the Inspector's UI than through Claude's interface.
Connect to Claude Desktop
Once tested, add your server to Claude Desktop's config:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/weather-mcp-server/dist/index.js"]
}
}
}9. Deployment and Distribution
To share your server with others, publish it to npm:
Update package.json
{
"name": "@yourname/weather-mcp-server",
"version": "1.0.0",
"description": "MCP server for weather data",
"main": "dist/index.js",
"bin": {
"weather-mcp-server": "dist/index.js"
},
"files": ["dist"],
"keywords": ["mcp", "weather", "ai", "claude"]
}Publish
npm run build
npm publish --access publicUsers can then run your server with:
npx @yourname/weather-mcp-serverConclusion
Congratulations! You've built a complete MCP server with tools, resources, and proper error handling. This pattern can be extended to connect to databases, send emails, control smart home devices, or integrate with any API. The possibilities are endless.
Outdated Content Warning
This guide was last updated on January 10, 2025 (12 months ago).
The information presented here may be significantly outdated. Technologies, APIs, and best practices may have changed since this content was written.
We strive to keep our content current, but with rapidly evolving technologies, some details may no longer be accurate.
Last updated
January 10, 2025
376 days ago
This content may be outdated
This content may contain outdated information. Please verify details before use.