"""
Maple Budget MCP Server

Exposes All MapleBudget REST API Endpoints As MCP Tools So That Claude
And Other MCP-Compatible Clients Can Read And Write Budget, Investment,
Net-Worth, And Market Data On Behalf Of An Authenticated User.

Environment Variables:
    MAPLE_BUDGET_API_KEY  – A Valid mbk_xxx API Key (Required).
    MAPLE_BUDGET_API_URL  – Base URL Of The API (Default: https://api.maplebudget.com/api/).
"""

import hashlib
import os
import json
import sys
from typing import Optional

import requests
from mcp.server.fastmcp import FastMCP

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

API_URL = os.environ.get("MAPLE_BUDGET_API_URL", "https://api.maplebudget.com/api/")
API_KEY = os.environ.get("MAPLE_BUDGET_API_KEY", "")

mcp = FastMCP("Maple Budget")

# ---------------------------------------------------------------------------
# HTTP helper
# ---------------------------------------------------------------------------


def api_request(method: str, path: str, data: dict = None) -> dict:
    """Make An Authenticated Request To The MapleBudget REST API."""
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    url = f"{API_URL}{path}"
    response = requests.request(method, url, headers=headers, json=data, timeout=30)
    response.raise_for_status()
    if response.status_code == 204:
        return {"status": "success"}
    return response.json()


# ===========================================================================================
# BUDGET – READ
# ===========================================================================================


