Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[project]
dependencies = [
"fastmcp>=2.12.4",
"httpx>=0.28.1",
"fastmcp>=2.14",
"pydantic>=2.11.7",
"pydantic-settings>=2.10.1",
]
Expand Down
45 changes: 32 additions & 13 deletions src/lampyrid/server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from typing import Optional

from fastmcp import FastMCP
Expand All @@ -9,7 +10,7 @@

from .clients.firefly import FireflyClient
from .config import settings
from .tools import register_all_tools
from .tools import compose_all_servers
from .utils import get_assets_path, register_custom_routes


Expand All @@ -33,20 +34,38 @@ def _create_auth_provider() -> Optional[AuthProvider]:
return None


# Initialize FastMCP with optional authentication
auth_provider = _create_auth_provider()
def _initialize_server() -> FastMCP:
"""
Initialize and configure the FastMCP server with all domain servers.

This function:
1. Creates the main FastMCP server with authentication and icons
2. Composes domain-specific servers (accounts, transactions, budgets) using static composition
3. Registers custom HTTP routes

Returns:
Fully configured FastMCP server instance
"""
# Initialize FastMCP with optional authentication
auth_provider = _create_auth_provider()

# Load favicon icon
favicon_icon = Icon(src=Image(path=str(get_assets_path('favicon.png'))).to_data_uri())

server = FastMCP('lampyrid', auth=auth_provider, icons=[favicon_icon])
client = FireflyClient()

# Configure logging
configure_logging(level='DEBUG')

# Load favicon icon
favicon_icon = Icon(src=Image(path=str(get_assets_path('favicon.png'))).to_data_uri())
# Compose all domain servers using static composition (import_server)
asyncio.run(compose_all_servers(server, client))

mcp = FastMCP('lampyrid', auth=auth_provider, icons=[favicon_icon])
_client = FireflyClient()
# Register custom HTTP routes
Comment thread
RadCod3 marked this conversation as resolved.
register_custom_routes(server)

# Configure logging
configure_logging(level='DEBUG')
return server

# Register all MCP tools
register_all_tools(mcp, _client)

# Register custom HTTP routes
register_custom_routes(mcp)
# Create the main MCP server instance
mcp = _initialize_server()
35 changes: 17 additions & 18 deletions src/lampyrid/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
"""
MCP Tools for LamPyrid.

This module coordinates the registration of all MCP tools organized by domain.
This module coordinates the composition of all MCP tool servers organized by domain.
Uses FastMCP's native import_server() for static server composition.
"""

from fastmcp import FastMCP

from ..clients.firefly import FireflyClient
from .accounts import register_tools as register_account_tools
from .budgets import register_tools as register_budget_tools
from .transactions import register_tools as register_transaction_tools
from .accounts import create_accounts_server
from .budgets import create_budgets_server
from .transactions import create_transactions_server


def register_all_tools(mcp: FastMCP, client: FireflyClient) -> None:
async def compose_all_servers(mcp: FastMCP, client: FireflyClient) -> None:
"""
Register all MCP tools with the FastMCP server.

This function coordinates tool registration from all domain modules:
- Account management tools (accounts.py)
- Transaction management tools (transactions.py)
- Budget management tools (budgets.py)
Compose all domain-specific MCP servers into the main server using static composition.

Args:
mcp: The FastMCP server instance
mcp: The main FastMCP server instance
client: The FireflyClient instance for API interactions
"""
# Import registration functions from each tool module

# Register tools from each domain
register_account_tools(mcp, client)
register_transaction_tools(mcp, client)
register_budget_tools(mcp, client)
# Create standalone servers for each domain
accounts_server = create_accounts_server(client)
transactions_server = create_transactions_server(client)
budgets_server = create_budgets_server(client)

# Import all servers into the main server without prefixes (static composition)
await mcp.import_server(accounts_server)
await mcp.import_server(transactions_server)
await mcp.import_server(budgets_server)
17 changes: 11 additions & 6 deletions src/lampyrid/tools/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
)


def register_tools(mcp: FastMCP, client: FireflyClient) -> None:
def create_accounts_server(client: FireflyClient) -> FastMCP:
"""
Register account management tools with the FastMCP server.
Create a standalone FastMCP server for account management tools.

Args:
mcp: The FastMCP server instance
client: The FireflyClient instance for API interactions

Returns:
FastMCP server instance with account management tools registered
"""
accounts_mcp = FastMCP('accounts')

