diff --git a/src/lampyrid/models/lampyrid_models.py b/src/lampyrid/models/lampyrid_models.py index 8410a8f..b964110 100644 --- a/src/lampyrid/models/lampyrid_models.py +++ b/src/lampyrid/models/lampyrid_models.py @@ -23,10 +23,14 @@ def utc_now(): class Account(BaseModel): - id: str = Field(..., examples=['2']) - name: str = Field(..., examples=['Cash']) - currency_code: Optional[str] = Field(None, examples=['GBP']) - current_balance: Optional[float] = Field(None, examples=[1000.0]) + id: str = Field(..., description='Unique identifier for the account', examples=['2']) + name: str = Field(..., description='Display name of the account', examples=['Cash']) + currency_code: Optional[str] = Field( + None, description='Currency code (ISO 4217) for the account', examples=['GBP'] + ) + current_balance: Optional[float] = Field( + None, description='Current account balance as a decimal number', examples=[1000.0] + ) @classmethod def from_account_read(cls, account_read: 'AccountRead') -> 'Account': @@ -44,11 +48,21 @@ def from_account_read(cls, account_read: 'AccountRead') -> 'Account': class Budget(BaseModel): - id: str = Field(..., examples=['2']) - name: str = Field(..., examples=['Groceries']) - active: Optional[bool] = Field(None, examples=[True]) - notes: Optional[str] = Field(None, examples=['Monthly grocery budget']) - order: Optional[int] = Field(None, examples=[1]) + id: str = Field(..., description='Unique identifier for the budget', examples=['2']) + name: str = Field(..., description='Display name of the budget', examples=['Groceries']) + active: Optional[bool] = Field( + None, + description='Whether this budget is currently active for new transactions', + examples=[True], + ) + notes: Optional[str] = Field( + None, + description='Optional notes or description about this budget', + examples=['Monthly grocery budget'], + ) + order: Optional[int] = Field( + None, description='Display order for sorting budgets', examples=[1] + ) @classmethod def from_budget_read(cls, budget_read: 'BudgetRead') -> 'Budget': @@ -145,106 +159,157 @@ def to_transaction_split_store(self) -> TransactionSplitStore: class ListAccountRequest(BaseModel): - type: AccountTypeFilter = Field(..., description='Type of account to filter by') + type: AccountTypeFilter = Field( + ..., + description='Account type: asset (your accounts), expense (spending categories), revenue (income sources), liability (debts), or all', + ) class SearchAccountRequest(BaseModel): - query: str = Field(..., description='Search query for account names') + query: str = Field( + ..., description='Text to search for in account names (supports partial matching)' + ) type: AccountTypeFilter = Field( - AccountTypeFilter.all, description='Optional type filter for accounts' + AccountTypeFilter.all, + description='Limit search to specific account type (asset, expense, revenue, liability, or all)', ) class GetAccountRequest(BaseModel): - id: str = Field(..., description='ID of the account to retrieve') + id: str = Field( + ..., description='Unique identifier of the account (from list_accounts or search_accounts)' + ) class CreateWithdrawalRequest(BaseModel): - amount: float = Field(..., description='Amount of the withdrawal') - description: str = Field(..., description='Description of the withdrawal') - date: datetime = Field(default_factory=utc_now, description='Date and time of the withdrawal') + amount: float = Field( + ..., description='Amount to withdraw as positive number (e.g., 25.50 for $25.50 expense)' + ) + description: str = Field( + ..., description='What this expense was for (e.g., "Grocery shopping at Whole Foods")' + ) + date: datetime = Field( + default_factory=utc_now, + description='When the expense occurred (defaults to current time if not specified)', + ) source_id: str = Field( ..., - description='ID of the source account for the withdrawal. This must always be an asset account.', + description='ID of your account the money comes from (checking, savings, cash, etc.). Must be an asset account you own.', ) destination_name: Optional[str] = Field( default=None, - description='Name of the destination account for the withdrawal. This account is automatically created if it does not exist. Leave it blank for cash withdrawals.', + description='Where the money went ("Groceries", "Gas Station", "ATM"). Creates expense account if new. Leave blank for cash withdrawals.', ) budget_id: Optional[str] = Field( - None, description='ID of the budget to allocate this withdrawal to' + None, description='Budget to track this expense against (from list_budgets)' ) budget_name: Optional[str] = Field( None, - description='Name of the budget to allocate this withdrawal to. If the budget name is unknown, the ID will be used or the value will be ignored.', + description='Name of budget if ID is unknown. Will use ID if both provided.', ) class CreateDepositRequest(BaseModel): - amount: float = Field(..., description='Amount of the deposit') - description: str = Field(..., description='Description of the deposit') - date: datetime = Field(default_factory=utc_now, description='Date and time of the deposit') + amount: float = Field( + ..., description='Amount received as positive number (e.g., 2500.00 for $2500 salary)' + ) + description: str = Field( + ..., description='What this income was for (e.g., "Monthly salary", "Freelance payment")' + ) + date: datetime = Field( + default_factory=utc_now, + description='When the income was received (defaults to current time if not specified)', + ) source_name: Optional[str] = Field( default=None, - description='Name of the source account for the deposit. This account is automatically created if it does not exist.', + description='Where the money came from ("Employer", "Client Name", "Gift"). Creates revenue account if new.', ) destination_id: str = Field( ..., - description='ID of the destination account for the deposit. This must always be an asset account.', + description='ID of your account receiving the money (checking, savings, etc.). Must be an asset account you own.', ) class CreateTransferRequest(BaseModel): - amount: float = Field(..., description='Amount of the transfer') - description: str = Field(..., description='Description of the transfer') - date: datetime = Field(default_factory=utc_now, description='Date and time of the transfer') + amount: float = Field( + ..., description='Amount to move as positive number (e.g., 500.00 to move $500)' + ) + description: str = Field( + ..., + description='Purpose of the transfer (e.g., "Transfer to savings", "Credit card payment")', + ) + date: datetime = Field( + default_factory=utc_now, + description='When the transfer occurred (defaults to current time if not specified)', + ) source_id: str = Field( ..., - description='ID of the source account for the transfer. This must always be an asset account.', + description='ID of your account the money comes from. Must be an asset account you own.', ) destination_id: str = Field( ..., - description='ID of the destination account for the transfer. This must always be an asset account.', + description='ID of your account receiving the money. Must be an asset account you own.', ) class GetTransactionsRequest(BaseModel): start_date: Optional[date] = Field( - None, description='Start date for transaction range (YYYY-MM-DD), inclusive' + None, + description='Start date for filtering transactions (YYYY-MM-DD format). If not specified, returns recent transactions.', ) end_date: Optional[date] = Field( - None, description='End date for transaction range (YYYY-MM-DD), inclusive' + None, + description='End date for filtering transactions (YYYY-MM-DD format). If not specified, returns up to current date.', ) transaction_type: Optional[TransactionTypeFilter] = Field( - None, description='Optional filter on transaction type' + None, + description='Filter by transaction type: withdrawal (expenses), deposit (income), or transfer (between accounts)', + ) + page: Optional[int] = Field( + 1, + description='Page number to retrieve (1-based). Use for browsing large result sets.', + ge=1, + ) + limit: Optional[int] = Field( + 50, description='Maximum number of transactions to return per page (1-500)', ge=1, le=500 ) - page: Optional[int] = Field(1, description='Page number for pagination', ge=1) - limit: Optional[int] = Field(50, description='Number of items per page', ge=1, le=500) class SearchTransactionsRequest(BaseModel): query: str = Field( - ..., description='Search query to find transactions (e.g., "groceries", "salary")' + ..., + description='Text to search for in transaction descriptions, account names, and other transaction fields', + ) + page: Optional[int] = Field( + 1, + description='Page number to retrieve (1-based). Use for browsing large result sets.', + ge=1, + ) + limit: Optional[int] = Field( + 50, description='Maximum number of transactions to return per page (1-500)', ge=1, le=500 ) - page: Optional[int] = Field(1, description='Page number for pagination', ge=1) - limit: Optional[int] = Field(50, description='Number of items per page', ge=1, le=500) class DeleteTransactionRequest(BaseModel): - id: str = Field(..., description='ID of the transaction to delete') + id: str = Field(..., description='Unique identifier of the transaction to permanently remove') class GetTransactionRequest(BaseModel): - id: str = Field(..., description='ID of the transaction to retrieve') + id: str = Field(..., description='Unique identifier of the transaction to get details for') class TransactionListResponse(BaseModel): """Response model for transaction listings.""" - transactions: List[Transaction] = Field(..., description='List of transactions') - total_count: Optional[int] = Field(None, description='Total number of transactions available') - current_page: int = Field(..., description='Current page number') - per_page: int = Field(..., description='Number of items per page') + transactions: List[Transaction] = Field( + ..., description='Array of transaction objects matching the request' + ) + total_count: Optional[int] = Field( + None, + description='Total transactions available across all pages (if pagination metadata available)', + ) + current_page: int = Field(..., description='Current page number in the result set') + per_page: int = Field(..., description='Number of transactions included in this page') @classmethod def from_transaction_array( @@ -267,30 +332,44 @@ def from_transaction_array( class ListBudgetsRequest(BaseModel): """Request for listing budgets.""" - active: Optional[bool] = Field(None, description='Filter budgets by active status') + active: Optional[bool] = Field( + None, + description='Show only active budgets (true), inactive budgets (false), or all budgets (not specified)', + ) class GetBudgetRequest(BaseModel): """Request for getting a single budget by ID.""" - id: str = Field(..., description='ID of the budget to retrieve') + id: str = Field(..., description='Unique identifier of the budget to get details for') class BudgetSpending(BaseModel): """Budget spending information for a specific period.""" - budget_id: str = Field(..., description='ID of the budget') - budget_name: str = Field(..., description='Name of the budget') - spent: float = Field(..., description='Amount spent in this budget during the period') - budgeted: Optional[float] = Field(None, description='Budgeted amount for this period') - remaining: Optional[float] = Field(None, description='Remaining budget amount') - percentage_spent: Optional[float] = Field(None, description='Percentage of budget spent') + budget_id: str = Field(..., description='Unique identifier of the budget') + budget_name: str = Field(..., description='Display name of the budget') + spent: float = Field( + ..., description='Total amount spent from this budget in the specified period' + ) + budgeted: Optional[float] = Field( + None, description='Amount allocated to this budget for the period' + ) + remaining: Optional[float] = Field( + None, description='Money left in this budget (budgeted minus spent)' + ) + percentage_spent: Optional[float] = Field( + None, + description='Percentage of allocated budget used (0-100+, can exceed 100 if overspent)', + ) class GetBudgetSpendingRequest(BaseModel): """Request for getting budget spending data.""" - budget_id: str = Field(..., description='ID of the budget to get spending for') + budget_id: str = Field( + ..., description='Unique identifier of the budget to analyze spending for' + ) start_date: Optional[date] = Field( None, description='Start date for spending period (YYYY-MM-DD), inclusive' ) @@ -302,12 +381,15 @@ class GetBudgetSpendingRequest(BaseModel): class BudgetSummary(BaseModel): """Summary of all budgets with spending information.""" - budgets: List[BudgetSpending] = Field(..., description='List of budget spending data') - total_budgeted: Optional[float] = Field(None, description='Total budgeted amount') - total_spent: float = Field(..., description='Total amount spent across all budgets') - total_remaining: Optional[float] = Field(None, description='Total remaining budget') + budgets: List[BudgetSpending] = Field(..., description='Spending analysis for each budget') + total_budgeted: Optional[float] = Field(None, description='Sum of all allocated budget amounts') + total_spent: float = Field(..., description='Sum of all spending across budgets') + total_remaining: Optional[float] = Field( + None, description='Total money left across all budgets' + ) available_budget: Optional[float] = Field( - None, description='Available budget amount not allocated to specific budgets' + None, + description='Unallocated money available for new budgets or unexpected expenses', ) @@ -325,20 +407,22 @@ class GetBudgetSummaryRequest(BaseModel): class AvailableBudget(BaseModel): """Available budget information for a period.""" - amount: float = Field(..., description='Available budget amount') - currency_code: str = Field(..., description='Currency code for the amount') - start_date: date = Field(..., description='Start date of the budget period') - end_date: date = Field(..., description='End date of the budget period') + amount: float = Field(..., description='Total unallocated budget available for the period') + currency_code: str = Field(..., description='Currency code (ISO 4217) for the budget amount') + start_date: date = Field(..., description='Beginning of the budget period this amount covers') + end_date: date = Field(..., description='End of the budget period this amount covers') class GetAvailableBudgetRequest(BaseModel): """Request for getting available budget.""" start_date: Optional[date] = Field( - None, description='Start date for budget period (YYYY-MM-DD), defaults to current month' + None, + description='Start date for budget analysis (YYYY-MM-DD format). Defaults to beginning of current month.', ) end_date: Optional[date] = Field( - None, description='End date for budget period (YYYY-MM-DD), defaults to current month' + None, + description='End date for budget analysis (YYYY-MM-DD format). Defaults to end of current month.', ) @@ -356,16 +440,26 @@ class CreateBulkTransactionsRequest(BaseModel): class UpdateTransactionRequest(BaseModel): """Update an existing transaction.""" - transaction_id: str = Field(..., description='ID of the transaction to update') - amount: Optional[float] = Field(None, description='New amount for the transaction') - description: Optional[str] = Field(None, description='New description for the transaction') - date: Optional[datetime] = Field(None, description='New date for the transaction') - source_id: Optional[str] = Field(None, description='New source account ID') - destination_id: Optional[str] = Field(None, description='New destination account ID') + transaction_id: str = Field(..., description='Unique identifier of the transaction to modify') + amount: Optional[float] = Field(None, description='New transaction amount (positive number)') + description: Optional[str] = Field( + None, description='New description for what the transaction was for' + ) + date: Optional[datetime] = Field( + None, description='New date/time when the transaction occurred' + ) + source_id: Optional[str] = Field( + None, description='New source account ID (where money comes from)' + ) + destination_id: Optional[str] = Field( + None, description='New destination account ID (where money goes to)' + ) budget_id: Optional[str] = Field( - None, description='New budget ID (set to None to clear budget)' + None, description='New budget ID to assign, or None to remove budget assignment' + ) + category_name: Optional[str] = Field( + None, description='New category name for transaction classification' ) - category_name: Optional[str] = Field(None, description='New category name') class BulkUpdateTransactionsRequest(BaseModel): @@ -373,7 +467,7 @@ class BulkUpdateTransactionsRequest(BaseModel): updates: List[UpdateTransactionRequest] = Field( ..., - description='List of transaction updates to apply', + description='Array of transaction modifications to apply in a single operation', min_length=1, max_length=50, ) diff --git a/src/lampyrid/server.py b/src/lampyrid/server.py index bebc310..f4a832f 100644 --- a/src/lampyrid/server.py +++ b/src/lampyrid/server.py @@ -37,7 +37,7 @@ @mcp.tool(tags={'accounts'}) async def list_accounts(req: ListAccountRequest) -> List[Account]: - """List Firefly-III accounts.""" + """Retrieve accounts from Firefly III. Use 'asset' for checking/savings accounts, 'expense' for spending accounts, 'revenue' for income sources. Essential for finding account IDs before creating transactions.""" account_list = await _client.list_accounts(type=req.type) accounts: List[Account] = [ @@ -49,13 +49,13 @@ async def list_accounts(req: ListAccountRequest) -> List[Account]: @mcp.tool(tags={'accounts'}) async def get_account(req: GetAccountRequest) -> Account: - """Get a single Firefly-III account by ID.""" + """Retrieve detailed account information including current balance and currency. Use this to verify account details before transactions.""" return await _client.get_account(req) @mcp.tool(tags={'accounts'}) async def search_accounts(req: SearchAccountRequest) -> List[Account]: - """Search Firefly-III accounts by name.""" + """Find accounts by partial name matching. Useful when you know the account name but not the ID. Supports filtering by account type.""" account_list = await _client.search_accounts(req) accounts: List[Account] = [ @@ -67,41 +67,41 @@ async def search_accounts(req: SearchAccountRequest) -> List[Account]: @mcp.tool(tags={'transactions', 'create'}) async def create_withdrawal(req: CreateWithdrawalRequest) -> Transaction: - """Create a new Firefly-III withdrawal.""" + """Record expenses and spending. Money leaves your asset accounts to pay for goods, services, or cash withdrawals. Can be assigned to budgets for expense tracking.""" transaction = await _client.create_withdrawal(req) return transaction @mcp.tool(tags={'transactions', 'create'}) async def create_deposit(req: CreateDepositRequest) -> Transaction: - """Create a new Firefly-III deposit.""" + """Record income and money received. Represents salary, refunds, gifts, or any money coming into your asset accounts from external sources.""" transaction = await _client.create_deposit(req) return transaction @mcp.tool(tags={'transactions', 'create'}) async def create_transfer(req: CreateTransferRequest) -> Transaction: - """Create a new Firefly-III transfer.""" + """Move money between your own accounts. Use for transferring to savings, paying credit cards from checking, or consolidating funds.""" transaction = await _client.create_transfer(req) return transaction @mcp.tool(tags={'transactions', 'create', 'bulk'}) async def create_bulk_transactions(req: CreateBulkTransactionsRequest) -> List[Transaction]: - """Create multiple transactions at once (can be mixed types: withdrawals, deposits, transfers).""" + """Efficiently create multiple transactions in one operation. Ideal for importing transaction batches, recording monthly bills, or processing CSV data.""" transactions = await _client.create_bulk_transactions(req) return transactions @mcp.tool(tags={'transactions', 'query'}) async def get_transaction(req: GetTransactionRequest) -> Transaction: - """Get a single transaction by ID.""" + """Retrieve complete transaction details. Use this to verify transaction information before updates or to examine specific transactions.""" return await _client.get_transaction(req) @mcp.tool(tags={'transactions', 'query'}) async def get_transactions(req: GetTransactionsRequest) -> TransactionListResponse: - """Get past transactions with optional time range and type filtering.""" + """Retrieve transaction history with flexible filtering and pagination. Essential for financial analysis, spending pattern review, and account activity monitoring.""" transaction_array = await _client.get_transactions(req) return TransactionListResponse.from_transaction_array( @@ -111,7 +111,7 @@ async def get_transactions(req: GetTransactionsRequest) -> TransactionListRespon @mcp.tool(tags={'transactions', 'query'}) async def search_transactions(req: SearchTransactionsRequest) -> TransactionListResponse: - """Search transactions by description or other text fields.""" + """Find transactions by searching text content. Perfect for locating specific purchases, payments, or merchants by description.""" transaction_array = await _client.search_transactions(req) return TransactionListResponse.from_transaction_array( @@ -121,25 +121,25 @@ async def search_transactions(req: SearchTransactionsRequest) -> TransactionList @mcp.tool(tags={'transactions', 'manage'}) async def delete_transaction(req: DeleteTransactionRequest) -> bool: - """Delete a transaction by ID.""" + """Permanently remove a transaction. Use to correct mistakes, remove duplicates, or delete test data. This action cannot be undone.""" return await _client.delete_transaction(req) @mcp.tool(tags={'transactions', 'manage'}) async def update_transaction(req: UpdateTransactionRequest) -> Transaction: - """Update an existing transaction with new values.""" + """Modify transaction details such as amounts, descriptions, dates, accounts, or budget assignments. Useful for correcting imported data or updating incomplete information.""" return await _client.update_transaction(req) @mcp.tool(tags={'transactions', 'manage', 'bulk'}) async def bulk_update_transactions(req: BulkUpdateTransactionsRequest) -> List[Transaction]: - """Update multiple transactions at once.""" + """Efficiently update multiple transactions in one operation. Ideal for batch account changes, budget reassignments, or correcting imported data.""" return await _client.bulk_update_transactions(req) @mcp.tool(tags={'budgets'}) async def list_budgets(req: ListBudgetsRequest) -> List[Budget]: - """List Firefly-III budgets.""" + """Retrieve your budgets for expense tracking and financial planning. Filter by active status to see current or all budgets.""" budget_array = await _client.list_budgets(req) budgets: List[Budget] = [ @@ -151,23 +151,23 @@ async def list_budgets(req: ListBudgetsRequest) -> List[Budget]: @mcp.tool(tags={'budgets'}) async def get_budget(req: GetBudgetRequest) -> Budget: - """Get a single budget by ID.""" + """Retrieve detailed budget information including name, status, and notes. Use this to verify budget details before assigning transactions.""" return await _client.get_budget(req) @mcp.tool(tags={'budgets', 'analysis'}) async def get_budget_spending(req: GetBudgetSpendingRequest) -> BudgetSpending: - """Get budget spending data for a specific budget and period.""" + """Analyze spending against a budget including amount spent, remaining budget, and percentage used. Essential for budget monitoring and overspending alerts.""" return await _client.get_budget_spending(req) @mcp.tool(tags={'budgets', 'analysis'}) async def get_budget_summary(req: GetBudgetSummaryRequest) -> BudgetSummary: - """Get summary of all budgets with spending information.""" + """Comprehensive overview of all budget performance with totals and spending analysis. Perfect for monthly reviews and financial dashboards.""" return await _client.get_budget_summary(req) @mcp.tool(tags={'budgets', 'analysis'}) async def get_available_budget(req: GetAvailableBudgetRequest) -> AvailableBudget: - """Get available budget for a period.""" + """Check unallocated budget available for new budgets or unexpected expenses. Shows money set aside but not assigned to specific budgets.""" return await _client.get_available_budget(req)