fix(server): normalize tool schemas for gemini/vertex clients - #119
Merged
Conversation
schemars 1.2.2 renders every Option<T> tool parameter as a JSON-Schema
type union ("type": [T, "null"]) plus a stray "default": null, and
every Rust integer parameter carries a uint64/uint32 format Gemini
doesn't recognize. rmcp's SchemaSettings::draft2020_12() runs zero
transforms, so this is exactly what list_tools/get_tool served. A
Gemini/Vertex client rewrites the type union into anyOf while keeping
description/format/default as siblings of it, a shape Vertex itself
then refuses ("when using any_of, it must be the only field set") -
so the whole tool list was rejected, not just the one property Vertex
happened to name.
BugWarden::new now runs a portable_schema pass once, at router
construction, over every route's input_schema: collapse the type
union to a plain type, drop the resulting default:null, and drop any
format outside a portable keep-list (date-time, int32, int64, float,
double, enum). serde still accepts an explicit null for every
Option<T> field - only the advertised schema got stricter, not what
the server accepts, so no invariant (I1-I16) is touched.
Verified against a real stdio tools/list probe: 0 type unions, 0
default:null, formats reduced to exactly {date-time} across all 20
served tools.
3 tasks
This was referenced Aug 18, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
BugWarden::newnow runs aportable_schemapass over every route'sinput_schemaonce, at router construction: it collapses schemars'Option<T>type union ("type": [T, "null"]) to a plain type, drops theresulting
"default": null, and drops anyformatoutside a portablekeep-list (
date-time,int32,int64,float,double,enum).Why
A Gemini (Google Vertex) client refused the whole tool list:
schemars 1.2.2 renders every
Option<T>tool parameter as a JSON-Schematype union plus a stray
"default": null, and every Rust integerparameter carries a
uint64/uint32formatGemini doesn't recognize.rmcp's
SchemaSettings::draft2020_12()runs zero transforms, so this isexactly what
list_tools/get_toolserved — confirmed by dumping thereal
tools/listpayload over stdio. The Gemini/Vertex client rewritesthe type union into
anyOfwhile keepingdescription/format/defaultas siblings of it, a shape Vertex itself then refuses.
serdestill accepts an explicitnullfor everyOption<T>field —only the advertised schema got stricter, not what the server accepts —
so no invariant (I1–I16) is touched.
How verified
cargo fmt --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo clippy -p bugwarden --features gen --all-targets -- -D warningscleancargo test --workspace --all-targets --lockedgreen, including threenew tests: a recursive schema-portability regression guard over all
served tools, an exact-shape assertion on
bug_comments.new_since, anda check that an explicit
nullstill deserializes for an optional paramtools/listprobe shows 0 type unions, 0"default": null,and formats reduced to exactly
{date-time}across all 20 served tools