@ryancardin/noaa-tides-currents-mcp-server
Version:
MCP Server that interfaces with NOAA Tides and Currents API using FastMCP
219 lines (218 loc) • 10.2 kB
JavaScript
import SunCalc from 'suncalc';
import { z } from 'zod';
/**
* Sun event types
*/
export var SunEventType;
(function (SunEventType) {
SunEventType["SUNRISE"] = "sunrise";
SunEventType["SUNSET"] = "sunset";
SunEventType["DAWN"] = "dawn";
SunEventType["DUSK"] = "dusk";
SunEventType["SOLAR_NOON"] = "solarNoon";
SunEventType["NIGHT_START"] = "night";
SunEventType["NIGHT_END"] = "nightEnd";
SunEventType["GOLDEN_HOUR_START"] = "goldenHourStart";
SunEventType["GOLDEN_HOUR_END"] = "goldenHourEnd";
SunEventType["NAUTICAL_DAWN"] = "nauticalDawn";
SunEventType["NAUTICAL_DUSK"] = "nauticalDusk";
SunEventType["ASTRONOMICAL_DAWN"] = "astronomicalDawn";
SunEventType["ASTRONOMICAL_DUSK"] = "astronomicalDusk";
})(SunEventType || (SunEventType = {}));
/**
* Parameters for getting sun times
*/
export const SunTimesParamsSchema = z.object({
date: z.string().optional().describe('Date to get sun times for (YYYY-MM-DD format). Defaults to current date.'),
latitude: z.number().min(-90).max(90).describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).describe('Longitude for location-specific calculations'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)'),
timezone: z.string().optional().describe('Timezone for the results. Defaults to UTC.')
});
/**
* Parameters for getting sun times for a date range
*/
export const SunTimesRangeParamsSchema = z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD format)'),
end_date: z.string().describe('End date (YYYY-MM-DD format)'),
latitude: z.number().min(-90).max(90).describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).describe('Longitude for location-specific calculations'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)'),
timezone: z.string().optional().describe('Timezone for the results. Defaults to UTC.')
});
/**
* Parameters for getting sun position
*/
export const SunPositionParamsSchema = z.object({
date: z.string().optional().describe('Date to get sun position for (YYYY-MM-DD format). Defaults to current date.'),
time: z.string().optional().describe('Time to get sun position for (HH:MM:SS format). Defaults to current time.'),
latitude: z.number().min(-90).max(90).describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).describe('Longitude for location-specific calculations'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)')
});
/**
* Parameters for finding the next sun event
*/
export const NextSunEventParamsSchema = z.object({
event: z.nativeEnum(SunEventType).describe('Sun event to find'),
date: z.string().optional().describe('Starting date (YYYY-MM-DD format). Defaults to current date.'),
latitude: z.number().min(-90).max(90).describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).describe('Longitude for location-specific calculations'),
count: z.number().positive().optional().describe('Number of occurrences to return. Defaults to 1.'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)'),
timezone: z.string().optional().describe('Timezone for the results. Defaults to UTC.')
});
/**
* Service for sun calculations
*/
export class SunService {
/**
* Get sun times for a specific date and location
* @param params Parameters for the request
* @returns Sun times information
*/
getSunTimes(params) {
const date = params.date ? new Date(params.date) : new Date();
const { latitude, longitude } = params;
// Get sun times data
const sunTimes = SunCalc.getTimes(date, latitude, longitude);
// Format times or return null if not available
const formatTime = (time) => {
if (!time || isNaN(time.getTime()))
return null;
if (params.timezone) {
try {
return time.toLocaleTimeString('en-US', { timeZone: params.timezone });
}
catch (error) {
// If timezone is invalid, fall back to ISO string
console.warn(`Invalid timezone: ${params.timezone}. Using UTC.`);
}
}
return time.toISOString();
};
// Calculate day length in minutes
const sunrise = sunTimes.sunrise;
const sunset = sunTimes.sunset;
let dayLength = 0;
if (sunrise && sunset && !isNaN(sunrise.getTime()) && !isNaN(sunset.getTime())) {
dayLength = (sunset.getTime() - sunrise.getTime()) / (60 * 1000);
}
return {
date: date.toISOString().split('T')[0],
sunrise: formatTime(sunTimes.sunrise),
sunset: formatTime(sunTimes.sunset),
solarNoon: formatTime(sunTimes.solarNoon),
dawn: formatTime(sunTimes.dawn),
dusk: formatTime(sunTimes.dusk),
nightStart: formatTime(sunTimes.night),
nightEnd: formatTime(sunTimes.nightEnd),
goldenHourStart: formatTime(sunTimes.goldenHour),
goldenHourEnd: formatTime(sunTimes.goldenHourEnd),
nauticalDawn: formatTime(sunTimes.nauticalDawn),
nauticalDusk: formatTime(sunTimes.nauticalDusk),
astronomicalDawn: formatTime(sunTimes.astronomicalDawn),
astronomicalDusk: formatTime(sunTimes.astronomicalDusk),
dayLength
};
}
/**
* Get sun times for a date range
* @param params Parameters for the request
* @returns Array of sun times information
*/
getSunTimesRange(params) {
const startDate = new Date(params.start_date);
const endDate = new Date(params.end_date);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error('Invalid date format. Please use YYYY-MM-DD format.');
}
if (startDate > endDate) {
throw new Error('Start date must be before end date.');
}
const result = [];
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
result.push(this.getSunTimes({
date: currentDate.toISOString().split('T')[0],
latitude: params.latitude,
longitude: params.longitude,
timezone: params.timezone
}));
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
return result;
}
/**
* Get sun position for a specific date, time, and location
* @param params Parameters for the request
* @returns Sun position information
*/
getSunPosition(params) {
const date = params.date ? new Date(params.date) : new Date();
// If time is provided, set it on the date object
if (params.time) {
const [hours, minutes, seconds] = params.time.split(':').map(Number);
date.setHours(hours || 0, minutes || 0, seconds || 0);
}
const { latitude, longitude } = params;
// Get sun position data
const position = SunCalc.getPosition(date, latitude, longitude);
// Calculate additional astronomical data
const declination = Math.asin(Math.sin(23.44 * Math.PI / 180) * Math.sin(position.azimuth));
const rightAscension = Math.atan2(Math.cos(23.44 * Math.PI / 180) * Math.sin(position.azimuth), Math.cos(position.azimuth));
return {
date: date.toISOString().split('T')[0],
time: date.toISOString().split('T')[1].split('.')[0],
azimuth: position.azimuth * (180 / Math.PI), // Convert to degrees
altitude: position.altitude * (180 / Math.PI), // Convert to degrees
declination: declination * (180 / Math.PI), // Convert to degrees
rightAscension: rightAscension * (180 / Math.PI) // Convert to degrees
};
}
/**
* Get the next occurrence(s) of a specific sun event
* @param params Parameters for the request
* @returns Array of dates for the next occurrences of the specified event
*/
getNextSunEvent(params) {
const startDate = params.date ? new Date(params.date) : new Date();
const count = params.count || 1;
const { latitude, longitude, event } = params;
const results = [];
let currentDate = new Date(startDate);
// Find the next occurrences
while (results.length < count) {
// Get sun times for the current date
const sunTimes = SunCalc.getTimes(currentDate, latitude, longitude);
const eventTime = sunTimes[event];
// If the event time is valid and in the future
if (eventTime && !isNaN(eventTime.getTime()) && eventTime > startDate) {
let timeString = eventTime.toISOString();
// Format time based on timezone if provided
if (params.timezone) {
try {
timeString = eventTime.toLocaleTimeString('en-US', { timeZone: params.timezone });
}
catch (error) {
// If timezone is invalid, fall back to ISO string
console.warn(`Invalid timezone: ${params.timezone}. Using UTC.`);
}
}
results.push({
date: eventTime.toISOString().split('T')[0],
time: timeString.split('T')[1]?.split('.')[0] || timeString,
event
});
}
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
// Safety check to prevent infinite loops
if (results.length === 0 && currentDate.getTime() - startDate.getTime() > 366 * 24 * 60 * 60 * 1000) {
throw new Error('Could not find the specified sun event within a year.');
}
}
return results;
}
}