Skip to main content
Glama
CupOfOwls

Kroger MCP Server

search_locations

Find Kroger store locations near a specific zip code with customizable search radius and result limits.

Instructions

    Search for Kroger store locations near a zip code.
    
    Args:
        zip_code: Zip code to search near (uses environment default if not provided)
        radius_in_miles: Search radius in miles (1-100)
        limit: Number of results to return (1-200)
        chain: Filter by specific chain name
    
    Returns:
        Dictionary containing location search results
    

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
zip_codeNo
radius_in_milesNoSearch radius in miles (1-100)
limitNoNumber of results to return (1-200)
chainNo

Implementation Reference

  • The main handler function for the 'search_locations' MCP tool. Searches Kroger store locations near a given zip code using the Kroger API client, formats the results into a structured dictionary, and handles errors.
    @mcp.tool()
    async def search_locations(
        zip_code: Optional[str] = None,
        radius_in_miles: int = Field(default=10, ge=1, le=100, description="Search radius in miles (1-100)"),
        limit: int = Field(default=10, ge=1, le=200, description="Number of results to return (1-200)"),
        chain: Optional[str] = None,
        ctx: Context = None
    ) -> Dict[str, Any]:
        """
        Search for Kroger store locations near a zip code.
        
        Args:
            zip_code: Zip code to search near (uses environment default if not provided)
            radius_in_miles: Search radius in miles (1-100)
            limit: Number of results to return (1-200)
            chain: Filter by specific chain name
        
        Returns:
            Dictionary containing location search results
        """
        if ctx:
            await ctx.info(f"Searching for Kroger locations near {zip_code or 'default zip code'}")
        
        if not zip_code:
            # If Claude isn't provided with a zip code, he will sometimes generate one.
            # Which is why get_user_zip_code() is provided as a tool, to ensure Claude uses the
            # default zip code provided in the env.
            zip_code = get_default_zip_code()
        
        client = get_client_credentials_client()
        
        try:
            locations = client.location.search_locations(
                zip_code=zip_code,
                radius_in_miles=radius_in_miles,
                limit=limit,
                chain=chain
            )
            
            if not locations or "data" not in locations or not locations["data"]:
                return {
                    "success": False,
                    "message": f"No locations found near zip code {zip_code}",
                    "data": []
                }
            
            # Format location data for easier consumption
            formatted_locations = []
            for loc in locations["data"]:
                address = loc.get("address", {})
                formatted_loc = {
                    "location_id": loc.get("locationId"),
                    "name": loc.get("name"),
                    "chain": loc.get("chain"),
                    "phone": loc.get("phone"),
                    "address": {
                        "street": address.get("addressLine1", ""),
                        "city": address.get("city", ""),
                        "state": address.get("state", ""),
                        "zip_code": address.get("zipCode", "")
                    },
                    "full_address": f"{address.get('addressLine1', '')}, {address.get('city', '')}, {address.get('state', '')} {address.get('zipCode', '')}",
                    "coordinates": loc.get("geolocation", {}),
                    "departments": [dept.get("name") for dept in loc.get("departments", [])],
                    "department_count": len(loc.get("departments", []))
                }
                
                # Add hours info if available
                if "hours" in loc and "monday" in loc["hours"]:
                    monday = loc["hours"]["monday"]
                    if monday.get("open24", False):
                        formatted_loc["hours_monday"] = "Open 24 hours"
                    elif "open" in monday and "close" in monday:
                        formatted_loc["hours_monday"] = f"{monday['open']} - {monday['close']}"
                    else:
                        formatted_loc["hours_monday"] = "Hours not available"
                
                formatted_locations.append(formatted_loc)
            
            if ctx:
                await ctx.info(f"Found {len(formatted_locations)} locations")
            
            return {
                "success": True,
                "search_params": {
                    "zip_code": zip_code,
                    "radius_miles": radius_in_miles,
                    "limit": limit,
                    "chain": chain
                },
                "count": len(formatted_locations),
                "data": formatted_locations
            }
            
        except Exception as e:
            if ctx:
                await ctx.error(f"Error searching locations: {str(e)}")
            return {
                "success": False,
                "error": str(e),
                "data": []
            }
  • Registers all location tools, including 'search_locations', by calling the register_tools function from the location_tools module.
    location_tools.register_tools(mcp)
  • Input schema for the search_locations tool using Pydantic Field for validation and descriptions of parameters: zip_code, radius_in_miles, limit, chain.
        zip_code: Optional[str] = None,
        radius_in_miles: int = Field(default=10, ge=1, le=100, description="Search radius in miles (1-100)"),
        limit: int = Field(default=10, ge=1, le=200, description="Number of results to return (1-200)"),
        chain: Optional[str] = None,
        ctx: Context = None
    ) -> Dict[str, Any]:
  • The register_tools function in location_tools.py where the search_locations tool is defined and registered via @mcp.tool() decorator.
    def register_tools(mcp):
        """Register location-related tools with the FastMCP server"""
        
        @mcp.tool()
        async def search_locations(
            zip_code: Optional[str] = None,
            radius_in_miles: int = Field(default=10, ge=1, le=100, description="Search radius in miles (1-100)"),
            limit: int = Field(default=10, ge=1, le=200, description="Number of results to return (1-200)"),
            chain: Optional[str] = None,
            ctx: Context = None
        ) -> Dict[str, Any]:
            """
            Search for Kroger store locations near a zip code.
            
            Args:
                zip_code: Zip code to search near (uses environment default if not provided)
                radius_in_miles: Search radius in miles (1-100)
                limit: Number of results to return (1-200)
                chain: Filter by specific chain name
            
            Returns:
                Dictionary containing location search results
            """
            if ctx:
                await ctx.info(f"Searching for Kroger locations near {zip_code or 'default zip code'}")
            
            if not zip_code:
                # If Claude isn't provided with a zip code, he will sometimes generate one.
                # Which is why get_user_zip_code() is provided as a tool, to ensure Claude uses the
                # default zip code provided in the env.
                zip_code = get_default_zip_code()
            
            client = get_client_credentials_client()
            
            try:
                locations = client.location.search_locations(
                    zip_code=zip_code,
                    radius_in_miles=radius_in_miles,
                    limit=limit,
                    chain=chain
                )
                
                if not locations or "data" not in locations or not locations["data"]:
                    return {
                        "success": False,
                        "message": f"No locations found near zip code {zip_code}",
                        "data": []
                    }
                
                # Format location data for easier consumption
                formatted_locations = []
                for loc in locations["data"]:
                    address = loc.get("address", {})
                    formatted_loc = {
                        "location_id": loc.get("locationId"),
                        "name": loc.get("name"),
                        "chain": loc.get("chain"),
                        "phone": loc.get("phone"),
                        "address": {
                            "street": address.get("addressLine1", ""),
                            "city": address.get("city", ""),
                            "state": address.get("state", ""),
                            "zip_code": address.get("zipCode", "")
                        },
                        "full_address": f"{address.get('addressLine1', '')}, {address.get('city', '')}, {address.get('state', '')} {address.get('zipCode', '')}",
                        "coordinates": loc.get("geolocation", {}),
                        "departments": [dept.get("name") for dept in loc.get("departments", [])],
                        "department_count": len(loc.get("departments", []))
                    }
                    
                    # Add hours info if available
                    if "hours" in loc and "monday" in loc["hours"]:
                        monday = loc["hours"]["monday"]
                        if monday.get("open24", False):
                            formatted_loc["hours_monday"] = "Open 24 hours"
                        elif "open" in monday and "close" in monday:
                            formatted_loc["hours_monday"] = f"{monday['open']} - {monday['close']}"
                        else:
                            formatted_loc["hours_monday"] = "Hours not available"
                    
                    formatted_locations.append(formatted_loc)
                
                if ctx:
                    await ctx.info(f"Found {len(formatted_locations)} locations")
                
                return {
                    "success": True,
                    "search_params": {
                        "zip_code": zip_code,
                        "radius_miles": radius_in_miles,
                        "limit": limit,
                        "chain": chain
                    },
                    "count": len(formatted_locations),
                    "data": formatted_locations
                }
                
            except Exception as e:
                if ctx:
                    await ctx.error(f"Error searching locations: {str(e)}")
                return {
                    "success": False,
                    "error": str(e),
                    "data": []
                }
    
        @mcp.tool()
        async def get_user_zip_code() -> Dict[str, Any]:
            """
            Returns user zip code, it is anticipated that by exposing this to the LLM it will choose to use it
            rather than generating a zip code based on system data.
    
            Args: 
                N/A
            Returns:
                Dictionary containing user Zip Code
            """
            zip_code = get_default_zip_code()
            # And thats literally it!
            user_zip_dict = {"user_zip_code": zip_code}
            return user_zip_dict
    
    
        @mcp.tool()
        async def get_location_details(
            location_id: str,
            ctx: Context = None
        ) -> Dict[str, Any]:
            """
            Get detailed information about a specific Kroger store location.
            
            Args:
                location_id: The unique identifier for the store location
            
            Returns:
                Dictionary containing detailed location information
            """
            if ctx:
                await ctx.info(f"Getting details for location {location_id}")
            
            client = get_client_credentials_client()
            
            try:
                location_details = client.location.get_location(location_id)
                
                if not location_details or "data" not in location_details:
                    return {
                        "success": False,
                        "message": f"Location {location_id} not found"
                    }
                
                loc = location_details["data"]
                
                # Format department information
                departments = []
                for dept in loc.get("departments", []):
                    dept_info = {
                        "department_id": dept.get("departmentId"),
                        "name": dept.get("name"),
                        "phone": dept.get("phone")
                    }
                    
                    # Add department hours
                    if "hours" in dept and "monday" in dept["hours"]:
                        monday = dept["hours"]["monday"]
                        if monday.get("open24", False):
                            dept_info["hours_monday"] = "Open 24 hours"
                        elif "open" in monday and "close" in monday:
                            dept_info["hours_monday"] = f"{monday['open']} - {monday['close']}"
                    
                    departments.append(dept_info)
                
                # Format the response
                address = loc.get("address", {})
                result = {
                    "success": True,
                    "location_id": loc.get("locationId"),
                    "name": loc.get("name"),
                    "chain": loc.get("chain"),
                    "phone": loc.get("phone"),
                    "address": {
                        "street": address.get("addressLine1", ""),
                        "street2": address.get("addressLine2", ""),
                        "city": address.get("city", ""),
                        "state": address.get("state", ""),
                        "zip_code": address.get("zipCode", "")
                    },
                    "coordinates": loc.get("geolocation", {}),
                    "departments": departments,
                    "department_count": len(departments)
                }
                
                return result
                
            except Exception as e:
                if ctx:
                    await ctx.error(f"Error getting location details: {str(e)}")
                return {
                    "success": False,
                    "error": str(e)
                }
    
        @mcp.tool()
        async def set_preferred_location(
            location_id: str,
            ctx: Context = None
        ) -> Dict[str, Any]:
            """
            Set a preferred store location for future operations.
            
            Args:
                location_id: The unique identifier for the store location
            
            Returns:
                Dictionary confirming the preferred location has been set
            """
            if ctx:
                await ctx.info(f"Setting preferred location to {location_id}")
            
            # Verify the location exists
            client = get_client_credentials_client()
            
            try:
                exists = client.location.location_exists(location_id)
                if not exists:
                    return {
                        "success": False,
                        "error": f"Location {location_id} does not exist"
                    }
                
                # Get location details for confirmation
                location_details = client.location.get_location(location_id)
                loc_data = location_details.get("data", {})
                
                set_preferred_location_id(location_id)
                
                if ctx:
                    await ctx.info(f"Preferred location set to {loc_data.get('name', location_id)}")
                
                return {
                    "success": True,
                    "preferred_location_id": location_id,
                    "location_name": loc_data.get("name"),
                    "message": f"Preferred location set to {loc_data.get('name', location_id)}"
                }
                
            except Exception as e:
                if ctx:
                    await ctx.error(f"Error setting preferred location: {str(e)}")
                return {
                    "success": False,
                    "error": str(e)
                }
    
        @mcp.tool()
        async def get_preferred_location(ctx: Context = None) -> Dict[str, Any]:
            """
            Get the currently set preferred store location.
            
            Returns:
                Dictionary containing the preferred location information
            """
            preferred_location_id = get_preferred_location_id()
            
            if not preferred_location_id:
                return {
                    "success": False,
                    "message": "No preferred location set. Use set_preferred_location to set one."
                }
            
            if ctx:
                await ctx.info(f"Getting preferred location details for {preferred_location_id}")
            
            # Get location details
            client = get_client_credentials_client()
            
            try:
                location_details = client.location.get_location(preferred_location_id)
                loc_data = location_details.get("data", {})
                
                return {
                    "success": True,
                    "preferred_location_id": preferred_location_id,
                    "location_details": {
                        "name": loc_data.get("name"),
                        "chain": loc_data.get("chain"),
                        "phone": loc_data.get("phone"),
                        "address": loc_data.get("address", {})
                    }
                }
                
            except Exception as e:
                if ctx:
                    await ctx.error(f"Error getting preferred location details: {str(e)}")
                return {
                    "success": False,
                    "error": str(e),
                    "preferred_location_id": preferred_location_id
                }
    
        @mcp.tool()
        async def check_location_exists(
            location_id: str,
            ctx: Context = None
        ) -> Dict[str, Any]:
            """
            Check if a location exists in the Kroger system.
            
            Args:
                location_id: The unique identifier for the store location
            
            Returns:
                Dictionary indicating whether the location exists
            """
            if ctx:
                await ctx.info(f"Checking if location {location_id} exists")
            
            client = get_client_credentials_client()
            
            try:
                exists = client.location.location_exists(location_id)
                
                return {
                    "success": True,
                    "location_id": location_id,
                    "exists": exists,
                    "message": f"Location {location_id} {'exists' if exists else 'does not exist'}"
                }
                
            except Exception as e:
                if ctx:
                    await ctx.error(f"Error checking location existence: {str(e)}")
                return {
                    "success": False,
                    "error": str(e)
                }
  • Uses the shared helper get_client_credentials_client() to obtain the Kroger API client for public location search.
    client = get_client_credentials_client()
    
    try:
        locations = client.location.search_locations(
Behavior2/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions the tool searches for locations but doesn't describe the search behavior (e.g., how results are sorted, whether it's paginated, error conditions, or rate limits). It states returns a 'Dictionary containing location search results' but gives no details on structure or content, leaving significant gaps in understanding the tool's operation.

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 purpose statement followed by Args and Returns sections. It's appropriately sized with no wasted sentences, though the formatting uses extra whitespace. Every sentence earns its place by adding value, making it easy to scan and understand.

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

Completeness3/5

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

Given no annotations and no output schema, the description is moderately complete but has gaps. It covers the purpose and parameters well, but lacks details on behavioral traits (e.g., sorting, pagination, errors) and the structure of the return dictionary. For a search tool with 4 parameters, it provides a baseline but could be more comprehensive about the search behavior and results.

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

Parameters4/5

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

Schema description coverage is 50% (only radius_in_miles and limit have descriptions in the schema). The description compensates by providing clear semantics for all four parameters: zip_code (search near, with default), radius_in_miles (search radius with range), limit (results to return with range), and chain (filter by name). It adds meaningful context beyond the schema, especially for zip_code and chain which lack schema descriptions.

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

Purpose5/5

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

The description clearly states the specific action ('Search for Kroger store locations') and resource ('near a zip code'), distinguishing it from sibling tools like 'get_location_details' or 'check_location_exists' which focus on individual locations rather than searching. It's precise about what the tool does without being tautological.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention sibling tools like 'check_location_exists' or 'get_location_details', nor does it specify prerequisites (e.g., authentication status) or contextual constraints. Usage is implied but not explicitly stated.

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

Install Server

Other Tools

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/CupOfOwls/kroger-mcp'

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