@mcp.tool(tags={'accounts'})
@accounts_mcp.tool(tags={'accounts'})
async def list_accounts(req: ListAccountRequest) -> List[Account]:
"""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)
Expand All @@ -38,12 +41,12 @@ async def list_accounts(req: ListAccountRequest) -> List[Account]:

return accounts

@mcp.tool(tags={'accounts'})
@accounts_mcp.tool(tags={'accounts'})
async def get_account(req: GetAccountRequest) -> Account:
"""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'})
@accounts_mcp.tool(tags={'accounts'})
async def search_accounts(req: SearchAccountRequest) -> List[Account]:
"""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)
Expand All @@ -53,3 +56,5 @@ async def search_accounts(req: SearchAccountRequest) -> List[Account]:
]

return accounts

return accounts_mcp
21 changes: 13 additions & 8 deletions src/lampyrid/tools/budgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@
)


def register_tools(mcp: FastMCP, client: FireflyClient) -> None:
def create_budgets_server(client: FireflyClient) -> FastMCP:
"""
Register budget management tools with the FastMCP server.
Create a standalone FastMCP server for budget management tools.

Args:
mcp: The FastMCP server instance
client: The FireflyClient instance for API interactions

Returns:
FastMCP server instance with budget management tools registered
"""
budgets_mcp = FastMCP('budgets')

@mcp.tool(tags={'budgets'})
@budgets_mcp.tool(tags={'budgets'})
async def list_budgets(req: ListBudgetsRequest) -> List[Budget]:
"""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)
Expand All @@ -43,22 +46,24 @@ async def list_budgets(req: ListBudgetsRequest) -> List[Budget]:

return budgets

@mcp.tool(tags={'budgets'})
@budgets_mcp.tool(tags={'budgets'})
async def get_budget(req: GetBudgetRequest) -> Budget:
"""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'})
@budgets_mcp.tool(tags={'budgets', 'analysis'})
async def get_budget_spending(req: GetBudgetSpendingRequest) -> BudgetSpending:
"""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'})
@budgets_mcp.tool(tags={'budgets', 'analysis'})
async def get_budget_summary(req: GetBudgetSummaryRequest) -> BudgetSummary:
"""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'})
@budgets_mcp.tool(tags={'budgets', 'analysis'})
async def get_available_budget(req: GetAvailableBudgetRequest) -> AvailableBudget:
"""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)

return budgets_mcp
31 changes: 18 additions & 13 deletions src/lampyrid/tools/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,48 @@
)


def register_tools(mcp: FastMCP, client: FireflyClient) -> None:
def create_transactions_server(client: FireflyClient) -> FastMCP:
"""
Register transaction management tools with the FastMCP server.
Create a standalone FastMCP server for transaction management tools.

Args:
mcp: The FastMCP server instance
client: The FireflyClient instance for API interactions

Returns:
FastMCP server instance with transaction management tools registered
"""
transactions_mcp = FastMCP('transactions')

@mcp.tool(tags={'transactions', 'create'})
@transactions_mcp.tool(tags={'transactions', 'create'})
async def create_withdrawal(req: CreateWithdrawalRequest) -> Transaction:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'create'})
async def create_deposit(req: CreateDepositRequest) -> Transaction:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'create'})
async def create_transfer(req: CreateTransferRequest) -> Transaction:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'create', 'bulk'})
async def create_bulk_transactions(req: CreateBulkTransactionsRequest) -> List[Transaction]:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'query'})
async def get_transaction(req: GetTransactionRequest) -> Transaction:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'query'})
async def get_transactions(req: GetTransactionsRequest) -> TransactionListResponse:
"""Retrieve transaction history with flexible filtering and pagination. Essential for financial analysis, spending pattern review, and account activity monitoring."""
if req.account_id is not None:
Expand All @@ -76,7 +79,7 @@ async def get_transactions(req: GetTransactionsRequest) -> TransactionListRespon
transaction_array, current_page=req.page or 1, per_page=req.limit or 50
)

@mcp.tool(tags={'transactions', 'query'})
@transactions_mcp.tool(tags={'transactions', 'query'})
async def search_transactions(req: SearchTransactionsRequest) -> TransactionListResponse:
"""Find transactions by searching text content. Perfect for locating specific purchases, payments, or merchants by description."""
transaction_array = await client.search_transactions(req)
Expand All @@ -85,17 +88,19 @@ async def search_transactions(req: SearchTransactionsRequest) -> TransactionList
transaction_array, current_page=req.page or 1, per_page=req.limit or 50
)

@mcp.tool(tags={'transactions', 'manage'})
@transactions_mcp.tool(tags={'transactions', 'manage'})
async def delete_transaction(req: DeleteTransactionRequest) -> bool:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'manage'})
async def update_transaction(req: UpdateTransactionRequest) -> Transaction:
"""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'})
@transactions_mcp.tool(tags={'transactions', 'manage', 'bulk'})
async def bulk_update_transactions(req: BulkUpdateTransactionsRequest) -> List[Transaction]:
"""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)

return transactions_mcp
Loading