Files
vela-platform/mcp/server.py
oimwiodev d1160673a7 🎉 v1.0.0 — Vela Platform launch
Self-hosted P&L tracking app with component-level pricing.
Offers: Atlas/Atlas+/Rif/Rif+ with granular cost breakdown.
API + MCP + multi-user auth.
2026-06-15 23:05:59 +01:00

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()