# Directory Structure ``` ├── .gitignore ├── CLAUDE.md ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── index.ts │ ├── interfaces │ │ ├── moon.ts │ │ ├── noaa.ts │ │ ├── parameters.ts │ │ └── sun.ts │ ├── schemas │ │ ├── common.ts │ │ └── dpapi.ts │ ├── server │ │ ├── config.ts │ │ └── mcp-server.ts │ ├── services │ │ ├── dpapi-service.ts │ │ ├── moon-phase-service.ts │ │ ├── noaa-parameters-service.ts │ │ ├── noaa-service.ts │ │ └── sun-service.ts │ ├── tools │ │ ├── derived-product-tools.ts │ │ ├── index.ts │ │ ├── moon-tools.ts │ │ ├── parameter-tools.ts │ │ ├── station-tools.ts │ │ ├── sun-tools.ts │ │ └── water-tools.ts │ └── types │ ├── moon.ts │ ├── sun.ts │ └── suncalc.d.ts ├── test-dpapi.js └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # dependencies /node_modules /.pnp .pnp.js # production /dist /build # misc .DS_Store .env .env.local .env.development.local .env.test.local .env.production.local # logs npm-debug.log* yarn-debug.log* yarn-error.log* # IDE files .idea/ .vscode/ *.swp *.swo .cursor ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # 🌊 NOAA Tides & Currents MCP Server <div align="center"> [](https://www.npmjs.com/package/@ryancardin/noaa-tides-currents-mcp-server) [](https://opensource.org/licenses/MIT) [](https://www.typescriptlang.org/) [](https://modelcontextprotocol.io/) [](https://smithery.ai/server/@RyanCardin15/noaa-tidesandcurrents-mcp) **🚀 Lightning-fast access to NOAA's oceanic and atmospheric data through MCP** *Your one-stop solution for tides, currents, weather, astronomy, and climate data* [📦 Quick Start](#-quick-start) • [🛠️ Tools](#️-available-tools) • [📖 Examples](#-usage-examples) • [🏗️ Advanced](#️-advanced-usage) </div> --- ## ✨ What Makes This Awesome 🌊 **25+ Specialized Tools** - From basic tide data to advanced climate projections ⚡ **Lightning Fast** - Built on FastMCP for optimal performance 🎯 **Zero Config** - Works out of the box with Claude Desktop 🌍 **Comprehensive Data** - Water levels, currents, weather, moon phases, sun data 📊 **Climate Research Ready** - Sea level trends, flooding projections, extreme events 🚀 **NPX Ready** - Install and run with a single command --- ## 🚀 Quick Start ### ⚡ NPX Installation (Recommended) ```bash # Install and run immediately - no setup required! npx @ryancardin/noaa-tides-currents-mcp-server # Or use the shorter alias npx noaa-mcp ``` #### 🔌 Transport Modes **STDIO Mode (Default - MCP Protocol)** ```bash # Standard MCP server for Claude Desktop integration npx @ryancardin/noaa-tides-currents-mcp-server # Or use the shorter alias npx noaa-mcp ``` **HTTP Streamable Mode (Web Integration)** ```bash # Start HTTP server on default port 3000 npx @ryancardin/noaa-tides-currents-mcp-server --http # Specify custom port npx @ryancardin/noaa-tides-currents-mcp-server --http --port 8080 # Using shorter alias npx noaa-mcp --http --port 8080 # Access via Server-Sent Events curl http://localhost:3000/sse ``` ### 🎯 Claude Desktop Integration Install directly to Claude Desktop via [Smithery](https://smithery.ai/server/@RyanCardin15/tidesandcurrents): ```bash npx -y @smithery/cli install @RyanCardin15/tidesandcurrents --client claude ``` ### 🔧 Manual Development Setup ```bash # Clone and build git clone https://github.com/RyanCardin15/NOAA-Tides-And-Currents-MCP.git cd NOAA-Tides-And-Currents-MCP npm install && npm run build # Start the server npm start # Test with FastMCP npx fastmcp dev dist/index.js ``` --- ## 🛠️ Available Tools <details> <summary><strong>🌊 Water Data Tools (6 tools)</strong></summary> ### Water Levels & Tides - **`get_water_levels`** - Real-time and historical water level data - **`get_tide_predictions`** - High/low tide predictions and continuous data - **`get_currents`** - Real-time and historical current measurements - **`get_current_predictions`** - Current speed and direction forecasts - **`get_meteorological_data`** - Wind, air temp, water temp, pressure, etc. ### Station Information - **`get_stations`** - Search and list monitoring stations - **`get_station_details`** - Detailed station metadata and capabilities </details> <details> <summary><strong>🔬 Climate & Research Tools (9 tools)</strong></summary> ### Sea Level Analysis - **`get_sea_level_trends`** - Long-term sea level rise trends and rates - **`get_extreme_water_levels`** - Statistical analysis of extreme events ### High Tide Flooding Analysis - **`get_high_tide_flooding_daily`** - Daily flood event counts - **`get_high_tide_flooding_monthly`** - Monthly flooding patterns - **`get_high_tide_flooding_seasonal`** - Seasonal flood analysis - **`get_high_tide_flooding_annual`** - Yearly flooding trends - **`get_high_tide_flooding_projections`** - Future flood risk scenarios - **`get_high_tide_flooding_likelihoods`** - Daily flood probability ### Historical Extremes - **`get_top_ten_water_levels`** - Highest/lowest water levels on record </details> <details> <summary><strong>🌙 Astronomy Tools (7 tools)</strong></summary> ### Moon Phase Calculations - **`get_moon_phase`** - Current moon phase and illumination - **`get_moon_phases_range`** - Moon phases over date ranges - **`get_next_moon_phase`** - Find next new/full/quarter moons ### Solar Calculations - **`get_sun_times`** - Sunrise, sunset, dawn, dusk times - **`get_sun_times_range`** - Solar times over date ranges - **`get_sun_position`** - Real-time sun azimuth and elevation - **`get_next_sun_event`** - Next sunrise, sunset, or solar noon </details> <details> <summary><strong>⚙️ Configuration Tools (1 tool)</strong></summary> ### API Parameters - **`get_parameter_definitions`** - Valid values for all API parameters </details> --- ## 📖 Usage Examples ### 🌊 Get Current Tide Conditions ```bash # Get latest water levels for Boston Harbor get_water_levels station="8443970" date="latest" # Get today's tide predictions for Miami get_tide_predictions station="8723214" begin_date="today" end_date="today" interval="hilo" ``` ### 🌀 Hurricane Preparedness ```bash # Get extreme water level statistics for storm planning get_extreme_water_levels station="8518750" units="english" # Check flooding likelihood for tomorrow get_high_tide_flooding_likelihoods station="8518750" date="2024-12-16" threshold="minor" ``` ### 🔬 Climate Research ```bash # Analyze 30-year sea level trends get_sea_level_trends station="8518750" affiliation="US" # Get high tide flooding projections for 2050s under intermediate sea level rise get_high_tide_flooding_projections station="8518750" scenario="intermediate" decade="2050s" ``` ### 🌙 Astronomy & Navigation ```bash # Get tonight's moon phase for navigation get_moon_phase date="2024-12-15" latitude="42.3601" longitude="-71.0589" # Calculate sunrise/sunset for sailing get_sun_times date="2024-12-15" latitude="25.7617" longitude="-80.1918" timezone="America/New_York" ``` ### 🎣 Fishing & Recreation ```bash # Best fishing times with current predictions get_current_predictions station="ACT0446" date="today" interval="MAX_SLACK" # Wind and weather conditions get_meteorological_data station="8443970" product="wind" date="today" ``` --- ## 🏗️ Advanced Usage ### 🔧 Development & Testing ```bash # Run in development mode (stdio) npm run dev # Development with HTTP transport npm run dev:http # Production builds with different transports npm start # STDIO mode (default) npm run start:http # HTTP on port 3000 npm run start:http:3001 # HTTP on port 3001 npm run start:http:8080 # HTTP on port 8080 # Inspect server capabilities npx fastmcp inspect dist/index.js ``` ### 🌐 HTTP Stream Integration When running in HTTP mode, the server provides Server-Sent Events (SSE) at `/sse`: ```bash # Start HTTP server npx @ryancardin/noaa-tides-currents-mcp-server --http --port 3000 # Test the endpoint curl -N http://localhost:3000/sse # Or integrate with web applications fetch('http://localhost:3000/sse') .then(response => response.body.getReader()) .then(reader => { // Handle streaming MCP responses }); ``` **Use Cases for HTTP Mode:** - 🌐 **Web Applications** - Integrate with React, Vue, Angular apps - 📱 **Mobile Apps** - REST-like access from mobile applications - 🔗 **API Gateways** - Proxy through load balancers or API gateways - 🧪 **Testing** - Easy curl-based testing and debugging ### 📊 Data Formats & Export All tools support multiple output formats: - **JSON** (default) - Perfect for programmatic use - **XML** - Legacy system integration - **CSV** - Direct spreadsheet import ### 🌍 Global Station Coverage - **13,000+ stations** worldwide - **Real-time data** from NOAA's CO-OPS network - **Historical records** dating back decades - **Global tide predictions** and current forecasts --- ## 🚦 API Endpoints This server integrates with three NOAA APIs: | API | Purpose | Base URL | |-----|---------|----------| | **Data API** | Real-time observations & predictions | `api.tidesandcurrents.noaa.gov/api/prod/` | | **Metadata API** | Station information & capabilities | `api.tidesandcurrents.noaa.gov/mdapi/prod/` | | **Derived Products API** | Climate analysis & research data | `api.tidesandcurrents.noaa.gov/dpapi/prod/` | --- ## 🛠️ Technical Details ### Architecture - **🚀 FastMCP Framework** - High-performance MCP server - **📝 TypeScript** - Full type safety and IntelliSense - **🔧 Zod Validation** - Runtime parameter validation - **⚡ Axios HTTP Client** - Reliable API communication - **🌙 SunCalc Integration** - Precise astronomical calculations ### Transport Options - **📡 STDIO Transport** - Standard MCP protocol for desktop clients - **🌐 HTTP Stream Transport** - Server-Sent Events for web integration - **🔄 Dual Mode Support** - Switch between transports via command-line flags ### System Requirements - **Node.js** 18+ - **NPM** 8+ - **MCP Client** (Claude Desktop, etc.) ### Package Size - **📦 Bundled**: 43.9 KB - **📂 Installed**: 286.2 KB - **⚡ Load Time**: <100ms --- ## 🐛 Troubleshooting <details> <summary><strong>Common Issues & Solutions</strong></summary> ### Server Won't Start ```bash # Check Node.js version node --version # Should be 18+ # Rebuild TypeScript npm run build ``` ### API Errors - **Invalid Station ID**: Use `get_stations` to find valid stations - **Date Format Issues**: Use YYYYMMDD or MM/DD/YYYY formats - **Rate Limiting**: NOAA APIs have usage limits - space out requests ### MCP Connection Issues - Ensure Claude Desktop MCP settings are configured correctly - Check that the server binary has execute permissions: `chmod +x dist/index.js` </details> --- ## 📈 Roadmap - [ ] 🌊 **Real-time Alerts** - Webhook support for tide/weather alerts - [ ] 📱 **Mobile SDK** - React Native integration - [ ] 🗺️ **GIS Integration** - Shapefile and KML export - [ ] 🤖 **AI Insights** - Automated pattern recognition - [ ] ⚡ **GraphQL API** - Modern query interface - [ ] 🌐 **Multi-language** - I18n support --- ## 🤝 Contributing We love contributions! Here's how to get started: 1. **🍴 Fork** the repository 2. **🌿 Branch** for your feature (`git checkout -b amazing-feature`) 3. **💻 Code** your improvements 4. **✅ Test** with `npm test` 5. **📤 Submit** a pull request ### Development Commands ```bash npm run build # Build TypeScript npm run dev # Development mode npm run test # Run test suite npm run format # Format with Prettier ``` --- ## 📄 License **MIT License** - see [LICENSE](LICENSE) file for details. Built with ❤️ by [Ryan Cardin](https://github.com/RyanCardin15) --- ## 🔗 Links & Resources - **📦 NPM Package**: [@ryancardin/noaa-tides-currents-mcp-server](https://www.npmjs.com/package/@ryancardin/noaa-tides-currents-mcp-server) - **🏪 Smithery**: [Auto-install for Claude Desktop](https://smithery.ai/server/@RyanCardin15/noaa-tidesandcurrents-mcp) - **🌊 NOAA CO-OPS**: [Official NOAA Data Portal](https://tidesandcurrents.noaa.gov/) - **🤖 MCP Protocol**: [Model Context Protocol Docs](https://modelcontextprotocol.io/) - **⚡ FastMCP**: [FastMCP Framework](https://github.com/jlowin/fastmcp) <div align="center"> **⭐ Star this repo if it helped you!** Made possible by NOAA's commitment to open oceanic data 🌊 </div> ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Development Commands ### Building and Running - `npm run build` - Build TypeScript to JavaScript in `dist/` directory - `npm start` - Start the MCP server (requires build first) - `npm run dev` - Run in development mode with ts-node-esm - `npm run format` - Format code with Prettier - `npm test` - Run tests with Vitest ### Testing and Development - `npx fastmcp dev dist/index.js` - Test server with fastmcp CLI - `npx fastmcp inspect dist/index.js` - Inspect server capabilities ## Architecture Overview This is a FastMCP server that provides tools for accessing NOAA Tides and Currents data, moon phase information, and sun position calculations. ### Core Structure - **Entry Point**: `src/index.ts` - Creates server, registers tools, and starts stdio transport - **Server Configuration**: `src/server/config.ts` - FastMCP server setup with fixed stdio transport - **Tool Registration**: `src/tools/index.ts` - Centralized tool registration hub ### Service Layer The application follows a service-oriented architecture: - `NoaaService` - Handles all NOAA API interactions (data and metadata APIs) - `MoonPhaseService` - Calculates moon phases and lunar information - `SunService` - Calculates sun position, rise/set times using suncalc library - `NoaaParametersService` - Provides parameter definitions for NOAA API ### Tool Categories Tools are organized by functional area: - **Water Tools** (`water-tools.ts`) - Water levels, tide predictions, currents - **Station Tools** (`station-tools.ts`) - Station metadata and information - **Moon Tools** (`moon-tools.ts`) - Moon phase calculations - **Sun Tools** (`sun-tools.ts`) - Sun position and event calculations - **Parameter Tools** (`parameter-tools.ts`) - API parameter definitions ### Data Validation - Uses Zod schemas for parameter validation in `src/schemas/common.ts` - Common validation patterns for dates, stations, units, formats, etc. - Refined validation for date parameter combinations ### API Integration - **NOAA Data API**: `https://api.tidesandcurrents.noaa.gov/api/prod/datagetter` - **NOAA Metadata API**: `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi` - Uses axios for HTTP requests with proper error handling ### Key Dependencies - `fastmcp` - MCP server framework - `axios` - HTTP client for NOAA API calls - `suncalc` - Sun position and timing calculations - `zod` - Schema validation - TypeScript with ES modules and Node16 module resolution ## Important Implementation Notes ### MCP Server Configuration The server uses stdio transport exclusively - do not modify to use other transports as this is designed for MCP client integration. ### Error Handling NOAA API errors are caught and re-thrown with structured error messages including status codes and response data. ### Date Handling The codebase supports multiple date formats and has specific validation logic for date parameter combinations (date vs begin_date/end_date). ### Tool Organization Each tool category has its own registration function that accepts the server instance and relevant service(s). This modular approach makes it easy to add new tools or modify existing ones. ``` -------------------------------------------------------------------------------- /test-dpapi.js: -------------------------------------------------------------------------------- ```javascript ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile FROM node:lts-alpine WORKDIR /app # Install dependencies COPY package*.json ./ RUN npm install --ignore-scripts # Copy source code and build the project COPY . . RUN npm run build # Start the server in stdio mode CMD [ "npm", "start" ] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "esModuleInterop": true, "strict": true, "outDir": "./dist", "rootDir": "./src", "declaration": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.ts"] } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { FastMCP } from 'fastmcp'; import { createServer, startServer } from './server/config.js'; import { registerAllTools } from './tools/index.js'; // Create and configure the MCP server const server = createServer(); // Register all tools const services = registerAllTools(server); // Start the server startServer(server); ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # Minimal JSON Schema for configuration type: object properties: {} exampleConfig: {} commandFunction: # Simple command to start the MCP server on stdio without additional configuration |- () => ({ command: 'node', args: ['dist/index.js'] }) ``` -------------------------------------------------------------------------------- /src/types/moon.ts: -------------------------------------------------------------------------------- ```typescript /** * Moon phase names and their approximate ranges */ export enum MoonPhaseName { NEW_MOON = 'New Moon', WAXING_CRESCENT = 'Waxing Crescent', FIRST_QUARTER = 'First Quarter', WAXING_GIBBOUS = 'Waxing Gibbous', FULL_MOON = 'Full Moon', WANING_GIBBOUS = 'Waning Gibbous', LAST_QUARTER = 'Last Quarter', WANING_CRESCENT = 'Waning Crescent' } /** * Moon phase information */ export interface MoonPhaseInfo { date: string; phase: number; phaseName: MoonPhaseName; illumination: number; age: number; distance: number; diameter: number; isWaxing: boolean; } ``` -------------------------------------------------------------------------------- /src/interfaces/parameters.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Parameter type schema export const ParameterSchema = z.object({ parameter: z.string().optional().describe('Parameter type to get information about (time_zones, datums, units, tide_intervals, current_intervals, velocity_types, products, station_types, date_formats, output_formats). If not provided, returns information about all parameter types.') }); export type GetParametersParams = z.infer<typeof ParameterSchema>; // Interface for parameter info responses export interface ParameterInfo { id: string; description: string; } export interface DateFormatInfo { format: string; description: string; example: string; } ``` -------------------------------------------------------------------------------- /src/server/config.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; /** * Create and configure the FastMCP server */ export function createServer() { // Create the MCP server with fixed configuration const server = new FastMCP({ name: 'LocalTides', version: '1.0.0' }); return server; } /** * Start the FastMCP server with configurable transport */ export function startServer(server: FastMCP) { // Check command line arguments for transport type const args = process.argv.slice(2); const httpIndex = args.indexOf('--http'); const portIndex = args.indexOf('--port'); if (httpIndex !== -1) { // HTTP transport mode const port = portIndex !== -1 && args[portIndex + 1] ? parseInt(args[portIndex + 1]) : 3000; console.log(`Starting NOAA MCP server on HTTP port ${port}`); server.start({ transportType: 'httpStream', httpStream: { endpoint: '/sse', port: port } }); } else { // Default stdio transport console.log('Starting NOAA MCP server with stdio transport'); server.start({ transportType: 'stdio' }); } } ``` -------------------------------------------------------------------------------- /src/types/sun.ts: -------------------------------------------------------------------------------- ```typescript /** * Sun event types */ export enum SunEventType { SUNRISE = 'sunrise', SUNSET = 'sunset', DAWN = 'dawn', DUSK = 'dusk', SOLAR_NOON = 'solarNoon', NIGHT_START = 'night', NIGHT_END = 'nightEnd', GOLDEN_HOUR_START = 'goldenHourStart', GOLDEN_HOUR_END = 'goldenHourEnd', NAUTICAL_DAWN = 'nauticalDawn', NAUTICAL_DUSK = 'nauticalDusk', ASTRONOMICAL_DAWN = 'astronomicalDawn', ASTRONOMICAL_DUSK = 'astronomicalDusk' } /** * Sun times information */ export interface SunTimesInfo { date: string; sunrise: string | null; sunset: string | null; solarNoon: string | null; dawn: string | null; dusk: string | null; nightStart: string | null; nightEnd: string | null; goldenHourStart: string | null; goldenHourEnd: string | null; nauticalDawn: string | null; nauticalDusk: string | null; astronomicalDawn: string | null; astronomicalDusk: string | null; dayLength: number; // in minutes } /** * Sun position information */ export interface SunPositionInfo { date: string; time: string; azimuth: number; altitude: number; declination: number; rightAscension: number; } ``` -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; import { NoaaService } from '../services/noaa-service.js'; import { MoonPhaseService } from '../services/moon-phase-service.js'; import { SunService } from '../services/sun-service.js'; import { NoaaParametersService } from '../services/noaa-parameters-service.js'; import { DpapiService } from '../services/dpapi-service.js'; import { registerWaterTools } from './water-tools.js'; import { registerStationTools } from './station-tools.js'; import { registerMoonTools } from './moon-tools.js'; import { registerSunTools } from './sun-tools.js'; import { registerParameterTools } from './parameter-tools.js'; import { registerDerivedProductTools } from './derived-product-tools.js'; /** * Register all tools with the MCP server */ export function registerAllTools(server: FastMCP) { // Create service instances const noaaService = new NoaaService(); const moonPhaseService = new MoonPhaseService(); const sunService = new SunService(); const parametersService = new NoaaParametersService(); const dpapiService = new DpapiService(); // Register tools by category registerWaterTools(server, noaaService); registerStationTools(server, noaaService); registerMoonTools(server, moonPhaseService); registerSunTools(server, sunService); registerParameterTools(server, parametersService); registerDerivedProductTools(server, dpapiService); return { noaaService, moonPhaseService, sunService, parametersService, dpapiService }; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@ryancardin/noaa-tides-currents-mcp-server", "version": "1.0.0", "description": "MCP Server that interfaces with NOAA Tides and Currents API using FastMCP", "main": "dist/index.js", "bin": { "noaa-mcp": "dist/index.js" }, "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "start:http": "node dist/index.js --http --port 3000", "start:http:3001": "node dist/index.js --http --port 3001", "start:http:8080": "node dist/index.js --http --port 8080", "dev": "ts-node-esm src/index.ts", "dev:http": "ts-node-esm src/index.ts --http --port 3000", "test": "vitest run", "format": "prettier --write .", "inspector": "npx @modelcontextprotocol/inspector ./dist/index.js" }, "keywords": [ "noaa", "tides", "currents", "mcp", "fastmcp", "api" ], "author": "Ryan Cardin", "repository": { "type": "git", "url": "https://github.com/RyanCardin15/NOAA-Tides-And-Currents-MCP.git" }, "bugs": { "url": "https://github.com/RyanCardin15/NOAA-Tides-And-Currents-MCP/issues" }, "homepage": "https://github.com/RyanCardin15/NOAA-Tides-And-Currents-MCP#readme", "publishConfig": { "access": "public" }, "license": "MIT", "dependencies": { "axios": "^1.6.2", "fastmcp": "^1.16.3", "suncalc": "^1.9.0", "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.10.0", "prettier": "^3.0.3", "ts-node": "^10.9.1", "typescript": "^5.3.2", "vitest": "^3.2.4" } } ``` -------------------------------------------------------------------------------- /src/schemas/common.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Common parameter schemas export const StationSchema = z.string().min(1).describe('Station ID'); export const DateSchema = z.string().optional().describe('Date to retrieve data for ("today", "latest", "recent", or specific date)'); export const BeginDateSchema = z.string().optional().describe('Start date (YYYYMMDD or MM/DD/YYYY)'); export const EndDateSchema = z.string().optional().describe('End date (YYYYMMDD or MM/DD/YYYY)'); export const RangeSchema = z.number().optional().describe('Number of hours to retrieve data for'); export const DatumSchema = z.string().optional().describe('Datum to use (MLLW, MSL, etc.)'); export const UnitsSchema = z.enum(['english', 'metric']).optional().describe('Units to use ("english" or "metric")'); export const TimeZoneSchema = z.enum(['gmt', 'lst', 'lst_ldt']).optional().describe('Time zone (gmt, lst, lst_ldt)'); export const FormatSchema = z.enum(['json', 'xml', 'csv']).optional().describe('Output format (json, xml, csv)'); export const BinSchema = z.number().optional().describe('Bin number'); export const IntervalSchema = z.string().optional().describe('Interval (hilo, hl, h, or a number for minutes)'); // Schema refinement function for date parameters export const refineDateParams = (data: any) => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range); export const dateRefinementMessage = "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'"; ``` -------------------------------------------------------------------------------- /src/tools/station-tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { FastMCP } from 'fastmcp'; import { NoaaService } from '../services/noaa-service.js'; import { StationSchema, UnitsSchema, FormatSchema } from '../schemas/common.js'; /** * Register station-related tools with the MCP server */ export function registerStationTools(server: FastMCP, noaaService: NoaaService) { // Add stations tool server.addTool({ name: 'get_stations', description: 'Get list of stations', parameters: z.object({ type: z.string().optional().describe('Station type (waterlevels, currents, etc.)'), format: z.enum(['json', 'xml']).optional().describe('Output format (json, xml)'), units: UnitsSchema, }), execute: async (params) => { try { const result = await noaaService.getStations(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get stations: ${error.message}`); } throw new Error('Failed to get stations'); } } }); // Add station details tool server.addTool({ name: 'get_station_details', description: 'Get detailed information about a station', parameters: z.object({ station: StationSchema, format: z.enum(['json', 'xml']).optional().describe('Output format (json, xml)'), units: UnitsSchema, }), execute: async (params) => { try { const result = await noaaService.getStationDetails(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get station details: ${error.message}`); } throw new Error('Failed to get station details'); } } }); } ``` -------------------------------------------------------------------------------- /src/interfaces/moon.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { MoonPhaseName } from '../types/moon.js'; /** * 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)') }); export type MoonPhaseParams = z.infer<typeof MoonPhaseParamsSchema>; /** * 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)') }); export type MoonPhasesRangeParams = z.infer<typeof MoonPhasesRangeParamsSchema>; /** * 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)') }); export type NextMoonPhaseParams = z.infer<typeof NextMoonPhaseParamsSchema>; ``` -------------------------------------------------------------------------------- /src/tools/moon-tools.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; import { MoonPhaseService } from '../services/moon-phase-service.js'; import { MoonPhaseParamsSchema, MoonPhasesRangeParamsSchema, NextMoonPhaseParamsSchema } from '../interfaces/moon.js'; /** * Register moon-related tools with the MCP server */ export function registerMoonTools(server: FastMCP, moonPhaseService: MoonPhaseService) { // Add moon phase tool server.addTool({ name: 'get_moon_phase', description: 'Get moon phase information for a specific date', parameters: MoonPhaseParamsSchema, execute: async (params) => { try { const result = moonPhaseService.getMoonPhase(params); if (params.format === 'text') { return `Moon phase for ${result.date}: ${result.phaseName} (${(result.illumination * 100).toFixed(1)}% illuminated)`; } return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get moon phase: ${error.message}`); } throw new Error('Failed to get moon phase'); } } }); // Add moon phases range tool server.addTool({ name: 'get_moon_phases_range', description: 'Get moon phase information for a date range', parameters: MoonPhasesRangeParamsSchema, execute: async (params) => { try { const results = moonPhaseService.getMoonPhasesRange(params); if (params.format === 'text') { return results.map(result => `${result.date}: ${result.phaseName} (${(result.illumination * 100).toFixed(1)}% illuminated)` ).join('\n'); } return JSON.stringify(results); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get moon phases: ${error.message}`); } throw new Error('Failed to get moon phases'); } } }); // Add next moon phase tool server.addTool({ name: 'get_next_moon_phase', description: 'Get the next occurrence(s) of a specific moon phase', parameters: NextMoonPhaseParamsSchema, execute: async (params) => { try { const results = moonPhaseService.getNextMoonPhase(params); if (params.format === 'text') { return results.map(result => `Next ${result.phase}: ${result.date}` ).join('\n'); } return JSON.stringify(results); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get next moon phase: ${error.message}`); } throw new Error('Failed to get next moon phase'); } } }); } ``` -------------------------------------------------------------------------------- /src/interfaces/sun.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { SunEventType } from '../types/sun.js'; /** * 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.') }); export type SunTimesParams = z.infer<typeof SunTimesParamsSchema>; /** * 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.') }); export type SunTimesRangeParams = z.infer<typeof SunTimesRangeParamsSchema>; /** * 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)') }); export type SunPositionParams = z.infer<typeof SunPositionParamsSchema>; /** * 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.') }); export type NextSunEventParams = z.infer<typeof NextSunEventParamsSchema>; ``` -------------------------------------------------------------------------------- /src/tools/parameter-tools.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; import { ParameterSchema } from '../interfaces/parameters.js'; import { NoaaParametersService } from '../services/noaa-parameters-service.js'; /** * Register parameter tools with the MCP server */ export function registerParameterTools(server: FastMCP, parametersService: NoaaParametersService) { /** * Add parameter definitions tool */ server.addTool({ name: 'get_parameter_definitions', description: 'Get information about valid parameter values for NOAA API requests', parameters: ParameterSchema, execute: async (params) => { try { const { parameter } = params; // If no parameter specified, return all parameter types and their descriptions if (!parameter) { return JSON.stringify({ time_zones: parametersService.getTimeZones(), datums: parametersService.getDatums(), units: parametersService.getUnits(), tide_intervals: parametersService.getTidePredictionIntervals(), current_intervals: parametersService.getCurrentPredictionIntervals(), velocity_types: parametersService.getVelocityTypes(), products: parametersService.getMeteorologicalProducts(), station_types: parametersService.getStationTypes(), date_formats: parametersService.getDateFormats(), output_formats: parametersService.getOutputFormats() }); } // Return specific parameter information based on the parameter type let result; switch (parameter) { case 'time_zones': result = parametersService.getTimeZones(); break; case 'datums': result = parametersService.getDatums(); break; case 'units': result = parametersService.getUnits(); break; case 'tide_intervals': result = parametersService.getTidePredictionIntervals(); break; case 'current_intervals': result = parametersService.getCurrentPredictionIntervals(); break; case 'velocity_types': result = parametersService.getVelocityTypes(); break; case 'products': result = parametersService.getMeteorologicalProducts(); break; case 'station_types': result = parametersService.getStationTypes(); break; case 'date_formats': result = parametersService.getDateFormats(); break; case 'output_formats': result = parametersService.getOutputFormats(); break; default: throw new Error(`Unknown parameter type: ${parameter}`); } return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get parameter definitions: ${error.message}`); } throw new Error('Failed to get parameter definitions'); } } }); } ``` -------------------------------------------------------------------------------- /src/server/mcp-server.ts: -------------------------------------------------------------------------------- ```typescript import { NoaaService } from '../services/noaa-service.js'; import { ZodSchema } from 'zod'; import { GetWaterLevelsSchema, GetTidePredictionsSchema, GetCurrentsSchema, GetCurrentPredictionsSchema, GetMeteorologicalDataSchema, GetStationsSchema, GetStationDetailsSchema } from '../interfaces/noaa.js'; // MCP Tool type interface MCPTool<TParams = any, TResult = any> { name: string; description: string; inputSchema: ZodSchema<TParams>; handler: (params: TParams) => Promise<TResult>; } // Class for the MCP server export class McpServer { private tools: MCPTool[]; private noaaService: NoaaService; constructor(noaaService: NoaaService) { this.noaaService = noaaService; this.tools = this.initializeTools(); } // Initialize the tools private initializeTools(): MCPTool[] { // Water Levels tool const getWaterLevels: MCPTool = { name: "get_water_levels", description: "Get water level data for a station", inputSchema: GetWaterLevelsSchema, handler: async (params) => { return this.noaaService.getWaterLevels(params); } }; // Tide Predictions tool const getTidePredictions: MCPTool = { name: "get_tide_predictions", description: "Get tide prediction data", inputSchema: GetTidePredictionsSchema, handler: async (params) => { return this.noaaService.getTidePredictions(params); } }; // Currents tool const getCurrents: MCPTool = { name: "get_currents", description: "Get currents data for a station", inputSchema: GetCurrentsSchema, handler: async (params) => { return this.noaaService.getCurrents(params); } }; // Current Predictions tool const getCurrentPredictions: MCPTool = { name: "get_current_predictions", description: "Get current predictions", inputSchema: GetCurrentPredictionsSchema, handler: async (params) => { return this.noaaService.getCurrentPredictions(params); } }; // Meteorological Data tool const getMeteorologicalData: MCPTool = { name: "get_meteorological_data", description: "Get meteorological data", inputSchema: GetMeteorologicalDataSchema, handler: async (params) => { return this.noaaService.getMeteorologicalData(params); } }; // Stations tool const getStations: MCPTool = { name: "get_stations", description: "Get list of stations", inputSchema: GetStationsSchema, handler: async (params) => { return this.noaaService.getStations(params); } }; // Station Details tool const getStationDetails: MCPTool = { name: "get_station_details", description: "Get detailed information about a station", inputSchema: GetStationDetailsSchema, handler: async (params) => { return this.noaaService.getStationDetails(params); } }; return [ getWaterLevels, getTidePredictions, getCurrents, getCurrentPredictions, getMeteorologicalData, getStations, getStationDetails ]; } // Method to get all tools getTools(): { name: string, description: string }[] { return this.tools.map(tool => ({ name: tool.name, description: tool.description })); } // Method to handle tool execution async executeTool(toolName: string, params: any): Promise<any> { const tool = this.tools.find(t => t.name === toolName); if (!tool) { throw new Error(`Tool '${toolName}' not found`); } // Validate the parameters against the schema const validatedParams = tool.inputSchema.parse(params); // Execute the tool handler return tool.handler(validatedParams); } } ``` -------------------------------------------------------------------------------- /src/types/suncalc.d.ts: -------------------------------------------------------------------------------- ```typescript declare module 'suncalc' { export interface MoonIllumination { /** fraction of moon's visible disk that is illuminated */ fraction: number; /** moon phase (0.0-1.0) */ phase: number; /** midpoint angle in radians of the illuminated limb of the moon reckoned eastward from the north point of the disk */ angle: number; } export interface MoonPosition { /** moon azimuth in radians */ azimuth: number; /** moon altitude above the horizon in radians */ altitude: number; /** distance to moon in kilometers */ distance: number; /** parallactic angle of the moon in radians */ parallacticAngle: number; } export interface SunPosition { /** sun azimuth in radians (direction along the horizon, measured from south to west) */ azimuth: number; /** sun altitude above the horizon in radians */ altitude: number; } export interface SunTimes { /** sunrise (top edge of the sun appears on the horizon) */ sunrise: Date; /** sunrise ends (bottom edge of the sun touches the horizon) */ sunriseEnd: Date; /** morning golden hour (soft light, best time for photography) starts */ goldenHourEnd: Date; /** solar noon (sun is in the highest position) */ solarNoon: Date; /** evening golden hour starts */ goldenHour: Date; /** sunset starts (bottom edge of the sun touches the horizon) */ sunsetStart: Date; /** sunset (sun disappears below the horizon, evening civil twilight starts) */ sunset: Date; /** dusk (evening nautical twilight starts) */ dusk: Date; /** nautical dusk (evening astronomical twilight starts) */ nauticalDusk: Date; /** astronomical dusk (evening astronomical twilight starts) */ astronomicalDusk: Date; /** night starts (dark enough for astronomical observations) */ night: Date; /** nadir (darkest moment of the night, sun is in the lowest position) */ nadir: Date; /** night ends (morning astronomical twilight starts) */ nightEnd: Date; /** astronomical dawn (morning astronomical twilight starts) */ astronomicalDawn: Date; /** nautical dawn (morning nautical twilight starts) */ nauticalDawn: Date; /** dawn (morning nautical twilight ends, morning civil twilight starts) */ dawn: Date; } export interface MoonTimes { /** moonrise time as Date */ rise: Date | null; /** moonset time as Date */ set: Date | null; /** true if the moon never rises/sets and is always above the horizon during the day */ alwaysUp: boolean; /** true if the moon is always below the horizon */ alwaysDown: boolean; } /** * Calculates sun position for a given date and latitude/longitude */ export function getPosition(date: Date | number, lat: number, lng: number): SunPosition; /** * Calculates sun times for a given date, latitude/longitude, and, optionally, the observer height (in meters) relative to the horizon */ export function getTimes(date: Date | number, lat: number, lng: number, height?: number): SunTimes; /** * Returns an object with the following properties: * altitude: moon altitude above the horizon in radians * azimuth: moon azimuth in radians * distance: distance to moon in kilometers * parallacticAngle: parallactic angle of the moon in radians */ export function getMoonPosition(date: Date | number, lat: number, lng: number): MoonPosition; /** * Returns an object with the following properties: * fraction: illuminated fraction of the moon; varies from 0.0 (new moon) to 1.0 (full moon) * phase: moon phase; varies from 0.0 to 1.0, described below * angle: midpoint angle in radians of the illuminated limb of the moon reckoned eastward from the north point of the disk */ export function getMoonIllumination(date: Date | number): MoonIllumination; /** * Returns an object with the following properties: * rise: moonrise time as Date * set: moonset time as Date * alwaysUp: true if the moon never rises/sets and is always above the horizon during the day * alwaysDown: true if the moon is always below the horizon */ export function getMoonTimes(date: Date | number, lat: number, lng: number, inUTC?: boolean): MoonTimes; } ``` -------------------------------------------------------------------------------- /src/schemas/dpapi.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { StationSchema, UnitsSchema, FormatSchema } from './common.js'; // DPAPI-specific parameter schemas export const AffiliationSchema = z.enum(['Global', 'US']).optional().describe('Station affiliation (Global or US)'); export const ScenarioSchema = z.enum(['all', 'low', 'intermediate-low', 'intermediate', 'intermediate-high', 'high', 'extreme']).optional().describe('Sea level rise scenario'); export const SeasonSchema = z.enum(['DJF', 'MAM', 'JJA', 'SON']).optional().describe('Season months (DJF-Winter, MAM-Spring, JJA-Summer, SON-Fall)'); export const DpapiDatumSchema = z.enum(['STND', 'MLLW', 'MHHW', 'GT', 'MSL', 'MLW', 'MHW']).optional().describe('Datum reference for DPAPI'); export const ThresholdSchema = z.enum(['minor', 'moderate', 'major']).optional().describe('Flood threshold level'); export const DecadeSchema = z.string().optional().describe('Decade for projections (e.g., "2050")'); export const YearSchema = z.string().optional().describe('Year for analysis (YYYY format)'); export const YearRangeSchema = z.string().optional().describe('Year range (YYYY-YYYY format)'); // Sea Level Trends schema export const SeaLevelTrendsSchema = z.object({ station: StationSchema, affil: AffiliationSchema, format: FormatSchema }).describe('Get sea level trends for a station'); // Extreme Water Levels schema export const ExtremeWaterLevelsSchema = z.object({ station: StationSchema, units: UnitsSchema, format: FormatSchema }).describe('Get extreme water levels for a station'); // High Tide Flooding Daily schema export const HighTideFloodingDailySchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, begin_date: z.string().optional().describe('Start date (YYYYMMDD format)'), end_date: z.string().optional().describe('End date (YYYYMMDD format)'), year: YearSchema }).describe('Get high tide flooding daily count data'); // High Tide Flooding Monthly schema export const HighTideFloodingMonthlySchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, begin_date: z.string().optional().describe('Start date (YYYYMMDD format)'), end_date: z.string().optional().describe('End date (YYYYMMDD format)'), year: YearSchema }).describe('Get high tide flooding monthly count data'); // High Tide Flooding Seasonal schema export const HighTideFloodingSeasonalSchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, season_months: SeasonSchema, begin_date: z.string().optional().describe('Start date (YYYYMMDD format)'), end_date: z.string().optional().describe('End date (YYYYMMDD format)'), year: YearSchema }).describe('Get high tide flooding seasonal count data'); // High Tide Flooding Annual schema export const HighTideFloodingAnnualSchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, begin_date: z.string().optional().describe('Start date (YYYYMMDD format)'), end_date: z.string().optional().describe('End date (YYYYMMDD format)'), year_range: YearRangeSchema }).describe('Get high tide flooding annual count data'); // High Tide Flooding Projections schema export const HighTideFloodingProjectionsSchema = z.object({ station: StationSchema, format: FormatSchema, scenario: ScenarioSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, decade: DecadeSchema }).describe('Get high tide flooding decadal projections'); // High Tide Flooding Likelihoods schema export const HighTideFloodingLikelihoodsSchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, threshold: ThresholdSchema, date: z.string().optional().describe('Specific date (YYYYMMDD format)') }).describe('Get high tide flooding daily likelihoods'); // Top Ten Water Levels schema export const TopTenWaterLevelsSchema = z.object({ station: StationSchema, format: FormatSchema, datum: DpapiDatumSchema, units: UnitsSchema, analysis_type: z.enum(['highest', 'lowest']).optional().describe('Analysis type (highest or lowest)') }).describe('Get top ten water levels for a station'); ``` -------------------------------------------------------------------------------- /src/tools/sun-tools.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; import { SunService } from '../services/sun-service.js'; import { SunTimesParamsSchema, SunTimesRangeParamsSchema, SunPositionParamsSchema, NextSunEventParamsSchema } from '../interfaces/sun.js'; /** * Register sun-related tools with the MCP server */ export function registerSunTools(server: FastMCP, sunService: SunService) { // Add sun times tool server.addTool({ name: 'get_sun_times', description: 'Get sun rise/set and other sun event times for a specific date and location', parameters: SunTimesParamsSchema, execute: async (params) => { try { const result = sunService.getSunTimes(params); if (params.format === 'text') { let text = `Sun times for ${result.date} at latitude ${params.latitude}, longitude ${params.longitude}:\n`; text += `Sunrise: ${result.sunrise || 'N/A'}\n`; text += `Sunset: ${result.sunset || 'N/A'}\n`; text += `Day length: ${Math.floor(result.dayLength / 60)}h ${Math.round(result.dayLength % 60)}m\n`; text += `Solar noon: ${result.solarNoon || 'N/A'}\n`; text += `Dawn: ${result.dawn || 'N/A'}\n`; text += `Dusk: ${result.dusk || 'N/A'}\n`; return text; } return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get sun times: ${error.message}`); } throw new Error('Failed to get sun times'); } } }); // Add sun times range tool server.addTool({ name: 'get_sun_times_range', description: 'Get sun rise/set and other sun event times for a date range and location', parameters: SunTimesRangeParamsSchema, execute: async (params) => { try { const results = sunService.getSunTimesRange(params); if (params.format === 'text') { let text = `Sun times from ${params.start_date} to ${params.end_date} at latitude ${params.latitude}, longitude ${params.longitude}:\n\n`; results.forEach(result => { text += `Date: ${result.date}\n`; text += `Sunrise: ${result.sunrise || 'N/A'}\n`; text += `Sunset: ${result.sunset || 'N/A'}\n`; text += `Day length: ${Math.floor(result.dayLength / 60)}h ${Math.round(result.dayLength % 60)}m\n\n`; }); return text; } return JSON.stringify(results); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get sun times range: ${error.message}`); } throw new Error('Failed to get sun times range'); } } }); // Add sun position tool server.addTool({ name: 'get_sun_position', description: 'Get sun position information for a specific date, time, and location', parameters: SunPositionParamsSchema, execute: async (params) => { try { const result = sunService.getSunPosition(params); if (params.format === 'text') { let text = `Sun position for ${result.date} ${result.time} at latitude ${params.latitude}, longitude ${params.longitude}:\n`; text += `Azimuth: ${result.azimuth.toFixed(2)}°\n`; text += `Altitude: ${result.altitude.toFixed(2)}°\n`; text += `Declination: ${result.declination.toFixed(2)}°\n`; text += `Right Ascension: ${result.rightAscension.toFixed(2)}h\n`; return text; } return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get sun position: ${error.message}`); } throw new Error('Failed to get sun position'); } } }); // Add next sun event tool server.addTool({ name: 'get_next_sun_event', description: 'Get the next occurrence(s) of a specific sun event', parameters: NextSunEventParamsSchema, execute: async (params) => { try { const results = sunService.getNextSunEvent(params); if (params.format === 'text') { return results.map(result => `Next ${result.event}: ${result.date} at ${result.time}` ).join('\n'); } return JSON.stringify(results); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get next sun event: ${error.message}`); } throw new Error('Failed to get next sun event'); } } }); } ``` -------------------------------------------------------------------------------- /src/services/noaa-service.ts: -------------------------------------------------------------------------------- ```typescript import axios from 'axios'; // Base URLs for the different NOAA APIs const DATA_API_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter'; const METADATA_API_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi'; /** * Service for interacting with NOAA Tides and Currents APIs */ export class NoaaService { /** * Build parameters for the API request * @param params Parameters for the request * @returns URL-encoded parameters string */ private buildParams(params: Record<string, any>): string { // Remove undefined and null values const filteredParams = Object.entries(params) .filter(([_, value]) => value !== undefined && value !== null) .reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {} as Record<string, any>); // Convert to URL parameters return new URLSearchParams(filteredParams as Record<string, string>).toString(); } /** * Make a request to the Data API * @param params Parameters for the request * @returns Response data */ async fetchDataApi(params: Record<string, any>): Promise<any> { try { const queryParams = this.buildParams(params); const url = `${DATA_API_BASE_URL}?${queryParams}`; const response = await axios.get(url); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response) { throw new Error(`NOAA API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`); } throw error; } } /** * Make a request to the Metadata API * @param endpoint Endpoint path * @param params Parameters for the request * @returns Response data */ async fetchMetadataApi(endpoint: string, params: Record<string, any> = {}): Promise<any> { try { const queryParams = this.buildParams(params); const url = `${METADATA_API_BASE_URL}${endpoint}${queryParams ? '?' + queryParams : ''}`; const response = await axios.get(url); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response) { throw new Error(`NOAA API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`); } throw error; } } /** * Get water level data */ async getWaterLevels(params: Record<string, any>): Promise<any> { return this.fetchDataApi({ ...params, product: 'water_level' }); } /** * Get tide predictions */ async getTidePredictions(params: Record<string, any>): Promise<any> { return this.fetchDataApi({ ...params, product: 'predictions' }); } /** * Get currents data */ async getCurrents(params: Record<string, any>): Promise<any> { return this.fetchDataApi({ ...params, product: 'currents' }); } /** * Get current predictions */ async getCurrentPredictions(params: Record<string, any>): Promise<any> { return this.fetchDataApi({ ...params, product: 'currents_predictions' }); } /** * Get meteorological data (air_temperature, wind, etc.) */ async getMeteorologicalData(params: Record<string, any>): Promise<any> { const { product, ...rest } = params; return this.fetchDataApi({ ...rest, product }); } /** * Get list of stations */ async getStations(params: Record<string, any>): Promise<any> { const { type, name, lat_min, lat_max, lon_min, lon_max, state, limit, offset, sort_by, sort_order, ...rest } = params; const endpoint = '/stations.' + (rest.format || 'json'); // Build query parameters with all the filters const queryParams: Record<string, any> = { ...rest }; // Add filters only if they are defined if (type) queryParams.type = type; if (name) queryParams.name = name; if (lat_min !== undefined) queryParams.lat_min = lat_min; if (lat_max !== undefined) queryParams.lat_max = lat_max; if (lon_min !== undefined) queryParams.lon_min = lon_min; if (lon_max !== undefined) queryParams.lon_max = lon_max; if (state) queryParams.state = state; if (limit !== undefined) queryParams.limit = limit; if (offset !== undefined) queryParams.offset = offset; if (sort_by) queryParams.sort_by = sort_by; if (sort_order) queryParams.sort_order = sort_order; return this.fetchMetadataApi(endpoint, queryParams); } /** * Get station details */ async getStationDetails(params: Record<string, any>): Promise<any> { const { station, ...rest } = params; const endpoint = `/stations/${station}/details.` + (rest.format || 'json'); return this.fetchMetadataApi(endpoint, rest); } } ``` -------------------------------------------------------------------------------- /src/services/noaa-parameters-service.ts: -------------------------------------------------------------------------------- ```typescript import axios from 'axios'; /** * Provides information about valid NOAA Tides and Currents API parameters */ export class NoaaParametersService { /** * Get valid time zone values */ getTimeZones(): { id: string, description: string }[] { return [ { id: 'gmt', description: 'Greenwich Mean Time' }, { id: 'lst', description: 'Local Standard Time' }, { id: 'lst_ldt', description: 'Local Standard/Local Daylight Time' } ]; } /** * Get valid datum values */ getDatums(): { id: string, description: string }[] { return [ { id: 'MHHW', description: 'Mean Higher High Water' }, { id: 'MHW', description: 'Mean High Water' }, { id: 'MTL', description: 'Mean Tide Level' }, { id: 'MSL', description: 'Mean Sea Level' }, { id: 'MLW', description: 'Mean Low Water' }, { id: 'MLLW', description: 'Mean Lower Low Water' }, { id: 'NAVD', description: 'North American Vertical Datum' }, { id: 'STND', description: 'Station Datum' } ]; } /** * Get valid units */ getUnits(): { id: string, description: string }[] { return [ { id: 'english', description: 'English units (feet, mph, etc.)' }, { id: 'metric', description: 'Metric units (meters, kph, etc.)' } ]; } /** * Get valid intervals for tide predictions */ getTidePredictionIntervals(): { id: string, description: string }[] { return [ { id: 'hilo', description: 'High/low tide predictions only' }, { id: 'h', description: 'Hourly predictions' }, { id: '6', description: '6-minute predictions' }, { id: '30', description: '30-minute predictions' }, { id: '60', description: '60-minute predictions' } ]; } /** * Get valid intervals for current predictions */ getCurrentPredictionIntervals(): { id: string, description: string }[] { return [ { id: 'MAX_SLACK', description: 'Maximum flood/ebb and slack predictions only' }, { id: '6', description: '6-minute predictions' }, { id: '30', description: '30-minute predictions' }, { id: '60', description: '60-minute predictions' } ]; } /** * Get valid velocity types for current predictions */ getVelocityTypes(): { id: string, description: string }[] { return [ { id: 'default', description: 'Default velocity reporting (flood/ebb direction)' }, { id: 'speed_dir', description: 'Speed and direction format' } ]; } /** * Get valid meteorological products */ getMeteorologicalProducts(): { id: string, description: string }[] { return [ { id: 'air_temperature', description: 'Air temperature' }, { id: 'water_temperature', description: 'Water temperature' }, { id: 'wind', description: 'Wind speed and direction' }, { id: 'air_pressure', description: 'Barometric pressure' }, { id: 'air_gap', description: 'Air gap (distance between bridge and water surface)' }, { id: 'conductivity', description: 'Conductivity' }, { id: 'visibility', description: 'Visibility' }, { id: 'humidity', description: 'Relative humidity' }, { id: 'salinity', description: 'Salinity' }, { id: 'hourly_height', description: 'Verified hourly height water level' }, { id: 'high_low', description: 'Verified high/low water level' }, { id: 'daily_mean', description: 'Verified daily mean water level' }, { id: 'monthly_mean', description: 'Verified monthly mean water level' }, { id: 'one_minute_water_level', description: 'One-minute water level data' }, { id: 'datums', description: 'Datums' } ]; } /** * Get valid station types */ getStationTypes(): { id: string, description: string }[] { return [ { id: 'waterlevels', description: 'Water level stations' }, { id: 'currentpredictions', description: 'Current prediction stations' }, { id: 'currents', description: 'Current observation stations' }, { id: 'tidepredictions', description: 'Tide prediction stations' }, { id: 'weather', description: 'Weather stations' }, { id: 'ports', description: 'Physical Oceanographic Real-Time System (PORTS) stations' } ]; } /** * Get valid date formats */ getDateFormats(): { format: string, description: string, example: string }[] { return [ { format: 'YYYYMMDD', description: 'Year, month, day without separators', example: '20230401' }, { format: 'MM/DD/YYYY', description: 'Month/day/year with separators', example: '04/01/2023' }, { format: 'today', description: 'Current date', example: 'today' }, { format: 'latest', description: 'Latest available data', example: 'latest' }, { format: 'recent', description: 'Most recent data', example: 'recent' } ]; } /** * Get valid output formats */ getOutputFormats(): { id: string, description: string }[] { return [ { id: 'json', description: 'JSON format' }, { id: 'xml', description: 'XML format' }, { id: 'csv', description: 'CSV format (not available for all endpoints)' } ]; } } ``` -------------------------------------------------------------------------------- /src/tools/water-tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { FastMCP } from 'fastmcp'; import { NoaaService } from '../services/noaa-service.js'; import { StationSchema, DateSchema, BeginDateSchema, EndDateSchema, RangeSchema, DatumSchema, UnitsSchema, TimeZoneSchema, FormatSchema, BinSchema, IntervalSchema, refineDateParams, dateRefinementMessage } from '../schemas/common.js'; /** * Register water-related tools with the MCP server */ export function registerWaterTools(server: FastMCP, noaaService: NoaaService) { // Add water levels tool server.addTool({ name: 'get_water_levels', description: 'Get water level data for a station', parameters: z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, datum: DatumSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine(refineDateParams, { message: dateRefinementMessage }), execute: async (params) => { try { const result = await noaaService.getWaterLevels(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get water levels: ${error.message}`); } throw new Error('Failed to get water levels'); } } }); // Add tide predictions tool server.addTool({ name: 'get_tide_predictions', description: 'Get tide prediction data', parameters: z.object({ station: StationSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, date: DateSchema, range: RangeSchema, datum: DatumSchema, units: UnitsSchema, time_zone: TimeZoneSchema, interval: IntervalSchema, format: FormatSchema, }).refine(refineDateParams, { message: dateRefinementMessage }), execute: async (params) => { try { const result = await noaaService.getTidePredictions(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get tide predictions: ${error.message}`); } throw new Error('Failed to get tide predictions'); } } }); // Add currents tool server.addTool({ name: 'get_currents', description: 'Get currents data for a station', parameters: z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, bin: BinSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine(refineDateParams, { message: dateRefinementMessage }), execute: async (params) => { try { const result = await noaaService.getCurrents(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get currents: ${error.message}`); } throw new Error('Failed to get currents'); } } }); // Add current predictions tool server.addTool({ name: 'get_current_predictions', description: 'Get current predictions', parameters: z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, bin: BinSchema, interval: z.string().optional().describe('Interval (MAX_SLACK or a number for minutes)'), vel_type: z.enum(['speed_dir', 'default']).optional().describe('Velocity type (speed_dir or default)'), units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine(refineDateParams, { message: dateRefinementMessage }), execute: async (params) => { try { const result = await noaaService.getCurrentPredictions(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get current predictions: ${error.message}`); } throw new Error('Failed to get current predictions'); } } }); // Add meteorological data tool server.addTool({ name: 'get_meteorological_data', description: 'Get meteorological data', parameters: z.object({ station: StationSchema, product: z.string().min(1).describe('Product (air_temperature, wind, etc.)'), date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine(refineDateParams, { message: dateRefinementMessage }), execute: async (params) => { try { const result = await noaaService.getMeteorologicalData(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get meteorological data: ${error.message}`); } throw new Error('Failed to get meteorological data'); } } }); } ``` -------------------------------------------------------------------------------- /src/services/dpapi-service.ts: -------------------------------------------------------------------------------- ```typescript import axios from 'axios'; // Base URL for the NOAA Derived Product API const DPAPI_BASE_URL = 'https://api.tidesandcurrents.noaa.gov/dpapi/prod'; /** * Service for interacting with NOAA Derived Product API (DPAPI) */ export class DpapiService { /** * Build parameters for the API request * @param params Parameters for the request * @returns URL-encoded parameters string */ private buildParams(params: Record<string, any>): string { // Remove undefined and null values const filteredParams = Object.entries(params) .filter(([_, value]) => value !== undefined && value !== null) .reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {} as Record<string, any>); // Convert to URL parameters return new URLSearchParams(filteredParams as Record<string, string>).toString(); } /** * Make a request to the DPAPI * @param endpoint Endpoint path * @param params Parameters for the request * @returns Response data */ async fetchDpapi(endpoint: string, params: Record<string, any> = {}): Promise<any> { try { const queryParams = this.buildParams(params); const url = `${DPAPI_BASE_URL}${endpoint}${queryParams ? '?' + queryParams : ''}`; const response = await axios.get(url); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response) { throw new Error(`NOAA DPAPI Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`); } throw error; } } /** * Get sea level trends for a station * @param params Parameters including station ID and affiliation * @returns Sea level trend data */ async getSeaLevelTrends(params: Record<string, any>): Promise<any> { const { station, affil = 'Global', format = 'json', ...rest } = params; return this.fetchDpapi('/sltrends', { station, affil, format, ...rest }); } /** * Get extreme water levels for a station * @param params Parameters including station ID and units * @returns Extreme water level data */ async getExtremeWaterLevels(params: Record<string, any>): Promise<any> { const { station, units = 'english', format = 'json', ...rest } = params; return this.fetchDpapi('/ewl', { station, units, format, ...rest }); } /** * Get high tide flooding daily count data * @param params Parameters including station ID, date range, and thresholds * @returns Daily flood count data */ async getHighTideFloodingDaily(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/htf/daily', { station, format, ...rest }); } /** * Get high tide flooding monthly count data * @param params Parameters including station ID, date range, and thresholds * @returns Monthly flood count data */ async getHighTideFloodingMonthly(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/htf/monthly', { station, format, ...rest }); } /** * Get high tide flooding seasonal count data * @param params Parameters including station ID, seasons, and thresholds * @returns Seasonal flood count data */ async getHighTideFloodingSeasonal(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/htf/seasonal', { station, format, ...rest }); } /** * Get high tide flooding annual count data * @param params Parameters including station ID, year range, and thresholds * @returns Annual flood count data */ async getHighTideFloodingAnnual(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/htf/annual', { station, format, ...rest }); } /** * Get high tide flooding decadal projections * @param params Parameters including station ID, scenario, and decade * @returns Decadal projection data */ async getHighTideFloodingProjections(params: Record<string, any>): Promise<any> { const { station, scenario = 'all', format = 'json', ...rest } = params; return this.fetchDpapi('/htf/projections', { station, scenario, format, ...rest }); } /** * Get high tide flooding daily likelihoods * @param params Parameters including station ID and date * @returns Daily likelihood data */ async getHighTideFloodingLikelihoods(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/htf/likelihoods', { station, format, ...rest }); } /** * Get top ten water levels for a station * @param params Parameters including station ID and analysis type * @returns Top ten water level data */ async getTopTenWaterLevels(params: Record<string, any>): Promise<any> { const { station, format = 'json', ...rest } = params; return this.fetchDpapi('/topten', { station, format, ...rest }); } } ``` -------------------------------------------------------------------------------- /src/services/moon-phase-service.ts: -------------------------------------------------------------------------------- ```typescript import SunCalc from 'suncalc'; import { MoonPhaseParams, MoonPhasesRangeParams, NextMoonPhaseParams } from '../interfaces/moon.js'; import { MoonPhaseName, MoonPhaseInfo } from '../types/moon.js'; /** * 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: MoonPhaseParams): MoonPhaseInfo { 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: MoonPhasesRangeParams): MoonPhaseInfo[] { 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: MoonPhaseInfo[] = []; 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: NextMoonPhaseParams): { date: string, phase: string }[] { 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: Record<string, number> = { [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: { date: string, phase: string }[] = []; 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 moon phase name based on the phase value (0-1) * @param phase Phase value (0-1) * @returns Moon phase name */ private getMoonPhaseName(phase: number): MoonPhaseName { // Normalize phase to 0-1 range const normalizedPhase = phase < 0 ? phase + 1 : phase > 1 ? phase - 1 : phase; // Determine moon phase based on the value if (normalizedPhase < 0.0625 || normalizedPhase >= 0.9375) { return MoonPhaseName.NEW_MOON; } else if (normalizedPhase < 0.1875) { return MoonPhaseName.WAXING_CRESCENT; } else if (normalizedPhase < 0.3125) { return MoonPhaseName.FIRST_QUARTER; } else if (normalizedPhase < 0.4375) { return MoonPhaseName.WAXING_GIBBOUS; } else if (normalizedPhase < 0.5625) { return MoonPhaseName.FULL_MOON; } else if (normalizedPhase < 0.6875) { return MoonPhaseName.WANING_GIBBOUS; } else if (normalizedPhase < 0.8125) { return MoonPhaseName.LAST_QUARTER; } else { return MoonPhaseName.WANING_CRESCENT; } } } ``` -------------------------------------------------------------------------------- /src/interfaces/noaa.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Common parameter schemas const StationSchema = z.string().min(1).describe('Station ID'); const DateSchema = z.string().optional().describe('Date to retrieve data for ("today", "latest", "recent", or specific date)'); const BeginDateSchema = z.string().optional().describe('Start date (YYYYMMDD or MM/DD/YYYY)'); const EndDateSchema = z.string().optional().describe('End date (YYYYMMDD or MM/DD/YYYY)'); const RangeSchema = z.number().optional().describe('Number of hours to retrieve data for'); const DatumSchema = z.string().optional().describe('Datum to use (MLLW, MSL, etc.)'); const UnitsSchema = z.enum(['english', 'metric']).optional().describe('Units to use ("english" or "metric")'); const TimeZoneSchema = z.enum(['gmt', 'lst', 'lst_ldt']).optional().describe('Time zone (gmt, lst, lst_ldt)'); const FormatSchema = z.enum(['json', 'xml', 'csv']).optional().describe('Output format (json, xml, csv)'); const BinSchema = z.number().optional().describe('Bin number'); const IntervalSchema = z.string().optional().describe('Interval (hilo, hl, h, or a number for minutes)'); // Water Level Schema export const GetWaterLevelsSchema = z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, datum: DatumSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine( data => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range), { message: "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'" } ); // Tide Predictions Schema export const GetTidePredictionsSchema = z.object({ station: StationSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, date: DateSchema, range: RangeSchema, datum: DatumSchema, units: UnitsSchema, time_zone: TimeZoneSchema, interval: IntervalSchema, format: FormatSchema, }).refine( data => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range), { message: "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'" } ); // Currents Schema export const GetCurrentsSchema = z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, bin: BinSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine( data => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range), { message: "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'" } ); // Current Predictions Schema export const GetCurrentPredictionsSchema = z.object({ station: StationSchema, date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, bin: BinSchema, interval: z.string().optional().describe('Interval (MAX_SLACK or a number for minutes)'), vel_type: z.enum(['speed_dir', 'default']).optional().describe('Velocity type (speed_dir or default)'), units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine( data => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range), { message: "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'" } ); // Meteorological Data Schema export const GetMeteorologicalDataSchema = z.object({ station: StationSchema, product: z.string().min(1).describe('Product (air_temperature, wind, etc.)'), date: DateSchema, begin_date: BeginDateSchema, end_date: EndDateSchema, range: RangeSchema, units: UnitsSchema, time_zone: TimeZoneSchema, format: FormatSchema, }).refine( data => (data.date || (data.begin_date && data.end_date) || (data.begin_date && data.range) || (data.end_date && data.range) || data.range), { message: "You must provide either 'date', 'begin_date' and 'end_date', 'begin_date' and 'range', 'end_date' and 'range', or just 'range'" } ); // Station List Schema export const GetStationsSchema = z.object({ type: z.string().optional().describe('Station type (waterlevels, currents, etc.)'), units: UnitsSchema, format: z.enum(['json', 'xml']).optional().describe('Output format (json, xml)'), name: z.string().optional().describe('Filter stations by name (partial match)'), lat_min: z.number().optional().describe('Minimum latitude boundary'), lat_max: z.number().optional().describe('Maximum latitude boundary'), lon_min: z.number().optional().describe('Minimum longitude boundary'), lon_max: z.number().optional().describe('Maximum longitude boundary'), state: z.string().optional().describe('Filter stations by state code (e.g., CA, NY)'), limit: z.number().optional().describe('Maximum number of stations to return'), offset: z.number().optional().describe('Number of stations to skip for pagination'), sort_by: z.enum(['name', 'id', 'state']).optional().describe('Field to sort results by'), sort_order: z.enum(['asc', 'desc']).optional().describe('Sort order (ascending or descending)'), }); // Station Details Schema export const GetStationDetailsSchema = z.object({ station: StationSchema, units: UnitsSchema, format: z.enum(['json', 'xml']).optional().describe('Output format (json, xml)'), }); // Define exported types export type GetWaterLevelsParams = z.infer<typeof GetWaterLevelsSchema>; export type GetTidePredictionsParams = z.infer<typeof GetTidePredictionsSchema>; export type GetCurrentsParams = z.infer<typeof GetCurrentsSchema>; export type GetCurrentPredictionsParams = z.infer<typeof GetCurrentPredictionsSchema>; export type GetMeteorologicalDataParams = z.infer<typeof GetMeteorologicalDataSchema>; export type GetStationsParams = z.infer<typeof GetStationsSchema>; export type GetStationDetailsParams = z.infer<typeof GetStationDetailsSchema>; ``` -------------------------------------------------------------------------------- /src/tools/derived-product-tools.ts: -------------------------------------------------------------------------------- ```typescript import { FastMCP } from 'fastmcp'; import { DpapiService } from '../services/dpapi-service.js'; import { SeaLevelTrendsSchema, ExtremeWaterLevelsSchema, HighTideFloodingDailySchema, HighTideFloodingMonthlySchema, HighTideFloodingSeasonalSchema, HighTideFloodingAnnualSchema, HighTideFloodingProjectionsSchema, HighTideFloodingLikelihoodsSchema, TopTenWaterLevelsSchema } from '../schemas/dpapi.js'; /** * Register derived product tools with the MCP server */ export function registerDerivedProductTools(server: FastMCP, dpapiService: DpapiService) { // Sea Level Trends tool server.addTool({ name: 'get_sea_level_trends', description: 'Get sea level trends and error margins for a station', parameters: SeaLevelTrendsSchema, execute: async (params) => { try { const result = await dpapiService.getSeaLevelTrends(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get sea level trends: ${error.message}`); } throw new Error('Failed to get sea level trends'); } } }); // Extreme Water Levels tool server.addTool({ name: 'get_extreme_water_levels', description: 'Get extreme water levels and exceedance probabilities for a station', parameters: ExtremeWaterLevelsSchema, execute: async (params) => { try { const result = await dpapiService.getExtremeWaterLevels(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get extreme water levels: ${error.message}`); } throw new Error('Failed to get extreme water levels'); } } }); // High Tide Flooding Daily tool server.addTool({ name: 'get_high_tide_flooding_daily', description: 'Get high tide flooding daily count data for a station', parameters: HighTideFloodingDailySchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingDaily(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding daily data: ${error.message}`); } throw new Error('Failed to get high tide flooding daily data'); } } }); // High Tide Flooding Monthly tool server.addTool({ name: 'get_high_tide_flooding_monthly', description: 'Get high tide flooding monthly count data for a station', parameters: HighTideFloodingMonthlySchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingMonthly(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding monthly data: ${error.message}`); } throw new Error('Failed to get high tide flooding monthly data'); } } }); // High Tide Flooding Seasonal tool server.addTool({ name: 'get_high_tide_flooding_seasonal', description: 'Get high tide flooding seasonal count data for a station', parameters: HighTideFloodingSeasonalSchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingSeasonal(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding seasonal data: ${error.message}`); } throw new Error('Failed to get high tide flooding seasonal data'); } } }); // High Tide Flooding Annual tool server.addTool({ name: 'get_high_tide_flooding_annual', description: 'Get high tide flooding annual count data for a station', parameters: HighTideFloodingAnnualSchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingAnnual(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding annual data: ${error.message}`); } throw new Error('Failed to get high tide flooding annual data'); } } }); // High Tide Flooding Projections tool server.addTool({ name: 'get_high_tide_flooding_projections', description: 'Get high tide flooding decadal projections for sea level rise scenarios', parameters: HighTideFloodingProjectionsSchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingProjections(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding projections: ${error.message}`); } throw new Error('Failed to get high tide flooding projections'); } } }); // High Tide Flooding Likelihoods tool server.addTool({ name: 'get_high_tide_flooding_likelihoods', description: 'Get high tide flooding daily likelihoods for a station', parameters: HighTideFloodingLikelihoodsSchema, execute: async (params) => { try { const result = await dpapiService.getHighTideFloodingLikelihoods(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get high tide flooding likelihoods: ${error.message}`); } throw new Error('Failed to get high tide flooding likelihoods'); } } }); // Top Ten Water Levels tool server.addTool({ name: 'get_top_ten_water_levels', description: 'Get top ten highest or lowest water levels for a station', parameters: TopTenWaterLevelsSchema, execute: async (params) => { try { const result = await dpapiService.getTopTenWaterLevels(params); return JSON.stringify(result); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get top ten water levels: ${error.message}`); } throw new Error('Failed to get top ten water levels'); } } }); } ``` -------------------------------------------------------------------------------- /src/services/sun-service.ts: -------------------------------------------------------------------------------- ```typescript import SunCalc from 'suncalc'; import { SunTimesParams, SunTimesRangeParams, SunPositionParams, NextSunEventParams } from '../interfaces/sun.js'; import { SunTimesInfo, SunPositionInfo, SunEventType } from '../types/sun.js'; /** * 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: SunTimesParams): SunTimesInfo { 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: Date | null): string | null => { 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: SunTimesRangeParams): SunTimesInfo[] { 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: SunTimesInfo[] = []; 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: SunPositionParams): SunPositionInfo { const date = params.date ? new Date(params.date) : new Date(); const time = params.time; const { latitude, longitude } = params; // Set the time if provided if (time) { const [hours, minutes, seconds] = time.split(':').map(Number); if (!isNaN(hours) && !isNaN(minutes) && (!seconds || !isNaN(seconds))) { date.setHours(hours, minutes, seconds || 0, 0); } else { throw new Error('Invalid time format. Please use HH:MM:SS format.'); } } // Get sun position data const position = SunCalc.getPosition(date, latitude, longitude); // Calculate right ascension and declination (approximate values) // Note: These are approximate calculations and may not be precise const equatorialCoords = this.calculateEquatorialCoordinates(date, position.azimuth, position.altitude, latitude, longitude); return { date: date.toISOString().split('T')[0], time: date.toISOString().split('T')[1].split('.')[0], azimuth: position.azimuth * (180 / Math.PI), altitude: position.altitude * (180 / Math.PI), declination: equatorialCoords.declination, rightAscension: equatorialCoords.rightAscension }; } /** * 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: NextSunEventParams): { date: string, time: string, event: string }[] { const startDate = params.date ? new Date(params.date) : new Date(); const count = params.count !== undefined ? params.count : 1; const { latitude, longitude } = params; const timezone = params.timezone !== undefined ? params.timezone : 'UTC'; const results: { date: string, time: string, event: string }[] = []; let currentDate = new Date(startDate); // Find the next occurrences while (results.length < count) { const sunTimes = SunCalc.getTimes(currentDate, latitude, longitude); const eventTime = sunTimes[params.event as keyof typeof sunTimes]; if (eventTime && !isNaN(eventTime.getTime()) && eventTime > startDate) { let formattedTime: string; try { formattedTime = eventTime.toLocaleTimeString('en-US', { timeZone: timezone }); } catch (error) { // If timezone is invalid, fall back to ISO string console.warn(`Invalid timezone: ${timezone}. Using UTC.`); formattedTime = eventTime.toISOString().split('T')[1].split('.')[0]; } results.push({ date: eventTime.toISOString().split('T')[0], time: formattedTime, event: params.event as string }); // Move to next day to find the next occurrence currentDate.setDate(currentDate.getDate() + 1); } else { // Event not found for this day, try 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; } /** * Calculate approximate equatorial coordinates (right ascension and declination) * from horizontal coordinates (azimuth and altitude) * Note: This is a simplified calculation and may not be precise * @param date Date of observation * @param azimuth Azimuth in radians * @param altitude Altitude in radians * @param latitude Observer's latitude * @param longitude Observer's longitude * @returns Approximate equatorial coordinates */ private calculateEquatorialCoordinates(date: Date, azimuth: number, altitude: number, latitude: number, longitude: number): { rightAscension: number, declination: number } { // Convert degrees to radians const lat = latitude * (Math.PI / 180); // Calculate hour angle and declination const sinDec = Math.sin(altitude) * Math.sin(lat) + Math.cos(altitude) * Math.cos(lat) * Math.cos(azimuth); const declination = Math.asin(sinDec) * (180 / Math.PI); const cosH = (Math.sin(altitude) - Math.sin(lat) * sinDec) / (Math.cos(lat) * Math.cos(declination * (Math.PI / 180))); const hourAngle = Math.acos(Math.max(-1, Math.min(1, cosH))); // Adjust hour angle based on azimuth const adjustedHourAngle = (azimuth > 0 && azimuth < Math.PI) ? (2 * Math.PI - hourAngle) : hourAngle; // Calculate right ascension const localSiderealTime = this.calculateLocalSiderealTime(date, longitude); let rightAscension = (localSiderealTime - adjustedHourAngle) * (12 / Math.PI); // Normalize right ascension to 0-24 hours rightAscension = rightAscension % 24; if (rightAscension < 0) rightAscension += 24; return { rightAscension, declination }; } /** * Calculate approximate local sidereal time * @param date Date of observation * @param longitude Observer's longitude * @returns Local sidereal time in radians */ private calculateLocalSiderealTime(date: Date, longitude: number): number { // Calculate days since J2000.0 const jd = this.calculateJulianDay(date); const d = jd - 2451545.0; // Calculate Greenwich Mean Sidereal Time const gmst = (18.697374558 + 24.06570982441908 * d) % 24; // Convert longitude to hours and calculate local sidereal time const longitudeHours = longitude / 15; let lst = gmst + longitudeHours; // Normalize to 0-24 hours lst = lst % 24; if (lst < 0) lst += 24; // Convert to radians return lst * (Math.PI / 12); } /** * Calculate Julian day from date * @param date Date to convert * @returns Julian day */ private calculateJulianDay(date: Date): number { const y = date.getFullYear(); const m = date.getMonth() + 1; const d = date.getDate(); // Calculate Julian day const jd = 367 * y - Math.floor(7 * (y + Math.floor((m + 9) / 12)) / 4) - Math.floor(3 * (Math.floor((y + (m - 9) / 7) / 100) + 1) / 4) + Math.floor(275 * m / 9) + d + 1721028.5; // Add time of day const hours = date.getUTCHours(); const minutes = date.getUTCMinutes(); const seconds = date.getUTCSeconds(); const milliseconds = date.getUTCMilliseconds(); return jd + (hours + minutes / 60 + seconds / 3600 + milliseconds / 3600000) / 24; } } ```