"""
Shared utilities and client management for Kroger MCP server
"""
import os
import json
from typing import Optional, Dict, Any
from dotenv import load_dotenv
from kroger_api.kroger_api import KrogerAPI
from kroger_api.utils.env import load_and_validate_env, get_zip_code
from kroger_api.token_storage import load_token
# Load environment variables
load_dotenv()
# Global state for clients and preferred location
_authenticated_client: Optional[KrogerAPI] = None
_client_credentials_client: Optional[KrogerAPI] = None
# JSON files for configuration storage
PREFERENCES_FILE = "kroger_preferences.json"
def get_client_credentials_client() -> KrogerAPI:
"""Get or create a client credentials authenticated client for public data"""
global _client_credentials_client
if _client_credentials_client is not None and _client_credentials_client.test_current_token():
return _client_credentials_client
_client_credentials_client = None
try:
load_and_validate_env(["KROGER_CLIENT_ID", "KROGER_CLIENT_SECRET"])
_client_credentials_client = KrogerAPI()
# Try to load existing token first
token_file = ".kroger_token_client_product.compact.json"
token_info = load_token(token_file)
if token_info:
# Test if the token is still valid
_client_credentials_client.client.token_info = token_info
if _client_credentials_client.test_current_token():
# Token is valid, use it
return _client_credentials_client
# Token is invalid or not found, get a new one
token_info = _client_credentials_client.authorization.get_token_with_client_credentials("product.compact")
return _client_credentials_client
except Exception as e:
raise Exception(f"Failed to get client credentials: {str(e)}")
def get_authenticated_client() -> KrogerAPI:
"""Get or create a user-authenticated client for cart operations
This function attempts to load an existing token or prompts for authentication.
In an MCP context, the user needs to explicitly call start_authentication and
complete_authentication tools to authenticate.
Returns:
KrogerAPI: Authenticated client
Raises:
Exception: If no valid token is available and authentication is required
"""
global _authenticated_client
if _authenticated_client is not None and _authenticated_client.test_current_token():
# Client exists and token is still valid
return _authenticated_client
# Clear the reference if token is invalid
_authenticated_client = None
try:
load_and_validate_env(["KROGER_CLIENT_ID", "KROGER_CLIENT_SECRET", "KROGER_REDIRECT_URI"])
# Try to load existing user token first
token_file = ".kroger_token_user.json"
token_info = load_token(token_file)
if token_info:
# Create a new client with the loaded token
_authenticated_client = KrogerAPI()
_authenticated_client.client.token_info = token_info
_authenticated_client.client.token_file = token_file
if _authenticated_client.test_current_token():
# Token is valid, use it
return _authenticated_client
# Token is invalid, try to refresh it
if "refresh_token" in token_info:
try:
_authenticated_client.authorization.refresh_token(token_info["refresh_token"])
# If refresh was successful, return the client
if _authenticated_client.test_current_token():
return _authenticated_client
except Exception:
# Refresh failed, need to re-authenticate
_authenticated_client = None
# No valid token available, need user-initiated authentication
raise Exception(
"Authentication required. Please use the start_authentication tool to begin the OAuth flow, "
"then complete it with the complete_authentication tool."
)
except Exception as e:
if "Authentication required" in str(e):
# This is an expected error when authentication is needed
raise
else:
# Other unexpected errors
raise Exception(f"Authentication failed: {str(e)}")
def invalidate_authenticated_client():
"""Invalidate the authenticated client to force re-authentication"""
global _authenticated_client
_authenticated_client = None
def invalidate_client_credentials_client():
"""Invalidate the client credentials client to force re-authentication"""
global _client_credentials_client
_client_credentials_client = None
def _load_preferences() -> dict:
"""Load preferences from file"""
try:
if os.path.exists(PREFERENCES_FILE):
with open(PREFERENCES_FILE, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Warning: Could not load preferences: {e}")
return {"preferred_location_id": None}
def _save_preferences(preferences: dict) -> None:
"""Save preferences to file"""
try:
with open(PREFERENCES_FILE, 'w') as f:
json.dump(preferences, f, indent=2)
except Exception as e:
print(f"Warning: Could not save preferences: {e}")
def get_preferred_location_id() -> Optional[str]:
"""Get the current preferred location ID from preferences file"""
preferences = _load_preferences()
return preferences.get("preferred_location_id")
def set_preferred_location_id(location_id: str) -> None:
"""Set the preferred location ID in preferences file"""
preferences = _load_preferences()
preferences["preferred_location_id"] = location_id
_save_preferences(preferences)
def format_currency(value: Optional[float]) -> str:
"""Format a value as currency"""
if value is None:
return "N/A"
return f"${value:.2f}"
def get_default_zip_code() -> str:
"""Get the default zip code from environment or fallback"""
return get_zip_code(default="10001")