@ryancardin/noaa-tides-currents-mcp-server
Version:
MCP Server that interfaces with NOAA Tides and Currents API using FastMCP
188 lines (187 loc) • 8.1 kB
JavaScript
import SunCalc from 'suncalc';
import { z } from 'zod';
/**
* Moon phase names and their approximate ranges
*/
export var MoonPhaseName;
(function (MoonPhaseName) {
MoonPhaseName["NEW_MOON"] = "New Moon";
MoonPhaseName["WAXING_CRESCENT"] = "Waxing Crescent";
MoonPhaseName["FIRST_QUARTER"] = "First Quarter";
MoonPhaseName["WAXING_GIBBOUS"] = "Waxing Gibbous";
MoonPhaseName["FULL_MOON"] = "Full Moon";
MoonPhaseName["WANING_GIBBOUS"] = "Waning Gibbous";
MoonPhaseName["LAST_QUARTER"] = "Last Quarter";
MoonPhaseName["WANING_CRESCENT"] = "Waning Crescent";
})(MoonPhaseName || (MoonPhaseName = {}));
/**
* Parameters for getting moon phase
*/
export const MoonPhaseParamsSchema = z.object({
date: z.string().optional().describe('Date to get moon phase for (YYYY-MM-DD format). Defaults to current date.'),
latitude: z.number().min(-90).max(90).optional().describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).optional().describe('Longitude for location-specific calculations'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)')
});
/**
* Parameters for getting moon phases for a date range
*/
export const MoonPhasesRangeParamsSchema = 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).optional().describe('Latitude for location-specific calculations'),
longitude: z.number().min(-180).max(180).optional().describe('Longitude for location-specific calculations'),
format: z.enum(['json', 'text']).optional().describe('Output format (json or text)')
});
/**
* Parameters for getting next moon phase
*/
export const NextMoonPhaseParamsSchema = z.object({
phase: z.enum([
MoonPhaseName.NEW_MOON,
MoonPhaseName.FIRST_QUARTER,
MoonPhaseName.FULL_MOON,
MoonPhaseName.LAST_QUARTER
]).describe('Moon phase to find'),
date: z.string().optional().describe('Starting date (YYYY-MM-DD format). Defaults to current date.'),
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)')
});
/**
* Service for moon phase calculations
*/
export class MoonPhaseService {
/**
* Get the moon phase for a specific date
* @param params Parameters for the request
* @returns Moon phase information
*/
getMoonPhase(params) {
const date = params.date ? new Date(params.date) : new Date();
// Get moon illumination data
const illuminationData = SunCalc.getMoonIllumination(date);
// Get moon position data (requires location)
const latitude = params.latitude ?? 0;
const longitude = params.longitude ?? 0;
const positionData = SunCalc.getMoonPosition(date, latitude, longitude);
// Calculate moon phase name
const phaseName = this.getMoonPhaseName(illuminationData.phase);
// Calculate if the moon is waxing (increasing illumination)
const isWaxing = illuminationData.phase < 0.5;
// Calculate approximate moon age (0-29.53 days)
const lunarMonth = 29.53; // days
const age = illuminationData.phase * lunarMonth;
// Calculate apparent diameter (in degrees)
const diameter = 0.5181 * (384400 / positionData.distance);
return {
date: date.toISOString().split('T')[0],
phase: illuminationData.phase,
phaseName,
illumination: illuminationData.fraction,
age,
distance: positionData.distance,
diameter,
isWaxing
};
}
/**
* Get moon phases for a date range
* @param params Parameters for the request
* @returns Array of moon phase information
*/
getMoonPhasesRange(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.getMoonPhase({
date: currentDate.toISOString().split('T')[0],
latitude: params.latitude,
longitude: params.longitude
}));
// Move to next day
currentDate.setDate(currentDate.getDate() + 1);
}
return result;
}
/**
* Get the next occurrence(s) of a specific moon phase
* @param params Parameters for the request
* @returns Array of dates for the next occurrences of the specified phase
*/
getNextMoonPhase(params) {
const startDate = params.date ? new Date(params.date) : new Date();
const count = params.count || 1;
const targetPhase = params.phase;
// Map phase names to their approximate values
const phaseValues = {
[MoonPhaseName.NEW_MOON]: 0,
[MoonPhaseName.FIRST_QUARTER]: 0.25,
[MoonPhaseName.FULL_MOON]: 0.5,
[MoonPhaseName.LAST_QUARTER]: 0.75
};
const targetPhaseValue = phaseValues[targetPhase];
const results = [];
let currentDate = new Date(startDate);
// Find the next occurrences
while (results.length < count) {
// Check every day (could be optimized with better algorithms)
const illuminationData = SunCalc.getMoonIllumination(currentDate);
const prevDate = new Date(currentDate);
prevDate.setDate(prevDate.getDate() - 1);
const prevIlluminationData = SunCalc.getMoonIllumination(prevDate);
// Check if we've passed the target phase between yesterday and today
const prevDiff = Math.abs(prevIlluminationData.phase - targetPhaseValue);
const currentDiff = Math.abs(illuminationData.phase - targetPhaseValue);
// If we're getting closer to the target phase and then further away, we've passed it
// Or if we're very close to the target phase (within 0.01)
if ((prevDiff > currentDiff && currentDiff < 0.01) || currentDiff < 0.005) {
results.push({
date: currentDate.toISOString().split('T')[0],
phase: targetPhase
});
}
// 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 moon phase within a year.');
}
}
return results;
}
/**
* Get the name of the moon phase based on the phase value
* @param phase Phase value (0-1)
* @returns Moon phase name
*/
getMoonPhaseName(phase) {
// Normalize phase to 0-1 range
phase = phase % 1;
if (phase < 0)
phase += 1;
// Determine phase name based on value ranges
if (phase < 0.0625 || phase >= 0.9375)
return MoonPhaseName.NEW_MOON;
if (phase < 0.1875)
return MoonPhaseName.WAXING_CRESCENT;
if (phase < 0.3125)
return MoonPhaseName.FIRST_QUARTER;
if (phase < 0.4375)
return MoonPhaseName.WAXING_GIBBOUS;
if (phase < 0.5625)
return MoonPhaseName.FULL_MOON;
if (phase < 0.6875)
return MoonPhaseName.WANING_GIBBOUS;
if (phase < 0.8125)
return MoonPhaseName.LAST_QUARTER;
return MoonPhaseName.WANING_CRESCENT;
}
}