1616
1717package com .google .adk .tools .mcp ;
1818
19+ import com .fasterxml .jackson .core .type .TypeReference ;
20+ import com .google .adk .JsonBaseModel ;
1921import com .google .adk .tools .BaseTool ;
22+ import com .google .common .base .Ascii ;
23+ import com .google .common .collect .ImmutableList ;
2024import com .google .common .collect .ImmutableMap ;
2125import com .google .genai .types .FunctionDeclaration ;
2226import com .google .genai .types .Schema ;
23- import io .modelcontextprotocol .json .McpJsonDefaults ;
24- import io .modelcontextprotocol .json .McpJsonMapper ;
27+ import com .google .genai .types .Type ;
2528import io .modelcontextprotocol .spec .McpSchema ;
29+ import java .util .ArrayList ;
30+ import java .util .List ;
31+ import java .util .Map ;
32+ import java .util .Objects ;
2633import java .util .Optional ;
34+ import org .jspecify .annotations .Nullable ;
2735
28- /** Utility class for converting between different representations of MCP tools. */
36+ /** Static helpers for converting ADK tools to MCP tools. */
2937public final class ConversionUtils {
3038
31- private static final McpJsonMapper jsonMapper = McpJsonDefaults . getMapper () ;
39+ private static final TypeReference < Map < String , Object >> MAP_TYPE = new TypeReference <>() {} ;
3240
3341 /**
3442 * Input schema for a tool with no parameters. This is the schema the deprecated {@code
@@ -38,16 +46,101 @@ public final class ConversionUtils {
3846 private static final ImmutableMap <String , Object > NO_PARAMETERS_SCHEMA =
3947 ImmutableMap .of ("type" , "object" );
4048
49+ private static final ImmutableMap <String , Object > NULL_SCHEMA = ImmutableMap .of ("type" , "null" );
50+
51+ /**
52+ * Converts an ADK tool to an MCP tool. Uses the declaration's {@code parametersJsonSchema} when
53+ * present and non-empty, falls back to {@code parameters} converted to JSON Schema, and defaults
54+ * to {@code {"type": "object"}}.
55+ */
4156 public static McpSchema .Tool adkToMcpToolType (BaseTool tool ) {
42- Optional <Schema > parameters = tool .declaration ().flatMap (FunctionDeclaration ::parameters );
43- if (parameters .isEmpty ()) {
44- return McpSchema .Tool .builder (tool .name (), NO_PARAMETERS_SCHEMA )
45- .description (tool .description ())
46- .build ();
47- }
48- return McpSchema .Tool .builder (tool .name (), jsonMapper , parameters .get ().toJson ())
49- .description (tool .description ())
50- .build ();
57+ Optional <FunctionDeclaration > declaration = tool .declaration ();
58+ Map <String , Object > inputSchema =
59+ declaration
60+ .flatMap (FunctionDeclaration ::parametersJsonSchema )
61+ // ADK's Jackson 2 mapper: a Jackson 3 MCP SDK mapper would read a JsonNode as a bean.
62+ .map (schema -> JsonBaseModel .getMapper ().convertValue (schema , MAP_TYPE ))
63+ .filter (schema -> !schema .isEmpty ())
64+ .or (
65+ () ->
66+ declaration
67+ .flatMap (FunctionDeclaration ::parameters )
68+ .map (ConversionUtils ::toJsonSchema ))
69+ .orElse (NO_PARAMETERS_SCHEMA );
70+ return McpSchema .Tool .builder (tool .name (), inputSchema ).description (tool .description ()).build ();
71+ }
72+
73+ /** Converts a genai {@link Schema}, which spells types in uppercase, to JSON Schema. */
74+ private static Map <String , Object > toJsonSchema (Schema schema ) {
75+ Map <String , Object > json = JsonBaseModel .getMapper ().convertValue (schema , MAP_TYPE );
76+ // MCP requires the input schema itself to have "type": "object", so it cannot be nullable.
77+ json .remove ("nullable" );
78+ normalizeSchema (json );
79+ return json ;
80+ }
81+
82+ /**
83+ * Normalizes {@code node} and its nested schemas in place: lowercases {@code type}, drops a type
84+ * with no JSON Schema name such as {@code TYPE_UNSPECIFIED} so the schema accepts any value, and
85+ * replaces genai's {@code nullable}, which JSON Schema does not have.
86+ */
87+ private static void normalizeSchema (Object node ) {
88+ if (!(node instanceof Map <?, ?> rawSchema )) {
89+ return ;
90+ }
91+ // Safe: convertValue built the whole tree from Map<String, Object> nodes.
92+ @ SuppressWarnings ("unchecked" )
93+ Map <String , Object > schema = (Map <String , Object >) rawSchema ;
94+ normalizeType (schema );
95+ normalizeSubschemas (schema );
96+ if (Objects .equals (schema .remove ("nullable" ), true )) {
97+ allowNull (schema );
98+ }
99+ }
100+
101+ private static void normalizeType (Map <String , Object > schema ) {
102+ if (!(schema .get ("type" ) instanceof String type )) {
103+ return ;
104+ }
105+ // Type maps a name it does not know to TYPE_UNSPECIFIED; dropping it keeps the schema valid.
106+ Type .Known known = new Type (type ).knownEnum ();
107+ if (known == Type .Known .TYPE_UNSPECIFIED ) {
108+ schema .remove ("type" );
109+ } else {
110+ schema .put ("type" , Ascii .toLowerCase (known .name ()));
111+ }
112+ }
113+
114+ private static void normalizeSubschemas (Map <String , Object > schema ) {
115+ if (schema .get ("properties" ) instanceof Map <?, ?> properties ) {
116+ properties .values ().forEach (ConversionUtils ::normalizeSchema );
117+ }
118+ normalizeSchema (schema .get ("items" ));
119+ if (schema .get ("anyOf" ) instanceof List <?> anyOf ) {
120+ anyOf .forEach (ConversionUtils ::normalizeSchema );
121+ }
122+ }
123+
124+ /**
125+ * Allows null in whichever of {@code type}, {@code enum} and {@code anyOf} {@code schema} has,
126+ * since JSON Schema has no {@code nullable} keyword.
127+ */
128+ private static void allowNull (Map <String , Object > schema ) {
129+ if (schema .get ("type" ) instanceof String type && !type .equals ("null" )) {
130+ schema .put ("type" , ImmutableList .of (type , "null" ));
131+ }
132+ if (schema .get ("enum" ) instanceof List <?> values && !values .contains (null )) {
133+ schema .put ("enum" , append (values , null ));
134+ }
135+ if (schema .get ("anyOf" ) instanceof List <?> anyOf && !anyOf .contains (NULL_SCHEMA )) {
136+ schema .put ("anyOf" , append (anyOf , NULL_SCHEMA ));
137+ }
138+ }
139+
140+ private static List <Object > append (List <?> list , @ Nullable Object element ) {
141+ List <Object > result = new ArrayList <>(list );
142+ result .add (element );
143+ return result ;
51144 }
52145
53146 private ConversionUtils () {}
0 commit comments