diff --git a/.env.example b/.env.example index 77617c4..1aef3b1 100644 --- a/.env.example +++ b/.env.example @@ -13,5 +13,5 @@ MONARCH_MFA_SECRET= # Server Configuration PORT=8000 -MONARCH_PORT=8070 +MONARCH_PORT=8070 # Docker Compose host port mapping only LOG_LEVEL=INFO diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 835949c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -fastmcp>=0.4.1 -monarchmoney>=0.1.15 -gql>=3.4,<4.0 -python-dotenv>=1.0.0 -pydantic>=2.0.0 -starlette>=0.35.0 -uvicorn>=0.27.0 -pyotp==2.9.0 diff --git a/src/monarch_mcp_custom/auth.py b/src/monarch_mcp_custom/auth.py index b54a4be..87814ec 100644 --- a/src/monarch_mcp_custom/auth.py +++ b/src/monarch_mcp_custom/auth.py @@ -23,7 +23,7 @@ def load_token() -> Optional[str]: # 1. Check environment variable (Best for Docker) token = os.getenv("MONARCH_TOKEN") if token: - logger.info("✅ Token loaded from MONARCH_TOKEN environment variable") + logger.info("Token loaded from MONARCH_TOKEN environment variable") return token return None @@ -37,9 +37,9 @@ def save_token(token: str) -> None: KEYRING_SERVICE = "com.mcp.monarch-mcp-server" KEYRING_USERNAME = "monarch-token" keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME, token) - logger.info("✅ Token saved securely to keyring") + logger.info("Token saved securely to keyring") except Exception as e: - logger.warning(f"⚠️ Failed to save token to keyring (non-fatal): {e}") + logger.warning(f"Failed to save token to keyring (non-fatal): {e}") async def get_authenticated_client() -> MonarchMoney: @@ -58,11 +58,11 @@ async def get_authenticated_client() -> MonarchMoney: _client_instance = MonarchMoney(token=token) return _client_instance except Exception as e: - logger.error(f"❌ Failed to initialize MonarchMoney with token: {e}") + logger.error(f"Failed to initialize MonarchMoney with token: {e}") raise raise RuntimeError( - "🔐 Authentication required. Please provide MONARCH_TOKEN or run login_setup.py" + "Authentication required. Please provide MONARCH_TOKEN or run login_setup.py" ) @@ -86,11 +86,11 @@ async def refresh_authentication() -> MonarchMoney: try: await mm.login(email, password, mfa_secret_key=mfa_secret, save_session=False) - logger.info("✅ Re-authentication successful") + logger.info("Re-authentication successful") _client_instance = mm return mm except Exception as e: - logger.error(f"❌ Re-authentication failed: {e}") + logger.error(f"Re-authentication failed: {e}") raise @@ -127,12 +127,16 @@ def retry_on_auth_error(max_retries: int = 1): if is_auth_error and attempt < max_retries: logger.warning( - f"⚠️ Authentication failure detected (attempt {attempt + 1}/{max_retries + 1}), re-authenticating..." + f"Authentication failed in {func.__name__}, refreshing token... " + f"(attempt {attempt + 1}/{max_retries + 1})" ) await refresh_authentication() continue - else: - raise + + # Only log error for non-auth errors (auth errors were already logged as warnings) + if not is_auth_error: + logger.error(f"Request failed in {func.__name__}: {e}") + raise return wrapper diff --git a/src/monarch_mcp_custom/server.py b/src/monarch_mcp_custom/server.py index abb34d8..5948461 100644 --- a/src/monarch_mcp_custom/server.py +++ b/src/monarch_mcp_custom/server.py @@ -4,6 +4,7 @@ Monarch Money MCP Server - Custom SSE Implementation. import logging import json +import os from typing import Optional, Any from dotenv import load_dotenv @@ -17,9 +18,11 @@ from monarch_mcp_custom.auth import get_authenticated_client, retry_on_auth_erro # Load environment variables load_dotenv() -# Configure logging +# Configure logging with LOG_LEVEL from environment +log_level = os.getenv("LOG_LEVEL", "INFO").upper() logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=getattr(logging, log_level, logging.INFO), + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) @@ -124,13 +127,23 @@ async def get_budgets(reason: Optional[str] = None) -> str: return serialize_json(budget_list) +def validate_account_id(account_id: str) -> int: + """Validate and convert account_id to integer.""" + if not account_id or not account_id.strip(): + raise ValueError("account_id must be provided and cannot be empty") + try: + return int(account_id.strip()) + except (ValueError, TypeError): + raise ValueError("account_id must be a valid integer") + + @mcp.tool() @retry_on_auth_error() async def get_account_holdings(account_id: str, reason: Optional[str] = None) -> str: """Get investment holdings for a specific account.""" + validated_id = validate_account_id(account_id) client = await get_authenticated_client() - # The library expects an int for account_id - holdings = await client.get_account_holdings(int(account_id)) + holdings = await client.get_account_holdings(validated_id) return serialize_json(holdings) @@ -176,4 +189,5 @@ app = create_app() if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + port = int(os.getenv("PORT", "8000")) + uvicorn.run(app, host="0.0.0.0", port=port)