Source code for squadds_mcp.utils
"""
Shared utilities for the SQuADDS MCP server.
=============================================
Helpers for serialization, formatting, and error handling used across
all tools and resources.
Adding a New Utility
--------------------
1. Add your function here.
2. Keep it stateless — no imports of ``SQuADDS_DB`` or ``Analyzer`` at module level.
3. Add a docstring with Args/Returns so AI agents can understand usage.
"""
from __future__ import annotations
import json
from typing import Any
import numpy as np
import pandas as pd
[docs]
def dataframe_to_records(
df: pd.DataFrame,
limit: int = 50,
offset: int = 0,
) -> list[dict[str, Any]]:
"""Convert a DataFrame slice to a list of JSON-safe dicts.
Args:
df: The pandas DataFrame.
limit: Maximum number of rows to return.
offset: Starting row index.
Returns:
List of dicts, one per row, with numpy types converted to Python builtins.
"""
sliced = df.iloc[offset : offset + limit]
records = sliced.to_dict(orient="records")
return [sanitize_for_json(r) for r in records]
[docs]
def sanitize_for_json(obj: Any) -> Any:
"""Recursively convert numpy/pandas types to JSON-serializable Python types.
Handles: ndarray, int64/float64, NaN, Timestamp, bytes, sets.
Args:
obj: Any Python object that may contain numpy/pandas types.
Returns:
A JSON-safe version of the object.
"""
if isinstance(obj, dict):
return {sanitize_for_json(k): sanitize_for_json(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [sanitize_for_json(item) for item in obj]
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, (np.integer,)):
return int(obj)
if isinstance(obj, (np.floating,)):
val = float(obj)
if np.isnan(val) or np.isinf(val):
return None
return val
if isinstance(obj, np.bool_):
return bool(obj)
if isinstance(obj, pd.Timestamp):
return obj.isoformat()
if isinstance(obj, bytes):
return obj.decode("utf-8", errors="replace")
if isinstance(obj, set):
return list(obj)
if isinstance(obj, float) and (np.isnan(obj) or np.isinf(obj)):
return None
return obj
[docs]
def safe_get(obj: Any, *keys: str, default: Any = None) -> Any:
"""Safely traverse nested dicts/objects.
Args:
obj: The root object to traverse.
*keys: Sequence of keys to follow.
default: Value to return if any key is missing.
Returns:
The value at the nested path, or *default*.
"""
current = obj
for key in keys:
if isinstance(current, dict):
current = current.get(key, default)
elif hasattr(current, key):
current = getattr(current, key, default)
else:
return default
if current is default:
return default
return current
[docs]
def build_error_response(message: str, details: dict[str, Any] | None = None) -> str:
"""Build a formatted error message for tool responses.
Args:
message: Human-readable error message.
details: Optional extra context.
Returns:
Formatted error string.
"""
parts = [f"❌ Error: {message}"]
if details:
for k, v in details.items():
parts.append(f" • {k}: {v}")
return "\n".join(parts)