πŸš€Building Your First MCP Server: A Complete Guide



This content originally appeared on DEV Community and was authored by Diego Fernando Castillo Mamani

Introduction

The Model Context Protocol (MCP) is revolutionizing how AI applications interact with external tools and data sources. Whether you’re building Claude integrations, VSCode extensions, or custom AI workflows, understanding MCP servers is essential for modern AI development.

In this comprehensive guide, we’ll build a fully functional MCP server from scratch, deploy it to the cloud, and connect it to popular MCP clients. By the end, you’ll have a working weather information server that any MCP client can use.

What is MCP?

MCP (Model Context Protocol) is an open protocol that standardizes how AI applications connect to external data sources and tools. Think of it as a universal adapter that allows AI models like Claude to safely access your databases, APIs, file systems, and business tools.

Key Benefits

  • Standardized Integration: One protocol works across all MCP-compatible clients
  • Security First: Built-in authentication and permission controls
  • Flexible Architecture: Support for local and remote servers
  • Tool Discovery: Clients automatically discover available capabilities

Architecture Overview

An MCP server consists of three main components:

  1. Resources: Data sources the server can provide (files, database records, API data)
  2. Tools: Actions the server can perform (create, update, delete operations)
  3. Prompts: Pre-defined templates for common tasks
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             β”‚         β”‚             β”‚         β”‚             β”‚
β”‚ MCP Client  │◄───────►│ MCP Server  │◄───────►│  External   β”‚
β”‚  (Claude)   β”‚   MCP   β”‚   (Your     β”‚         β”‚   APIs      β”‚
β”‚             β”‚ Protocolβ”‚    Code)    β”‚         β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Building Our Weather MCP Server

Let’s build a practical MCP server that provides weather information using a public API.

Project Setup

First, create a new project and install dependencies:

mkdir weather-mcp-server
cd weather-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk axios dotenv
npm install -D @types/node typescript

Project Structure

weather-mcp-server/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.ts
β”‚   β”œβ”€β”€ server.ts
β”‚   └── weatherService.ts
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
└── .env

Configuration Files

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

package.json (add scripts)

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsc && node dist/index.js"
  },
  "type": "module"
}

Weather Service Implementation

src/weatherService.ts

import axios from 'axios';

export interface WeatherData {
  location: string;
  temperature: number;
  condition: string;
  humidity: number;
  windSpeed: number;
  description: string;
}

export class WeatherService {
  private apiKey: string;
  private baseUrl = 'https://api.openweathermap.org/data/2.5';

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async getCurrentWeather(city: string): Promise<WeatherData> {
    try {
      const response = await axios.get(`${this.baseUrl}/weather`, {
        params: {
          q: city,
          appid: this.apiKey,
          units: 'metric'
        }
      });

      const data = response.data;

      return {
        location: `${data.name}, ${data.sys.country}`,
        temperature: Math.round(data.main.temp),
        condition: data.weather[0].main,
        humidity: data.main.humidity,
        windSpeed: data.wind.speed,
        description: data.weather[0].description
      };
    } catch (error) {
      throw new Error(`Failed to fetch weather data: ${error}`);
    }
  }

  async getForecast(city: string, days: number = 5): Promise<any> {
    try {
      const response = await axios.get(`${this.baseUrl}/forecast`, {
        params: {
          q: city,
          appid: this.apiKey,
          units: 'metric',
          cnt: days * 8 // API returns 3-hour intervals
        }
      });

      return response.data;
    } catch (error) {
      throw new Error(`Failed to fetch forecast data: ${error}`);
    }
  }
}

MCP Server Implementation

src/server.ts

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';
import { WeatherService } from './weatherService.js';

export class WeatherMCPServer {
  private server: Server;
  private weatherService: WeatherService;

  constructor(apiKey: string) {
    this.weatherService = new WeatherService(apiKey);
    this.server = new Server(
      {
        name: 'weather-mcp-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
          resources: {}
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'get_current_weather',
          description: 'Get current weather information for a city',
          inputSchema: {
            type: 'object',
            properties: {
              city: {
                type: 'string',
                description: 'City name (e.g., "London", "New York")'
              }
            },
            required: ['city']
          }
        },
        {
          name: 'get_forecast',
          description: 'Get weather forecast for upcoming days',
          inputSchema: {
            type: 'object',
            properties: {
              city: {
                type: 'string',
                description: 'City name'
              },
              days: {
                type: 'number',
                description: 'Number of days (1-5)',
                default: 5
              }
            },
            required: ['city']
          }
        }
      ]
    }));

    // Handle tool calls
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      try {
        if (name === 'get_current_weather') {
          const weather = await this.weatherService.getCurrentWeather(
            args.city as string
          );

          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(weather, null, 2)
              }
            ]
          };
        }

        if (name === 'get_forecast') {
          const forecast = await this.weatherService.getForecast(
            args.city as string,
            (args.days as number) || 5
          );

          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify(forecast, null, 2)
              }
            ]
          };
        }

        throw new Error(`Unknown tool: ${name}`);
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: `Error: ${error}`
            }
          ],
          isError: true
        };
      }
    });

    // List available resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
      resources: [
        {
          uri: 'weather://cities',
          name: 'Popular Cities',
          description: 'List of popular cities for weather queries',
          mimeType: 'application/json'
        }
      ]
    }));

    // Read resources
    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const { uri } = request.params;

      if (uri === 'weather://cities') {
        const cities = [
          'London', 'New York', 'Tokyo', 'Paris', 'Sydney',
          'Berlin', 'Toronto', 'Singapore', 'Dubai', 'Mumbai'
        ];

        return {
          contents: [
            {
              uri,
              mimeType: 'application/json',
              text: JSON.stringify(cities, null, 2)
            }
          ]
        };
      }

      throw new Error(`Unknown resource: ${uri}`);
    });
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Weather MCP Server running on stdio');
  }
}

