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:
- Resources: Data sources the server can provide (files, database records, API data)
- Tools: Actions the server can perform (create, update, delete operations)
- 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
- Error Handling: Always wrap external API calls in try-catch blocks
- Validation: Validate all input parameters before processing
- Rate Limiting: Implement rate limiting for external API calls
- Caching: Cache frequently requested data to reduce API costs
- Logging: Use structured logging for debugging and monitoring
- 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.jsonis 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 buildto 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
- Extend functionality: Add more weather features (air quality, UV index, alerts)
- Add tests: Implement unit and integration tests
- Monitor performance: Add metrics and logging
- Build a remote server: Convert to HTTP-based MCP server for multiple clients
- Create documentation: Document your API for other developers
Top comments (0)