🎉 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.
This commit is contained in:
494
mcp/server.py
Normal file
494
mcp/server.py
Normal file
@@ -0,0 +1,494 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user