Skip to main content
Glama
numa08

NOAA Space Weather MCP Server

by numa08
client.ts8.76 kB
import { CacheManager, DATA_TTL, DEFAULT_CACHE_CONFIG } from '../cache/manager'; import { type ApiResponse, type EndpointKey, type KpIndex, NOAA_ENDPOINTS, type QueryOptions, type SolarWind, type XrayFlux, } from './types'; /** * NOAA Space Weather API Client * Handles data fetching, caching, and querying */ export class NoaaClient { private cache: CacheManager; private userAgent: string; constructor(cacheManager?: CacheManager) { this.cache = cacheManager ?? new CacheManager(DEFAULT_CACHE_CONFIG); this.userAgent = 'NOAA-Space-Weather-MCP/0.1.0 (Amateur Radio Propagation Tool)'; } /** * Fetch data from NOAA API with caching */ async fetchEndpoint<T>( endpoint: EndpointKey, options?: { forceRefresh?: boolean; ttl?: number }, ): Promise<ApiResponse<T>> { const url = NOAA_ENDPOINTS[endpoint]; const cacheKey = `noaa_${endpoint}`; // Check cache first if (!options?.forceRefresh) { const cached = this.cache.get<T>(cacheKey); if (cached) { return { success: true, data: cached.data, cachedAt: cached.fetchedAt, source: 'cache', }; } } try { const response = await fetch(url, { headers: { 'User-Agent': this.userAgent, Accept: 'application/json', }, }); if (!response.ok) { // Try to use stale cache as fallback const stale = this.cache.getStale<T>(cacheKey); if (stale) { return { success: true, data: stale.data, cachedAt: stale.fetchedAt, source: 'cache', error: `API returned ${response.status}, using stale cache`, }; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = (await response.json()) as T; // Determine TTL based on endpoint type const ttl = options?.ttl ?? this.getTtlForEndpoint(endpoint); this.cache.set(cacheKey, data, ttl); return { success: true, data, cachedAt: Date.now(), source: 'fetch', }; } catch (error) { // Try stale cache as last resort const stale = this.cache.getStale<T>(cacheKey); if (stale) { return { success: true, data: stale.data, cachedAt: stale.fetchedAt, source: 'cache', error: `Fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}, using stale cache`, }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error', source: 'fetch', }; } } private getTtlForEndpoint(endpoint: EndpointKey): number { switch (endpoint) { case 'XRAY_FLUX': case 'PROTON_FLUX': return DATA_TTL.XRAY_FLUX; case 'KP_INDEX': return DATA_TTL.KP_INDEX; case 'SOLAR_WIND_REALTIME': case 'SOLAR_WIND_MAG': return DATA_TTL.SOLAR_WIND; case 'FORECAST_27DAY': case 'PREDICTED_SFI': return DATA_TTL.FORECAST; case 'SUNSPOT_NUMBER': return DATA_TTL.HISTORICAL; case 'ALERTS': return DATA_TTL.ALERTS; default: return DATA_TTL.KP_INDEX; } } /** * Query data with filters - reduces context size for AI */ queryData<T extends { time_tag: string }>(data: T[], options: QueryOptions): T[] { let result = [...data]; // Time range filter if (options.startTime || options.endTime) { result = result.filter((item) => { const timeTag = item.time_tag; if (!timeTag) return true; const itemTime = new Date(timeTag).getTime(); const start = options.startTime ? new Date(options.startTime).getTime() : 0; const end = options.endTime ? new Date(options.endTime).getTime() : Infinity; return itemTime >= start && itemTime <= end; }); } // Custom filter if (options.filter) { const filter = options.filter; result = result.filter((item) => { const record = item as unknown as Record<string, unknown>; for (const [key, value] of Object.entries(filter)) { if (record[key] !== value) return false; } return true; }); } // Sort if (options.sortBy) { const sortKey = options.sortBy; const order = options.sortOrder === 'asc' ? 1 : -1; result.sort((a, b) => { const aRecord = a as unknown as Record<string, unknown>; const bRecord = b as unknown as Record<string, unknown>; const aVal = aRecord[sortKey]; const bVal = bRecord[sortKey]; if (aVal === bVal) return 0; if (aVal === null || aVal === undefined) return 1; if (bVal === null || bVal === undefined) return -1; return aVal < bVal ? -order : order; }); } // Limit if (options.limit && options.limit > 0) { result = result.slice(0, options.limit); } return result; } /** * Get X-ray flux data from GOES satellite */ async getXrayFlux(options?: QueryOptions): Promise<ApiResponse<XrayFlux[]>> { const response = await this.fetchEndpoint<XrayFlux[]>('XRAY_FLUX'); if (!response.success || !response.data) return response; if (options) { response.data = this.queryData(response.data, options); } return response; } /** * Get Kp index data */ async getKpIndex(options?: QueryOptions): Promise<ApiResponse<KpIndex[]>> { const response = await this.fetchEndpoint<Array<string[]>>('KP_INDEX'); if (!response.success || !response.data) { return { ...response, data: undefined } as ApiResponse<KpIndex[]>; } // NOAA returns Kp data as array of arrays, convert to objects const [_header, ...rows] = response.data; const kpData: KpIndex[] = rows.map((row) => ({ time_tag: row[0], kp: parseFloat(row[1]), a_running: parseFloat(row[2]), station_count: Number.parseInt(row[3], 10), })); const result: ApiResponse<KpIndex[]> = { ...response, data: options ? this.queryData(kpData, options) : kpData, }; return result; } /** * Get solar wind data */ async getSolarWind(options?: QueryOptions): Promise<ApiResponse<SolarWind[]>> { const [plasma, mag] = await Promise.all([ this.fetchEndpoint<Array<string[]>>('SOLAR_WIND_REALTIME'), this.fetchEndpoint<Array<string[]>>('SOLAR_WIND_MAG'), ]); if (!plasma.success || !plasma.data || !mag.success || !mag.data) { return { success: false, error: 'Failed to fetch solar wind data', source: 'fetch', }; } // Merge plasma and magnetic field data by time const [, ...plasmaRows] = plasma.data; const [, ...magRows] = mag.data; const magMap = new Map(magRows.map((row) => [row[0], row])); const solarWindData: SolarWind[] = plasmaRows .map((row) => { const magRow = magMap.get(row[0]); return { time_tag: row[0], density: parseFloat(row[1]) || 0, speed: parseFloat(row[2]) || 0, temperature: parseFloat(row[3]) || 0, bx: magRow ? parseFloat(magRow[1]) || 0 : 0, by: magRow ? parseFloat(magRow[2]) || 0 : 0, bz: magRow ? parseFloat(magRow[3]) || 0 : 0, bt: magRow ? parseFloat(magRow[6]) || 0 : 0, }; }) .filter((item) => !Number.isNaN(item.speed)); return { success: true, data: options ? this.queryData(solarWindData, options) : solarWindData, source: plasma.source, cachedAt: plasma.cachedAt, }; } /** * Get current space weather summary */ async getSummary(): Promise< ApiResponse<{ latestKp: KpIndex | null; latestXray: XrayFlux | null; latestSolarWind: SolarWind | null; fetchedAt: number; }> > { const [kp, xray, solarWind] = await Promise.all([ this.getKpIndex({ limit: 1, sortBy: 'time_tag', sortOrder: 'desc' }), this.getXrayFlux({ limit: 1, sortBy: 'time_tag', sortOrder: 'desc' }), this.getSolarWind({ limit: 1, sortBy: 'time_tag', sortOrder: 'desc' }), ]); return { success: true, data: { latestKp: kp.data?.[0] ?? null, latestXray: xray.data?.[0] ?? null, latestSolarWind: solarWind.data?.[0] ?? null, fetchedAt: Date.now(), }, source: 'fetch', }; } /** * Get cache statistics */ getCacheStats(): { size: number; maxEntries: number; ttlSeconds: number } { return this.cache.getStats(); } /** * Clear cache */ clearCache(): void { this.cache.invalidateAll(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/numa08/noaa-space-weather-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server