You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
6.1 KiB
Python

"""
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")