Self-hosted P&L tracking app with component-level pricing. Offers: Atlas/Atlas+/Rif/Rif+ with granular cost breakdown. API + MCP + multi-user auth.
495 lines
18 KiB
Python
495 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Vela Platform — MCP Server (stdio transport).
|
|
|
|
Provides tools and resources for interacting with the Vela Platform data.
|
|
Includes component-level pricing breakdown for each offer.
|
|
|
|
Usage:
|
|
python mcp/server.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
|
|
|
from database import SessionLocal
|
|
from models import Service, ServiceComponent, Transaction
|
|
|
|
OPEX = 800.0
|
|
|
|
|
|
def compute_pnl(month: str) -> dict:
|
|
"""Compute P&L for a given month."""
|
|
db = SessionLocal()
|
|
try:
|
|
transactions = db.query(Transaction).filter(Transaction.month == month).all()
|
|
total_revenue = sum(t.revenue for t in transactions)
|
|
total_cost = sum(t.cost for t in transactions)
|
|
gross_profit = total_revenue - total_cost
|
|
gross_margin = (gross_profit / total_revenue * 100) if total_revenue > 0 else 0.0
|
|
net_profit = gross_profit - OPEX
|
|
net_margin = (net_profit / total_revenue * 100) if total_revenue > 0 else 0.0
|
|
return {
|
|
"month": month,
|
|
"total_revenue": round(total_revenue, 2),
|
|
"total_cost": round(total_cost, 2),
|
|
"gross_profit": round(gross_profit, 2),
|
|
"gross_margin_pct": round(gross_margin, 2),
|
|
"opex": OPEX,
|
|
"net_profit": round(net_profit, 2),
|
|
"net_margin_pct": round(net_margin, 2),
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def get_services() -> list:
|
|
"""Get all services with their component breakdowns."""
|
|
db = SessionLocal()
|
|
try:
|
|
services = db.query(Service).order_by(Service.name).all()
|
|
return [
|
|
{
|
|
"id": s.id,
|
|
"name": s.name,
|
|
"description": s.description,
|
|
"sell_price": s.sell_price,
|
|
"cost_price": s.cost_price,
|
|
"active": s.active,
|
|
"margin": round(s.sell_price - s.cost_price, 2),
|
|
"components": [
|
|
{
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"unit_cost": c.unit_cost,
|
|
"unit_sell": c.unit_sell,
|
|
"quantity": c.quantity,
|
|
"row_cost": round(c.unit_cost * c.quantity, 2),
|
|
"row_sell": round(c.unit_sell * c.quantity, 2),
|
|
"notes": c.notes,
|
|
}
|
|
for c in s.components
|
|
],
|
|
}
|
|
for s in services
|
|
]
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def get_service(service_id: int) -> dict:
|
|
"""Get a single service with components."""
|
|
db = SessionLocal()
|
|
try:
|
|
s = db.query(Service).filter(Service.id == service_id).first()
|
|
if not s:
|
|
return {"error": "Service not found"}
|
|
return {
|
|
"id": s.id,
|
|
"name": s.name,
|
|
"description": s.description,
|
|
"sell_price": s.sell_price,
|
|
"cost_price": s.cost_price,
|
|
"active": s.active,
|
|
"components": [
|
|
{
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"unit_cost": c.unit_cost,
|
|
"unit_sell": c.unit_sell,
|
|
"quantity": c.quantity,
|
|
"row_cost": round(c.unit_cost * c.quantity, 2),
|
|
"row_sell": round(c.unit_sell * c.quantity, 2),
|
|
}
|
|
for c in s.components
|
|
],
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def add_component(service_id: int, name: str, unit_cost: float, unit_sell: float,
|
|
quantity: int = 1, notes: str = "") -> dict:
|
|
"""Add a component to a service."""
|
|
db = SessionLocal()
|
|
try:
|
|
svc = db.query(Service).filter(Service.id == service_id).first()
|
|
if not svc:
|
|
return {"error": "Service not found"}
|
|
comp = ServiceComponent(
|
|
service_id=service_id, name=name,
|
|
unit_cost=unit_cost, unit_sell=unit_sell,
|
|
quantity=quantity, notes=notes,
|
|
)
|
|
db.add(comp)
|
|
db.flush()
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
db.refresh(comp)
|
|
return {
|
|
"id": comp.id,
|
|
"name": comp.name,
|
|
"unit_cost": comp.unit_cost,
|
|
"unit_sell": comp.unit_sell,
|
|
"quantity": comp.quantity,
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def update_component(component_id: int, name: str = None, unit_cost: float = None,
|
|
unit_sell: float = None, quantity: int = None,
|
|
notes: str = None) -> dict:
|
|
"""Update a component."""
|
|
db = SessionLocal()
|
|
try:
|
|
comp = db.query(ServiceComponent).filter(ServiceComponent.id == component_id).first()
|
|
if not comp:
|
|
return {"error": "Component not found"}
|
|
if name is not None:
|
|
comp.name = name
|
|
if unit_cost is not None:
|
|
comp.unit_cost = unit_cost
|
|
if unit_sell is not None:
|
|
comp.unit_sell = unit_sell
|
|
if quantity is not None:
|
|
comp.quantity = quantity
|
|
if notes is not None:
|
|
comp.notes = notes
|
|
db.flush()
|
|
svc = db.query(Service).filter(Service.id == comp.service_id).first()
|
|
if svc:
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
return {"ok": True, "id": comp.id}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def delete_component(component_id: int) -> dict:
|
|
"""Delete a component."""
|
|
db = SessionLocal()
|
|
try:
|
|
comp = db.query(ServiceComponent).filter(ServiceComponent.id == component_id).first()
|
|
if not comp:
|
|
return {"error": "Component not found"}
|
|
svc = db.query(Service).filter(Service.id == comp.service_id).first()
|
|
db.delete(comp)
|
|
db.flush()
|
|
if svc:
|
|
svc.recompute_prices()
|
|
db.commit()
|
|
return {"ok": True}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def add_transaction(service_id: int, quantity: int, month: str, notes: str = "") -> dict:
|
|
"""Add a new transaction."""
|
|
db = SessionLocal()
|
|
try:
|
|
service = db.query(Service).filter(Service.id == service_id).first()
|
|
if not service or not service.active:
|
|
return {"error": "Invalid or inactive service"}
|
|
txn = Transaction(
|
|
service_id=service_id, quantity=quantity,
|
|
month=month, notes=notes, created_by=1,
|
|
)
|
|
db.add(txn)
|
|
db.commit()
|
|
db.refresh(txn)
|
|
return {
|
|
"id": txn.id,
|
|
"service_name": txn.service.name,
|
|
"quantity": txn.quantity,
|
|
"revenue": txn.revenue,
|
|
"cost": txn.cost,
|
|
"month": txn.month,
|
|
"notes": txn.notes,
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def get_dashboard() -> dict:
|
|
"""Get current month dashboard overview."""
|
|
from datetime import datetime, timezone
|
|
current_month = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
pnl = compute_pnl(current_month)
|
|
db = SessionLocal()
|
|
try:
|
|
month_rows = (
|
|
db.query(Transaction.month)
|
|
.distinct()
|
|
.order_by(Transaction.month.desc())
|
|
.limit(12)
|
|
.all()
|
|
)
|
|
month_list = [row[0] for row in reversed(month_rows)]
|
|
trend = [compute_pnl(m) for m in month_list]
|
|
return {"current_month": pnl, "trend": trend}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# MCP Protocol Handler
|
|
# ---------------------------------------------------------------------------
|
|
def handle_request(request: dict) -> dict:
|
|
method = request.get("method", "")
|
|
req_id = request.get("id")
|
|
|
|
if method == "initialize":
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": req_id,
|
|
"result": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {"tools": {}, "resources": {}},
|
|
"serverInfo": {"name": "vela-platform", "version": "1.0.0"},
|
|
},
|
|
}
|
|
|
|
if method == "tools/list":
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": req_id,
|
|
"result": {
|
|
"tools": [
|
|
{
|
|
"name": "get_pnl",
|
|
"description": "Get P&L data for a given month (YYYY-MM format).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"month": {"type": "string", "description": "Month in YYYY-MM format"}
|
|
},
|
|
"required": ["month"],
|
|
},
|
|
},
|
|
{
|
|
"name": "get_services",
|
|
"description": "List all offers (services) with full component pricing breakdown.",
|
|
"inputSchema": {"type": "object", "properties": {}},
|
|
},
|
|
{
|
|
"name": "get_service",
|
|
"description": "Get a single offer (service) with its component breakdown.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"service_id": {"type": "integer", "description": "Service ID"}
|
|
},
|
|
"required": ["service_id"],
|
|
},
|
|
},
|
|
{
|
|
"name": "add_component",
|
|
"description": "Add a pricing component (RAM, HDD, Transport, etc.) to an offer.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"service_id": {"type": "integer"},
|
|
"name": {"type": "string", "description": "Component name"},
|
|
"unit_cost": {"type": "number", "description": "Unit cost price"},
|
|
"unit_sell": {"type": "number", "description": "Unit sell price"},
|
|
"quantity": {"type": "integer", "description": "Quantity, default 1"},
|
|
"notes": {"type": "string", "description": "Optional notes"},
|
|
},
|
|
"required": ["service_id", "name", "unit_cost", "unit_sell"],
|
|
},
|
|
},
|
|
{
|
|
"name": "update_component",
|
|
"description": "Update a pricing component. Only provided fields are changed.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"component_id": {"type": "integer"},
|
|
"name": {"type": "string"},
|
|
"unit_cost": {"type": "number"},
|
|
"unit_sell": {"type": "number"},
|
|
"quantity": {"type": "integer"},
|
|
"notes": {"type": "string"},
|
|
},
|
|
"required": ["component_id"],
|
|
},
|
|
},
|
|
{
|
|
"name": "delete_component",
|
|
"description": "Delete a pricing component from an offer.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"component_id": {"type": "integer"}
|
|
},
|
|
"required": ["component_id"],
|
|
},
|
|
},
|
|
{
|
|
"name": "add_transaction",
|
|
"description": "Add a new transaction (sale).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"service_id": {"type": "integer"},
|
|
"quantity": {"type": "integer"},
|
|
"month": {"type": "string", "description": "YYYY-MM format"},
|
|
"notes": {"type": "string"},
|
|
},
|
|
"required": ["service_id", "quantity", "month"],
|
|
},
|
|
},
|
|
{
|
|
"name": "get_dashboard",
|
|
"description": "Get current month dashboard overview with 12-month trend.",
|
|
"inputSchema": {"type": "object", "properties": {}},
|
|
},
|
|
]
|
|
},
|
|
}
|
|
|
|
if method == "resources/list":
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": req_id,
|
|
"result": {
|
|
"resources": [
|
|
{
|
|
"uri": "vela://pnl/current",
|
|
"name": "Current Month P&L",
|
|
"description": "Profit & Loss for the current month",
|
|
"mimeType": "application/json",
|
|
},
|
|
{
|
|
"uri": "vela://services",
|
|
"name": "Service List with Components",
|
|
"description": "All offers with component pricing breakdown",
|
|
"mimeType": "application/json",
|
|
},
|
|
{
|
|
"uri": "vela://services/{id}",
|
|
"name": "Single Service Detail",
|
|
"description": "A specific offer with its components",
|
|
"mimeType": "application/json",
|
|
},
|
|
]
|
|
},
|
|
}
|
|
|
|
if method == "resources/read":
|
|
uri = request.get("params", {}).get("uri", "")
|
|
if uri == "vela://pnl/current":
|
|
from datetime import datetime, timezone
|
|
month = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
content = json.dumps(compute_pnl(month), indent=2)
|
|
elif uri == "vela://services":
|
|
content = json.dumps(get_services(), indent=2)
|
|
elif uri.startswith("vela://services/"):
|
|
try:
|
|
svc_id = int(uri.split("/")[-1])
|
|
content = json.dumps(get_service(svc_id), indent=2)
|
|
except (ValueError, IndexError):
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"error": {"code": -32602, "message": f"Invalid resource URI: {uri}"},
|
|
}
|
|
else:
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"error": {"code": -32602, "message": f"Unknown resource: {uri}"},
|
|
}
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"result": {
|
|
"contents": [{"uri": uri, "mimeType": "application/json", "text": content}]
|
|
},
|
|
}
|
|
|
|
if method == "tools/call":
|
|
tool_name = request.get("params", {}).get("name", "")
|
|
args = request.get("params", {}).get("arguments", {})
|
|
try:
|
|
if tool_name == "get_pnl":
|
|
result = compute_pnl(args["month"])
|
|
elif tool_name == "get_services":
|
|
result = get_services()
|
|
elif tool_name == "get_service":
|
|
result = get_service(args["service_id"])
|
|
elif tool_name == "add_component":
|
|
result = add_component(
|
|
args["service_id"], args["name"],
|
|
args["unit_cost"], args["unit_sell"],
|
|
args.get("quantity", 1), args.get("notes", ""),
|
|
)
|
|
elif tool_name == "update_component":
|
|
result = update_component(
|
|
args["component_id"],
|
|
args.get("name"), args.get("unit_cost"),
|
|
args.get("unit_sell"), args.get("quantity"),
|
|
args.get("notes"),
|
|
)
|
|
elif tool_name == "delete_component":
|
|
result = delete_component(args["component_id"])
|
|
elif tool_name == "add_transaction":
|
|
result = add_transaction(
|
|
args["service_id"], args["quantity"],
|
|
args["month"], args.get("notes", ""),
|
|
)
|
|
elif tool_name == "get_dashboard":
|
|
result = get_dashboard()
|
|
else:
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"error": {"code": -32601, "message": f"Unknown tool: {tool_name}"},
|
|
}
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"result": {
|
|
"content": [{"type": "text", "text": json.dumps(result, indent=2)}]
|
|
},
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"result": {
|
|
"content": [{"type": "text", "text": json.dumps({"error": str(e)})}],
|
|
"isError": True,
|
|
},
|
|
}
|
|
|
|
if "id" not in request:
|
|
return None
|
|
|
|
return {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
}
|
|
|
|
|
|
def main():
|
|
from main import seed_database
|
|
seed_database()
|
|
sys.stderr.write("Vela Platform MCP Server starting...\n")
|
|
sys.stderr.flush()
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
request = json.loads(line)
|
|
response = handle_request(request)
|
|
if response is not None:
|
|
sys.stdout.write(json.dumps(response) + "\n")
|
|
sys.stdout.flush()
|
|
except json.JSONDecodeError as e:
|
|
sys.stderr.write(f"JSON parse error: {e}\n")
|
|
sys.stderr.flush()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|