Create a custom MCP server from scratch in 30 minutes. We'll build a weather server that provides current weather data to AI assistants.
A fully functional MCP server that:
get_weather(location) — Fetch current weatherweather://forecast/{location} — 5-day forecastv18+ installed
Basic familiarity
30 minutes
Create a new directory and initialize the project:
mkdir mcp-weather-server cd mcp-weather-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}Update package.json with scripts:
{
"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"
},
"dependencies": {
"@modelcontextprotocol/sdk": "latest",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.3.0"
}
}Create src/index.ts:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// Initialize the MCP server
const server = new Server(
{
name: "weather-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// We'll add handlers here in the next steps
// Start the server
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);
});Key points:
Add the tool handlers before main():
// Define input schema with Zod
const GetWeatherArgsSchema = z.object({
location: z.string().describe("City name or zip code"),
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_weather",
description: "Get current weather for a location",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "City name or zip code",
},
},
required: ["location"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_weather") {
const args = GetWeatherArgsSchema.parse(request.params.arguments);
// In production, call a real API like OpenWeatherMap
// For this tutorial, we'll return mock data
const weatherData = {
location: args.location,
temperature: Math.floor(Math.random() * 30) + 10, // 10-40°C
conditions: ["Sunny", "Cloudy", "Rainy", "Partly Cloudy"][
Math.floor(Math.random() * 4)
],
humidity: Math.floor(Math.random() * 40) + 40, // 40-80%
wind: Math.floor(Math.random() * 20) + 5, // 5-25 km/h
};
return {
content: [
{
type: "text",
text: JSON.stringify(weatherData, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});PRODUCTION TIP
Replace the mock data with a real weather API like OpenWeatherMap. Sign up at openweathermap.org/api for a free API key.
Add resource handlers:
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "weather://forecast/new-york",
name: "5-day forecast for New York",
mimeType: "application/json",
description: "Extended weather forecast",
},
{
uri: "weather://forecast/london",
name: "5-day forecast for London",
mimeType: "application/json",
},
],
};
});
// Read resource content
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri.startsWith("weather://forecast/")) {
const location = uri.replace("weather://forecast/", "");
// Generate 5-day forecast (mock data)
const forecast = Array.from({ length: 5 }, (_, i) => ({
day: i + 1,
temperature: Math.floor(Math.random() * 15) + 15,
conditions: ["Sunny", "Cloudy", "Rainy"][Math.floor(Math.random() * 3)],
}));
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify({ location, forecast }, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
});Add prompt handlers:
// List available prompts
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "weather-report",
description: "Generate a weather report for a location",
arguments: [
{
name: "location",
description: "City name",
required: true,
},
],
},
],
};
});
// Get prompt content
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name === "weather-report") {
const location = request.params.arguments?.location || "Unknown";
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Generate a friendly weather report for ${location}. Include temperature, conditions, and recommendations for outdoor activities.`,
},
},
],
};
}
throw new Error(`Unknown prompt: ${request.params.name}`);
});Build and test the server:
npm run build npm run dev
Add to Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/mcp-weather-server/dist/index.js"]
}
}
}Restart Claude Desktop and try:
"What's the weather in New York?"
→ Claude should use your weather server!
Make your server available to others:
# Build the production version npm run build # Login to npm (if not already) npm login # Publish npm publish --access public
Now anyone can use your server:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "mcp-weather-server"]
}
}
}get_forecast(), get_alerts()