Entry Point

src/index.ts

import dotenv from 'dotenv';
import { WeatherMCPServer } from './server.js';

dotenv.config();

const apiKey = process.env.OPENWEATHER_API_KEY;

if (!apiKey) {
  console.error('Error: OPENWEATHER_API_KEY environment variable is required');
  process.exit(1);
}

const server = new WeatherMCPServer(apiKey);
server.start().catch((error) => {
  console.error('Server error:', error);
  process.exit(1);
});

Environment Configuration

.env

OPENWEATHER_API_KEY=your_api_key_here

Get a free API key from OpenWeatherMap.

Connecting to Claude Desktop

To use your MCP server with Claude Desktop, add it to your configuration:

MacOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/weather-mcp-server/dist/index.js"],
      "env": {
        "OPENWEATHER_API_KEY": "your_api_key_here"
      }
    }
  }
}

Restart Claude Desktop, and you’ll see the weather tools available!

Testing Your Server

Build and run the server:

npm run build
npm start

You can test it with Claude Desktop or create a simple test client:

test-client.ts

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function testWeatherServer() {
  const transport = new StdioClientTransport({
    command: 'node',
    args: ['dist/index.js']
  });

  const client = new Client({
    name: 'test-client',
    version: '1.0.0'
  }, {
    capabilities: {}
  });

  await client.connect(transport);

  // List available tools
  const tools = await client.listTools();
  console.log('Available tools:', tools);

  // Call weather tool
  const result = await client.callTool({
    name: 'get_current_weather',
    arguments: { city: 'London' }
  });

  console.log('Weather result:', result);
}

testWeatherServer();

Deploying to Production

Option 1: Docker Deployment

Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY dist ./dist

ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Option 2: Google Cloud Run

gcloud run deploy weather-mcp-server \
  --source . \
  --platform managed \
  --region us-central1 \
  --set-env-vars OPENWEATHER_API_KEY=your_key

Best Practices

  1. Error Handling: Always wrap external API calls in try-catch blocks
  2. Validation: Validate all input parameters before processing
  3. Rate Limiting: Implement rate limiting for external API calls
  4. Caching: Cache frequently requested data to reduce API costs
  5. Logging: Use structured logging for debugging and monitoring
  6. Security: Never expose API keys in code or version control

Advanced Features

Adding Authentication

private validateRequest(request: any): boolean {
  const token = request.params._meta?.authorization;
  return token === process.env.AUTH_TOKEN;
}

Implementing Caching

import NodeCache from 'node-cache';

private cache = new NodeCache({ stdTTL: 600 }); // 10 minutes

async getCachedWeather(city: string): Promise<WeatherData> {
  const cacheKey = `weather:${city}`;
  const cached = this.cache.get<WeatherData>(cacheKey);

  if (cached) return cached;

  const weather = await this.weatherService.getCurrentWeather(city);
  this.cache.set(cacheKey, weather);

  return weather;
}

Troubleshooting

Common Issues

Server not appearing in Claude Desktop

  • Verify the path in claude_desktop_config.json is absolute
  • Check that the build succeeded (npm run build)
  • Restart Claude Desktop completely

API errors

  • Confirm your OpenWeather API key is valid
  • Check rate limits on your API tier
  • Verify network connectivity

TypeScript errors

  • Ensure all dependencies are installed
  • Run npm run build to check for compilation errors

Conclusion

You’ve now built a complete MCP server that can integrate with any MCP-compatible client. This weather server demonstrates the core concepts:

  • Tool definition and execution
  • Resource management
  • Error handling
  • Client integration

The same patterns can be extended to build MCP servers for:

  • Database access
  • File system operations
  • API integrations
  • Business tool automation
  • Custom workflows

Next Steps

  1. Extend functionality: Add more weather features (air quality, UV index, alerts)
  2. Add tests: Implement unit and integration tests
  3. Monitor performance: Add metrics and logging
  4. Build a remote server: Convert to HTTP-based MCP server for multiple clients
  5. Create documentation: Document your API for other developers

Resources


This content originally appeared on DEV Community and was authored by Diego Fernando Castillo Mamani