1717package com .google .adk .tools .mcp ;
1818
1919import static com .google .common .truth .Truth .assertThat ;
20+ import static org .mockito .Mockito .mock ;
2021
22+ import com .fasterxml .jackson .databind .node .JsonNodeFactory ;
23+ import com .fasterxml .jackson .databind .node .ObjectNode ;
24+ import com .google .adk .tools .Annotations ;
2125import com .google .adk .tools .BaseTool ;
26+ import com .google .adk .tools .FunctionTool ;
27+ import com .google .common .collect .ImmutableList ;
28+ import com .google .common .collect .ImmutableMap ;
2229import com .google .genai .types .FunctionDeclaration ;
2330import com .google .genai .types .Schema ;
31+ import com .google .genai .types .Type ;
32+ import io .modelcontextprotocol .client .McpSyncClient ;
33+ import io .modelcontextprotocol .json .McpJsonDefaults ;
2434import io .modelcontextprotocol .spec .McpSchema ;
35+ import java .util .Map ;
2536import java .util .Optional ;
2637import org .junit .Test ;
2738import org .junit .runner .RunWith ;
@@ -45,20 +56,266 @@ public Optional<FunctionDeclaration> declaration() {
4556 }
4657 }
4758
59+ /** Backs the {@link FunctionTool} test: ADK derives the tool schema from this signature. */
60+ public static ImmutableMap <String , Object > getWeather (
61+ @ Annotations .Schema (name = "city" , description = "The city name." ) String city ,
62+ @ Annotations .Schema (name = "count" ) int count ) {
63+ return ImmutableMap .of ("city" , city , "count" , count );
64+ }
65+
66+ private static Map <String , Object > inputSchemaFor (Schema parameters ) {
67+ FunctionDeclaration declaration =
68+ FunctionDeclaration .builder ().name ("tool" ).parameters (parameters ).build ();
69+ return ConversionUtils .adkToMcpToolType (
70+ new FakeTool ("tool" , "description" , Optional .of (declaration )))
71+ .inputSchema ();
72+ }
73+
4874 @ Test
4975 public void adkToMcpToolType_declarationWithParameters_setsInputSchema () {
5076 FunctionDeclaration declaration =
5177 FunctionDeclaration .builder ()
5278 .name ("withParams" )
53- .parameters (Schema .builder ().type ("OBJECT" ).build ())
79+ .parameters (
80+ Schema .builder ()
81+ .type ("OBJECT" )
82+ .properties (ImmutableMap .of ("city" , Schema .builder ().type ("STRING" ).build ()))
83+ .build ())
5484 .build ();
5585 BaseTool tool = new FakeTool ("withParams" , "has params" , Optional .of (declaration ));
5686
5787 McpSchema .Tool result = ConversionUtils .adkToMcpToolType (tool );
5888
5989 assertThat (result .name ()).isEqualTo ("withParams" );
6090 assertThat (result .description ()).isEqualTo ("has params" );
61- assertThat (result .inputSchema ()).containsEntry ("type" , "OBJECT" );
91+ assertThat (result .inputSchema ())
92+ .containsExactly (
93+ "type" ,
94+ "object" ,
95+ "properties" ,
96+ ImmutableMap .of ("city" , ImmutableMap .of ("type" , "string" )));
97+ }
98+
99+ @ Test
100+ public void adkToMcpToolType_functionTool_emitsJsonSchema () {
101+ FunctionTool tool = FunctionTool .create (ConversionUtilsTest .class , "getWeather" );
102+
103+ McpSchema .Tool result = ConversionUtils .adkToMcpToolType (tool );
104+
105+ assertThat (result .inputSchema ())
106+ .containsExactly (
107+ "type" ,
108+ "object" ,
109+ "properties" ,
110+ ImmutableMap .of (
111+ "city" , ImmutableMap .of ("type" , "string" , "description" , "The city name." ),
112+ "count" , ImmutableMap .of ("type" , "integer" )),
113+ "required" ,
114+ ImmutableList .of ("city" , "count" ));
115+ // The same meta-schema check an MCP SDK server runs on every tool it registers.
116+ assertThat (McpJsonDefaults .getSchemaValidator ().validateSchema (result .inputSchema ()).valid ())
117+ .isTrue ();
118+ }
119+
120+ @ Test
121+ public void adkToMcpToolType_mcpTool_keepsServerInputSchema () {
122+ ImmutableMap <String , Object > inputSchema =
123+ ImmutableMap .of (
124+ "type" ,
125+ "object" ,
126+ "properties" ,
127+ ImmutableMap .of ("query" , ImmutableMap .of ("type" , "string" )),
128+ "required" ,
129+ ImmutableList .of ("query" ),
130+ "additionalProperties" ,
131+ false );
132+ McpTool tool =
133+ new McpTool (
134+ McpSchema .Tool .builder ("search" , inputSchema ).description ("Searches" ).build (),
135+ mock (McpSyncClient .class ),
136+ mock (McpSessionManager .class ));
137+
138+ McpSchema .Tool result = ConversionUtils .adkToMcpToolType (tool );
139+
140+ assertThat (result .name ()).isEqualTo ("search" );
141+ assertThat (result .inputSchema ()).isEqualTo (inputSchema );
142+ }
143+
144+ @ Test
145+ public void adkToMcpToolType_parametersAndParametersJsonSchema_prefersParametersJsonSchema () {
146+ ImmutableMap <String , Object > jsonSchema =
147+ ImmutableMap .of (
148+ "type" ,
149+ "object" ,
150+ "properties" ,
151+ ImmutableMap .of ("jsonParam" , ImmutableMap .of ("type" , "string" )));
152+ FunctionDeclaration declaration =
153+ FunctionDeclaration .builder ()
154+ .name ("both" )
155+ .parameters (
156+ Schema .builder ()
157+ .type (Type .Known .OBJECT )
158+ .properties (
159+ ImmutableMap .of (
160+ "schemaParam" , Schema .builder ().type (Type .Known .STRING ).build ()))
161+ .build ())
162+ .parametersJsonSchema (jsonSchema )
163+ .build ();
164+
165+ McpSchema .Tool result =
166+ ConversionUtils .adkToMcpToolType (new FakeTool ("both" , "both" , Optional .of (declaration )));
167+
168+ assertThat (result .inputSchema ()).isEqualTo (jsonSchema );
169+ }
170+
171+ @ Test
172+ public void adkToMcpToolType_jsonNodeParametersJsonSchema_convertsToMap () {
173+ ObjectNode jsonSchema = JsonNodeFactory .instance .objectNode ().put ("type" , "object" );
174+ jsonSchema .putObject ("properties" ).putObject ("query" ).put ("type" , "string" );
175+ FunctionDeclaration declaration =
176+ FunctionDeclaration .builder ().name ("node" ).parametersJsonSchema (jsonSchema ).build ();
177+
178+ McpSchema .Tool result =
179+ ConversionUtils .adkToMcpToolType (new FakeTool ("node" , "node" , Optional .of (declaration )));
180+
181+ assertThat (result .inputSchema ())
182+ .containsExactly (
183+ "type" ,
184+ "object" ,
185+ "properties" ,
186+ ImmutableMap .of ("query" , ImmutableMap .of ("type" , "string" )));
187+ }
188+
189+ @ Test
190+ public void adkToMcpToolType_emptyParametersJsonSchema_usesParameters () {
191+ FunctionDeclaration declaration =
192+ FunctionDeclaration .builder ()
193+ .name ("empty" )
194+ .parameters (
195+ Schema .builder ()
196+ .type (Type .Known .OBJECT )
197+ .properties (
198+ ImmutableMap .of ("city" , Schema .builder ().type (Type .Known .STRING ).build ()))
199+ .build ())
200+ .parametersJsonSchema (ImmutableMap .of ())
201+ .build ();
202+
203+ McpSchema .Tool result =
204+ ConversionUtils .adkToMcpToolType (new FakeTool ("empty" , "empty" , Optional .of (declaration )));
205+
206+ assertThat (result .inputSchema ())
207+ .containsExactly (
208+ "type" ,
209+ "object" ,
210+ "properties" ,
211+ ImmutableMap .of ("city" , ImmutableMap .of ("type" , "string" )));
212+ }
213+
214+ @ Test
215+ public void adkToMcpToolType_nestedSchemas_lowercasesEveryType () {
216+ Schema schema =
217+ Schema .builder ()
218+ .type (Type .Known .OBJECT )
219+ .properties (
220+ ImmutableMap .of (
221+ "tags" ,
222+ Schema .builder ()
223+ .type (Type .Known .ARRAY )
224+ .items (Schema .builder ().type (Type .Known .STRING ).build ())
225+ .build (),
226+ "id" ,
227+ Schema .builder ()
228+ .anyOf (
229+ ImmutableList .of (
230+ Schema .builder ().type (Type .Known .STRING ).build (),
231+ Schema .builder ().type (Type .Known .INTEGER ).build ()))
232+ .build ()))
233+ .build ();
234+
235+ assertThat (inputSchemaFor (schema ))
236+ .containsExactly (
237+ "type" ,
238+ "object" ,
239+ "properties" ,
240+ ImmutableMap .of (
241+ "tags" ,
242+ ImmutableMap .of ("type" , "array" , "items" , ImmutableMap .of ("type" , "string" )),
243+ "id" ,
244+ ImmutableMap .of (
245+ "anyOf" ,
246+ ImmutableList .of (
247+ ImmutableMap .of ("type" , "string" ), ImmutableMap .of ("type" , "integer" )))));
248+ }
249+
250+ @ Test
251+ public void adkToMcpToolType_unspecifiedType_isDropped () {
252+ Schema schema =
253+ Schema .builder ()
254+ .type (Type .Known .OBJECT )
255+ .properties (
256+ ImmutableMap .of (
257+ "anything" , Schema .builder ().type (Type .Known .TYPE_UNSPECIFIED ).build ()))
258+ .build ();
259+
260+ assertThat (inputSchemaFor (schema ))
261+ .containsExactly (
262+ "type" , "object" , "properties" , ImmutableMap .of ("anything" , ImmutableMap .of ()));
263+ }
264+
265+ @ Test
266+ public void adkToMcpToolType_everyGenaiKeyword_passesMetaSchemaCheck () {
267+ // Some keywords sit on a type they do not apply to, such as minLength on an array; they pass
268+ // through and stay valid JSON Schema.
269+ Schema schema =
270+ Schema .builder ()
271+ .type (Type .Known .OBJECT )
272+ .title ("Request" )
273+ .description ("All keywords." )
274+ .nullable (true )
275+ .example (ImmutableMap .of ("name" , "a" ))
276+ .properties (
277+ ImmutableMap .of (
278+ "name" ,
279+ Schema .builder ()
280+ .type (Type .Known .STRING )
281+ .pattern ("^a" )
282+ .minLength (1L )
283+ .maxLength (5L )
284+ .format ("enum" )
285+ .enum_ (ImmutableList .of ("a" , "ab" ))
286+ .default_ ("a" )
287+ .minimum (1.0 )
288+ .build (),
289+ "tags" ,
290+ Schema .builder ()
291+ .type (Type .Known .ARRAY )
292+ .items (
293+ Schema .builder ()
294+ .type (Type .Known .NUMBER )
295+ .minimum (0.0 )
296+ .maximum (9.0 )
297+ .build ())
298+ .minItems (1L )
299+ .maxItems (3L )
300+ .minLength (2L )
301+ .build (),
302+ "id" ,
303+ Schema .builder ()
304+ .anyOf (
305+ ImmutableList .of (
306+ Schema .builder ().type (Type .Known .STRING ).build (),
307+ Schema .builder ().type (Type .Known .INTEGER ).build ()))
308+ .build ()))
309+ .required (ImmutableList .of ("name" ))
310+ .minProperties (1L )
311+ .maxProperties (2L )
312+ .propertyOrdering (ImmutableList .of ("tags" , "name" ))
313+ .build ();
314+
315+ Map <String , Object > inputSchema = inputSchemaFor (schema );
316+
317+ // The meta-schema check an MCP SDK server runs on every tool it registers.
318+ assertThat (McpJsonDefaults .getSchemaValidator ().validateSchema (inputSchema ).valid ()).isTrue ();
62319 }
63320
64321 @ Test
0 commit comments