Initial commit: Monarch MCP Custom SSE server
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Monarch Money Credentials
|
||||||
|
# You can use MONARCH_TOKEN (recommended) OR Email/Password
|
||||||
|
MONARCH_TOKEN=
|
||||||
|
|
||||||
|
# Fallback credentials
|
||||||
|
MONARCH_EMAIL=
|
||||||
|
MONARCH_PASSWORD=
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=8000
|
||||||
|
LOG_LEVEL=INFO
|
||||||
30
.gitea/workflows/build.yaml
Normal file
30
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build and Push Monarch MCP Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.ext.ben.io
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.CR_PAT }}
|
||||||
|
|
||||||
|
- name: Build and Push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: gitea.ext.ben.io/${{ gitea.repository }}:latest
|
||||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Stage 1: Builder
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install uv for fast dependency management
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# Copy only requirements first to leverage Docker cache
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies into the system python environment of the builder
|
||||||
|
RUN uv pip install --system -r requirements.txt
|
||||||
|
|
||||||
|
# Stage 2: Final Image
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install curl for health checks
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy installed packages from builder
|
||||||
|
COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
|
||||||
|
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY src/ ./src/
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY README.md .
|
||||||
|
|
||||||
|
# Set Python path to find the package
|
||||||
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
|
# Default port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
CMD ["python", "src/monarch_mcp_custom/server.py"]
|
||||||
41
README.md
Normal file
41
README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Monarch Money Custom MCP Server
|
||||||
|
|
||||||
|
A custom Model Context Protocol (MCP) server for Monarch Money, designed for Docker deployment with SSE (Server-Sent Events) support.
|
||||||
|
|
||||||
|
## 🚀 Setup
|
||||||
|
|
||||||
|
### 1. Obtain Authentication Token
|
||||||
|
Run the setup script locally to authenticate and generate a token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python login_setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the prompts to log in. Once successful, copy the `MONARCH_TOKEN` printed in the terminal.
|
||||||
|
|
||||||
|
### 2. Configure Environment
|
||||||
|
Create a `.env` file based on `.env.example` and paste your token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env and set MONARCH_TOKEN=your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy with Docker
|
||||||
|
Start the server using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Connection
|
||||||
|
The server will be available at:
|
||||||
|
- **SSE Endpoint**: `http://localhost:8000/mcp/sse`
|
||||||
|
- **Health Check**: `http://localhost:8000/health`
|
||||||
|
|
||||||
|
## 🛠️ Tools Included
|
||||||
|
- `get_accounts`: View all financial accounts.
|
||||||
|
- `get_transactions`: Fetch recent transactions with filtering.
|
||||||
|
- `get_budgets`: View budget status.
|
||||||
|
- `get_account_holdings`: Detailed investment holdings.
|
||||||
|
- `refresh_accounts`: Trigger a refresh of account data.
|
||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
monarch-mcp:
|
||||||
|
image: gitea.ext.ben.io/b3nw/monarch-mcp-custom:latest
|
||||||
|
container_name: monarch-mcp-custom
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
restart: unless-stopped
|
||||||
64
login_setup.py
Normal file
64
login_setup.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Interactive Monarch Money login with MFA support.
|
||||||
|
Saves session securely and provides the token for Docker environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import getpass
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add src to sys.path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||||
|
|
||||||
|
from monarchmoney import MonarchMoney, RequireMFAException
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from monarch_mcp_custom.auth import save_token
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
print("\n🏦 Monarch Money - Custom MCP Setup")
|
||||||
|
print("=" * 45)
|
||||||
|
print("This script will help you obtain an authentication token")
|
||||||
|
print("for use in your Docker .env file.\n")
|
||||||
|
|
||||||
|
mm = MonarchMoney()
|
||||||
|
|
||||||
|
try:
|
||||||
|
email = input("Email: ")
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await mm.login(email, password, use_saved_session=False, save_session=True)
|
||||||
|
print("✅ Login successful!")
|
||||||
|
except RequireMFAException:
|
||||||
|
print("🔐 MFA code required")
|
||||||
|
mfa_code = input("Two Factor Code: ")
|
||||||
|
await mm.multi_factor_authenticate(email, password, mfa_code)
|
||||||
|
print("✅ MFA authentication successful")
|
||||||
|
|
||||||
|
token = mm.token
|
||||||
|
if token:
|
||||||
|
print("\n" + "!" * 50)
|
||||||
|
print("🔑 YOUR MONARCH_TOKEN:")
|
||||||
|
print(f"\n{token}\n")
|
||||||
|
print("!" * 50)
|
||||||
|
print("\nCopy the token above into your .env file as:")
|
||||||
|
print(f"MONARCH_TOKEN={token}")
|
||||||
|
|
||||||
|
# Also save to local keyring for convenience
|
||||||
|
save_token(token)
|
||||||
|
print("\n✅ Token also saved to local system keyring.")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to retrieve token from MonarchMoney instance.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Setup failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
33
pyproject.toml
Normal file
33
pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "monarch-mcp-custom"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Custom Monarch Money MCP Server with SSE support"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [
|
||||||
|
{ name = "opencode" }
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"mcp[cli]>=1.0.0",
|
||||||
|
"monarchmoney>=0.1.15",
|
||||||
|
"gql>=3.4,<4.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
"pydantic>=2.0.0",
|
||||||
|
"starlette>=0.35.0",
|
||||||
|
"uvicorn>=0.27.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
monarch-mcp-custom = "monarch_mcp_custom.server:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mcp[cli]>=1.0.0
|
||||||
|
monarchmoney>=0.1.15
|
||||||
|
gql>=3.4,<4.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
pydantic>=2.0.0
|
||||||
|
starlette>=0.35.0
|
||||||
|
uvicorn>=0.27.0
|
||||||
60
src/monarch_mcp_custom.egg-info/PKG-INFO
Normal file
60
src/monarch_mcp_custom.egg-info/PKG-INFO
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: monarch-mcp-custom
|
||||||
|
Version: 0.1.0
|
||||||
|
Summary: Custom Monarch Money MCP Server with SSE support
|
||||||
|
Author: opencode
|
||||||
|
License: MIT
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Requires-Python: >=3.12
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
Requires-Dist: mcp[cli]>=1.0.0
|
||||||
|
Requires-Dist: monarchmoney>=0.1.15
|
||||||
|
Requires-Dist: gql<4.0,>=3.4
|
||||||
|
Requires-Dist: keyring>=24.0.0
|
||||||
|
Requires-Dist: python-dotenv>=1.0.0
|
||||||
|
Requires-Dist: pydantic>=2.0.0
|
||||||
|
Requires-Dist: starlette>=0.35.0
|
||||||
|
Requires-Dist: uvicorn>=0.27.0
|
||||||
|
|
||||||
|
# Monarch Money Custom MCP Server
|
||||||
|
|
||||||
|
A custom Model Context Protocol (MCP) server for Monarch Money, designed for Docker deployment with SSE (Server-Sent Events) support.
|
||||||
|
|
||||||
|
## 🚀 Setup
|
||||||
|
|
||||||
|
### 1. Obtain Authentication Token
|
||||||
|
Run the setup script locally to authenticate and generate a token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python login_setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the prompts to log in. Once successful, copy the `MONARCH_TOKEN` printed in the terminal.
|
||||||
|
|
||||||
|
### 2. Configure Environment
|
||||||
|
Create a `.env` file based on `.env.example` and paste your token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env and set MONARCH_TOKEN=your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy with Docker
|
||||||
|
Start the server using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Connection
|
||||||
|
The server will be available at:
|
||||||
|
- **SSE Endpoint**: `http://localhost:8000/mcp/sse`
|
||||||
|
- **Health Check**: `http://localhost:8000/health`
|
||||||
|
|
||||||
|
## 🛠️ Tools Included
|
||||||
|
- `get_accounts`: View all financial accounts.
|
||||||
|
- `get_transactions`: Fetch recent transactions with filtering.
|
||||||
|
- `get_budgets`: View budget status.
|
||||||
|
- `get_account_holdings`: Detailed investment holdings.
|
||||||
|
- `refresh_accounts`: Trigger a refresh of account data.
|
||||||
11
src/monarch_mcp_custom.egg-info/SOURCES.txt
Normal file
11
src/monarch_mcp_custom.egg-info/SOURCES.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
src/monarch_mcp_custom/__init__.py
|
||||||
|
src/monarch_mcp_custom/auth.py
|
||||||
|
src/monarch_mcp_custom/server.py
|
||||||
|
src/monarch_mcp_custom.egg-info/PKG-INFO
|
||||||
|
src/monarch_mcp_custom.egg-info/SOURCES.txt
|
||||||
|
src/monarch_mcp_custom.egg-info/dependency_links.txt
|
||||||
|
src/monarch_mcp_custom.egg-info/entry_points.txt
|
||||||
|
src/monarch_mcp_custom.egg-info/requires.txt
|
||||||
|
src/monarch_mcp_custom.egg-info/top_level.txt
|
||||||
1
src/monarch_mcp_custom.egg-info/dependency_links.txt
Normal file
1
src/monarch_mcp_custom.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
src/monarch_mcp_custom.egg-info/entry_points.txt
Normal file
2
src/monarch_mcp_custom.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
monarch-mcp-custom = monarch_mcp_custom.server:main
|
||||||
8
src/monarch_mcp_custom.egg-info/requires.txt
Normal file
8
src/monarch_mcp_custom.egg-info/requires.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mcp[cli]>=1.0.0
|
||||||
|
monarchmoney>=0.1.15
|
||||||
|
gql<4.0,>=3.4
|
||||||
|
keyring>=24.0.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
pydantic>=2.0.0
|
||||||
|
starlette>=0.35.0
|
||||||
|
uvicorn>=0.27.0
|
||||||
1
src/monarch_mcp_custom.egg-info/top_level.txt
Normal file
1
src/monarch_mcp_custom.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
monarch_mcp_custom
|
||||||
1
src/monarch_mcp_custom/__init__.py
Normal file
1
src/monarch_mcp_custom/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.1.0"
|
||||||
BIN
src/monarch_mcp_custom/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/monarch_mcp_custom/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/monarch_mcp_custom/__pycache__/auth.cpython-313.pyc
Normal file
BIN
src/monarch_mcp_custom/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
70
src/monarch_mcp_custom/auth.py
Normal file
70
src/monarch_mcp_custom/auth.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Authentication and session management for Monarch Money.
|
||||||
|
Prioritizes environment variables for Docker compatibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from monarchmoney import MonarchMoney
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Loads the authentication token.
|
||||||
|
Checks MONARCH_TOKEN environment variable first.
|
||||||
|
"""
|
||||||
|
# 1. Check environment variable (Best for Docker)
|
||||||
|
token = os.getenv("MONARCH_TOKEN")
|
||||||
|
if token:
|
||||||
|
logger.info("✅ Token loaded from MONARCH_TOKEN environment variable")
|
||||||
|
return token
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_token(token: str) -> None:
|
||||||
|
"""Saves the token to the system keyring if available."""
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
|
||||||
|
KEYRING_SERVICE = "com.mcp.monarch-mcp-server"
|
||||||
|
KEYRING_USERNAME = "monarch-token"
|
||||||
|
keyring.set_password(KEYRING_SERVICE, KEYRING_USERNAME, token)
|
||||||
|
logger.info("✅ Token saved securely to keyring")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Failed to save token to keyring (non-fatal): {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_authenticated_client() -> MonarchMoney:
|
||||||
|
"""
|
||||||
|
Returns an authenticated MonarchMoney client.
|
||||||
|
Raises RuntimeError if no authentication is found.
|
||||||
|
"""
|
||||||
|
token = load_token()
|
||||||
|
if token:
|
||||||
|
try:
|
||||||
|
# The monarchmoney library supports passing the token directly
|
||||||
|
return MonarchMoney(token=token)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to initialize MonarchMoney with token: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Fallback to email/password if token is missing (only if both are present)
|
||||||
|
email = os.getenv("MONARCH_EMAIL")
|
||||||
|
password = os.getenv("MONARCH_PASSWORD")
|
||||||
|
if email and password:
|
||||||
|
try:
|
||||||
|
mm = MonarchMoney()
|
||||||
|
await mm.login(email, password)
|
||||||
|
logger.info("✅ Logged in using email/password credentials")
|
||||||
|
return mm
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Login failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"🔐 Authentication required. Please provide MONARCH_TOKEN or run login_setup.py"
|
||||||
|
)
|
||||||
200
src/monarch_mcp_custom/server.py
Normal file
200
src/monarch_mcp_custom/server.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
"""
|
||||||
|
Monarch Money MCP Server - Custom SSE Implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.routing import Route, Mount
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from monarch_mcp_custom.auth import get_authenticated_client
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize FastMCP
|
||||||
|
mcp = FastMCP("Monarch Money Custom")
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_json(data: Any) -> str:
|
||||||
|
"""Helper to serialize data to JSON safely."""
|
||||||
|
return json.dumps(data, indent=2, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
# --- MCP Tools ---
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_accounts() -> str:
|
||||||
|
"""Get all financial accounts from Monarch Money."""
|
||||||
|
try:
|
||||||
|
client = await get_authenticated_client()
|
||||||
|
accounts = await client.get_accounts()
|
||||||
|
|
||||||
|
account_list = []
|
||||||
|
for account in accounts.get("accounts", []):
|
||||||
|
account_info = {
|
||||||
|
"id": account.get("id"),
|
||||||
|
"name": account.get("displayName") or account.get("name"),
|
||||||
|
"type": (account.get("type") or {}).get("name"),
|
||||||
|
"balance": account.get("currentBalance"),
|
||||||
|
"institution": (account.get("institution") or {}).get("name"),
|
||||||
|
"is_active": not account.get("deactivatedAt"),
|
||||||
|
}
|
||||||
|
account_list.append(account_info)
|
||||||
|
|
||||||
|
return serialize_json(account_list)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get accounts: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_transactions(
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
start_date: Optional[str] = None,
|
||||||
|
end_date: Optional[str] = None,
|
||||||
|
account_id: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get transactions from Monarch Money.
|
||||||
|
Dates should be in YYYY-MM-DD format.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = await get_authenticated_client()
|
||||||
|
filters = {}
|
||||||
|
if start_date:
|
||||||
|
filters["start_date"] = start_date
|
||||||
|
if end_date:
|
||||||
|
filters["end_date"] = end_date
|
||||||
|
if account_id:
|
||||||
|
filters["account_id"] = account_id
|
||||||
|
|
||||||
|
transactions = await client.get_transactions(
|
||||||
|
limit=limit, offset=offset, **filters
|
||||||
|
)
|
||||||
|
|
||||||
|
results = transactions.get("allTransactions", {}).get("results", [])
|
||||||
|
formatted = []
|
||||||
|
for txn in results:
|
||||||
|
formatted.append(
|
||||||
|
{
|
||||||
|
"id": txn.get("id"),
|
||||||
|
"date": txn.get("date"),
|
||||||
|
"amount": txn.get("amount"),
|
||||||
|
"description": txn.get("description"),
|
||||||
|
"category": (txn.get("category") or {}).get("name"),
|
||||||
|
"account": (txn.get("account") or {}).get("displayName"),
|
||||||
|
"merchant": (txn.get("merchant") or {}).get("name"),
|
||||||
|
"is_pending": txn.get("isPending", False),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return serialize_json(formatted)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get transactions: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_budgets() -> str:
|
||||||
|
"""Get current budget information."""
|
||||||
|
try:
|
||||||
|
client = await get_authenticated_client()
|
||||||
|
budgets = await client.get_budgets()
|
||||||
|
|
||||||
|
budget_list = []
|
||||||
|
for b in budgets.get("budgets", []):
|
||||||
|
budget_list.append(
|
||||||
|
{
|
||||||
|
"name": b.get("name"),
|
||||||
|
"amount": b.get("amount"),
|
||||||
|
"spent": b.get("spent"),
|
||||||
|
"remaining": b.get("remaining"),
|
||||||
|
"category": (b.get("category") or {}).get("name"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return serialize_json(budget_list)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get budgets: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_account_holdings(account_id: str) -> str:
|
||||||
|
"""Get investment holdings for a specific account."""
|
||||||
|
try:
|
||||||
|
client = await get_authenticated_client()
|
||||||
|
# Ensure account_id is treated correctly (usually string ID in Monarch)
|
||||||
|
holdings = await client.get_account_holdings(account_id)
|
||||||
|
return serialize_json(holdings)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get holdings: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def refresh_accounts() -> str:
|
||||||
|
"""Request a refresh of account data from financial institutions."""
|
||||||
|
try:
|
||||||
|
client = await get_authenticated_client()
|
||||||
|
result = await client.request_accounts_refresh()
|
||||||
|
return serialize_json(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to refresh accounts: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Health Check ---
|
||||||
|
|
||||||
|
|
||||||
|
async def health_check(request):
|
||||||
|
"""Simple health check endpoint."""
|
||||||
|
return JSONResponse({"status": "ok", "timestamp": datetime.now().isoformat()})
|
||||||
|
|
||||||
|
|
||||||
|
# --- ASGI App Setup ---
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
"""Create the Starlette application with MCP mounted at /mcp."""
|
||||||
|
mcp_app = mcp.http_app()
|
||||||
|
|
||||||
|
routes = [
|
||||||
|
Route("/health", health_check, methods=["GET"]),
|
||||||
|
Mount("/mcp", app=mcp_app),
|
||||||
|
]
|
||||||
|
|
||||||
|
return Starlette(routes=routes, lifespan=mcp_app.lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for running the server."""
|
||||||
|
port = int(os.getenv("PORT", 8000))
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user