Skip to content

Commit 6b9616a

Browse files
Doris26copybara-github
authored andcommitted
feat: Support Configurable Agent with MCP Tools
This change introduces the ability to specify mcp tools in the `LlmAgent` YAML configuration. PiperOrigin-RevId: 793872410
1 parent 55fffb7 commit 6b9616a

File tree

7 files changed

+591
-42
lines changed

7 files changed

+591
-42
lines changed

core/src/main/java/com/google/adk/agents/LlmAgent.java

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import io.reactivex.rxjava3.core.Maybe;
6565
import io.reactivex.rxjava3.core.Single;
6666
import java.lang.reflect.Constructor;
67+
import java.lang.reflect.Method;
6768
import java.util.ArrayList;
6869
import java.util.List;
6970
import java.util.Map;
@@ -782,6 +783,13 @@ public List<BaseTool> tools() {
782783
return canonicalTools().toList().blockingGet();
783784
}
784785

786+
public List<BaseToolset> toolsets() {
787+
return toolsUnion.stream()
788+
.filter(t -> t instanceof BaseToolset)
789+
.map(t -> (BaseToolset) t)
790+
.collect(ImmutableList.toImmutableList());
791+
}
792+
785793
public List<Object> toolsUnion() {
786794
return toolsUnion;
787795
}
@@ -939,14 +947,14 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
939947
return agent;
940948
}
941949