@mcp.tool()
def get_profile() -> str:
    """Get The Authenticated User's Profile.

    Returns User Profile Information Including Name, Email, Country, And Subscription Status.
    """
    response = api_request("GET", "profiles/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_on_launch_data(month: int, year: int) -> str:
    """Get All Data Needed On App Launch For A Specific Month.

    This Is A Combined Endpoint That Returns The User Profile, Monthly Summary,
    Categories, Tags, Payment Methods, And Other Data Needed To Render The Main Screen.

    Args:
        month: Month number (1-12).
        year: Four-digit year (e.g. 2025).

    Returns Everything The App Needs On Launch In A Single Response.
    """
    response = api_request("GET", f"profiles/on-launch/?month={month}&year={year}")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_monthly_summary(month: int, year: int) -> str:
    """Get The Monthly Spending Summary For A Specific Month.

    Returns Monthly Spend, Monthly Earn, And Transaction Details For The Given Month/Year.

    Args:
        month: Month number (1-12).
        year: Four-digit year (e.g. 2025).
    """
    response = api_request(
        "GET",
        f"transactions/monthly-transaction-summary/get/?month={month}&year={year}",
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def get_transactions(month: int, year: int) -> str:
    """Get All Transactions For A Specific Month.

    Returns A List Of Individual Transactions With Their Dates, Amounts, Categories,
    Titles, Subtitles, Tags, And Payment Methods.

    Args:
        month: Month number (1-12).
        year: Four-digit year (e.g. 2025).
    """
    response = api_request(
        "GET",
        f"transactions/monthly-transaction-summary/get/?month={month}&year={year}",
    )
    if isinstance(response, dict) and "transaction_details" in response:
        response = response["transaction_details"]
    return json.dumps(response, indent=2)


@mcp.tool()
def get_categories(month: int, year: int) -> str:
    """Get Spending Categories With Their Budgets And Totals For A Specific Month.

    Returns Each Category's Monthly Spend, Budget Amount, Icon, And Color.

    Args:
        month: Month number (1-12).
        year: Four-digit year (e.g. 2025).
    """
    response = api_request(
        "GET",
        f"categories/categorical-overview/get/?month={month}&year={year}",
    )
    if isinstance(response, dict) and "categories" in response:
        response = response["categories"]
    return json.dumps(response, indent=2)


@mcp.tool()
def get_categorical_overview(month: int, year: int) -> str:
    """Get Categorical Spending Overview For A Specific Month.

    Returns Total Spending, Spending Percentages Per Category, And Category Breakdowns
    For The Given Month.

    Args:
        month: Month number (1-12).
        year: Four-digit year (e.g. 2025).
    """
    response = api_request(
        "GET",
        f"categories/categorical-overview/get/?month={month}&year={year}",
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def get_recurring_transactions() -> str:
    """Get All Active Recurring Transactions.

    Returns A List Of Recurring Transaction Rules Including Frequency, Next Occurrence,
    End Conditions, And The Associated Transaction Template.
    """
    response = api_request("GET", "transactions/recurring/get/")
    return json.dumps(response, indent=2)


# ===========================================================================================
# BUDGET – WRITE
# ===========================================================================================


@mcp.tool()
def add_transaction(
    date: str,
    amount_spent: str,
    category: int,
    title: str,
    subtitle: Optional[str] = None,
    tags: Optional[list[int]] = None,
    payment_forms: Optional[list[int]] = None,
) -> str:
    """Add A New Transaction.

    Args:
        date: Transaction Date In YYYY-MM-DD Format.
        amount_spent: Amount As A Decimal String (e.g. "42.50"). Positive = Expense, Negative = Income.
        category: Category ID (1-25).
        title: Short Title For The Transaction (max 100 chars).
        subtitle: Optional Longer Description (max 100 chars).
        tags: Optional List Of Tag IDs To Attach.
        payment_forms: Optional List Of Payment Method IDs Used.

    Returns The Created Transaction Along With Updated Monthly Summary And Category Data.
    """
    payload = {
        "date": date,
        "amount_spent": amount_spent,
        "category": category,
        "title": title,
    }
    if subtitle is not None:
        payload["subtitle"] = subtitle
    if tags is not None:
        payload["tags"] = tags
    if payment_forms is not None:
        payload["payment_forms"] = payment_forms

    response = api_request("POST", "transactions/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def update_transaction(
    id: int,
    date: Optional[str] = None,
    amount_spent: Optional[str] = None,
    category: Optional[int] = None,
    title: Optional[str] = None,
    subtitle: Optional[str] = None,
) -> str:
    """Update An Existing Transaction.

    Only The Fields You Provide Will Be Changed; Omitted Fields Keep Their Current Value.

    Args:
        id: The Transaction ID To Update.
        date: New Date In YYYY-MM-DD Format.
        amount_spent: New Amount As A Decimal String.
        category: New Category ID (1-25).
        title: New Title (Max 100 Chars).
        subtitle: New Subtitle (Max 100 Chars).

    Returns The Updated Transaction And Refreshed Summary Data.
    """
    payload = {"transaction_id": id}
    if date is not None:
        payload["date"] = date
    if amount_spent is not None:
        payload["amount_spent"] = amount_spent
    if category is not None:
        payload["category"] = category
    if title is not None:
        payload["title"] = title
    if subtitle is not None:
        payload["subtitle"] = subtitle

    response = api_request("PATCH", "transactions/update/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_transaction(transaction_id: int) -> str:
    """Delete A Transaction.

    Args:
        transaction_id: The ID Of The Transaction To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request(
        "DELETE",
        f"transactions/delete/?transaction_id={transaction_id}",
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def add_tag(name: str, category: int, color: str) -> str:
    """Create A New Tag Within A Spending Category.

    Tags Let You Further Classify Transactions Within A Category (e.g. "Groceries" Within
    The Food Category).

    Args:
        name: Tag Display Name.
        category: Category ID (1-25) This Tag Belongs To.
        color: Hex Color Code For The Tag (e.g. "#FF5733").

    Returns The Created Tag.
    """
    payload = {"name": name, "category": category, "color": color}
    response = api_request("POST", "categories/tags/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def update_tag(
    id: int,
    name: Optional[str] = None,
    category: Optional[int] = None,
    color: Optional[str] = None,
) -> str:
    """Update An Existing Tag.

    Args:
        id: The Tag ID To Update.
        name: New Tag Name.
        category: New Category ID.
        color: New Hex Color Code.

    Returns The Updated Tag.
    """
    payload = {}
    if name is not None:
        payload["name"] = name
    if category is not None:
        payload["category"] = category
    if color is not None:
        payload["color"] = color

    response = api_request("PATCH", f"categories/tags/update/?tag_id={id}", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_tag(id: int) -> str:
    """Delete A Tag.

    Args:
        id: The ID Of The Tag To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"categories/tags/delete/?tag_id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def add_payment_method(name: str) -> str:
    """Add A New Payment Method (e.g. A Credit Card Name).

    Args:
        name: Display Name For The Payment Method (Max 100 Chars).

    Returns The Created Payment Method With Its ID.
    """
    payload = {"name": name, "registered_payment_method": False}
    response = api_request("POST", "profiles/payment-methods/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def add_recurring_transaction(
    amount_spent: str,
    category: int,
    title: str,
    next_occurrence: str,
    frequency: int,
    end_type: int,
    subtitle: Optional[str] = None,
    tags: Optional[list[int]] = None,
    payment_forms: Optional[list[int]] = None,
    end_occurrence: Optional[int] = None,
    end_date: Optional[str] = None,
) -> str:
    """Add A New Recurring Transaction Rule.

    The System Will Automatically Create Transactions According To The Recurrence Schedule.

    Args:
        amount_spent: Amount As A Decimal String (e.g. "42.50").
        category: Category ID (1-25).
        title: Short Title (Max 100 Chars).
        next_occurrence: Date Of The Next Occurrence In YYYY-MM-DD Format.
        frequency: Recurrence frequency — 0=weekly, 1=biweekly, 2=monthly, 3=yearly.
        end_type: When To Stop — 0=Never, 1=After N Occurrences, 2=On A Specific Date.
        subtitle: Optional Description (Max 100 Chars).
        tags: Optional list of tag IDs.
        payment_forms: Optional List Of Payment Method IDs.
        end_occurrence: Number Of Occurrences Before Stopping (Required If End Type = 1).
        end_date: End Date In YYYY-MM-DD Format (Required If End Type = 2).

    Returns The List Of All Active Recurring Transactions.
    """
    payload = {
        "amount_spent": amount_spent,
        "category": category,
        "title": title,
        "next_occurrence": next_occurrence,
        "frequency": frequency,
        "end_type": end_type,
    }
    if subtitle is not None:
        payload["subtitle"] = subtitle
    if tags is not None:
        payload["tags"] = tags
    if payment_forms is not None:
        payload["payment_forms"] = payment_forms
    if end_occurrence is not None:
        payload["end_occurrence"] = end_occurrence
    if end_date is not None:
        payload["end_date"] = end_date

    response = api_request("POST", "transactions/recurring/add/", data=payload)
    return json.dumps(response, indent=2)


# ===========================================================================================
# INVESTMENTS – READ
# ===========================================================================================


@mcp.tool()
def get_investment_accounts() -> str:
    """Get All Investment Accounts.

    Returns A List Of Investment Accounts With Their Names, Types, Institutions,
    Groups, Currencies, And Associated Positions.
    """
    response = api_request("GET", "investments/investment-accounts/get/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_investment_account_groups() -> str:
    """Get All Investment Account Groups.

    Groups Are Used To Organize Investment Accounts (e.g. "Retirement", "Taxable").
    Returns Group Names, IDs, And Ordering.
    """
    response = api_request("GET", "investments/investment-account-groups/get/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_positions(
    account_id: Optional[int] = None,
    group_id: Optional[int] = None,
    is_active: Optional[bool] = None,
) -> str:
    """Get Investment Positions (Holdings).

    Can Be Filtered By Account, Group, Or Active Status.

    Args:
        account_id: Filter Positions To A Specific Investment Account.
        group_id: Filter Positions To Accounts In A Specific Group.
        is_active: If True, Only Return Positions With Non-Zero Quantity.

    Returns A List Of Positions With Ticker, Quantity, Book Cost, And Account Info.
    """
    params = []
    if account_id is not None:
        params.append(f"account_id={account_id}")
    if group_id is not None:
        params.append(f"group_id={group_id}")
    if is_active is not None:
        params.append(f"is_active={'true' if is_active else 'false'}")
    query = "&".join(params)
    path = f"investments/positions/get/?{query}" if query else "investments/positions/get/"
    response = api_request("GET", path)
    return json.dumps(response, indent=2)


@mcp.tool()
def get_trades(
    account_id: Optional[int] = None,
    ticker: Optional[str] = None,
    offset: Optional[int] = None,
    limit: Optional[int] = None,
) -> str:
    """Get Trade History.

    Supports Filtering And Pagination.

    Args:
        account_id: Filter Trades To A Specific Investment Account.
        ticker: Filter Trades By Yahoo Ticker Symbol (e.g. "AAPL").
        offset: Number Of Trades To Skip (For Pagination).
        limit: Maximum Number Of Trades To Return.

    Returns A List Of Trades With Ticker, Side (BUY/SELL), Quantity, Price, Currency,
    And Date.
    """
    params = []
    if account_id is not None:
        params.append(f"account_id={account_id}")
    if ticker is not None:
        params.append(f"ticker={ticker}")
    if offset is not None:
        params.append(f"offset={offset}")
    if limit is not None:
        params.append(f"limit={limit}")
    query = "&".join(params)
    path = f"investments/trades/get/?{query}" if query else "investments/trades/get/"
    response = api_request("GET", path)
    return json.dumps(response, indent=2)


@mcp.tool()
def get_investment_settings() -> str:
    """Get The User's Investment-Related Settings.

    Returns Display Currency Preference And Registered-Account Contribution Room
    (TFSA, RRSP, FHSA).
    """
    response = api_request("GET", "profiles/investment-settings/get/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_portfolio_summary(display_currency: Optional[str] = None) -> str:
    """Get The Portfolio Summary Computed By The Server.

    Includes Net Worth, Invested Value, Cash, Book Cost, Unrealized And Realized Gains,
    Day Change, Per-Account Totals, Holdings (Merged Across Accounts) And Allocation
    By Account, Account Type, Group, Holding And Currency — All In One Currency.

    Args:
        display_currency: Currency To Report In ("CAD", "USD", "GBP"). Defaults To The User's Setting.
    """
    query = f"?display_currency={display_currency}" if display_currency else ""
    response = api_request("GET", f"investments/portfolio/summary/{query}")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_trade_ledger(
    display_currency: Optional[str] = None,
    account_id: Optional[int] = None,
    ticker: Optional[str] = None,
    offset: Optional[int] = None,
    limit: Optional[int] = None,
) -> str:
    """Get Trades With Server-Computed Cost Basis And Realized Gain On Each Sell.

    Args:
        display_currency: Currency For The Converted Columns.
        account_id: Filter To One Investment Account.
        ticker: Filter By Yahoo Ticker.
        offset: Number Of Trades To Skip.
        limit: Maximum Number Of Trades To Return.
    """
    params = {
        "display_currency": display_currency, "account_id": account_id,
        "ticker": ticker, "offset": offset, "limit": limit,
    }
    query = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
    response = api_request("GET", f"investments/trades/ledger/{'?' + query if query else ''}")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_lots() -> str:
    """Get Every Buy As A Lot, Matched To The Sells That Closed It (Oldest First, Per Account).

    Each Lot Has Its Buy, Exits, Realized Gain On The Sold Shares, A Weekly 500-Day Price Path
    (% Since The Buy-Date Close), Where It Was When Sold, Where It Is Now, And A Verdict:
    "early" (Rose 10+ Points After You Sold), "well" (Fell 10+), "even", "partial", "held" Or "na".
    """
    response = api_request("GET", "investments/lots/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_net_worth_history(display_currency: Optional[str] = None) -> str:
    """Get Net Worth History Converted With Each Snapshot's Own FX Rates.

    Returns Points With Total, Change Since The Previous Snapshot, Per-Account Values
    (Keyed By Account) And Any Unattributed Amount (Backdated Totals With No Breakdown).

    Args:
        display_currency: Currency To Report In. Defaults To The User's Setting.
    """
    query = f"?display_currency={display_currency}" if display_currency else ""
    response = api_request("GET", f"networth/history/{query}")
    return json.dumps(response, indent=2)


# ===========================================================================================
# INVESTMENTS – WRITE
# ===========================================================================================


@mcp.tool()
def add_investment_account(
    name: str,
    account_type: str,
    institution: Optional[str] = None,
    group_id: Optional[int] = None,
    currency: Optional[str] = None,
    is_investment: Optional[bool] = None,
    cash_balances: Optional[dict] = None,
    total_contributed: Optional[str] = None,
) -> str:
    """Create A New Investment Account.

    Args:
        name: Display Name For The Account (e.g. "TFSA - Wealthsimple").
        account_type: One Of "TFSA", "RRSP", "FHSA", "NON_REGISTERED", "SAVINGS", "CRYPTO".
        institution: Financial Institution Name.
        group_id: ID Of The Group To Place This Account In.
        currency: Account Currency Code (e.g. "CAD", "USD").
        is_investment: Whether The Account Holds Positions. Defaults To True For Every
            Type Except SAVINGS.
        cash_balances: Starting Cash By Currency, e.g. {"CAD": "1500.00"}.
        total_contributed: Total Amount Contributed So Far, As A String.

    Returns The Created Investment Account.
    """
    payload = {"name": name, "account_type": account_type}
    if institution is not None:
        payload["institution"] = institution
    if group_id is not None:
        payload["group_id"] = group_id
    if currency is not None:
        payload["currency"] = currency
    if is_investment is not None:
        payload["is_investment"] = is_investment
    if cash_balances is not None:
        payload["cash_balances"] = cash_balances
    if total_contributed is not None:
        payload["total_contributed"] = total_contributed

    response = api_request("POST", "investments/investment-accounts/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def update_investment_account(
    id: int,
    name: Optional[str] = None,
    account_type: Optional[str] = None,
    institution: Optional[str] = None,
    group_id: Optional[int] = None,
    currency: Optional[str] = None,
    is_investment: Optional[bool] = None,
    cash_balances: Optional[dict] = None,
    total_contributed: Optional[str] = None,
    remove_from_group: Optional[bool] = None,
) -> str:
    """Update An Existing Investment Account.

    Args:
        id: The Investment Account ID.
        name: New Display Name.
        account_type: New Account Type.
        institution: New Institution Name.
        group_id: New Group ID.
        currency: New Currency Code.
        is_investment: Whether The Account Holds Positions.
        cash_balances: Cash By Currency To Set, e.g. {"CAD": "1500.00", "USD": "20.00"}.
        total_contributed: Total Amount Contributed So Far, As A String.
        remove_from_group: True To Take The Account Out Of Its Group.

    Returns The Updated Investment Account.
    """
    payload = {"id": id}
    if name is not None:
        payload["name"] = name
    if account_type is not None:
        payload["account_type"] = account_type
    if institution is not None:
        payload["institution"] = institution
    if group_id is not None:
        payload["group_id"] = group_id
    if currency is not None:
        payload["currency"] = currency
    if is_investment is not None:
        payload["is_investment"] = is_investment
    if cash_balances is not None:
        payload["cash_balances"] = cash_balances
    if total_contributed is not None:
        payload["total_contributed"] = total_contributed
    if remove_from_group:
        payload["group_id"] = None

    response = api_request("PUT", "investments/investment-accounts/update/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_investment_account(id: int) -> str:
    """Delete An Investment Account And All Its Positions And Trades.

    Args:
        id: The Investment Account ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"investments/investment-accounts/delete/?id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def add_investment_account_group(name: str) -> str:
    """Create A New Investment Account Group.

    Groups Let You Organize Accounts (e.g. "Retirement", "Taxable", "Crypto").

    Args:
        name: Display Name For The Group.

    Returns The Created Group.
    """
    payload = {"name": name}
    response = api_request(
        "POST", "investments/investment-account-groups/add/", data=payload
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_investment_account_group(id: int) -> str:
    """Delete An Investment Account Group.

    Accounts In The Group Will Be Ungrouped, Not Deleted.

    Args:
        id: The Group ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request(
        "DELETE", f"investments/investment-account-groups/delete/?id={id}"
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def reorder_investment_account_groups(group_ids: list[int]) -> str:
    """Reorder Investment Account Groups.

    Args:
        group_ids: List Of Group IDs In The Desired Display Order.

    Returns Confirmation Of The New Order.
    """
    payload = {"group_ids": group_ids}
    response = api_request(
        "POST", "investments/investment-account-groups/reorder/", data=payload
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def add_trade(
    account_id: int,
    ticker_yahoo: str,
    ticker_display: str,
    side: str,
    quantity: str,
    currency: str,
    date: str,
    price_total: Optional[str] = None,
    sale_price_total: Optional[str] = None,
    exchange_rate: Optional[str] = None,
    is_option: Optional[bool] = None,
    option_type: Optional[str] = None,
    strike_price: Optional[str] = None,
    expiry_date: Optional[str] = None,
    contract_size: Optional[int] = None,
    manual_price: Optional[str] = None,
) -> str:
    """Record A New Trade (Buy Or Sell).

    Args:
        account_id: The Investment Account ID Where This Trade Occurred.
        ticker_yahoo: Yahoo Finance Ticker Symbol (e.g. "AAPL", "XIU.TO"). For Private Companies Use Any Unique Identifier.
        ticker_display: Display Ticker (e.g. "AAPL", "XIU", "Startup Inc").
        side: "BUY" Or "SELL".
        quantity: Number Of Shares/Units As A String (e.g. "10.5").
        currency: Currency Code (e.g. "CAD", "USD").
        date: Trade Date In YYYY-MM-DD Format.
        price_total: For A BUY, The Total Cost (Required). For A SELL, The Cost Basis Removed —
            Leave It Out And The Server Computes It From Average Cost.
        sale_price_total: For SELL Trades, The Total Sale Proceeds.
        exchange_rate: Exchange Rate At The Time Of Trade (If Foreign Currency).
        is_option: True If This Trade Is For An Options Contract.
        option_type: "CALL" Or "PUT" (Required If is_option Is True).
        strike_price: Strike Price As A String (Required If is_option Is True).
        expiry_date: Option Expiry Date In YYYY-MM-DD Format (Required If is_option Is True).
        contract_size: Number Of Shares Per Contract (Default 100 For Exchange-Traded, Use 1 For Employee Stock Options).
        manual_price: Current FMV Per Share For Private/Non-Public Positions.

    Returns The Created Trade Record.
    """
    payload = {
        "account_id": account_id,
        "ticker_yahoo": ticker_yahoo,
        "ticker_display": ticker_display,
        "side": side,
        "quantity": quantity,
        "currency": currency,
        "date": date,
    }
    if price_total is not None:
        payload["price_total"] = price_total
    if sale_price_total is not None:
        payload["sale_price_total"] = sale_price_total
    if exchange_rate is not None:
        payload["exchange_rate"] = exchange_rate
    if is_option is not None:
        payload["is_option"] = is_option
    if option_type is not None:
        payload["option_type"] = option_type
    if strike_price is not None:
        payload["strike_price"] = strike_price
    if expiry_date is not None:
        payload["expiry_date"] = expiry_date
    if contract_size is not None:
        payload["contract_size"] = contract_size
    if manual_price is not None:
        payload["manual_price"] = manual_price

    response = api_request("POST", "investments/trades/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_trade(id: int) -> str:
    """Delete A Trade Record And Reverse Its Effect On The Position.

    Fails If Later Sells Depend On The Shares This Trade Added.

    Args:
        id: The Trade ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"investments/trades/delete/?id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def update_trade(
    id: int,
    account_id: Optional[int] = None,
    ticker_yahoo: Optional[str] = None,
    ticker_display: Optional[str] = None,
    side: Optional[str] = None,
    quantity: Optional[str] = None,
    price_total: Optional[str] = None,
    sale_price_total: Optional[str] = None,
    currency: Optional[str] = None,
    exchange_rate: Optional[str] = None,
    date: Optional[str] = None,
) -> str:
    """Edit A Trade In Place. Positions Are Rebuilt From Trade History Automatically.

    Only The Fields You Pass Are Changed. Use This Instead Of Deleting And Re-Adding.
    For A SELL, Changing The Quantity Or Date Without Passing price_total Recomputes
    The Cost Basis. Edits That Would Sell More Than Was Held On That Date Are Rejected.

    Args:
        id: The Trade ID To Edit.
        account_id: Move The Trade To Another Investment Account.
        ticker_yahoo: New Yahoo Ticker.
        ticker_display: New Display Ticker.
        side: "BUY" Or "SELL".
        quantity: New Quantity As A String.
        price_total: For A BUY, Total Cost. For A SELL, Cost Basis Removed.
        sale_price_total: For A SELL, Total Proceeds.
        currency: Currency Code.
        exchange_rate: Exchange Rate At The Time Of Trade.
        date: Trade Date In YYYY-MM-DD Format.

    Returns The Updated Trade.
    """
    payload = {"id": id}
    for key, value in (
        ("account_id", account_id), ("ticker_yahoo", ticker_yahoo), ("ticker_display", ticker_display),
        ("side", side), ("quantity", quantity), ("price_total", price_total),
        ("sale_price_total", sale_price_total), ("currency", currency),
        ("exchange_rate", exchange_rate), ("date", date),
    ):
        if value is not None:
            payload[key] = value

    response = api_request("PUT", "investments/trades/update/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def add_position(
    account_id: int,
    ticker_yahoo: str,
    ticker_display: str,
    currency: str,
    quantity: str,
    book_price_total: str,
    is_option: Optional[bool] = None,
    option_type: Optional[str] = None,
    strike_price: Optional[str] = None,
    expiry_date: Optional[str] = None,
    contract_size: Optional[int] = None,
    manual_price: Optional[str] = None,
) -> str:
    """Directly Create A Position Without A Trade Record.

    Use This To Seed A Portfolio With Existing Holdings. If A Position Already
    Exists For The Same Account And Ticker, The Quantity And Book Cost Are Added
    To The Existing Position (Same As A BUY Trade).

    Args:
        account_id: The Investment Account ID.
        ticker_yahoo: Yahoo Finance Ticker Symbol (e.g. "AAPL", "XIU.TO").
        ticker_display: Display Ticker (e.g. "AAPL", "XIU").
        currency: Currency Code ("CAD", "USD", Or "GBP").
        quantity: Number Of Shares/Units As A String (e.g. "100").
        book_price_total: Total Book Cost As A String (e.g. "13000.00").
        is_option: True If This Is An Options Position.
        option_type: "CALL" Or "PUT" (Required If is_option Is True).
        strike_price: Strike Price As A String (Required If is_option Is True).
        expiry_date: Option Expiry Date In YYYY-MM-DD Format (Required If is_option Is True).
        contract_size: Number Of Shares Per Contract (Default 100).
        manual_price: Current FMV Per Share For Private/Non-Public Positions.

    Returns The Created Or Updated Position.
    """
    payload = {
        "account_id": account_id,
        "ticker_yahoo": ticker_yahoo,
        "ticker_display": ticker_display,
        "currency": currency,
        "quantity": quantity,
        "book_price_total": book_price_total,
    }
    if is_option is not None:
        payload["is_option"] = is_option
    if option_type is not None:
        payload["option_type"] = option_type
    if strike_price is not None:
        payload["strike_price"] = strike_price
    if expiry_date is not None:
        payload["expiry_date"] = expiry_date
    if contract_size is not None:
        payload["contract_size"] = contract_size
    if manual_price is not None:
        payload["manual_price"] = manual_price

    response = api_request("POST", "investments/positions/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def update_position(
    id: int,
    manual_price: Optional[str] = None,
    contract_size: Optional[int] = None,
    quantity: Optional[str] = None,
    book_price_total: Optional[str] = None,
    ticker_display: Optional[str] = None,
) -> str:
    """Update A Position's Fields.

    Use This To Edit Quantity, Book Cost, Display Ticker, Or Set The Current FMV
    (Fair Market Value) For Private/Non-Public Positions.

    Args:
        id: The Position ID To Update.
        manual_price: Current FMV Per Share As A Decimal String (e.g. "25.00").
        contract_size: Number Of Shares Per Contract.
        quantity: New Quantity As A Decimal String (e.g. "100").
        book_price_total: New Total Book Cost As A Decimal String (e.g. "13000.00").
        ticker_display: New Display Ticker (e.g. "TSE:SHOP").

    Returns The Updated Position.
    """
    payload = {"id": id}
    if manual_price is not None:
        payload["manual_price"] = manual_price
    if contract_size is not None:
        payload["contract_size"] = contract_size
    if quantity is not None:
        payload["quantity"] = quantity
    if book_price_total is not None:
        payload["book_price_total"] = book_price_total
    if ticker_display is not None:
        payload["ticker_display"] = ticker_display

    response = api_request("PUT", "investments/positions/update/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_position(id: int) -> str:
    """Delete A Position.

    Removes The Position Record. Does Not Affect Any Trade History.

    Args:
        id: The Position ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"investments/positions/delete/?id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def update_investment_settings(
    display_currency: Optional[str] = None,
    active_currencies: Optional[list[str]] = None,
    tfsa_contribution_room: Optional[str] = None,
    rrsp_contribution_room: Optional[str] = None,
    fhsa_contribution_room: Optional[str] = None,
) -> str:
    """Update Investment-Related Settings.

    Args:
        display_currency: Preferred Display Currency (e.g. "CAD", "USD", "GBP").
        active_currencies: List Of Currency Codes The User Interacts With (e.g. ["CAD", "USD"]).
        tfsa_contribution_room: Available TFSA Contribution Room As A Decimal String.
        rrsp_contribution_room: Available RRSP Contribution Room As A Decimal String.
        fhsa_contribution_room: Available FHSA Contribution Room As A Decimal String.

    Returns The Updated Investment Settings.
    """
    payload = {}
    if display_currency is not None:
        payload["display_currency"] = display_currency
    if active_currencies is not None:
        payload["active_currencies"] = active_currencies
    if tfsa_contribution_room is not None:
        payload["tfsa_contribution_room"] = tfsa_contribution_room
    if rrsp_contribution_room is not None:
        payload["rrsp_contribution_room"] = rrsp_contribution_room
    if fhsa_contribution_room is not None:
        payload["fhsa_contribution_room"] = fhsa_contribution_room

    response = api_request("PUT", "profiles/investment-settings/update/", data=payload)
    return json.dumps(response, indent=2)


# ===========================================================================================
# NET WORTH – READ
# ===========================================================================================


@mcp.tool()
def get_net_worth_snapshots() -> str:
    """Get All Net Worth Snapshots.

    Returns A List Of Snapshots With Dates, Total Values, And Type (Live Vs Manual).
    """
    response = api_request("GET", "networth/snapshots/get/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_net_worth_snapshot(id: int) -> str:
    """Get A Specific Net Worth Snapshot With Full Details.

    Args:
        id: The Snapshot ID.

    Returns The Snapshot With All Its Entries (Individual Account Values).
    """
    response = api_request("GET", f"networth/snapshots/get/{id}/")
    return json.dumps(response, indent=2)


# ===========================================================================================
# NET WORTH – WRITE
# ===========================================================================================


@mcp.tool()
def create_live_snapshot() -> str:
    """Create A Live Net Worth Snapshot.

    Automatically Fetches Current Market Prices For All Positions And Calculates
    The Total Net Worth. This Is The Recommended Way To Take A Snapshot.

    Returns The Created Snapshot With All Its Entries.
    """
    response = api_request("POST", "networth/snapshots/live/")
    return json.dumps(response, indent=2)


@mcp.tool()
def create_manual_snapshot(
    date: str,
    total: str,
    fx_rates: Optional[dict] = None,
    storage_currency: Optional[str] = None,
    entries: Optional[list[dict]] = None,
) -> str:
    """Create A Manual Net Worth Snapshot With User-Provided Values.

    Args:
        date: Snapshot Date In YYYY-MM-DD Format.
        total: Total Net Worth In Storage Currency As A Decimal String.
        fx_rates: Optional Dict Of FX Rates (e.g. {"USD_CAD": "1.3679"}).
        storage_currency: Currency The Total Is Denominated In (Default: "CAD").
        entries: Optional List Of Entry Dicts With "value" And Either "account_id" (Preferred — Linked
            To The Account So It Survives Renames) Or "account_name".

    Returns The Created Snapshot.
    """
    payload = {"date": date, "total": total}
    if fx_rates is not None:
        payload["fx_rates"] = fx_rates
    if storage_currency is not None:
        payload["storage_currency"] = storage_currency
    if entries is not None:
        payload["entries"] = entries

    response = api_request("POST", "networth/snapshots/manual/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_snapshot(id: int) -> str:
    """Delete A Net Worth Snapshot.

    Args:
        id: The Snapshot ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"networth/snapshots/delete/?id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def update_snapshot_entry(id: int, value: str) -> str:
    """Update The Value Of A Single Entry In A Net Worth Snapshot.

    Args:
        id: The Snapshot Entry ID.
        value: New Value In Storage Currency As A Decimal String.

    Returns The Updated Entry.
    """
    payload = {"id": id, "value": value}
    response = api_request("PUT", "networth/snapshot-entries/update/", data=payload)
    return json.dumps(response, indent=2)


# ===========================================================================================
# MARKET – READ
# ===========================================================================================


@mcp.tool()
def get_currencies() -> str:
    """Get All Supported Currencies.

    Returns A List Of Currency Codes, Symbols, And Names That The System Supports.
    """
    response = api_request("GET", "market/currencies/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_fx_rates(target: str) -> str:
    """Get Exchange Rates For All Supported Currencies Against A Target Currency.

    Args:
        target: Target Currency Code (e.g. "CAD", "USD").

    Returns Exchange Rates From Each Currency To The Target.
    """
    response = api_request("GET", f"market/fx-rates/?target={target}")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_live_prices(tickers: list[str]) -> str:
    """Get Live Market Prices For A List Of Tickers.

    Args:
        tickers: List Of Yahoo Finance Ticker Symbols (e.g. ["AAPL", "XIU.TO", "MSFT"]).

    Returns Current Price Data For Each Ticker.
    """
    payload = {"tickers": tickers}
    response = api_request("POST", "market/live-prices/get/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
    """Get The Current Exchange Rate Between Two Currencies.

    Args:
        from_currency: Source Currency Code (e.g. "USD").
        to_currency: Target Currency Code (e.g. "CAD").

    Returns The Exchange Rate.
    """
    response = api_request(
        "GET",
        f"market/exchange-rate/get/?from_currency={from_currency}&to_currency={to_currency}",
    )
    return json.dumps(response, indent=2)


@mcp.tool()
def get_sp500_returns(dates: list[str]) -> str:
    """Get S&P 500 Returns For Specific Dates.

    Useful For Benchmarking Portfolio Performance Against The Market.

    Args:
        dates: List Of Dates In YYYY-MM-DD Format To Get S&P 500 Values For.

    Returns S&P 500 Values/Returns For The Requested Dates.
    """
    payload = {"dates": dates}
    response = api_request("POST", "market/sp500-returns/get/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def get_watchlist(period: Optional[str] = None) -> str:
    """Get The User's Stock Watchlist With Current Prices.

    Args:
        period: Optional Time Period For Price Change Display (e.g. "1d", "5d", "1mo", "3mo", "1y").

    Returns Watchlist Items With Current Prices And Period Change.
    """
    path = "market/watchlist/get/"
    if period is not None:
        path += f"?period={period}"
    response = api_request("GET", path)
    return json.dumps(response, indent=2)


@mcp.tool()
def get_ticker_history(ticker: str, period: Optional[str] = None) -> str:
    """Get Historical Price Data For A Ticker.

    Args:
        ticker: Yahoo Finance Ticker Symbol (e.g. "AAPL").
        period: Time Period — "1d", "5d", "1mo", "3mo", "6mo", "1y", "5y", "max".

    Returns Historical Price Data Points.
    """
    path = f"market/ticker-history/get/?ticker={ticker}"
    if period is not None:
        path += f"&period={period}"
    response = api_request("GET", path)
    return json.dumps(response, indent=2)


# ===========================================================================================
# MARKET – WRITE
# ===========================================================================================


@mcp.tool()
def add_watchlist_item(ticker_yahoo: str, ticker_display: str) -> str:
    """Add A Ticker To The Watchlist.

    Args:
        ticker_yahoo: Yahoo Finance Ticker Symbol (e.g. "AAPL", "XIU.TO").
        ticker_display: Display Ticker (e.g. "AAPL", "XIU").

    Returns The Created Watchlist Item.
    """
    payload = {"ticker_yahoo": ticker_yahoo, "ticker_display": ticker_display}
    response = api_request("POST", "market/watchlist/add/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_watchlist_item(id: int) -> str:
    """Remove A Ticker From The Watchlist.

    Args:
        id: The Watchlist Item ID To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"market/watchlist/delete/?id={id}")
    return json.dumps(response, indent=2)


@mcp.tool()
def reorder_watchlist(item_ids: list[int]) -> str:
    """Reorder The Watchlist.

    Args:
        item_ids: List Of Watchlist Item IDs In The Desired Display Order.

    Returns Confirmation Of The New Order.
    """
    payload = {"item_ids": item_ids}
    response = api_request("POST", "market/watchlist/reorder/", data=payload)
    return json.dumps(response, indent=2)


# ===========================================================================================
# API KEY MANAGEMENT
# ===========================================================================================


@mcp.tool()
def list_api_keys() -> str:
    """List All API Keys For The Authenticated User.

    Returns Key Prefix, Name, Creation Date, Last Used Date, And Active Status
    For Each Key. The Full Key Is Never Returned.
    """
    response = api_request("GET", "profiles/api-keys/")
    return json.dumps(response, indent=2)


@mcp.tool()
def create_api_key(name: Optional[str] = None) -> str:
    """Create A New API Key.

    The Full Key Is Returned Only Once In This Response. Store It Securely.

    Args:
        name: Optional Descriptive Name For The Key (Default: "Default").

    Returns The Full API Key (Store It — It Won't Be Shown Again), Prefix, And Metadata.
    """
    payload = {}
    if name is not None:
        payload["name"] = name
    response = api_request("POST", "profiles/api-keys/create/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def revoke_api_key(id: int) -> str:
    """Revoke (Deactivate) An API Key.

    The Key Will Immediately Stop Working For Authentication.

    Args:
        id: The API Key ID To Revoke.

    Returns Confirmation Of Revocation.
    """
    payload = {"id": id}
    response = api_request("DELETE", "profiles/api-keys/revoke/", data=payload)
    return json.dumps(response, indent=2)


# ===========================================================================================
# SHARED EXPENSES – READ
# ===========================================================================================


@mcp.tool()
def list_connections() -> str:
    """List All Connections For The Authenticated User.

    Returns Accepted Connections, Pending Received Invites, And Pending Sent Invites.
    """
    response = api_request("GET", "shared-expenses/connections/")
    return json.dumps(response, indent=2)


@mcp.tool()
def list_balances() -> str:
    """List Running Balances With All Connected Users.

    Returns The Amount Owed To Or From Each Connected User.
    Positive Amount Means You Owe Them, Negative Means They Owe You.
    """
    response = api_request("GET", "shared-expenses/balances/")
    return json.dumps(response, indent=2)


@mcp.tool()
def get_balance_detail(user_id: int) -> str:
    """Get Detailed Balance History With A Specific Connected User.

    Returns The Overall Balance, All Shared Splits (With Per-Expense Settlement Status),
    And All Settlement Records Between You And The Specified User.

    Args:
        user_id: The ID Of The Connected User To View Balance Details With.
    """
    response = api_request("GET", f"shared-expenses/balances/{user_id}/")
    return json.dumps(response, indent=2)


# ===========================================================================================
# SHARED EXPENSES – WRITE
# ===========================================================================================


@mcp.tool()
def invite_connection(
    email: Optional[str] = None,
    phone_number: Optional[str] = None,
) -> str:
    """Invite A User To Connect For Shared Expenses.

    Provide Either An Email Or Phone Number. If The Other User Has Already Sent You An
    Invite, This Will Auto-Accept The Connection.

    Args:
        email: The Email Address Of The User To Invite.
        phone_number: The Phone Number Of The User To Invite.

    Returns The Connection Status (Pending Or Accepted If Reciprocal).
    """
    payload = {}
    if email is not None:
        payload["email"] = email
    if phone_number is not None:
        payload["phone_number"] = phone_number
    response = api_request("POST", "shared-expenses/connections/invite/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def accept_connection(connection_id: int) -> str:
    """Accept A Pending Connection Invite.

    Args:
        connection_id: The ID Of The Connection Invite To Accept.

    Returns Confirmation That The Connection Is Now Accepted.
    """
    response = api_request("POST", f"shared-expenses/connections/{connection_id}/accept/")
    return json.dumps(response, indent=2)


@mcp.tool()
def decline_connection(connection_id: int) -> str:
    """Decline A Pending Connection Invite.

    Args:
        connection_id: The ID Of The Connection Invite To Decline.

    Returns Confirmation That The Connection Was Declined.
    """
    response = api_request("POST", f"shared-expenses/connections/{connection_id}/decline/")
    return json.dumps(response, indent=2)


@mcp.tool()
def remove_connection(connection_id: int) -> str:
    """Remove An Accepted Connection.

    Args:
        connection_id: The ID Of The Connection To Remove.

    Returns Confirmation Of Removal.
    """
    response = api_request("DELETE", f"shared-expenses/connections/{connection_id}/")
    return json.dumps(response, indent=2)


@mcp.tool()
def nudge_connection(connection_id: int) -> str:
    """Send A Nudge Reminder To A Connected User About Outstanding Balances.

    Rate Limited To One Nudge Per User Per 24 Hours.

    Args:
        connection_id: The ID Of The Connection To Nudge.

    Returns Confirmation That The Nudge Was Sent.
    """
    response = api_request("POST", f"shared-expenses/connections/{connection_id}/nudge/")
    return json.dumps(response, indent=2)


@mcp.tool()
def create_split(
    date: str,
    total_amount: str,
    category: int,
    title: str,
    participants: list[dict],
    subtitle: Optional[str] = None,
) -> str:
    """Create A New Shared Expense Split.

    Creates A Transaction For The Payer And Mirror Transactions For Each Participant.
    Automatically Updates Budgets And Connection Balances.

    Args:
        date: Transaction Date In YYYY-MM-DD Format.
        total_amount: Total Amount As A Decimal String (e.g. "42.50").
        category: Category ID (1-25).
        title: Short Title For The Expense (Max 100 Chars).
        participants: List Of Participant Dicts, Each With "user_id" (int) And "share_amount" (str).
            Must Include The Payer. Example: [{"user_id": 1, "share_amount": "21.25"}, {"user_id": 2, "share_amount": "21.25"}]
        subtitle: Optional Description (Max 100 Chars).

    Returns The Created Split With Participant Details.
    """
    payload = {
        "date": date,
        "total_amount": total_amount,
        "category": category,
        "title": title,
        "participants": participants,
    }
    if subtitle is not None:
        payload["subtitle"] = subtitle
    response = api_request("POST", "shared-expenses/splits/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def settle_split(split_id: int, user_id: int) -> str:
    """Mark A Split Participant As Settled (Paid).

    Sets The settled_at Timestamp And Adjusts The Connection Balance.

    Args:
        split_id: The ID Of The Split.
        user_id: The ID Of The Participant To Mark As Settled.

    Returns The Updated Settlement Status.
    """
    payload = {"user_id": user_id}
    response = api_request("POST", f"shared-expenses/splits/{split_id}/settle/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def unsettle_split(split_id: int, user_id: int) -> str:
    """Unsettle A Previously Settled Split Participant.

    Clears The settled_at Timestamp And Reverses The Connection Balance Adjustment.

    Args:
        split_id: The ID Of The Split.
        user_id: The ID Of The Participant To Unsettle.

    Returns The Updated Settlement Status.
    """
    payload = {"user_id": user_id}
    response = api_request("POST", f"shared-expenses/splits/{split_id}/unsettle/", data=payload)
    return json.dumps(response, indent=2)


@mcp.tool()
def delete_split(split_id: int) -> str:
    """Delete A Shared Expense Split.

    If You Are The Payer, All Mirror Transactions Are Deleted.
    If You Are A Participant, Only Your Mirror Transaction Is Removed.

    Args:
        split_id: The ID Of The Split To Delete.

    Returns Confirmation Of Deletion.
    """
    response = api_request("DELETE", f"shared-expenses/splits/{split_id}/")
    return json.dumps(response, indent=2)


@mcp.tool()
def create_settlement(
    to_user_id: int,
    amount: str,
    note: Optional[str] = None,
) -> str:
    """Record A Bulk Settlement Payment To A Connected User.

    Creates A Settlement Record, Adjusts The Connection Balance, And Marks All
    Outstanding Split Participants Between You As Settled.

    Args:
        to_user_id: The ID Of The User You Are Settling With.
        amount: Settlement Amount As A Decimal String (e.g. "100.00").
        note: Optional Note (e.g. "E-Transfer", "Cash").

    Returns The Created Settlement Details.
    """
    payload = {
        "to_user_id": to_user_id,
        "amount": amount,
    }
    if note is not None:
        payload["note"] = note
    response = api_request("POST", "shared-expenses/settlements/", data=payload)
    return json.dumps(response, indent=2)


# ---------------------------------------------------------------------------
# Auto-update
# ---------------------------------------------------------------------------


def _auto_update():
    """Check For A Newer Version Of This File And Update If Available."""
    try:
        server_path = os.path.abspath(__file__)
        with open(server_path, 'rb') as f:
            local_hash = hashlib.sha256(f.read()).hexdigest()

        version_url = f"{API_URL}profiles/mcp-server/version/"
        resp = requests.get(version_url, timeout=5)
        if resp.status_code != 200:
            return

        remote_hash = resp.json().get('hash')
        if not remote_hash or remote_hash == local_hash:
            return

        download_url = f"{API_URL}profiles/mcp-server/download/"
        resp = requests.get(download_url, timeout=10)
        if resp.status_code != 200:
            return

        with open(server_path, 'wb') as f:
            f.write(resp.content)

        # Re-exec so the new code is what actually runs.
        os.execv(sys.executable, [sys.executable, server_path])
    except Exception:
        # Update is best-effort — never block startup.
        pass


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    _auto_update()
    mcp.run()
