"GCP tools" in ADK Java is not one class. It is three families that look the same to the agent (each is something in the LlmAgent.builder().tools(...) list) but run in different places, use different credentials and fail in different ways. Model-side built-ins such as GoogleSearchTool do not execute any Java at all; they add a tool declaration to the Gemini request and Gemini does the work. Client-side tools such as VertexAiRagRetrieval and ApplicationIntegrationToolset call Google Cloud APIs from your process. And for most Google Cloud services, such as BigQuery or Cloud Storage, you write your own FunctionTool over the official client library and own every guardrail yourself.
This article maps the three families, shows working code for each, builds a guarded BigQuery tool, and covers credentials, IAM, failure modes and the composition limit on built-in tools. Class names and signatures were checked against the google/adk-java sources and the adk.dev documentation on 2026-10-04. No library version numbers are quoted; pin the versions your build actually resolves. VertexAiSearchTool has its own page, ADK Java + Vertex AI Search, and is only placed on the map here.
Three families of Google Cloud tools
The com.google.adk.tools package contains the built-ins: GoogleSearchTool, VertexAiSearchTool, BuiltInCodeExecutionTool, UrlContextTool and GoogleMapsTool, plus the workaround wrappers GoogleSearchAgentTool and VertexAiSearchAgentTool. Subpackages hold retrieval.VertexAiRagRetrieval and applicationintegrationtoolset.ApplicationIntegrationToolset. There is no BigQuery or Cloud Storage tool in the core module at the time of writing, which is why the third family exists.
The difference between families is where execution happens. GoogleSearchTool is short enough to read in full: its processLlmRequest copies the request's GenerateContentConfig, appends a Tool carrying GoogleSearch, and returns. No function call ever reaches your code, there is nothing to log at the tool layer, and grounding results come back on the model response. A client-side tool is the opposite: the model emits a function call, ADK dispatches it to runAsync, your process calls a Google API, and the result goes back as a function response.
That split decides observability (you see client-side calls in your traces, not built-in ones), cost attribution (built-ins bill through the model call), and permissions (client-side tools use whatever identity your process has).
Credentials and IAM
Client-side tools and Google client libraries authenticate with Application Default Credentials (ADC). Locally that is usually your user credentials from gcloud auth application-default login with a quota project set; on Cloud Run, GKE or Compute Engine it is the attached service account. The model client is configured separately: when the Google GenAI SDK runs against Vertex AI it reads GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION, and VertexAiRagRetrieval checks the first of those to decide its mode, as described below.
Give each agent deployment its own service account and grant roles per tool, not per project. For a read-only analytics agent that means BigQuery Job User on the project that runs jobs and BigQuery Data Viewer on only the datasets it may read. For Application Integration, the adk.dev page lists roles/integrations.integrationEditor, roles/connectors.invoker and roles/secretmanager.secretAccessor, and notes that Integration Editor rather than Integration Invoker avoids 403 errors when deploying with Agent Runtime. Treat IAM as the real boundary: instructions and in-code checks reduce mistakes, but only IAM stops a tool from doing what its identity is not allowed to do.
Built-in tools and the composition limit
The adk.dev limitations page states that Code Execution, Google Search and Agent Search cannot share an agent with any other tool, and that built-in tools cannot be used inside a sub-agent. GoogleSearchTool itself performs no check; it just appends its declaration, so expect a violating configuration to fail or misbehave at the model API rather than at build time. The documented workaround is to give each built-in its own agent and expose that agent to a root agent as a tool:
LlmAgent searchAgent = LlmAgent.builder()
.name("search_specialist")
.model(MODEL_ID)
.description("Answers questions that need current public web information.")
.instruction("Use Google Search. Cite the pages you used.")
.tools(GoogleSearchTool.INSTANCE)
.build();
LlmAgent root = LlmAgent.builder()
.name("ops_analyst")
.model(MODEL_ID)
.instruction(ROOT_INSTRUCTION)
.tools(
AgentTool.create(searchAgent), // built-in isolated in its own agent
runbookRag, // client-side tool
jiraTools, // toolset
FunctionTool.create(BigQueryTools.class, "runQuery"))
.build();ADK Java also ships GoogleSearchAgentTool.create(BaseLlm model), which builds exactly this kind of single-purpose search agent for you; its source describes it as a workaround for using google_search with other tools. Note the distinction the docs draw: wrapping through AgentTool works, while putting the built-in on an agent listed in subAgents(...) does not. The cost of the workaround is an extra model call per search and a second agent whose instruction you must maintain.
VertexAiRagRetrieval: one tool, two execution paths
VertexAiRagRetrieval searches a Vertex AI RAG Engine corpus and is the one tool that runs in either place. Its constructor takes a name, a description, a VertexRagServiceClient, a parent resource such as projects/P/locations/L, an optional list of RagResource entries and an optional vector distance threshold. When the agent's model is a Gemini model and GOOGLE_GENAI_USE_VERTEXAI is true, it adds a built-in retrieval tool to the request and Gemini retrieves directly. Otherwise it behaves as a normal function tool: the model calls it with a query argument, and runAsync calls retrieveContexts and returns the matching text, or a no-match message.
VertexRagServiceClient ragClient = VertexRagServiceClient.create(
VertexRagServiceSettings.newBuilder()
.setEndpoint("us-central1-aiplatform.googleapis.com:443")
.build());
VertexAiRagRetrieval runbookRag = new VertexAiRagRetrieval(
"search_runbooks",
"Search the SRE runbook corpus for remediation steps. Input: a short question.",
ragClient,
"projects/acme-ops/locations/us-central1",
List.of(RagResource.newBuilder()
.setRagCorpus("projects/acme-ops/locations/us-central1/ragCorpora/" + CORPUS_ID)
.build()),
0.5); // vector distance threshold; tune on your dataTwo operational notes follow from the dual mode. The same agent can behave differently in two environments if one sets GOOGLE_GENAI_USE_VERTEXAI and the other does not, so set it explicitly everywhere and test both paths. And the client is a gRPC client with its own channels: create one per process, reuse it, and close it on shutdown.
ApplicationIntegrationToolset: connectors as tools
ApplicationIntegrationToolset turns either an Application Integration workflow or an Integration Connectors connection into tools, which gives an agent governed access to systems such as Jira, Salesforce or ServiceNow without writing a client. Its Java constructor takes project, location, integration, triggers, connection, entity operations, actions, service account JSON, tool name prefix and tool instructions. The constructor validates that you supplied either an integration, or a connection plus entity operations or actions, and otherwise throws IllegalArgumentException.
ApplicationIntegrationToolset jiraTools = new ApplicationIntegrationToolset(
"acme-ops", // project
"us-central1", // location of the connection
null, // integration: unused in connection mode
null, // triggers
"jira-prod", // connection name
Map.of("Issues", List.of("LIST", "GET")), // read-only entity operations
null, // actions
null, // service account JSON: null means ADC
"jira_", // tool name prefix
"Read Jira issues to find related incidents. Never create or change issues.");Connection mode depends on an integration named ExecuteConnection in the connection's region; the setup normally creates it, and the adk.dev page explains how to create it from the Connection Tool template when it is missing. The adk.dev guide also says an empty operation list for an entity means all operations are allowed. Never rely on that default for an agent: list the operations explicitly, and keep write operations out unless the tool is wrapped in a confirmation step.
A guarded BigQuery tool
For BigQuery you write the tool. The model will produce SQL, so the tool's job is to make bad SQL cheap and harmless. The guards, in order: a dry run that rejects anything that is not a SELECT or that touches a dataset outside an allowlist, a byte budget checked before running and enforced by BigQuery with setMaximumBytesBilled, a row cap on what goes back to the model, and labels so every agent query can be found in billing and audit logs.
public final class BigQueryTools {
private static final BigQuery BQ = BigQueryOptions.getDefaultInstance().getService();
private static final long MAX_BYTES = 2L << 30; // 2 GiB per query
private static final String PROJECT = "acme-ops";
private static final Set<String> ALLOWED = Set.of("ops_analytics");
private static final int MAX_ROWS = 100;
@Annotations.Schema(description =
"Run one read-only GoogleSQL SELECT over the ops_analytics dataset. Returns at most 100 rows.")
public static Map<String, Object> runQuery(
@Annotations.Schema(name = "sql", description = "A single SELECT statement") String sql)
throws InterruptedException {
QueryJobConfiguration dry = QueryJobConfiguration.newBuilder(sql)
.setDryRun(true).setUseQueryCache(false).build();
JobStatistics.QueryStatistics stats;
try {
stats = BQ.create(JobInfo.of(dry)).getStatistics();
} catch (BigQueryException e) { // invalid SQL lands here
return Map.of("error", e.getMessage());
}
if (!JobStatistics.QueryStatistics.StatementType.SELECT.equals(stats.getStatementType()))
return Map.of("error", "Only SELECT statements are allowed.");
List<TableId> tables = stats.getReferencedTables();
for (TableId t : tables == null ? List.<TableId>of() : tables)
if (!PROJECT.equals(t.getProject()) || !ALLOWED.contains(t.getDataset()))
return Map.of("error", "Table not allowed: " + t);
if (stats.getTotalBytesProcessed() > MAX_BYTES)
return Map.of("error", "Query would scan " + stats.getTotalBytesProcessed()
+ " bytes; add a date filter or select fewer columns.");
TableResult result;
try {
result = BQ.query(QueryJobConfiguration.newBuilder(sql)
.setMaximumBytesBilled(MAX_BYTES)
.setLabels(Map.of("caller", "ops-analyst-agent"))
.build());
} catch (BigQueryException e) { // includes bytes-billed limit
return Map.of("error", e.getMessage());
}
List<String> cols = result.getSchema().getFields().stream().map(Field::getName).toList();
List<List<Object>> rows = new ArrayList<>();
for (FieldValueList row : result.iterateAll()) {
if (rows.size() == MAX_ROWS) break;
List<Object> r = new ArrayList<>();
for (int i = 0; i < cols.size(); i++) r.add(row.get(i).isNull() ? null : row.get(i).getValue());
rows.add(r);
}
return Map.of("columns", cols, "rows", rows, "truncated", rows.size() == MAX_ROWS);
}
}Errors are returned as data rather than thrown, so the model can read the message and retry with a narrower query; this is the same pattern described in writing a FunctionTool. The statement check is a convenience, not security: the service account should simply not hold write roles. The double byte guard is deliberate. The dry-run estimate gives the model a useful error message, and setMaximumBytesBilled makes BigQuery fail the job if the estimate was wrong.
Worked example: one question, four tool calls
Follow one question through the agent built above: "Did yesterday's checkout latency spike match a known runbook, and is there an open Jira issue for it?" The numbers are illustrative.
- The root model calls
runQuerywith a query over the whole latency table. The dry run reports 9.4 GB, over the 2 GiB budget, so the tool returns the error text. The model retries with a partition filter on yesterday's date and three columns; the dry run reports 310 MB, the query runs and returns 24 hourly rows, showing p99 rising from 420 ms to 2.8 s between 14:00 and 15:00. - The model calls
search_runbookswith "checkout latency spike connection pool". In client mode this is oneretrieveContextscall returning two runbook passages about pool exhaustion. - The model calls
jira_list_issues(the exact tool name is generated from the prefix, entity and operation, so read the names the toolset produces rather than assuming them). It finds one open issue that mentions the pool. - The model never needed web search, so the search agent was not invoked and no extra model call was made.
Total: two BigQuery dry runs, one BigQuery job, one RAG call, one connector call and the root model turns. Every client-side step appears in your traces with its own latency, which is what makes the agent debuggable; see tool observability metrics for what to record.
Failure modes
Failure modes to design for:
- Mixing built-ins with other tools. Expect failures at the model API, not when you build the agent. Isolate each built-in behind
AgentTool. - ADC differs by environment. Works on a laptop with user credentials, then gets 403 on Cloud Run because the service account lacks a role. Test with the deployed identity.
- Missing ExecuteConnection. Connection-mode tools fail until the integration exists in the right region.
- Runaway scans. Without a byte cap, one model-written query over an unpartitioned table can scan the whole table. Keep both guards.
- Context flooding. Returning thousands of rows or long RAG passages fills the context window. Cap rows and passage counts.
- Unclosed clients. RAG and BigQuery clients hold connections and threads; leaking one per request exhausts the process. Share clients and close them on shutdown.
- Silent mode switches.
VertexAiRagRetrievalchanges behaviour with an environment variable. Pin it in configuration.
Trade-offs
Built-in versus client-side: built-ins are the fastest path and need no code, but you cannot log, filter or rate-limit what they do, and they constrain composition. Client-side tools cost more code and give you control.
Prebuilt toolsets versus your own tools: ApplicationIntegrationToolset gives you many systems through one governed path and centralizes credentials in Integration Connectors, at the price of another managed dependency and generated tool names. Your own FunctionTool over a client library is more work but exposes exactly one well-described operation, which models use more reliably than a broad generated surface.
Function tools versus MCP or OpenAPI: if a team already runs an MCP server or publishes an OpenAPI spec for a Google Cloud-backed service, consuming that is often better than wrapping the client library again; see ADK Java + MCP and ADK Java + OpenAPI.
What to do next
- List the Google Cloud capabilities your agent needs and assign each to a family: built-in, prebuilt client-side tool, or your own FunctionTool.
- Create a dedicated service account per agent and grant dataset-level and connection-level roles only.
- Isolate every built-in tool in its own agent behind AgentTool, and test the composed agent against the real model API.
- Wrap BigQuery with dry-run checks, a dataset allowlist, setMaximumBytesBilled, a row cap and job labels.
- Set GOOGLE_GENAI_USE_VERTEXAI explicitly in every environment and test both RAG retrieval paths.
- Restrict ApplicationIntegrationToolset to listed read operations, and add confirmation before any write.
- Trace every client-side tool call and alert on error rate, latency and bytes scanned.