1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/// Integration test: Gemini with MCP-style tool schemas.
/// Tests that schemars-generated JSON Schema (with $schema, $defs, $ref, nullable types)
/// is correctly sanitized for Gemini's API.
///
/// Run: cargo test --test integration_gemini_tools -- --ignored
use serde_json::json;
fn router() -> llmshim::router::Router {
llmshim::router::Router::from_env()
}
/// MCP-style tools with all the problematic schema features:
/// $schema, $defs, $ref, "type": ["string", "null"], additionalProperties
fn mcp_tools() -> serde_json::Value {
json!([
{
"type": "function",
"function": {
"name": "get_quote",
"description": "Get stock quotes",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"symbols": {
"type": "array",
"items": {"type": "string"},
"description": "Ticker symbols"
}
},
"required": ["symbols"],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
"name": "get_options_chain",
"description": "Get options chain for a symbol",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"symbol": {"type": "string"},
"expiration": {"type": ["string", "null"], "description": "Filter by date"},
"strike_range_pct": {"type": ["number", "null"]}
},
"required": ["symbol"]
}
}
},
{
"type": "function",
"function": {
"name": "get_spread_analysis",
"description": "Analyze an options spread",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"underlying": {"type": "string"},
"legs": {
"type": "array",
"items": {"$ref": "#/$defs/SpreadLeg"}
}
},
"required": ["underlying", "legs"],
"$defs": {
"SpreadLeg": {
"type": "object",
"properties": {
"symbol": {"type": "string"},
"side": {"type": "string"},
"quantity": {"type": ["integer", "null"]}
},
"required": ["symbol", "side"]
}
}
}
}
}
])
}
#[tokio::test]
#[ignore]
async fn gemini_accepts_mcp_tools() {
let router = router();
let req = json!({
"model": "gemini/gemini-3-flash-preview",
"messages": [{"role": "user", "content": "Get me a quote for AAPL"}],
"max_tokens": 200,
"tools": mcp_tools(),
});
let result = llmshim::completion(&router, &req).await;
match &result {
Ok(resp) => {
println!("OK: model={}", resp["model"]);
// Should get a tool call for get_quote
let msg = &resp["choices"][0]["message"];
if let Some(tcs) = msg.get("tool_calls").and_then(|t| t.as_array()) {
println!("Tool calls: {}", serde_json::to_string_pretty(tcs).unwrap());
assert!(!tcs.is_empty(), "expected at least one tool call");
} else {
println!("Content: {}", msg["content"]);
}
}
Err(e) => {
panic!("Gemini rejected MCP tools: {e}");
}
}
}