#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { WeatherService } from './services/weather.js';
import { GeocodingService } from './services/geocoding.js';
import { LocationService } from './services/location.js';
import {
formatCurrentWeather,
formatHourlyWeather,
formatDailyWeather,
formatWeatherAlerts,
} from './utils/formatters.js';
import type { LocationInput, WeatherOptions } from './types/weather.js';
function parseArguments(): { apiKey: string } {
const args = process.argv.slice(2);
let apiKey = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--apikey' && i + 1 < args.length) {
apiKey = args[i + 1];
break;
}
}
if (!apiKey) {
console.error('Error: OpenWeatherMap API key is required');
console.error('');
console.error('To get an API key:');
console.error('1. Visit https://openweathermap.org');
console.error('2. Sign up for a free account');
console.error('3. Get your API key from your account dashboard');
console.error('');
console.error('Usage: npx @tristau/openweathermap-mcp --apikey YOUR_API_KEY');
console.error('');
console.error('For MCP configuration, please see the README for instructions on');
console.error('how to add your API key to your MCP client configuration.');
process.exit(1);
}
return { apiKey };
}
const { apiKey: API_KEY } = parseArguments();
class OpenWeatherMapMCPServer {
private server: Server;
private weatherService: WeatherService;
private geocodingService: GeocodingService;
private locationService: LocationService;
constructor() {
this.server = new Server(
{
name: 'openweathermap-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.weatherService = new WeatherService(API_KEY!);
this.geocodingService = new GeocodingService(API_KEY!);
this.locationService = new LocationService();
this.setupToolHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_current_weather',
description:
'Get current weather conditions for a location. If no location is provided, will automatically detect location from IP address. Supports coordinates, city names, or zip codes.',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'object',
properties: {
lat: { type: 'number', description: 'Latitude' },
lon: { type: 'number', description: 'Longitude' },
city: { type: 'string', description: 'City name' },
state: { type: 'string', description: 'State/region' },
country: { type: 'string', description: 'Country code' },
zipCode: { type: 'string', description: 'Zip/postal code' },
},
required: [],
additionalProperties: false,
description:
'Location to get weather for. If not provided, will auto-detect from IP address.',
},
units: {
type: 'string',
enum: ['standard', 'metric', 'imperial'],
description: 'Temperature units (default: metric)',
default: 'metric',
},
lang: {
type: 'string',
description:
'Language for weather descriptions (e.g., en, es, fr)',
default: 'en',
},
},
required: [],
additionalProperties: false,
},
},
{
name: 'get_hourly_forecast',
description:
'Get hourly weather forecast for up to 48 hours for a location. If no location is provided, will automatically detect location from IP address.',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'object',
properties: {
lat: { type: 'number', description: 'Latitude' },
lon: { type: 'number', description: 'Longitude' },
city: { type: 'string', description: 'City name' },
state: { type: 'string', description: 'State/region' },
country: { type: 'string', description: 'Country code' },
zipCode: { type: 'string', description: 'Zip/postal code' },
},
required: [],
additionalProperties: false,
description:
'Location to get forecast for. If not provided, will auto-detect from IP address.',
},
hours: {
type: 'number',
minimum: 1,
maximum: 48,
description: 'Number of hours to forecast (default: 24)',
default: 24,
},
units: {
type: 'string',
enum: ['standard', 'metric', 'imperial'],
description: 'Temperature units (default: metric)',
default: 'metric',
},
lang: {
type: 'string',
description: 'Language for weather descriptions',
default: 'en',
},
},
required: [],
additionalProperties: false,
},
},
{
name: 'get_daily_forecast',
description:
'Get daily weather forecast for up to 8 days for a location. If no location is provided, will automatically detect location from IP address.',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'object',
properties: {
lat: { type: 'number', description: 'Latitude' },
lon: { type: 'number', description: 'Longitude' },
city: { type: 'string', description: 'City name' },
state: { type: 'string', description: 'State/region' },
country: { type: 'string', description: 'Country code' },
zipCode: { type: 'string', description: 'Zip/postal code' },
},
required: [],
additionalProperties: false,
description:
'Location to get forecast for. If not provided, will auto-detect from IP address.',
},
days: {
type: 'number',
minimum: 1,
maximum: 8,
description: 'Number of days to forecast (default: 7)',
default: 7,
},
units: {
type: 'string',
enum: ['standard', 'metric', 'imperial'],
description: 'Temperature units (default: metric)',
default: 'metric',
},
lang: {
type: 'string',
description: 'Language for weather descriptions',
default: 'en',
},
},
required: [],
additionalProperties: false,
},
},
{
name: 'get_weather_alerts',
description:
'Get active weather alerts for a location. If no location is provided, will automatically detect location from IP address.',
inputSchema: {
type: 'object',
properties: {
location: {
type: 'object',
properties: {
lat: { type: 'number', description: 'Latitude' },
lon: { type: 'number', description: 'Longitude' },
city: { type: 'string', description: 'City name' },
state: { type: 'string', description: 'State/region' },
country: { type: 'string', description: 'Country code' },
zipCode: { type: 'string', description: 'Zip/postal code' },
},
required: [],
additionalProperties: false,
description:
'Location to get forecast for. If not provided, will auto-detect from IP address.',
},
lang: {
type: 'string',
description: 'Language for weather descriptions',
default: 'en',
},
},
required: [],
additionalProperties: false,
},
},
{
name: 'geocode_location',
description:
'Convert location names or addresses to coordinates using geocoding.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Location query (city, state, country)',
},
limit: {
type: 'number',
minimum: 1,
maximum: 5,
description: 'Maximum number of results (default: 5)',
default: 5,
},
},
required: ['query'],
additionalProperties: false,
},
},
{
name: 'geocode_zipcode',
description: 'Convert zip/postal codes to coordinates.',
inputSchema: {
type: 'object',
properties: {
zipCode: {
type: 'string',
description: 'Zip or postal code',
},
countryCode: {
type: 'string',
description: 'Country code (default: US)',
default: 'US',
},
},
required: ['zipCode'],
additionalProperties: false,
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_current_weather': {
const {
location,
units = 'metric',
lang = 'en',
} = args as {
location?: LocationInput;
units?: 'standard' | 'metric' | 'imperial';
lang?: string;
};
const options: WeatherOptions = { units, lang };
const result = await this.weatherService.getCurrentWeather(
location,
options
);
const locationName = result.detectedLocation
? `${result.detectedLocation.city}, ${result.detectedLocation.country}`
: location?.city ||
location?.zipCode ||
`${location?.lat}, ${location?.lon}`;
const locationMessage = result.detectedLocation
? this.locationService.formatLocationMessage(
result.detectedLocation
)
: '';
return {
content: [
{
type: 'text',
text: [
`# Current Weather for ${locationName}`,
'',
...(locationMessage ? [locationMessage, ''] : []),
formatCurrentWeather(result.weather.current, units),
'',
`š **Coordinates:** ${result.weather.lat}, ${result.weather.lon}`,
`š **Timezone:** ${result.weather.timezone}`,
].join('\n'),
},
],
};
}
case 'get_hourly_forecast': {
const {
location,
hours = 24,
units = 'metric',
lang = 'en',
} = args as {
location?: LocationInput;
hours?: number;
units?: 'standard' | 'metric' | 'imperial';
lang?: string;
};
const options: WeatherOptions = { units, lang };
const result = await this.weatherService.getHourlyForecast(
location,
hours,
options
);
const locationName = result.detectedLocation
? `${result.detectedLocation.city}, ${result.detectedLocation.country}`
: location?.city ||
location?.zipCode ||
`${location?.lat}, ${location?.lon}`;
const locationMessage = result.detectedLocation
? this.locationService.formatLocationMessage(
result.detectedLocation
)
: '';
return {
content: [
{
type: 'text',
text: [
`# ${hours}-Hour Weather Forecast for ${locationName}`,
'',
...(locationMessage ? [locationMessage, ''] : []),
formatHourlyWeather(result.forecast, units),
].join('\n'),
},
],
};
}
case 'get_daily_forecast': {
const {
location,
days = 7,
units = 'metric',
lang = 'en',
} = args as {
location?: LocationInput;
days?: number;
units?: 'standard' | 'metric' | 'imperial';
lang?: string;
};
const options: WeatherOptions = { units, lang };
const result = await this.weatherService.getDailyForecast(
location,
days,
options
);
const locationName = result.detectedLocation
? `${result.detectedLocation.city}, ${result.detectedLocation.country}`
: location?.city ||
location?.zipCode ||
`${location?.lat}, ${location?.lon}`;
const locationMessage = result.detectedLocation
? this.locationService.formatLocationMessage(
result.detectedLocation
)
: '';
return {
content: [
{
type: 'text',
text: [
`# ${days}-Day Weather Forecast for ${locationName}`,
'',
...(locationMessage ? [locationMessage, ''] : []),
formatDailyWeather(result.forecast, units),
].join('\n'),
},
],
};
}
case 'get_weather_alerts': {
const { location, lang = 'en' } = args as {
location?: LocationInput;
lang?: string;
};
const options: WeatherOptions = { lang };
const result = await this.weatherService.getWeatherAlerts(
location,
options
);
const locationName = result.detectedLocation
? `${result.detectedLocation.city}, ${result.detectedLocation.country}`
: location?.city ||
location?.zipCode ||
`${location?.lat}, ${location?.lon}`;
const locationMessage = result.detectedLocation
? this.locationService.formatLocationMessage(
result.detectedLocation
)
: '';
return {
content: [
{
type: 'text',
text: [
`# Weather Alerts for ${locationName}`,
'',
...(locationMessage ? [locationMessage, ''] : []),
formatWeatherAlerts(result.alerts || []),
].join('\n'),
},
],
};
}
case 'geocode_location': {
const { query, limit = 5 } = args as {
query: string;
limit?: number;
};
const results =
await this.geocodingService.getCoordinatesByLocationName(
query,
limit
);
return {
content: [
{
type: 'text',
text: [
`# Geocoding Results for "${query}"`,
'',
results
.map(
(result, index) =>
`**${index + 1}.** ${result.name}${result.state ? `, ${result.state}` : ''}, ${result.country}\nš Coordinates: ${result.lat}, ${result.lon}`
)
.join('\n\n'),
].join('\n'),
},
],
};
}
case 'geocode_zipcode': {
const { zipCode, countryCode = 'US' } = args as {
zipCode: string;
countryCode?: string;
};
const result = await this.geocodingService.getCoordinatesByZipCode(
zipCode,
countryCode
);
return {
content: [
{
type: 'text',
text: [
`# Geocoding Results for Zip Code "${zipCode}"`,
'',
`**Location:** ${result.name}, ${result.country}`,
`š **Coordinates:** ${result.lat}, ${result.lon}`,
`š® **Zip Code:** ${result.zip}`,
].join('\n'),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
},
],
isError: true,
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('OpenWeatherMap MCP Server running on stdio');
}
}
const server = new OpenWeatherMapMCPServer();
server.run().catch(console.error);