Skip to main content
Glama
jagan-shanmugam

OpenStreetMap MCP Server

find_nearby_places

Search for points of interest and amenities near a geographic location to support location-based recommendations and proximity analysis.

Instructions

Discover points of interest and amenities near a specific location.

This tool performs a comprehensive search around a geographic point to identify nearby establishments, amenities, and points of interest. Results are organized by category and subcategory, making it easy to find specific types of places. Essential for location-based recommendations, neighborhood analysis, and proximity-based decision making.

Args: latitude: Center point latitude (decimal degrees) longitude: Center point longitude (decimal degrees) radius: Search radius in meters (defaults to 1000m/1km) categories: List of OSM categories to search for (e.g., ["amenity", "shop", "tourism"]). If omitted, searches common categories. limit: Maximum number of total results to return

Returns: Structured dictionary containing: - Original query parameters - Total count of places found - Results grouped by category and subcategory - Each place includes name, coordinates, and associated tags

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
latitudeYes
longitudeYes
radiusNo
categoriesNo
limitNo

Implementation Reference

  • The primary handler function for the 'find_nearby_places' tool. It orchestrates the search for nearby points of interest using the OSMClient, groups results by category/subcategory, and formats the response.
    @mcp.tool()
    async def find_nearby_places(
        latitude: float,
        longitude: float,
        ctx: Context,
        radius: float = 1000,  # meters
        categories: List[str] = None,
        limit: int = 20
    ) -> Dict[str, Any]:
        """
        Discover points of interest and amenities near a specific location.
        
        This tool performs a comprehensive search around a geographic point to identify
        nearby establishments, amenities, and points of interest. Results are organized by
        category and subcategory, making it easy to find specific types of places. Essential
        for location-based recommendations, neighborhood analysis, and proximity-based decision making.
        
        Args:
            latitude: Center point latitude (decimal degrees)
            longitude: Center point longitude (decimal degrees)
            radius: Search radius in meters (defaults to 1000m/1km)
            categories: List of OSM categories to search for (e.g., ["amenity", "shop", "tourism"]).
                       If omitted, searches common categories.
            limit: Maximum number of total results to return
            
        Returns:
            Structured dictionary containing:
            - Original query parameters
            - Total count of places found
            - Results grouped by category and subcategory
            - Each place includes name, coordinates, and associated tags
        """
        osm_client = ctx.request_context.lifespan_context.osm_client
        
        # Set default categories if not provided
        if not categories:
            categories = ["amenity", "shop", "tourism", "leisure"]
        
        ctx.info(f"Searching for places within {radius}m of ({latitude}, {longitude})")
        places = await osm_client.get_nearby_pois(latitude, longitude, radius, categories)
        
        # Group results by category
        results_by_category = {}
        
        for place in places[:limit]:
            tags = place.get("tags", {})
            
            # Find the matching category
            for category in categories:
                if category in tags:
                    subcategory = tags[category]
                    if category not in results_by_category:
                        results_by_category[category] = {}
                    
                    if subcategory not in results_by_category[category]:
                        results_by_category[category][subcategory] = []
                    
                    # Add place to appropriate category and subcategory
                    place_info = {
                        "id": place.get("id"),
                        "name": tags.get("name", "Unnamed"),
                        "latitude": place.get("lat"),
                        "longitude": place.get("lon"),
                        "tags": tags
                    }
                    
                    results_by_category[category][subcategory].append(place_info)
        
        # Calculate total count
        total_count = sum(
            len(places)
            for category_data in results_by_category.values()
            for places in category_data.values()
        )
        
        return {
            "query": {
                "latitude": latitude,
                "longitude": longitude,
                "radius": radius
            },
            "categories": results_by_category,
            "total_count": total_count
        }
  • Supporting utility method in the OSMClient class that queries the Overpass API to retrieve raw points of interest data near the given coordinates, used by the find_nearby_places handler.
    async def get_nearby_pois(self, 
                             lat: float, 
                             lon: float, 
                             radius: float = 1000,
                             categories: List[str] = None) -> List[Dict]:
        """Get points of interest near a location"""
        if not self.session:
            raise RuntimeError("OSM client not connected")
        
        # Convert radius to bounding box (approximate)
        # 1 degree latitude ~= 111km
        # 1 degree longitude ~= 111km * cos(latitude)
        lat_delta = radius / 111000
        lon_delta = radius / (111000 * math.cos(math.radians(lat)))
        
        bbox = (
            lon - lon_delta,
            lat - lat_delta,
            lon + lon_delta,
            lat + lat_delta
        )
        
        # Build Overpass query
        overpass_url = "https://overpass-api.de/api/interpreter"
        
        # Default to common POI types if none specified
        if not categories:
            categories = ["amenity", "shop", "tourism", "leisure"]
        
        # Build tag filters
        tag_filters = []
        for category in categories:
            tag_filters.append(f'node["{category}"]({{bbox}});')
        
        query = f"""
        [out:json];
        (
            {" ".join(tag_filters)}
        );
        out body;
        """
        
        query = query.replace("{bbox}", f"{bbox[1]},{bbox[0]},{bbox[3]},{bbox[2]}")
        
        async with self.session.post(overpass_url, data={"data": query}) as response:
            if response.status == 200:
                data = await response.json()
                return data.get("elements", [])
            else:
                raise Exception(f"Failed to get nearby POIs: {response.status}")

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observed

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must disclose behavior itself. It explains the search scope, result grouping, and return structure. However, it does not explicitly state that it is read-only, how errors or empty results are handled, or any limitations, leaving some behavioral gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a clear opening sentence, a brief use-case statement, and detailed Args/Returns sections. Some redundancy with the schema exists, but the added defaults and examples justify the length.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the absence of an output schema and annotations, the description appropriately provides a detailed return structure and parameter semantics. It lacks details on sorting order and error handling, but overall it gives a complete picture for a search tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has no parameter descriptions, but the Args section compensates fully. It explains latitude/longitude as center point, radius with default and unit, categories with examples and default behavior, and limit as maximum total results. This goes well beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: discovering points of interest and amenities via a comprehensive search around a geographic point. It distinguishes itself from siblings by highlighting category grouping and broad coverage, though it does not explicitly name alternative tools for specific category searches.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear use cases: location-based recommendations, neighborhood analysis, and proximity-based decision making. It implies this is the general-purpose search while siblings may be more specific, but it does not explicitly say when not to use it or name alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.