Skip to main content
Glama
alexcz-a11y

Tessie MCP Server

by alexcz-a11y
geocoding.ts4.53 kB
/** * Simple reverse geocoding utility using OpenStreetMap Nominatim API * No API key required, respects rate limits */ export class GeocodingService { private static readonly NOMINATIM_BASE_URL = 'https://nominatim.openstreetmap.org/reverse'; private static readonly USER_AGENT = 'TessieMCP/1.1.1'; private static readonly RATE_LIMIT_MS = 1000; // 1 request per second private static lastRequestTime = 0; private static cache = new Map<string, string>(); /** * Convert latitude/longitude to human-readable address */ static async reverseGeocode(latitude: number, longitude: number): Promise<string> { if (!latitude || !longitude || isNaN(latitude) || isNaN(longitude)) { return 'Location unavailable'; } // Check cache first const cacheKey = `${latitude.toFixed(4)},${longitude.toFixed(4)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } try { // Respect rate limiting await this.enforceRateLimit(); const url = new URL(this.NOMINATIM_BASE_URL); url.searchParams.set('lat', latitude.toString()); url.searchParams.set('lon', longitude.toString()); url.searchParams.set('format', 'json'); url.searchParams.set('addressdetails', '1'); url.searchParams.set('zoom', '18'); // High detail level const response = await fetch(url.toString(), { headers: { 'User-Agent': this.USER_AGENT, 'Accept': 'application/json' }, signal: AbortSignal.timeout(5000) // 5 second timeout }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); let address = this.formatAddress(data); // Cache the result this.cache.set(cacheKey, address); // Limit cache size to prevent memory leaks if (this.cache.size > 100) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } return address; } catch (error) { console.warn(`Geocoding failed for ${latitude}, ${longitude}:`, error); return `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`; } } /** * Format the Nominatim response into a readable address */ private static formatAddress(data: any): string { if (!data || !data.address) { return data?.display_name?.split(',').slice(0, 3).join(', ') || 'Unknown location'; } const addr = data.address; const parts: string[] = []; // Build address from specific to general if (addr.house_number && addr.road) { parts.push(`${addr.house_number} ${addr.road}`); } else if (addr.road) { parts.push(addr.road); } else if (addr.amenity) { parts.push(addr.amenity); } if (addr.city || addr.town || addr.village) { parts.push(addr.city || addr.town || addr.village); } else if (addr.county) { parts.push(addr.county); } if (addr.state || addr.province) { parts.push(addr.state || addr.province); } if (addr.country_code) { parts.push(addr.country_code.toUpperCase()); } // If we couldn't build a good address, use display_name if (parts.length === 0) { return data.display_name?.split(',').slice(0, 4).join(', ') || 'Unknown location'; } return parts.join(', '); } /** * Enforce rate limiting to be respectful to the Nominatim service */ private static async enforceRateLimit(): Promise<void> { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.RATE_LIMIT_MS) { const waitTime = this.RATE_LIMIT_MS - timeSinceLastRequest; await new Promise(resolve => setTimeout(resolve, waitTime)); } this.lastRequestTime = Date.now(); } /** * Get approximate distance between two lat/lng points in miles */ static getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 3959; // Earth's radius in miles const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private static toRadians(degrees: number): number { return degrees * (Math.PI / 180); } }

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/alexcz-a11y/tessie-mcp-fix'

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