TUTORIAL

Build Your Own MCP Server

Create a custom MCP server from scratch in 30 minutes. We'll build a weather server that provides current weather data to AI assistants.

WHAT YOU'LL BUILD

A fully functional MCP server that:

  • Tool: get_weather(location) — Fetch current weather
  • Resource: weather://forecast/{location} — 5-day forecast
  • Prompt: Weather report template
  • Works with: Claude Desktop, Cursor, Windsurf

Prerequisites

NODE.JS

v18+ installed

TYPESCRIPT

Basic familiarity

TIME

30 minutes

Step 1: Project Setup

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"
  }
}

Step 2: Create the Server Skeleton

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:

  • • Server name and version identify your server to clients
  • • Capabilities declare what your server supports
  • • StdioServerTransport handles stdio communication
  • • console.error (not console.log) for debugging

Step 3: Implement Tools

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.

Step 4: Implement Resources

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}`);
});

Step 5: Implement Prompts

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}`);
});

Step 6: Test Locally

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!

Step 7: Publish to npm

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"]
    }
  }
}

Best Practices

✓ DO THIS

  • • Use Zod for input validation
  • • Provide clear tool descriptions
  • • Log errors to stderr (not stdout)
  • • Handle errors gracefully
  • • Use environment variables for API keys
  • • Document all tools and resources
  • • Add TypeScript types

✗ DON'T DO THIS

  • • Use console.log (breaks stdio)
  • • Expose sensitive data
  • • Make blocking synchronous calls
  • • Ignore error handling
  • • Hardcode API keys
  • • Skip input validation
  • • Assume input is safe

What's Next?

ENHANCEMENT IDEAS

  • • Add real weather API integration (OpenWeatherMap, WeatherAPI)
  • • Implement resource subscriptions for live updates
  • • Add more tools: get_forecast(), get_alerts()
  • • Cache API responses to reduce costs
  • • Add unit tests with Vitest or Jest
  • • Create GitHub Actions for CI/CD
  • • Add configuration file support