DEV Community

🚀Building Your First MCP Server: A Complete Guide

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)    │         │             │
└─────────────┘         └─────────────┘         └─────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Project Structure

weather-mcp-server/
├── src/
│   ├── index.ts
│   ├── server.ts
│   └── weatherService.ts
├── package.json
├── tsconfig.json
└── .env
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

package.json (add scripts)

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

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}`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Environment Configuration

.env

OPENWEATHER_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Testing Your Server

Build and run the server:

npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)