"""Einheitliches Ergebnisformat aller DB-Dienste. Jedes Tool liefert Text für das Modell (Markdown-Tabelle) und dieselben Daten strukturiert (structuredContent), damit Clients sie als Tabelle, CSV oder Diagramm zeigen können: {"columns": [...], "rows": [[...], ...], "row_count": n, "truncated": bool} """ import base64 import datetime import decimal import uuid from dataclasses import dataclass, field from typing import Any from mcp.types import CallToolResult, TextContent def jsonable(value: Any) -> Any: if value is None or isinstance(value, (bool, int, str)): return value if isinstance(value, float): return value if value == value and value not in (float("inf"), float("-inf")) else str(value) if isinstance(value, decimal.Decimal): # Einheitlich als Zahl (für Diagramme); nur was eine Kommazahl nicht genau fasst, als Text return float(value) if value.is_finite() and len(value.as_tuple().digits) <= 15 else str(value) if isinstance(value, (datetime.date, datetime.time)): return value.isoformat() if isinstance(value, datetime.timedelta): return str(value) if isinstance(value, uuid.UUID): return str(value) if isinstance(value, (bytes, bytearray, memoryview)): return "base64:" + base64.b64encode(bytes(value)[:1024]).decode() if isinstance(value, (list, tuple)): return [jsonable(v) for v in value] if isinstance(value, dict): return {str(k): jsonable(v) for k, v in value.items()} return str(value) def as_table(columns: list[str], rows: list[list[Any]], cell_chars: int) -> str: def cell(value: Any) -> str: text = "NULL" if value is None else str(value) text = text.replace("\n", " ").replace("|", "\\|") return text if len(text) <= cell_chars else text[:cell_chars] + "…" lines = ["| " + " | ".join(columns) + " |", "|" + "---|" * len(columns)] lines += ["| " + " | ".join(cell(v) for v in row) + " |" for row in rows] return "\n".join(lines) @dataclass class Table: columns: list[str] rows: list[list[Any]] truncated: bool = False extra: dict[str, Any] = field(default_factory=dict) def structured(self) -> dict[str, Any]: rows = [[jsonable(v) for v in row] for row in self.rows] return {"columns": self.columns, "rows": rows, "row_count": len(rows), "truncated": self.truncated, **self.extra} def result(self, text: str) -> CallToolResult: return CallToolResult(content=[TextContent(type="text", text=text)], structured_content=self.structured())