import json
import math
from http.server import BaseHTTPRequestHandler, HTTPServer
XP_THRESHOLDS = [0, 300, 900, 2700, 6500, 14000]
def initial_sheet_schema() -> dict:
return {
"node_type": "Character",
"properties": {
"name": {"value": "Unnamed Hero", "editable": True, "widget": "text"},
"ancestry": {
"value": "Human",
"editable": True,
"widget": "select",
"options": ["Human", "Elf", "Dwarf", "Halfling"],
},
"class": {
"value": "Fighter",
"editable": True,
"widget": "select",
"options": ["Fighter", "Ranger", "Rogue", "Wizard"],
},
},
"children": {
"Progression": {
"XP": {"value": 0, "editable": False, "widget": "number"},
"Level": {"value": 1, "editable": False, "widget": "badge"},
},
"Wealth": {
"Gold": {"value": 15, "editable": True, "widget": "number"},
},
"Attributes": {
"STR": {"value": 14, "editable": True, "widget": "number"},
"DEX": {"value": 12, "editable": True, "widget": "number"},
"CON": {"value": 13, "editable": True, "widget": "number"},
},
"Derived": {
"MaxHP": {"value": 11, "editable": False, "widget": "badge"},
"AC": {"value": 11, "editable": False, "widget": "badge"},
"CarryWeight": {"value": 0, "editable": False, "widget": "badge"},
"Encumbered": {"value": False, "editable": False, "widget": "flag"},
},
"Supplies": {
"Rations": {"qty": 5, "weight": 2},
"Torch": {"qty": 3, "weight": 1},
},
"MagicItems": {},
},
}
def compute_sheet(tree: dict) -> dict:
children = tree.setdefault("children", {})
attrs = children.get("Attributes", {})
prog = children.setdefault("Progression", {})
derived = children.setdefault("Derived", {})
str_score = attrs.get("STR", {}).get("value", 10)
dex_score = attrs.get("DEX", {}).get("value", 10)
con_score = attrs.get("CON", {}).get("value", 10)
dex_mod = math.floor((dex_score - 10) / 2)
con_mod = math.floor((con_score - 10) / 2)
if tree.get("node_type") == "NPC":
level = max(1, int(prog.get("Level", {}).get("value", 1)))
else:
xp = prog.get("XP", {}).get("value", 0)
level = max(1, sum(1 for threshold in XP_THRESHOLDS if xp >= threshold))
prog.setdefault("Level", {"editable": False, "widget": "badge"})["value"] = level
total_weight = 0
for bucket_name in ("Supplies", "MagicItems", "Inventory"):
for item_data in children.get(bucket_name, {}).values():
total_weight += item_data.get("qty", 1) * item_data.get("weight", 0)
derived["MaxHP"] = {"value": (10 + con_mod) + (level - 1) * (6 + con_mod), "editable": False, "widget": "badge"}
derived["AC"] = {"value": 10 + dex_mod, "editable": False, "widget": "badge"}
derived["CarryWeight"] = {"value": total_weight, "editable": False, "widget": "badge"}
derived["Encumbered"] = {"value": total_weight > (str_score * 15), "editable": False, "widget": "flag"}
return tree
class MCPHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
req = json.loads(self.rfile.read(length).decode("utf-8"))
tool = req.get("params", {}).get("name")
args = req.get("params", {}).get("arguments", {})
if tool == "get_sheet_schema":
out = initial_sheet_schema()
elif tool == "compute_sheet":
out = compute_sheet(args.get("node_tree", {}))
else:
out = {"error": f"Unknown tool {tool}"}
resp = {
"jsonrpc": "2.0",
"id": req.get("id", 1),
"result": {"content": [{"type": "text", "text": json.dumps(out)}]},
}
body = json.dumps(resp).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
print("Fantasy Ruleset MCP Server listening on http://127.0.0.1:8090")
HTTPServer(("127.0.0.1", 8090), MCPHandler).serve_forever()