Skip to main content
Glama
PoiSearchTool.ts9.65 kB
import { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; const PoiSearchInputSchema = z.object({ q: z .string() .max(256) .describe('Search query text. Limited to 256 characters.'), language: z .string() .optional() .describe( 'ISO language code for the response (e.g., "en", "es", "fr", "de", "ja")' ), limit: z .number() .min(1) .max(10) .optional() .default(10) .describe('Maximum number of results to return (1-10)'), proximity: z .union([ z.object({ longitude: z.number().min(-180).max(180), latitude: z.number().min(-90).max(90) }), z.string().transform((val) => { // Handle special case of 'ip' if (val === 'ip') { return 'ip' as const; } // Handle JSON-stringified object: "{\"longitude\": -82.458107, \"latitude\": 27.937259}" if (val.startsWith('{') && val.endsWith('}')) { try { const parsed = JSON.parse(val); if ( typeof parsed === 'object' && parsed !== null && typeof parsed.longitude === 'number' && typeof parsed.latitude === 'number' ) { return { longitude: parsed.longitude, latitude: parsed.latitude }; } } catch { // Fall back to other formats } } // Handle string that looks like an array: "[-82.451668, 27.942964]" if (val.startsWith('[') && val.endsWith(']')) { const coords = val .slice(1, -1) .split(',') .map((s) => Number(s.trim())); if (coords.length === 2 && !isNaN(coords[0]) && !isNaN(coords[1])) { return { longitude: coords[0], latitude: coords[1] }; } } // Handle comma-separated string: "-82.451668,27.942964" const parts = val.split(',').map((s) => Number(s.trim())); if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { return { longitude: parts[0], latitude: parts[1] }; } throw new Error( 'Invalid proximity format. Expected {longitude, latitude}, "longitude,latitude", or "ip"' ); }) ]) .optional() .describe( 'Location to bias results towards. Either coordinate object with longitude and latitude or "ip" for IP-based location. STRONGLY ENCOURAGED for relevant results.' ), bbox: z .object({ minLongitude: z.number().min(-180).max(180), minLatitude: z.number().min(-90).max(90), maxLongitude: z.number().min(-180).max(180), maxLatitude: z.number().min(-90).max(90) }) .optional() .describe('Bounding box to limit results within specified bounds'), country: z .array(z.string().length(2)) .optional() .describe('Array of ISO 3166 alpha 2 country codes to limit results'), types: z .array(z.string()) .optional() .describe( 'Array of feature types to filter results (e.g., ["poi", "address", "place"])' ), poi_category: z .array(z.string()) .optional() .describe( 'Array of POI categories to include (e.g., ["restaurant", "cafe"])' ), auto_complete: z .boolean() .optional() .describe('Enable partial and fuzzy matching'), eta_type: z .enum(['navigation']) .optional() .describe('Request estimated time of arrival (ETA) to results'), navigation_profile: z .enum(['driving', 'walking', 'cycling', 'driving-traffic']) .optional() .describe('Routing profile for ETA calculations'), origin: z .object({ longitude: z.number().min(-180).max(180), latitude: z.number().min(-90).max(90) }) .optional() .describe( 'Starting point for ETA calculations as coordinate object with longitude and latitude' ), format: z .enum(['json_string', 'formatted_text']) .optional() .default('formatted_text') .describe( 'Output format: "json_string" returns raw GeoJSON data as a JSON string that can be parsed; "formatted_text" returns human-readable text with place names, addresses, and coordinates. Both return as text content but json_string contains parseable JSON data while formatted_text is for display.' ) }); export class PoiSearchTool extends MapboxApiBasedTool< typeof PoiSearchInputSchema > { name = 'poi_search_tool'; description = "Find one specific place or brand location by its proper name or unique brand. Use only when the user's query includes a distinct title (e.g., \"The Met\", \"Starbucks Reserve Roastery\") or a brand they want all nearby branches of (e.g., \"Macy's stores near me\"). Do not use for generic place types such as 'museums', 'coffee shops', 'tacos', etc. Setting a proximity point is strongly encouraged for more relevant results. Always try to use a limit of at least 3 in case the user's intended result is not the first result. Supports both JSON and text output formats."; constructor() { super({ inputSchema: PoiSearchInputSchema }); } private formatGeoJsonToText(geoJsonResponse: any): string { if ( !geoJsonResponse || !geoJsonResponse.features || geoJsonResponse.features.length === 0 ) { return 'No results found.'; } const results = geoJsonResponse.features.map( (feature: any, index: number) => { const props = feature.properties || {}; const geom = feature.geometry || {}; let result = `${index + 1}. `; // POI name result += `${props.name}`; if (props.name_preferred) { result += ` (${props.name_preferred})`; } // Full address if (props.full_address) { result += `\n Address: ${props.full_address}`; } else if (props.place_formatted) { result += `\n Address: ${props.place_formatted}`; } // Geographic coordinates if (geom.coordinates && Array.isArray(geom.coordinates)) { const [lng, lat] = geom.coordinates; result += `\n Coordinates: ${lat}, ${lng}`; } // Feature type if (props.feature_type) { result += `\n Type: ${props.feature_type}`; } // Category information if (props.poi_category && Array.isArray(props.poi_category)) { result += `\n Category: ${props.poi_category.join(', ')}`; } else if (props.category) { result += `\n Category: ${props.category}`; } return result; } ); return results.join('\n\n'); } protected async execute( input: z.infer<typeof PoiSearchInputSchema>, accessToken: string ): Promise<{ type: 'text'; text: string }> { this.log( 'info', `PoiSearchTool: Starting search with input: ${JSON.stringify(input)}` ); const url = new URL( `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}search/searchbox/v1/forward` ); // Required parameters url.searchParams.append('q', input.q); url.searchParams.append('access_token', accessToken); // Optional parameters if (input.language) { url.searchParams.append('language', input.language); } if (input.limit !== undefined) { url.searchParams.append('limit', input.limit.toString()); } if (input.proximity) { if (input.proximity === 'ip') { url.searchParams.append('proximity', 'ip'); } else { const { longitude, latitude } = input.proximity; url.searchParams.append('proximity', `${longitude},${latitude}`); } } if (input.bbox) { const { minLongitude, minLatitude, maxLongitude, maxLatitude } = input.bbox; url.searchParams.append( 'bbox', `${minLongitude},${minLatitude},${maxLongitude},${maxLatitude}` ); } if (input.country && input.country.length > 0) { url.searchParams.append('country', input.country.join(',')); } if (input.types && input.types.length > 0) { url.searchParams.append('types', input.types.join(',')); } if (input.poi_category && input.poi_category.length > 0) { url.searchParams.append('poi_category', input.poi_category.join(',')); } if (input.auto_complete !== undefined) { url.searchParams.append('auto_complete', input.auto_complete.toString()); } if (input.eta_type) { url.searchParams.append('eta_type', input.eta_type); } if (input.navigation_profile) { url.searchParams.append('navigation_profile', input.navigation_profile); } if (input.origin) { const { longitude, latitude } = input.origin; url.searchParams.append('origin', `${longitude},${latitude}`); } this.log( 'info', `PoiSearchTool: Fetching from URL: ${url.toString().replace(accessToken, '[REDACTED]')}` ); const response = await fetch(url.toString()); if (!response.ok) { const errorBody = await response.text(); this.log( 'error', `PoiSearchTool: API Error - Status: ${response.status}, Body: ${errorBody}` ); throw new Error( `Failed to search: ${response.status} ${response.statusText}` ); } const data = await response.json(); this.log( 'info', `PoiSearchTool: Successfully completed search, found ${(data as any).features?.length || 0} results` ); if (input.format === 'json_string') { return { type: 'text', text: JSON.stringify(data, null, 2) }; } else { return { type: 'text', text: this.formatGeoJsonToText(data) }; } } }

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/Waldzell-Agentics/mcp-server'

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