Skip to main content
Glama
schema_utils.py5.02 kB
""" Schema transformation utilities for MCP tool schemas. Provides functions to transform JSON schemas for compatibility with different LLM providers (OpenAI, Anthropic, etc.). """ from copy import deepcopy from typing import Any def sanitize_for_openai_tools(schema: dict) -> dict: """ Make a Pydantic/JSON Schema object compatible with OpenAI tool schema. OpenAI's function calling API has stricter requirements than standard JSON Schema: - Does not support 'integer' type (only 'number') - Does not support 'null' in union types - Requires simplification of oneOf/anyOf in certain cases Transformations applied: - 'integer' -> 'number' (+ multipleOf: 1 to preserve integer semantics) - Remove 'null' from union type arrays - Coerce integer-only enums to number type - Simplify oneOf/anyOf when they only differ by integer/number Args: schema: The JSON Schema to sanitize Returns: A new schema dict compatible with OpenAI's tool schema requirements Note: Original implementation by GPT-5 for serena, moved to nisaba framework. """ s = deepcopy(schema) def walk(node: Any) -> Any: if not isinstance(node, dict): # lists get handled by parent calls return node # ---- handle type ---- t = node.get("type") if isinstance(t, str): if t == "integer": node["type"] = "number" # preserve existing multipleOf but ensure it's integer-like if "multipleOf" not in node: node["multipleOf"] = 1 elif isinstance(t, list): # remove 'null' (OpenAI tools don't support nullables) t2 = [x if x != "integer" else "number" for x in t if x != "null"] if not t2: # fall back to object if it somehow becomes empty t2 = ["object"] node["type"] = t2[0] if len(t2) == 1 else t2 if "integer" in t or "number" in t2: # if integers were present, keep integer-like restriction node.setdefault("multipleOf", 1) # ---- enums of integers -> number ---- if "enum" in node and isinstance(node["enum"], list): vals = node["enum"] if vals and all(isinstance(v, int) for v in vals): node.setdefault("type", "number") # keep them as ints; JSON 'number' covers ints node.setdefault("multipleOf", 1) # ---- simplify anyOf/oneOf if they only differ by integer/number ---- for key in ("oneOf", "anyOf"): if key in node and isinstance(node[key], list): # Special case: anyOf or oneOf with "type X" and "null" if len(node[key]) == 2: types = [sub.get("type") for sub in node[key]] if "null" in types: non_null_type = next(t for t in types if t != "null") if isinstance(non_null_type, str): node["type"] = non_null_type node.pop(key, None) continue simplified = [] changed = False for sub in node[key]: sub = walk(sub) # recurse simplified.append(sub) # If all subs are the same after integer→number, collapse try: import json canon = [json.dumps(x, sort_keys=True) for x in simplified] if len(set(canon)) == 1: # copy the single schema up only = simplified[0] node.pop(key, None) for k, v in only.items(): if k not in node: node[k] = v changed = True except Exception: pass if not changed: node[key] = simplified # ---- recurse into known schema containers ---- for child_key in ("properties", "patternProperties", "definitions", "$defs"): if child_key in node and isinstance(node[child_key], dict): for k, v in list(node[child_key].items()): node[child_key][k] = walk(v) # arrays/items if "items" in node: node["items"] = walk(node["items"]) # allOf/if/then/else - pass through with integer→number conversions applied inside for key in ("allOf",): if key in node and isinstance(node[key], list): node[key] = [walk(x) for x in node[key]] if "if" in node: node["if"] = walk(node["if"]) if "then" in node: node["then"] = walk(node["then"]) if "else" in node: node["else"] = walk(node["else"]) return node return walk(s)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/y3i12/nabu_nisaba'

If you have feedback or need assistance with the MCP directory API, please join our Discord server