942-
private static ImmutableList<BaseTool> resolveTools(
950+
private static ImmutableList<Object> resolveTools(
943951
List<ToolConfig> toolConfigs, String configAbsPath) throws ConfigurationException {
944952

945953
if (toolConfigs == null || toolConfigs.isEmpty()) {
946954
return ImmutableList.of();
947955
}
948956

949-
List<BaseTool> resolvedTools = new ArrayList<>();
957+
List<Object> resolvedTools = new ArrayList<>();
950958

951959
for (ToolConfig toolConfig : toolConfigs) {
952960
try {
@@ -956,20 +964,20 @@ private static ImmutableList<BaseTool> resolveTools(
956964
}
957965

958966
toolName = toolName.trim();
959-
BaseTool tool;
967+
Object toolOrToolset;
960968

961969
if (!toolName.contains(".")) {
962-
tool = resolveBuiltInTool(toolName, toolConfig);
970+
toolOrToolset = resolveBuiltInTool(toolName, toolConfig, configAbsPath);
963971
} else {
964972
// TODO: Support user-defined tools
965973
logger.debug("configAbsPath is: {}", configAbsPath);
966974
throw new ConfigurationException("User-defined tools are not yet supported");
967975
}
968976

969-
resolvedTools.add(tool);
970-
logger.debug("Successfully resolved tool: {}", toolConfig.name());
977+
resolvedTools.add(toolOrToolset);
978+
logger.debug("Successfully resolved tool/toolset: {}", toolConfig.name());
971979
} catch (Exception e) {
972-
String errorMsg = "Failed to resolve tool: " + toolConfig.name();
980+
String errorMsg = "Failed to resolve tool/toolset: " + toolConfig.name();
973981
logger.error(errorMsg, e);
974982
throw new ConfigurationException(errorMsg, e);
975983
}
@@ -978,25 +986,29 @@ private static ImmutableList<BaseTool> resolveTools(
978986
return ImmutableList.copyOf(resolvedTools);
979987
}
980988

981-
private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfig)
982-
throws ConfigurationException {
989+
private static Object resolveBuiltInTool(
990+
String toolName, ToolConfig toolConfig, String configAbsPath) throws ConfigurationException {
983991
try {
984-
logger.debug("Resolving built-in tool: {}", toolName);
992+
logger.debug("Resolving built-in tool/toolset: {}", toolName);
985993
// TODO: Handle built-in tool name end with Tool while config yaml file does not.
986994
// e.g.google_search in config yaml file and GoogleSearchTool in tool class name.
987995
String pascalCaseToolName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, toolName);
988996
String className = "com.google.adk.tools." + pascalCaseToolName;
997+
// TODO: use tool registry to handle this instead of hardcoding.
998+
if (toolName.equals("McpToolset")) {
999+
className = "com.google.adk.tools.mcp.McpToolset";
1000+
}
9891001
Class<?> toolClass;
9901002
try {
9911003
toolClass = Class.forName(className);
992-
logger.debug("Successfully loaded tool class: {}", className);
1004+
logger.debug("Successfully loaded tool/toolset class: {}", className);
9931005
} catch (ClassNotFoundException e) {
9941006
String fallbackClassName = "com.google.adk.tools." + toolName;
9951007
try {
9961008
toolClass = Class.forName(fallbackClassName);
9971009
} catch (ClassNotFoundException e2) {
9981010
throw new ConfigurationException(
999-
"Built-in tool not found: "
1011+
"Built-in tool/toolset not found: "
10001012
+ toolName
10011013
+ ". Expected class: "
10021014
+ className
@@ -1006,48 +1018,87 @@ private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfi
10061018
}
10071019
}
10081020

1009-
if (!BaseTool.class.isAssignableFrom(toolClass)) {
1021+
if (BaseTool.class.isAssignableFrom(toolClass)) {
1022+
logger.debug("Tool {} is a sub-class of BaseTool.", toolConfig.name());
1023+
@SuppressWarnings("unchecked")
1024+
Class<? extends BaseTool> baseToolClass = (Class<? extends BaseTool>) toolClass;
1025+
BaseTool tool = createToolInstance(baseToolClass, toolConfig, configAbsPath);
1026+
logger.info(
1027+
"Successfully created built-in tool: {} (class: {})", toolName, toolClass.getName());
1028+
return tool;
1029+
} else if (BaseToolset.class.isAssignableFrom(toolClass)) {
1030+
logger.debug("Tool {} is a sub-class of BaseToolset.", toolConfig.name());
1031+
@SuppressWarnings("unchecked")
1032+
Class<? extends BaseToolset> baseToolsetClass = (Class<? extends BaseToolset>) toolClass;
1033+
BaseToolset toolset = createToolsetInstance(baseToolsetClass, toolConfig, configAbsPath);
1034+
logger.info(
1035+
"Successfully created built-in toolset: {} (class: {})", toolName, toolClass.getName());
1036+
return toolset;
1037+
} else {
1038+
logger.info("configAbsPath is: {}", configAbsPath);
10101039
throw new ConfigurationException(
1011-
"Built-in tool class " + toolClass.getName() + " does not extend BaseTool");
1040+
"Built-in tool class "
1041+
+ toolClass.getName()
1042+
+ " does not extend BaseTool or BaseToolset");
10121043
}
10131044

1014-
@SuppressWarnings("unchecked")
1015-
Class<? extends BaseTool> baseToolClass = (Class<? extends BaseTool>) toolClass;
1016-
1017-
BaseTool tool = createToolInstance(baseToolClass, toolConfig);
1018-
logger.info(
1019-
"Successfully created built-in tool: {} (class: {})", toolName, toolClass.getName());
1020-
1021-
return tool;
1022-
10231045
} catch (Exception e) {
1024-
logger.error("Failed to create built-in tool: {}", toolName, e);
1025-
throw new ConfigurationException("Failed to create built-in tool: " + toolName, e);
1046+
logger.error("Failed to create built-in tool/toolset: {}", toolName, e);
1047+
throw new ConfigurationException("Failed to create built-in tool/toolset: " + toolName, e);
10261048
}
10271049
}
10281050

10291051
private static BaseTool createToolInstance(
1030-
Class<? extends BaseTool> toolClass, ToolConfig toolConfig)
1052+
Class<? extends BaseTool> toolClass, ToolConfig toolConfig, String configAbsPath)
10311053
throws ConfigAgentUtils.ConfigurationException {
10321054

10331055
try {
1034-
// TODO:implement constructor with ToolArgsConfig
1035-
logger.debug("ToolConfig is: {}", toolConfig);
1056+
// First, try to use the fromConfig static method
1057+
try {
1058+
Method fromConfigMethod = toolClass.getMethod("fromConfig", ToolConfig.class, String.class);
1059+
return (BaseTool) fromConfigMethod.invoke(null, toolConfig, configAbsPath);
1060+
} catch (ReflectiveOperationException e) {
1061+
// Continue
1062+
}
10361063

1037-
// Try default constructor
1064+
// Second, try default constructor
10381065
try {
1039-
Constructor<? extends BaseTool> constructor = toolClass.getConstructor();
1040-
return constructor.newInstance();
1066+
Constructor<? extends BaseTool> defaultConstructor = toolClass.getConstructor();
1067+
return defaultConstructor.newInstance();
10411068
} catch (NoSuchMethodException e) {
10421069
// Continue
10431070
}
10441071

1072+
// Third, try constructor with ToolArgsConfig
1073+
// TODO: implement constructor with ToolArgsConfig
1074+
1075+
logger.debug("ToolConfig is: {}", toolConfig);
1076+
logger.debug("configAbsPath is: {}", configAbsPath);
10451077
throw new ConfigAgentUtils.ConfigurationException(
1046-
"No suitable constructor found for tool class: " + toolClass.getName());
1078+
"No suitable constructor or fromConfig method found for tool class: "
1079+
+ toolClass.getName());
10471080

1081+
} catch (ConfigAgentUtils.ConfigurationException e) {
1082+
throw e;
10481083
} catch (Exception e) {
10491084
throw new ConfigAgentUtils.ConfigurationException(
10501085
"Failed to instantiate tool class: " + toolClass.getName(), e);
10511086
}
10521087
}
1088+
1089+
private static BaseToolset createToolsetInstance(
1090+
Class<? extends BaseToolset> toolsetClass, ToolConfig toolConfig, String configAbsPath)
1091+
throws ConfigAgentUtils.ConfigurationException {
1092+
try {
1093+
Method fromConfigMethod =
1094+
toolsetClass.getMethod("fromConfig", ToolConfig.class, String.class);
1095+
return (BaseToolset) fromConfigMethod.invoke(null, toolConfig, configAbsPath);
1096+
} catch (ReflectiveOperationException e) {
1097+
throw new ConfigAgentUtils.ConfigurationException(
1098+
"No suitable fromConfig method found for toolset class: " + toolsetClass.getName(), e);
1099+
} catch (RuntimeException e) {
1100+
throw new ConfigAgentUtils.ConfigurationException(
1101+
"Failed to instantiate toolset class: " + toolsetClass.getName(), e);
1102+
}
1103+
}
10531104
}

