mcp-schema-normalize - a gateway-side normalizer for the documented json-schema-to-grammar limitations #23918
rsclafani
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I kept hitting
Unable to generate parser for this template/Error resolving ref … anyOf not in {…}when routing MCP tool calls throughllama-server, and traced every failure back to the limitations already documented ingrammars/README.md.Rather than ask the converter to change, I wrote a normalizer that sits in front of it and rewrites tool schemas into the subset it accepts.
Sharing it here in case others on local backends hit the same wall.
PyPI:
pip install mcp-schema-normalize· Repo: https://github.com/rsclafani/mcp-schema-normalize · MITThe gap
MCP mandates JSON Schema 2020-12 (SEP-1613).
json-schema-to-grammar.cppcompiles a deliberately narrower subset. Schemas that pass fine against hosted Anthropic/OpenAI APIs (which forward them verbatim) die at the grammar step on a local backend. The same tool list that works on a cloud model will error (400) on Qwen3-Coder or Nemotron-Nano behind llama-server.These aren't bugs waiting on a fix, they're documented, accepted limitations. The library is a workaround for the documented gap, not a stopgap for something pending. It maps directly onto the converter's own limitation list:
anyOf/oneOfbesideproperties/type/required(#7703)$refs intoanyOfnodes (#8073){"not": {}}sentinels fromzod-to-json-schema(#17574)not; preserves realnotschemas$ref(azod-to-json-schemasingleton-union artifact){}so the request completes — see the caveat belowThe honest caveat
The dangling-
$refcase is upstream-schema corruption, not a llama.cpp issue:zod-to-json-schemacollapses singletonz.union(...)to its sole variant but still emits$refstrings pointing at the pre-collapseanyOfenvelope. The library makes the request go through by swapping the unresolvable ref for{}(match-anything)... which means that field loses its type spec. Every such event increments arefs_unresolvedcounter and logs a WARN line. If you can't watch telemetry for it, setSTRICT_UNRESOLVED_REFS = Trueand it fails loud (400) instead of degrading quietly.The README's "When NOT to use this" section is explicit about this tradeoff with the intent to scare off a misuse than earn a reputation for silently loosening validation.
Where it runs
Pure-Python core, zero runtime deps, operates on plain JSON Schema so it works with direct
llama-server, Ollama, or any OpenAI-compatible client. First-class LiteLLM proxy hook ships behind a[litellm]extra. vLLM/TabbyAPI adapters are a thin wrapper aroundnormalize_tools(); PRs welcome.Feedback I'm after
Beta Was this translation helpful? Give feedback.
All reactions