Skip to main content
Glama
CategorySearchTool.ts16.4 kB
import { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; // Full list of supported categories const SUPPORTED_CATEGORIES = [ 'services', 'shopping', 'food_and_drink', 'food', 'health_services', 'office', 'restaurant', 'education', 'transportation', 'grocery', 'apartment_or_condo', 'place_of_worship', 'outdoors', 'school', 'lodging', 'financial_services', 'clothing_store', 'supermarket', 'wholesale_store', 'beauty_store', 'auto_repair', 'cafe', 'salon', 'bus_stop', 'government', 'real_estate_agent', 'tourist_attraction', 'park', 'medical_practice', 'temple', 'sports', 'doctors_office', 'hotel', 'fast_food', 'bank', 'hairdresser', 'nightlife', 'entertainment', 'pharmacy', 'bar', 'church', 'consulting', 'farm', 'coffee', 'medical_clinic', 'lawyer', 'coffee_shop', 'bakery', 'repair_shop', 'fitness_center', 'dentist', 'factory', 'shipping_store', 'photographer', 'electronics_shop', 'home', 'furniture_store', 'shopping_mall', 'atm', 'car_dealership', 'historic_site', 'travel_agency', 'hospital', 'insurance_broker', 'clothing', 'post_office', 'advertising_agency', 'alternative_healthcare', 'it', 'gas_station', 'hospital_unit', 'hardware_store', 'monument', 'phone_store', 'mosque', 'parking_lot', 'commercial', 'jewelry_store', 'river', 'elementary_school', 'lake', 'convenience_store', 'physiotherapist', 'dessert_shop', 'psychotherapist', 'playground', 'gift_shop', 'tailor', 'nongovernmental_organization', 'bed_and_breakfast', 'laundry', 'warehouse', 'market', 'shoe_store', 'car_wash', 'pet_store', 'kindergarten', 'cemetery', 'sports_club', 'field', 'butcher_shop', 'landscaping', 'care_services', 'florist', 'spa', 'garden', 'psychological_services', 'chiropractor', 'photo_store', 'charging_station', 'car_rental', 'college', 'womens_clothing_store', 'public_transportation_station', 'internet_cafe', 'design_studio', 'mountain', 'social_club', 'copyshop', 'paper_goods_store', 'ice_cream', 'liquor_store', 'art', 'pizza_restaurant', 'event_planner', 'medical_supply_store', 'employment_agency', 'museum', 'community_center', 'book_store', 'massage_shop', 'childcare', 'nail_salon', 'teahouse', 'tobacco_shop', 'taxi', 'tax_advisor', 'veterinarian', 'mexican_restaurant', 'event_space', 'university', 'studio', 'assisted_living_facility', 'medical_laboratory', 'equipment_rental', 'hostel', 'forest', 'indian_restaurant', 'driving_school', 'asian_restaurant', 'high_school', 'optician', 'deli', 'sports_shop', 'bicycle_shop', 'library', 'arts_and_craft_store', 'island', 'department_store', 'boutique', 'dance_studio', 'food_court', 'diner_restaurant', 'storage', 'art_gallery', 'fashion_accessory_shop', 'health_food_store', 'counselling', 'bridge', 'tutor', 'tattoo_parlour', 'dry_cleaners', 'motorcycle_dealer', 'nightclub', 'fire_station', 'police_station', 'yoga_studio', 'indonesian_restaurant', 'fabric_store', 'chinese_restaurant', 'video_game_store', 'news_kiosk', 'music_school', 'funeral_home', 'theme_park', 'breakfast_restaurant', 'seafood_restaurant', 'toy_store', 'buddhist_temple', 'brunch_restaurant', 'japanese_restaurant', 'soccer_field', 'notary', 'juice_bar', 'noodle_restaurant', 'fishing_store', 'language_school', 'government_offices', 'baby_goods_shop', 'theatre', 'university_building', 'furniture_maker', 'bus_station', 'burger_restaurant', 'townhall', 'swimming_pool', 'winery', 'dormitory', 'recording_studio', 'conference_center', 'charity', 'campground', 'taco_shop', 'pub', 'snack_bar', 'nature_reserve', 'italian_restaurant', 'music_shop', 'public_artwork', 'outdoor_sculpture', 'laboratory', 'antique_shop', 'cinema', 'music_venue', 'barbeque_restaurant', 'garden_store', 'sandwich_shop', 'american_restaurant', 'stadium', 'tourist_information', 'waste_transfer_station', 'recycling_center', 'tours', 'canal', 'tennis_courts', 'lighting_store', 'pawnshop', 'rehabilitation_center', 'motel', 'railway_station', 'psychic', 'mattress_store', 'thrift_shop', 'vape_shop', 'golf_course', 'rest_area', 'radio_studio', 'locksmith', 'thai_restaurant', 'watch_store', 'labor_union', 'vacation_rental', 'bridal_shop', 'boat_rental', 'arts_center', 'video_store', 'brewery', 'sushi_restaurant', 'recreation_center', 'basketball_court', 'cobbler', 'party_store', 'kitchen_store', 'shoe_repair', 'bubble_tea', 'airport', 'leather_goods', 'martial_arts_studio', 'coworking_space', 'home_repair', 'outlet_store', 'sewing_shop', 'courthouse', 'cannabis_dispensary', 'beach', 'fair_grounds', 'pilates_studio', 'stable', 'concert_hall', 'currency_exchange', 'trade_school', 'hobby_shop', 'gymnastics', 'herbalist', 'discount_store', 'donut_shop', 'casino', 'carpet_store', 'frame_store', 'tanning_salon', 'karaoke_bar', 'viewpoint', 'camera_shop', 'gun_store', 'sports_center', 'marina', 'buffet_restaurant', 'animal_shelter', 'turkish_restaurant', 'wine_bar', 'tapas_restaurant', 'pier', 'spanish_restaurant', 'television_studio', 'korean_restaurant', 'billiards', 'vietnamese_restaurant', 'baseball_field', 'french_restaurant', 'waste_disposal', 'cocktail_bar', 'picnic_shelter', 'dog_park', 'photo_lab', 'steakhouse', 'skatepark', 'bookmaker', 'resort', 'fish_and_chips_restaurant', 'greek_restaurant', 'cheese_shop', 'fireworks_store', 'military_office', 'sauna', 'sports_bar', 'vineyard', 'beer_bar', 'hookah_lounge', 'mediterranean_restaurant', 'african_restaurant', 'gastropub', 'community_college', 'middle_eastern_restaurant', 'salad_bar', 'prison', 'zoo', 'racetrack', 'dam', 'climbing', 'climbing_gym', 'dialysis_center', 'coffee_roaster', 'bowling_alley', 'boxing_gym', 'emergency_room', 'hunting_store', 'synagogue', 'waterfall', 'luggage_store', 'filipino_restaurant', 'bagel_shop', 'fishing', 'cricket_club', 'light_rail_station', 'aquarium', 'hot_dog_stand', 'german_restaurant', 'motorsports_store', 'carribean_restaurant', 'ice_rink', 'ski_area', 'frozen_yogurt_shop', 'military_base', 'summer_camp', 'fishmonger', 'distillery', 'embassy', 'exhibit', 'treecare', 'biergarten', 'ramen_restaurant', 'fountain', 'surfboard_store', 'check_cashing', 'boat_or_ferry', 'hunting_area', 'disc_golf_course', 'miniature_golf', 'gay_bar', 'optometrist', 'water_park', 'observatory', 'cable_car', 'cave', 'plaza', 'lounge', 'korean_barbeque_restaurant', 'hawaiian_restaurant', 'town', 'portuguese_restaurant', 'latin_american_restaurant', 'duty_free_shop', 'brazilian_restaurant', 'skydiving_drop_zone', 'english_restaurant', 'cuban_restaurant', 'train', 'persian_restaurant', 'rugby_stadium', 'gluten_free_restaurant', 'creole_restaurant', 'trailhead', 'airport_terminal', 'carpet_cleaner', 'tiki_bar', 'street', 'intersection', 'windmill', 'information_technology_company', 'bike_rental', 'food_truck', 'veterans_service', 'wings_joint', 'arcade', 'outdoors_store', 'irish_pub', 'surf_spot', 'peruvian_restaurant', 'track', 'chocolate_shop', 'soccer_stadium', 'cruise', 'mountain_hut', 'theme_park_attraction', 'service_area', 'ski_shop', 'lighthouse', 'airport_gate', 'baseball_stadium', 'basketball_stadium', 'football_stadium', 'go_kart_racing', 'tech_startup', 'dive_bar', 'states_and_municipalities', 'hockey_stadium', 'laser_tag', 'political_party_office', 'driving_range', 'stripclub', 'well', 'beach_bar', 'tunnel', 'waffle_shop', 'ski_trail', 'tennis_stadium', 'boat_launch', 'meeting_room', 'rafting_spot', 'scuba_diving_shop', 'speakeasy', 'university_book_store', 'university_laboratory', 'zoo_exhibit', 'chairlift', 'corporate_amenity', 'indoor_cycling', 'racecourse', 'tree', 'turkish_coffeehouse', 'baggage_claim', 'champagne_bar', 'planetarium', 'sake_bar', 'village', 'airport_ticket_counter', 'beer_festival', 'bus_line', 'city', 'country', 'county', 'graffiti', 'hotel_bar', 'lgbtq_organization', 'moving_target', 'neighbourhood', 'pop_up_shop', 'railway_platform', 'road', 'state', 'variety_store', 'whiskey_bar' ].join(', '); const CategorySearchInputSchema = z.object({ category: z .string() .describe( `The canonical category ID to search for. Supported categories include: ${SUPPORTED_CATEGORIES}` ), language: z .string() .optional() .describe( 'ISO language code for the response (e.g., "en", "es", "fr", "de", "ja")' ), limit: z .number() .min(1) .max(25) .optional() .default(10) .describe('Maximum number of results to return (1-25)'), 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' ), 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'), poi_category_exclusions: z .array(z.string()) .optional() .describe('Array of POI categories to exclude from results'), 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 CategorySearchTool extends MapboxApiBasedTool< typeof CategorySearchInputSchema > { name = 'category_search_tool'; description = "Return all places that match a category (industry, amenity, or NAICS‑style code). Use when the user asks for a type of place, plural or generic terms like 'museums', 'coffee shops', 'electric‑vehicle chargers', or when the query includes is‑a phrases such as 'any', 'all', 'nearby'. Do not use when a unique name or brand is provided. Supports both JSON and text output formats."; constructor() { super({ inputSchema: CategorySearchInputSchema }); } 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 CategorySearchInputSchema>, accessToken: string ): Promise<{ type: 'text'; text: string }> { // Build URL with required parameters const url = new URL( `${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}search/searchbox/v1/category/${encodeURIComponent(input.category)}` ); // Add access token url.searchParams.append('access_token', accessToken); // Add 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.poi_category_exclusions && input.poi_category_exclusions.length > 0 ) { url.searchParams.append( 'poi_category_exclusions', input.poi_category_exclusions.join(',') ); } // Make the request const response = await fetch(url.toString()); if (!response.ok) { throw new Error( `Failed to search category: ${response.status} ${response.statusText}` ); } const data = await response.json(); 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