bifroest/services/db-mcp/dbmcp/core/result.py
2026-10-27 08:34:17 +01:00

66 lines
2.5 KiB
Python

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