core/src/main/java/com/google/adk/tools/BaseTool.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import static com.google.common.collect.ImmutableList.toImmutableList;
2020

21+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
22+
import com.fasterxml.jackson.annotation.JsonAnySetter;
2123
import com.fasterxml.jackson.annotation.JsonIgnore;
2224
import com.google.adk.JsonBaseModel;
2325
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
@@ -180,8 +182,12 @@ public static BaseTool fromConfig(ToolConfig config, String configAbsPath)
180182
"fromConfig not implemented for " + BaseTool.class.getSimpleName());
181183
}
182184

183-
/** Configuration class for tool arguments that allows arbitrary key-value pairs. */
184-
// TODO implement this class
185+
/**
186+
* Configuration class for tool arguments that allows arbitrary key-value pairs.
187+
*
188+
* <p>This class is used to parse tool arguments from a YAML configuration file. It supports
189+
* arbitrary key-value pairs, which can be accessed using the {@link #get(String)} method.
190+
*/
185191
public static class ToolArgsConfig extends JsonBaseModel {
186192

187193
@JsonIgnore private final Map<String, Object> additionalProperties = new HashMap<>();
@@ -193,6 +199,40 @@ public boolean isEmpty() {
193199
public int size() {
194200
return additionalProperties.size();
195201
}
202+
203+
@JsonAnyGetter
204+
public Map<String, Object> getAdditionalProperties() {
205+
return additionalProperties;
206+
}
207+
208+
@JsonAnySetter
209+
public void setAdditionalProperty(String name, Object value) {
210+
additionalProperties.put(name, value);
211+
}
212+
213+
public @Nullable Object get(String key) {
214+
return additionalProperties.get(key);
215+
}
216+
217+
public <T> @Nullable T get(String key, Class<T> type) {
218+
Object value = additionalProperties.get(key);
219+
if (value == null) {
220+
return null;
221+
}
222+
return type.cast(value);
223+
}
224+
225+
public void put(String key, Object value) {
226+
additionalProperties.put(key, value);
227+
}
228+
229+
public Object remove(String key) {
230+
return additionalProperties.remove(key);
231+
}
232+
233+
public boolean containsKey(String key) {
234+
return additionalProperties.containsKey(key);
235+
}
196236
}
197237

198238
/** Configuration class for a tool definition in YAML/JSON. */

core/src/main/java/com/google/adk/tools/BaseToolset.java

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
package com.google.adk.tools;
22

3+
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
34
import com.google.adk.agents.ReadonlyContext;
5+
import com.google.adk.tools.BaseTool.ToolConfig;
6+
import com.google.common.base.VerifyException;
7+
import com.google.errorprone.annotations.DoNotCall;
48
import io.reactivex.rxjava3.core.Flowable;
59
import java.util.List;
610
import java.util.Optional;
711

8-
/** Base interface for toolsets. */
9-
public interface BaseToolset extends AutoCloseable {
12+
/** Base abstract class for toolsets. */
13+
public abstract class BaseToolset implements AutoCloseable {
14+
15+
protected static Class<?> configType = Object.class;
1016

1117
/**
1218
* Return all tools in the toolset based on the provided context.
1319
*
1420
* @param readonlyContext Context used to filter tools available to the agent.
1521
* @return A Single emitting a list of tools available under the specified context.
1622
*/
17-
Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);
23+
public abstract Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);
1824

1925
/**
2026
* Performs cleanup and releases resources held by the toolset.
@@ -24,7 +30,33 @@ public interface BaseToolset extends AutoCloseable {
2430
* files, or other managed resources are properly released to prevent leaks.
2531
*/
2632
@Override
27-
void close() throws Exception;
33+
public abstract void close() throws Exception;
34+
35+
/**
36+
* Gets the config type for this toolset class, supporting inheritance. Subclasses can override
37+
* the configType field to use their own config class.
38+
*/
39+
public Class<?> getConfigType() {
40+
try {
41+
// First try to get the field from the current class
42+
return (Class<?>) this.getClass().getDeclaredField("configType").get(null);
43+
} catch (NoSuchFieldException e) {
44+
// If subclass doesn't declare it, walk up to parent class
45+
Class<?> superclass = this.getClass().getSuperclass();
46+
while (superclass != null) {
47+
try {
48+
return (Class<?>) superclass.getDeclaredField("configType").get(null);
49+
} catch (NoSuchFieldException ex) {
50+
superclass = superclass.getSuperclass();
51+
} catch (IllegalAccessException ex) {
52+
throw new VerifyException("Cannot access configType field", ex);
53+
}
54+
}
55+
throw new IllegalStateException("No configType field found in class hierarchy", e);
56+
} catch (IllegalAccessException e) {
57+
throw new VerifyException("Cannot access configType field", e);
58+
}
59+
}
2860

2961
/**
3062
* Helper method to be used by implementers that returns true if the given tool is in the provided
@@ -35,7 +67,7 @@ public interface BaseToolset extends AutoCloseable {
3567
* @param readonlyContext The current context.
3668
* @return true if the tool is selected.
3769
*/
38-
default boolean isToolSelected(
70+
protected boolean isToolSelected(
3971
BaseTool tool, Optional<Object> toolFilter, Optional<ReadonlyContext> readonlyContext) {
4072
if (toolFilter.isEmpty()) {
4173
return true;
@@ -51,4 +83,22 @@ default boolean isToolSelected(
5183
}
5284
return false;
5385
}
86+
87+
/**
88+
* Creates a toolset instance from a config.
89+
*
90+
* <p>Concrete classes implementing BaseToolset should provide their own static fromConfig method.
91+
* This method is here to guide the structure.
92+
*
93+
* @param config The config for the toolset.
94+
* @param configAbsPath The absolute path to the config file that contains the toolset config.
95+
* @return The toolset instance.
96+
* @throws ConfigurationException if the toolset cannot be created from the config.
97+
*/
98+
@DoNotCall("Always throws com.google.adk.agents.ConfigAgentUtils.ConfigurationException")
99+
public static BaseToolset fromConfig(ToolConfig config, String configAbsPath)
100+
throws ConfigurationException {
101+
throw new ConfigurationException(
102+
"fromConfig not implemented for " + BaseToolset.class.getSimpleName());
103+
}
54104
}

core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolset.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import org.jspecify.annotations.Nullable;
1919

2020
/** Application Integration Toolset */
21-
public class ApplicationIntegrationToolset implements BaseToolset {
21+
public class ApplicationIntegrationToolset extends BaseToolset {
2222
String project;
2323
String location;
2424
@Nullable String integration;

0 commit comments

Comments
 (0)