Production-ready agents, Part 1.9: How agents connect: MCP, A2A and UTCP
Before we build, the three ways an agent connects to the world: MCP from an agent to its tools, A2A from one agent to another, and direct calls to an existing API with UTCP or OpenAPI tools. Each explained from the ground up, with Acme examples, elicitation, the 2026 MCP spec and a test for choosing.
Look at any architecture diagram in this series and most of it is arrows. The router calls the marketing agent. The marketing agent reads BigQuery. Gemini Enterprise calls the router. Each arrow is a connection, and in agent systems there are only three kinds worth knowing: an agent using a tool on another server, an agent handing work to another agent, and an agent calling an existing API directly.
Each has a protocol, and you will meet all three names in every agent conversation: MCP (Model Context Protocol), A2A (Agent2Agent) and, more recently, UTCP (Universal Tool Calling Protocol). They used to be explained across several parts of this series. This part brings them together, so the build parts that follow can simply say “this is an MCP call, see Part 1.9” and get on with building.
Part 1 explained what an agent and a tool are, and Part 1.75 where ADK fits among the frameworks. Here we take the next step: how those agents and tools talk to each other.
If you prefer to watch first, here is the heart of this part as a short animated explainer.
Video: how agents connect to tools, to other agents and to existing APIs, in three minutes. The narration is a synthetic voice; the script and animation are mine.
Where are we in the architecture?
This part adds no new box. It explains what the arrows between the boxes are: MCP from an agent to its tools, A2A from the router to the specialists, and direct calls to existing APIs. Part 1.25 walks through the whole picture, layer by layer.
where Part 1.9 sits in the Acme architecture: highlighted boxes are the subject of this part, solid boxes come from earlier parts, faded boxes arrive later
What do I need to know first?
All three protocols are built from the same four pieces. Once these are clear, MCP, A2A and UTCP are mostly a question of which pieces each one uses.
An everyday example first: sending an order to a supplier by post. The postal service carries the envelope to an address and brings a reply back. Inside, you can either name a thing and say what to do with it (“The March invoices: send me a copy”), or fill in their standard order form, which has a box for the action you want, boxes for the details and a reference number so their reply can quote it. Printed on the form are the rules for each box: “Colour: red, green or blue only”.
In protocol terms, the postal service is HTTP, naming a thing versus filling in an action form is REST versus RPC, the order form with its reference number is JSON-RPC, and the rules printed on the form are JSON Schema. Each piece in turn, then the same pieces on an Acme call:
HTTP is the protocol a client and a server use to exchange requests and responses on the web. It
is an application-layer protocol (Layer 7 in the usual networking model); underneath, the bytes
travel over a transport protocol such as TCP, or QUIC for HTTP/3. A request names a method (GET,
POST), a path and some headers, and may carry a body. The client is the side that asks and the
server is the side that answers; the address it answers on is the endpoint, such as
https://bigquery.googleapis.com/mcp.
REST and RPC are two styles of API built on HTTP:
- REST is resource-oriented. The URL names a thing and the HTTP method says what to do with it:
GET /campaigns?brand=Verdereads campaigns,POST /orderscreates an order. It uses HTTP’s own methods and status codes directly. - RPC (remote procedure call) is action-oriented. Calling the API feels like calling a function:
get_spend(brand="Verde"). Over HTTP, that usually means one endpoint that receives a message saying which function to run and with what arguments.
JSON-RPC 2.0 is a small, strict format for RPC messages written in JSON. A request has a
"jsonrpc": "2.0" member, a "method", optional "params" and an "id"; the response carries the same
id with either a "result" or an "error". A request without an id is a notification, which gets
no response. How does this apply to Acme? Here is one Acme call, in the shape MCP uses: the method is
the action box, the params are the detail boxes and the id is the reference number:
Request {"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "get_spend", "arguments": {"brand": "Verde"}}}
Response {"jsonrpc": "2.0", "id": 7,
"result": {"content": [{"type": "text", "text": "Verde spend: EUR 120,400"}]}}
(Simplified; the result shape is illustrative.)
JSON Schema describes what a tool accepts. It lives inside each tool’s definition and tells the
model which arguments go in params, of which types, and which values are allowed:
{"type": "object",
"properties": {"brand": {"type": "string", "enum": ["Acme Pure", "Verde", "Harvest Row"]}},
"required": ["brand"]}
With that schema, the model knows brand must be one of three names, which is how Part 2’s narrow tools
stop it inventing a fourth.
What are the three ways an agent connects?
The short answer: what is on the other side decides the connection. A tool goes through MCP, another agent goes over A2A, and an API that already exists can be called directly.
An everyday example first: a café manager getting through a Monday. To check today’s takings, they press a button on the till: one operation, no judgement, the same answer every time. To plan next week’s menu, they ask the head chef, who has their own expertise, may take a day and may come back with questions. To reorder milk, they ring the dairy’s existing order line, which was there long before the café opened. The till is a tool, the head chef is another agent, and the dairy’s order line is an existing API.
three ways an agent connects: to a tool through an MCP server, to another agent over A2A, and to an existing API directly with UTCP or OpenAPI tools; what is on the other side decides which
How does this apply to Acme? Here are the three on Acme’s system, one example each, in the same order as the till, the head chef and the dairy:
Agent to tool (MCP) the marketing agent runs one read-only query through Google's managed
BigQuery MCP server: execute_sql_readonly(...) -> rows
Agent to agent (A2A) the router hands "What did the stockouts cost us?" to the supply agent,
which reasons over its own data under its own rules and returns findings
Agent to existing API a hypothetical promotions API, GET /promotions?brand=Verde, called
(UTCP or OpenAPI tools) directly from a published description, with no wrapper server
Here is how each one uses those building blocks, as of October 2026:
MCP connects an agent to a tool, through an MCP server
discovery JSON-RPC: tools/list returns each tool's name, description and JSON Schema
call JSON-RPC: tools/call with the tool name and arguments
server an MCP server is required, but you do not always build it: Google runs one for
BigQuery. Local servers talk over standard input and output instead of HTTP.
UTCP connects an agent to a tool or existing API, with no server in the middle
discovery fetch a manual (for example from GET /utcp), or convert an OpenAPI spec
call the API's own protocol, called directly: HTTP, CLI, gRPC and others
server none added; the existing API is the server
A2A connects an agent to another agent
discovery HTTP GET of the agent card at /.well-known/agent-card.json
call SendMessage (named message/send before v1.0), over the JSON-RPC binding by
default; gRPC and HTTP+JSON bindings also exist
server the remote agent itself; there is no middleman
A fourth connection, from the agent to the screen (cards, forms and charts rather than text), has its own standards: A2UI, AG-UI and MCP Apps. Because it depends on the front door, it comes later, in Part 12.75.
The question that separates them is what is on the other side. A tool does one well-defined thing and has no judgement of its own. An agent has its own model, instructions and permissions, and may take a while. An existing API is a tool that was built for programs before agents existed. The rest of this part takes each in turn, then gives a test for choosing.
What exactly goes over the wire?
Diagrams of arrows only go so far. This section shows the real messages, with every field name taken
from the current specifications: MCP 2026-07-28, A2A 1.0 and UTCP 1.1 (as of October 2026). Each
message mixes up to four layers, defined by different standards, and the confusion about these protocols
usually comes from mixing the layers up:
- HTTP carries the request (method, path, headers). Defined by the IETF (RFC 9110).
- The JSON-RPC 2.0 envelope is the outer wrapper:
jsonrpc,id,method,paramson the way in,jsonrpc,idandresult(orerror) on the way out. Defined by the JSON-RPC specification. MCP and A2A use it; UTCP does not. - Protocol fields are everything inside the envelope:
tools/call,content,SendMessage,Task,tool_call_template. Each protocol defines its own: MCP in its TypeScript schema (schema.ts), A2A in Protocol Buffers (the specification saysa2a.proto“is the single authoritative normative definition”, serialised to JSON on the wire), UTCP in its own specification. - JSON Schema describes a tool’s arguments: which names, which types, which values. It is a separate standard (json-schema.org) that MCP and UTCP embed inside their tool definitions. A2A does not use it for skills.
In the postal picture from the start of this part: HTTP is the van and the address, the JSON-RPC envelope is the order form’s frame with its reference number, the protocol fields are what each supplier asks you to write in the boxes, and JSON Schema is the rules printed beside each box. Most “MCP versus A2A” confusion is someone comparing a van with a form.
MCP, A2A and UTCP side by side, layer by layer: the HTTP request, the JSON-RPC 2.0 envelope, the fields each protocol defines, and where JSON Schema describes the arguments; MCP and A2A use the JSON-RPC envelope, UTCP has none; MCP and UTCP describe tool arguments with JSON Schema, A2A skills do not
The exact messages follow, for the same Acme job in each protocol: Verde’s media spend. Comments mark
the layers (jsonc is JSON with comments, used here only for the annotations); values are Acme’s, field
names are the specification’s.
MCP: discover with tools/list, call with tools/call
// HTTP: POST /mcp
// MCP-Protocol-Version: 2026-07-28
// Mcp-Method: tools/list
{
"jsonrpc": "2.0", "id": 1, "method": "tools/list", // JSON-RPC envelope
"params": { "_meta": { "io.modelcontextprotocol/clientInfo": // MCP: who is asking, on every
{ "name": "acme-marketing", "version": "1.0" } } } // request (no session)
}
// Response
{
"jsonrpc": "2.0", "id": 1, // JSON-RPC envelope
"result": {
"resultType": "complete", // MCP field
"tools": [{
"name": "get_spend", // MCP fields
"title": "Media spend by brand",
"description": "Return total media spend in EUR for one brand.",
"inputSchema": { // JSON Schema starts here
"type": "object",
"properties": { "brand": { "type": "string",
"enum": ["Acme Pure", "Verde", "Harvest Row"] } },
"required": ["brand"]
} // JSON Schema ends here
}],
"ttlMs": 300000, "cacheScope": "public" // MCP: the list may be cached
}
}
// HTTP: POST /mcp Mcp-Method: tools/call Mcp-Name: get_spend
{
"jsonrpc": "2.0", "id": 2, "method": "tools/call", // JSON-RPC envelope
"params": {
"name": "get_spend", // MCP field
"arguments": { "brand": "Verde" } // must match inputSchema above
}
}
// Response
{
"jsonrpc": "2.0", "id": 2, // JSON-RPC envelope
"result": {
"resultType": "complete", // MCP fields
"content": [{ "type": "text", "text": "Verde media spend: EUR 120,400" }],
"isError": false
}
}
(Shapes from the MCP 2026-07-28 tools page and release post; _meta abridged.) If the server needs
something from the person first, the result instead has "resultType": "input_required" and an
inputRequests object, and the client retries with inputResponses (the elicitation section below).
A2A: read the card, then SendMessage
// HTTP: GET /.well-known/agent-card.json (plain HTTP, no JSON-RPC)
{
"name": "acb_demo_supply", // A2A AgentCard fields
"description": "Supply specialist: OTIF, unmet demand and service penalties",
"supportedInterfaces": [{ "url": "https://.../a2a/app",
"protocolBinding": "JSONRPC", "protocolVersion": "1.0" }],
"capabilities": { "streaming": true },
"skills": [{ // A2A AgentSkill: no JSON Schema,
"id": "supply_measures", "name": "Supply measures", // only a description, tags and
"description": "OTIF, unmet demand and penalties by brand, region and week",
"tags": ["supply", "otif"], // example prompts
"examples": ["What did the Acme Pure stockouts cost us?"]
}]
}
// HTTP: POST https://.../a2a/app A2A-Version: 1.0
{
"jsonrpc": "2.0", "id": 1, "method": "SendMessage", // JSON-RPC envelope
"params": {
"message": { // A2A Message fields
"role": "ROLE_USER",
"parts": [{ "text": "What did the Acme Pure stockouts in Western Europe cost us?" }],
"messageId": "msg-7f3a",
"metadata": { "user_id": "u-maria", "home_team": "marketing" } // read by code, not the model
}
}
}
// Response: result holds either a "task" or a "message"
{
"jsonrpc": "2.0", "id": 1, // JSON-RPC envelope
"result": {
"task": { // A2A Task fields
"id": "task-91c2", "contextId": "ctx-55aa",
"status": { "state": "TASK_STATE_COMPLETED" },
"artifacts": [{ "artifactId": "art-1", "name": "Findings",
"parts": [{ "text": "Penalties: EUR 36,026 in Western Europe" }] }]
}
}
}
(Field names from the A2A 1.0 specification; IDs are illustrative, and the card is shortened. Before
v1.0 the method was message/send and the role was user.) Notice what is missing: no argument
schema. The supply agent receives a sentence and decides for itself how to answer it, which is exactly
the difference between handing work to an agent and calling a tool.
UTCP: read the manual, then call the API itself
// HTTP: GET https://promo.acme.example/utcp (plain HTTP, no JSON-RPC)
{
"manual_version": "1.0.0", "utcp_version": "1.0.1", // UTCP manual fields
"tools": [{
"name": "get_promotions", // UTCP tool fields
"description": "List planned promotions for one brand in one month.",
"inputs": { // JSON Schema starts here
"type": "object",
"properties": { "brand": { "type": "string" }, "month": { "type": "string" } },
"required": ["brand", "month"]
}, // JSON Schema ends here
"tool_call_template": { // UTCP: how to call the real API
"call_template_type": "http",
"url": "https://promo.acme.example/promotions",
"http_method": "GET"
}
}]
}
// The call: no envelope, no UTCP fields, just the API's own request and response
GET https://promo.acme.example/promotions?brand=Verde&month=2026-11
200 OK
[{"promotion": "Verde Winter Refill", "start": "2026-11-09", "discount_pct": 15}]
(Manual shape from utcp.io’s quick start; the promotions API and its response are Acme’s.) After discovery, UTCP is out of the way: the request and response belong to the API, so whatever the API returns is what the agent gets.
Is UTCP a competitor to MCP?
Partly, and it says so itself. UTCP’s documentation has a page titled “UTCP vs MCP”, and its pitch (“no wrapper servers required”) is aimed squarely at MCP’s server-in-the-middle design. For the specific job of letting an agent call an API that already exists, the two are alternatives, and you would usually pick one.
But it is not a full replacement, for three reasons. UTCP lists MCP among the protocols its call templates can use, so a UTCP client can call an MCP server too. UTCP has no answer to MCP’s server-side features, such as resources, prompts, elicitation or a single place to filter and log tool calls. And it does not compete with A2A at all: neither UTCP nor MCP hands work to another agent. As of October 2026, MCP is also the one the platforms in this series support natively (ADK, Google’s managed MCP servers and Gemini Enterprise’s connectors), which is why the MCP section below keeps MCP as Acme’s default and treats UTCP’s “no wrapper” idea as something to apply through ADK’s OpenAPI tools.
How does an agent use tools from an MCP server?
The short answer: the agent asks the MCP server which tools it has (tools/list), and the model
calls them by name (tools/call); the server does the backend-specific work. You write no
connection code for that backend.
In Part 2 we will build tools as Python functions inside Acme’s own codebase. That works well when you own the data and the API. But what if you need the agent to query a system managed by another team, or a managed cloud database like BigQuery, where you do not want to write and maintain custom connection code for every data source?
This is where the Model Context Protocol (MCP) comes in. Think of an MCP server as a universal socket. Instead of an agent knowing how to talk to BigQuery, Jira, or a filesystem, the agent knows how to talk MCP. The MCP server translates that standard protocol into the specific actions its backend requires.
An MCP server can expose three kinds of capability, called primitives. The easiest way to remember them is by who decides when each one is used, which the specification spells out:
the three MCP primitives sorted by who triggers them: prompts are chosen by the user, like a slash command; resources are attached by the application, like an at-mention or a file; tools are called by the model; in ADK, tools come through McpToolset, prompts through McpInstructionProvider, and resources through use_mcp_resources or application code
- Tools are functions the model can call, such as
execute_sql_readonly. The spec says tools are “model-controlled”: the model discovers and invokes them “based on its contextual understanding and the user’s prompts”. - Resources are read-only data the client can read, such as a file, a table schema or a log, each
with a URI. They are “application-driven”, “with host applications determining how to incorporate
context based on their needs”: the application, or the user through it, decides what to load, the
way you attach a file or type
@in a chat app. A client can also listen for changes to a resource; in the 2026-07-28 spec it sends asubscriptions/listenrequest and receivesnotifications/resources/updatedwhen a watched resource changes. - Prompts are reusable message templates with arguments, such as “weekly spend summary for one
brand”. They are “user-controlled”, “exposed from servers to clients with the intention of the user
being able to explicitly select them for use”; the spec’s own example is slash commands. The client
calls
prompts/getwith the arguments, and the server returns ready-made messages, which can embed resources so the template arrives with its data attached.
A simple way to hold the three apart is a restaurant table. The diner picks the set menu: that is a prompt, chosen by the user. The waiter puts the allergen card and the specials board on the table before anyone orders, because the restaurant knows they are needed: that is a resource, attached by the application. Which knife and pan to use is decided in the kitchen by the cook, while cooking: that is a tool, chosen by the model. Only the kitchen changes anything; the menu and the card are just things to read. The misconception to avoid is treating all three as “things the model calls”; only tools are.
How does this apply to Acme? Here is the same split on one server.
A worked example: one MCP server, all three primitives
At Acme, this is how it would work: the marketing team publishes an MCP server for its analysts, and Maria uses it from a chat client:
Resource Maria types @ and picks "campaign_performance schema". The client reads the resource and
puts the column list into the context before the model runs. No tool call, no guessing.
Prompt Maria picks /weekly_spend_summary and enters brand = Acme Pure. The client calls
prompts/get; the server returns the team's agreed template: "You are a marketing analyst.
Using the schema attached, summarise last week's spend for Acme Pure in four sections ..."
Tool The model, following that template, calls execute_sql_readonly(sql="SELECT ...") to fetch
the numbers, then writes the summary.
A real example: GitHub’s MCP server
The Acme server above shows the idea. GitHub’s own open-source MCP server
(github/github-mcp-server) uses all three primitives,
so it shows the split on a real product. Suppose you are signed in as octocat, working on
octocat/payment-service:
GitHub's open-source MCP server, signed in as one user: its one prompt, issue_to_fix_workflow, is picked by the user; its resource templates address repository files by repo URIs for a branch, commit, tag or pull request; its tools, such as list_issues, search_repositories, issue_write, push_files and create_pull_request, are called by the model, and only tools change anything
Tools: what the model calls. When the client connects, tools/list returns the tools and their
schemas. A few of them:
list_issuesandsearch_repositoriesare reads with filters the model chooses during the task, such as open issues labelledbug. That is why they are tools rather than resources: the model, not the user, knows the filter.push_filescommits one or more files to a branch in a single call. Its arguments, from the README, areowner,repo,branch,files(each with apathandcontent) andmessage:
{
"name": "push_files",
"arguments": {
"owner": "octocat",
"repo": "payment-service",
"branch": "fix/timeout-bug",
"message": "Fix retry backoff in the webhook handler",
"files": [{ "path": "src/webhook.py", "content": "import time\n..." }]
}
}
issue_write(withmethodset tocreateorupdate) andcreate_pull_requestchange state on GitHub. Anything that changes something must be a tool; resources are read-only data, and a prompt only returns messages.
Resources: what the application attaches. The server publishes resource templates, URI
patterns a client can fill in, for repository content: repo://{owner}/{repo}/contents{/path*}, and
the same for a branch (refs/heads/{branch}), a commit (sha/{sha}), a tag (refs/tags/{tag}) or a
pull request’s head (refs/pull/{prNumber}/head). When you type @README.md in a client that supports
resources, it can read repo://octocat/payment-service/contents/README.md and put the Markdown into the
context before the model starts, instead of hoping the model calls get_file_contents with the right
path.
Prompts: what the user picks. The server ships prompts too. One is issue_to_fix_workflow, with
arguments such as owner, repo, title, description and labels. Picking it does not create
anything. prompts/get returns a short scripted conversation that tells the model to create the issue,
assign it to Copilot coding agent and keep track of the pull request; the model then does that with
tools. A prompt is a recipe; the tools do the cooking.
A team could add its own prompt in the same style, for example a prepare_pull_request prompt that
embeds the team’s pull request checklist and asks the model to review the change and then call
create_pull_request. That one is illustrative; it is not in GitHub’s server.
Two of the server’s settings show the security side of the same split. Started with --read-only, it
offers only its read-only tools, which removes every write in one switch. Its lockdown mode filters
untrusted repository content, such as issue and pull request text, to reduce prompt injection; the
README is clear that this is a best-effort filter and not an authorisation boundary (Prerequisite 4).
When should something be a resource rather than a tool? When the user or the application already knows exactly what belongs in the context, such as one schema, one ticket or one log, a resource puts it there directly. That saves a model round trip and removes the chance of the model searching for the wrong thing. When the model must decide what to fetch, make it a tool. Prompts fit any repeatable workflow, such as a review checklist or a weekly summary, where the team wants everyone to start from the same tested instructions rather than writing their own.
Why do most agents only use tools, and what does ADK support?
Two reasons. Developers already know function calling, so wrapping everything as a tool is the shortest path. And resources and prompts assume a person or an application choosing things, which an agent running behind an API often does not have. ADK reflects this. From ADK Python’s source, as of November 2026:
Primitive Who triggers it MCP methods In ADK
Tools the model tools/list, tools/call McpToolset consumes them; tool_filter
narrows them; adk_to_mcp_tool_type
converts ADK tools so you can expose
them from your own MCP server
Resources the application or user resources/list, resources/read off by default; use_mcp_resources=True
adds a load_mcp_resource tool, and
McpToolset.read_resource lets your own
code read one
Prompts the user (or app start-up) prompts/list, prompts/get McpInstructionProvider fetches a named
prompt and uses it as the agent's
instruction
Two details are worth knowing:
use_mcp_resources=Truehands the choice to the model. It adds aload_mcp_resourcetool, so the model decides which resource to load. Treat what it loads like any other tool result. If your code should decide, callread_resourceyourself and put the result in the context.McpInstructionProviderturns an MCP prompt into an instruction. You pass it the server’s connection details and a prompt name, and use it as the agent’sinstruction. On each call it fetches the prompt, fills any arguments from session state keys with the same names, and joins the text messages into the instruction. That lets one team keep a shared, versioned instruction on its MCP server instead of copying it into every agent. Only text content is used.
ADK also recognises MCP Apps: a tool that declares a UI resource through meta.ui.resourceUri can
have that interface rendered by a client that supports it. Part 12.75 covers MCP Apps.
In Acme’s own build, the reusable-instructions job is done by skills (Part 4), which the agent loads itself; an MCP prompt is the alternative when the instructions belong to the team that owns the MCP server.
Local vs. remote MCP servers
MCP servers come in two main flavours based on how the agent connects to them. The choice defines your architecture.
Think of a calculator on your desk versus your bank’s phone line. The calculator sits next to you, starts when you switch it on and needs no network; the bank’s line is a separate service run by someone else, which you reach over the network and which serves many callers at once.
- Local (stdio) servers: These run as local processes on the exact same machine or container as the agent. The agent starts them (often using a command like
npxfor JavaScript servers) and talks to them over standard input and output streams. You might use this for a local filesystem server during development, or a local database proxy. - Remote (streamable HTTP) servers: These run as entirely separate services, often managed by a cloud provider, an enterprise IT team, or a third-party vendor. The agent connects to them over a network using HTTP, and the server may stream longer responses back using Server-Sent Events (SSE).
A quick test, cheapest first:
1. Does a managed remote server already exist for this backend? use it (nothing to run)
2. Is it a developer tool on one machine, such as local files? local (stdio) server
3. Will several agents or teams share it in production? remote (streamable HTTP) server
How does this apply to Acme? For Acme Consumer Brands, the marketing agent runs as a service in Google Cloud. If it needs to query BigQuery, writing a custom local tool means we carry the maintenance burden of keeping the BigQuery client libraries up to date, handling retries, and managing query quotas in our agent code.
Instead, Google Cloud provides a fully managed remote BigQuery MCP server (available at a global endpoint https://bigquery.googleapis.com/mcp). We just point our agent at the endpoint.
Connecting ADK to an MCP server
In the Agent Development Kit (ADK), you do not need to rewrite your agent’s core logic to use MCP. Instead, you give the agent an McpToolset. This class acts as an intelligent client that bridges the ADK agent’s native tool-calling system with the external MCP server’s protocol.
When building the toolset, you specify the connection parameters. If you were connecting to a local process, you would use StdioConnectionParams. To connect to a remote server like BigQuery, you use StreamableHTTPConnectionParams, providing the endpoint URL and any necessary authentication headers.
an ADK agent using McpToolset to connect over HTTP to a BigQuery MCP server, filtering tools to just execute_sql_readonly
Filtering broad tools to stay narrow
There is a catch when using managed MCP servers, and the bottom line is: connect to the broad server, but show the model only the tools its job needs. Think of a new starter handed a master key on day one and asked one question; they will open every door before answering. Hand them one key and they go straight to the right room.
A standard MCP server is built to be generally useful to many different kinds of agents, so it almost always exposes a broad, generic set of tools.
The BigQuery MCP server, for instance, exposes a full suite of exploration tools:
list_dataset_ids: to find datasets in a project.get_dataset_info: to read dataset metadata.list_table_ids: to find tables inside a dataset.get_table_info: to fetch the schema of a specific table.execute_sql_readonly: to run aSELECTstatement.
If we give our marketing agent this entire toolset, we break the series’ rule about narrow tools (Part 2 explains it). The agent might get confused by the generic list-and-search tools. When asked a simple question, it might decide to explore all the datasets, then list all the tables, then read their schemas, burning tokens and time in a complex planning loop, when it only really needs to run one specific query against a known table.
To fix this, McpToolset allows you to apply a tool_filter. You define exactly which tools from the server the agent is allowed to see. The server still exposes everything over the network, but the ADK harness intercepts the list and hides the unneeded tools from the model’s context window.
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
# Connect to the remote BigQuery MCP server
bq_mcp = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://bigquery.googleapis.com/mcp",
headers={"Authorization": "Bearer YOUR_GCP_TOKEN"}
),
# Keep the agent's tools narrow by filtering the broad server
tool_filter=["execute_sql_readonly"]
)
marketing_agent = Agent(
model="gemini-flash-latest",
name="marketing_agent",
tools=[bq_mcp]
)
(Code is illustrative. In a real system, you would generate this token dynamically using Google Cloud Application Default Credentials, rather than hardcoding it).
Here is the difference on one question, “What did we spend on the Acme Pure Refill Launch in Western Europe in the week of 4 May?” (a likely path, not a guaranteed one):
- Unfiltered, the model may explore first:
list_dataset_ids, thenlist_table_ids, thenget_table_infooncampaign_performance, and only thenexecute_sql_readonly. Four tool calls, four extra model calls, and every schema it read stays in the context. - With
tool_filter=["execute_sql_readonly"], the only tool it can see is the query tool, so it makes one call and gets back the row it needs: EUR 36,317.41.
But if the agent only has the query tool, how does it know the schema?
It has to know it already. With tool_filter=["execute_sql_readonly"], the model cannot call
get_table_info, so it can only write a correct query if the table names, columns and grain are
already in its context. Otherwise it will guess a column name, get an error, and guess again, which
costs more tokens and time than the exploration we removed. So filtering is only half the design; the
other half is deciding where the schema comes from. There are three options:
Option How Cost and risk
1. Schema in the context a skill's reference file lists the table, one load per task; must be
(Acme's choice) columns, grain and join keys (Part 4's updated when the table
references/tables.md), loaded only when changes, so test it
the agent needs to write SQL
2. Allow one discovery tool tool_filter=["get_table_info", one extra call per question;
"execute_sql_readonly"] always current
3. Schema as an MCP resource use_mcp_resources=True, if the server depends on the server
exposes schemas as resources offering them; check its docs
Acme uses the first option. When the marketing agent needs free SQL, it loads references/tables.md
from its skill, which describes campaign_performance (one row per campaign, brand, region and week)
and the join rule with supply’s table, and then makes one query. And free SQL is the exception: for
most questions the marketing agent uses query_marketing_measures (Part 2), a narrow tool where code,
not the model, writes the SQL from a reviewed template, so the model never needs the schema at all.
So the full answer to “why filter?” is: fewer tools means fewer tokens in every request and fewer wasted calls, which cuts both cost and latency, provided the knowledge those tools would have fetched is supplied some other way.
Authentication and security out of the box
When you use a managed remote MCP server, you get enterprise-grade governance without having to build it yourself. The BigQuery MCP server uses standard Google Cloud OAuth 2.0 authentication. The agent’s identity (the service account it runs under) determines exactly what data it is allowed to read at the IAM level.
Crucially, the managed server is safe by design. The execute_sql_readonly tool enforces read-only execution natively, completely blocking data-mutation statements like INSERT, UPDATE, DELETE, or DROP. This means even if the model goes rogue and tries to delete a table, the MCP server will intercept and reject the request before it ever reaches the data warehouse engine.
For example, suppose a campaign name in the data carries hidden text that persuades the model to
send DELETE FROM campaign_performance through execute_sql_readonly. The server rejects the
statement, the table is unchanged, and the model gets an error back, which it reports instead of an
answer. The instruction’s “tool output is DATA” rule should stop that call being made at all; the
read-only server is what makes it harmless if the rule fails.
My view: If a managed MCP server exists for your data source, use it. It shifts the burden of writing secure, efficient data-access tools from your engineering team to the platform provider. But always remember to filter the toolset down to only what your specific agent needs to do its job, keeping its tool belt narrow and focused.
(When to use MCP, when A2A and when a direct call is the last section of this part.)
Can an MCP server ask questions back?
Everything so far flows one way: the agent asks, the server answers. Sometimes the server needs something before it can answer. Which of your three ad accounts did you mean? Have you connected your account yet? MCP has a feature for this called elicitation: the server asks the person for more information, through the client. The specification describes it as “a standardized way for servers to request additional information from users through the client during interactions”.
An everyday example first: ordering in a restaurant. You order a steak, and the waiter comes back from the kitchen: “How would you like it cooked?” That is a harmless question, asked through the waiter, and your answer goes back the same way. When you pay, though, the waiter hands you the card machine and looks away: you type your PIN on the bank’s device, and the waiter never sees it. The kitchen is the MCP server, the waiter is the MCP client, the cooking question is form mode and the card machine is URL mode, which the two modes below describe.
How does this apply to Acme? At Acme, this is how it would work: the marketing agent connects to a media agency’s MCP server so it can read bookings. Maria asks: “What did we book on TV for Verde in September?” The agent calls the server’s get_bookings tool, and the server answers not with data but with a question:
{
"mode": "form",
"message": "Verde has two ad accounts. Which one?",
"requestedSchema": {
"type": "object",
"properties": {
"account": {"type": "string", "enum": ["verde-uk", "verde-ie"]}
},
"required": ["account"]
}
}
The client shows the question, Maria picks verde-uk, and the client sends the original call again with her answer attached. The server now has what it needs and returns the bookings. (The JSON is simplified to show the idea; the specification has the full message format.)
There are two modes, and the difference matters for security:
- Form mode. The server sends a small form described by a restricted JSON schema: a flat object whose fields are strings, numbers, booleans or fixed lists of choices. The answer goes back through the client. The specification is blunt that servers “MUST NOT use form mode elicitation to request sensitive information such as passwords, API keys, access tokens, or payment credentials”.
- URL mode. The server sends a link instead, for things that “must not pass through the MCP client”: signing in to a third-party service, a payment, or entering an API key on the vendor’s own page. The person completes it in their browser, and the secret never touches the agent. The client must show the person which domain the link goes to and get their consent before opening it. URL mode arrived in the
2025-11-25version of the specification, which says it “may change in future protocol revisions”, and it is not for signing the agent in to the MCP server itself.
The person can answer in three ways: accept (with the data), decline (no), or cancel (closed the question without choosing). Servers “MUST handle cases where the user declines or cancels”, so a well-built server returns a clear refusal rather than hanging. The client must also show which server is asking, so a person never answers a question without knowing who wants it.
A note on versions. In the current specification (2026-07-28), the server no longer sends the question as a separate request. It returns an “input required” result, and the client retries the original call with the answers. The idea is the same; only the plumbing changed, and your MCP library hides it.
an MCP server answers a tool call with a question, the agent's elicitation callback decides in code whether to ask a person, decline, or pass a sign-in link to the user, then the call is retried with the answer
Why is this hard for a production agent?
Elicitation was designed with a person sitting at the client: a desktop app or an IDE that can pop up a form. Our agent runs on Agent Runtime behind Gemini Enterprise. Here the MCP client is our agent code, running on a server, and Maria is a long way away. Nobody is there to see a pop-up. In the restaurant picture, this is a takeaway ordered through an app: there is no waiter at the table, so the kitchen’s question has to reach the customer some other way, or not be asked at all.
So you have to decide, in code, what happens when a server asks. ADK’s McpToolset takes an elicitation_callback for exactly this; its docstring says it handles “elicitation requests from the MCP server (elicitation/create), including URL-mode elicitations used for out-of-band flows such as auth challenges”. The sketch below shows the decisions, not the exact types, which come from the MCP Python SDK:
# Illustrative only: the real parameter and result types come from the MCP SDK.
ALLOWED_FORM_FIELDS = {"agency-mcp": {"account"}}
async def handle_elicitation(request):
server = request.server_name
if request.mode == "url":
# Never open the link here. Hand it to Maria through the front door,
# showing the domain; she opens it herself, or says no.
agreed = await send_link_to_user(server, request.message, request.url)
return {"action": "accept" if agreed else "decline"}
fields = set(request.requested_schema["properties"])
if fields <= ALLOWED_FORM_FIELDS.get(server, set()):
# A known, harmless question: put it to the person, not the model.
answer = await ask_user(server, request.message, request.requested_schema)
return answer or {"action": "cancel"}
return {"action": "decline"} # anything unexpected
bookings = McpToolset(
connection_params=StreamableHTTPConnectionParams(url="https://mcp.agency.example/mcp"),
tool_filter=["get_bookings"],
elicitation_callback=handle_elicitation,
)
Three rules sit behind that code:
- Decline by default. A server you did not expect to ask questions should get a no. An unexpected question is a sign the server changed, or is trying something.
- A person answers, never the model. It is tempting to let the model fill in the form, because it usually knows the answer. But that turns a question meant for Maria into one the server can put to the model, which is a route for prompt injection. This is the same rule as in Prerequisite 3: never let the model be the last check. For the asking itself, reuse the human approval flow from Part 9.
- Secrets only through URL mode, and only to the person. If a server asks for a key or a password in a form, decline and report it. That server is breaking the specification.
One honest gap: as of October 2026, I could not find anything in the public Gemini Enterprise documentation about showing an MCP server’s elicitation to the end user. So for now your callback has to carry the question to Maria itself, using whatever channel your front door gives you (Part 11.75 covers what Gemini Enterprise offers).
What about sampling and roots?
You may also meet two older client features. Sampling lets a server ask the client’s model to write something for it, and roots lets the client tell a server which folders or files are relevant. Both, along with logging, are deprecated in the 2026-07-28 specification. They still work for at least twelve months, but “new implementations should not add support for them”. The suggested replacements are to call a model provider directly instead of sampling, and to pass folders and files as tool parameters or configuration instead of roots. ADK still offers a sampling_callback; I would leave it unset. Letting a third-party server spend your model budget on prompts it wrote is not a trade I would make.
My view: elicitation is a good idea that solves a real problem, a tool that cannot finish without one more fact. But in a headless production agent it is also a new way in, so treat every question from a server like a tool result from a stranger: check it in code, put it to a person when it is safe, and decline everything else.
What changed in the 2026 spec, the release people call “MCP 2.0”?
You will hear “MCP 2.0” a lot. There is no specification with that name: MCP versions are dates,
and the current one is 2026-07-28. “2.0” is shorthand that spread through SDK releases (the official
C# SDK shipped as v2.0) and launch posts such as Cloudflare’s “The next generation of MCP”. What it
refers to is real, though. The MCP team’s release post
says the protocol is “transforming from a bidirectional stateful protocol into a request/response
stateless protocol”. The changes that matter for a production agent:
- No handshake, no sessions. The
initializeexchange and theMcp-Session-Idheader are gone. “Each request now travels on its own, carrying its protocol version, client identity, and client capabilities”, and an optionalserver/discovercall returns capabilities if you want them first. The result: “any request can land on any server instance behind a plain round-robin load balancer”. - Routing in headers. “Method and tool names travel in the
Mcp-MethodandMcp-NameHTTP headers, so gateways can route and authorize on headers directly”, without opening the request body. - Questions back without open streams. Elicitation, sampling and roots now use Multi Round-Trip
Requests: the server returns
input_required, and the client retries the call with the answers (the elicitation section above shows this). - Cacheable tool lists. List responses carry
ttlMsandcacheScopeand a deterministic order, “so clients can cache tool catalogs and keep upstream prompt caches stable across reconnects”. - Extensions, safer sign-in, and a deprecation policy. Tasks, MCP Apps (servers returning “interactive HTML interfaces (data visualizations, forms, dashboards) that render directly in the chat”) and Enterprise Managed Authorization become formal extensions; authorization servers return the issuer and clients validate it (RFC 9207); Roots, Sampling, Logging and the legacy HTTP+SSE transport are deprecated, each with at least twelve months’ notice.
“Stateless” is easiest to picture with a call centre. In the old style, you had to get the same adviser every time, because only their screen held your conversation; if they went home, you started again. In the new style, you read out your customer number and what you need on every call, so whichever adviser picks up can help. The adviser with your notes is the old session held on one server instance; reading out your details each time is every request carrying its own version, client identity and capabilities.
How does this apply to Acme? Here is what that means for one Acme call. Before, the marketing agent opened a session with the BigQuery MCP server, and every later call had to reach the instance that held that session. Now each call stands alone:
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql_readonly <- a gateway can allow or block this tool from the header
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "execute_sql_readonly", "arguments": {...},
"_meta": {"io.modelcontextprotocol/clientInfo": {...}}}}
(Shape from the release post; the arguments are left out.) Three practical effects: a server that
Acme runs itself can sit behind an ordinary load balancer or a serverless platform with no sticky
sessions; a gateway can enforce “the marketing agent may call execute_sql_readonly and nothing
else” from one header; and a cached, stable tool list keeps the start of every model request the same,
which is what context caching needs (Part 11). One caution from the same post: dropping the session
“doesn’t force your application to be stateless”. If a server needs state across calls, it hands the
model an explicit handle to pass back, so check how any MCP server you depend on carries state before
you upgrade, and check which MCP SDK version your ADK release uses.
What is UTCP, and is it a replacement for MCP?
The Universal Tool Calling Protocol (UTCP) is a different answer to the same question. Its site describes “a lightweight, secure, and scalable standard that enables AI agents and applications to discover and call tools directly using their native protocols - no wrapper servers required”. With MCP, a tool lives behind an MCP server that speaks MCP. With UTCP, the tool’s owner publishes a manual: a JSON document that lists each tool and a call template saying how to call the existing API directly (HTTP, CLI, gRPC, or even MCP). The agent’s UTCP client reads the manual and then calls the API itself. UTCP’s own line is: “If a human can call your API, an AI agent should be able to call it too”.
At Acme, this is how it would work. The trade marketing team already has a REST API for the promotions
calendar, GET /promotions?brand=Verde&month=2026-11, and the marketing agent should be able to read it.
With MCP build and run a small MCP server that wraps GET /promotions as a tool called
get_promotions; the agent calls the MCP server, which calls the API.
With UTCP publish a manual (for example at GET /utcp) that says: tool get_promotions, inputs
brand and month, call template: HTTP GET https://promo.acme.example/promotions,
auth: the API's existing key or token. The agent's UTCP client calls the API directly.
Both give the model the same tool. The difference is where the work and the control sit:
- UTCP saves you a server. There is nothing new to deploy, and the API keeps its own authentication, rate limits and logs. UTCP also says it can convert an existing OpenAPI spec into a manual automatically.
- MCP gives you one place to govern. Every call passes a server you control, which is where you
filter tools (like the
tool_filterabove), log, and now route on theMcp-Nameheader. With UTCP the agent calls each API directly, so that control moves into the client and into Agent Gateway’s egress rules (Part 12). - Ecosystem. As of October 2026 MCP is the protocol the platforms in this series support: ADK’s
McpToolset, Google’s managed MCP servers such as BigQuery’s, and Gemini Enterprise’s custom MCP connectors. ADK’s documentation lists MCP tools and OpenAPI tools; I found no UTCP integration in it. UTCP publishes SDKs for Python, TypeScript and Go, so using it from ADK today means wrapping a UTCP client in a function tool.
My view: for Acme, MCP stays the default, because the platforms speak it and a server is where we enforce narrow tools. But the idea behind UTCP is worth taking: if a team already has a clean API with an OpenAPI spec, ADK’s OpenAPI tools already let an agent call it directly, which is the same “no wrapper” idea inside the framework we use. Use a wrapper server only when it adds something: narrowing, logging, or a single point of control.
What is A2A?
A2A stands for Agent2Agent. In one plain sentence, it is a shared language that lets one agent find another, hand it work and get results back, even when the two are built with different frameworks or run by different companies. Google announced it on 9 April 2025, and it has been a Linux Foundation project since 23 June 2025. As of October 2026, the latest released version of the specification is 1.0.0 and the previous version is 0.3.0.
If it helps, think of hiring a contractor. You read their business card to see what they do and how to reach them, you send a job request, they open a job ticket and track it, and they hand back the finished work. In A2A the business card is the agent card, the job request is the message, the job ticket is the task and the finished work is the artifact. The misconception to avoid is treating the contractor like a vending machine: you send a request in plain words, not a filled-in argument list, and they decide how to do the job.
A2A defines a small set of building blocks:
the A2A building blocks: the client reads the server's agent card first, then sends a message made of parts; the server tracks the work as a task and returns the result as artifacts
- A2A client. The agent that makes the request; here, the router.
- A2A server, also called the remote agent. The agent that does the work; here, the marketing or supply agent.
- Agent card. A JSON document the server publishes to describe itself: its name, what it can do, its endpoint (the address you call) and the protocols it supports. The client reads it first.
- Message. One turn of the conversation. It has a
role(userfor the client,agentfor the server), a list of parts and optionalmetadata. - Part. One piece of content inside a message or artifact. A part holds exactly one of these: text, raw file bytes, a file URL or structured JSON data.
- Task. The unit of work. The server creates it, gives it an ID and tracks its status until it ends.
- Artifact. An output of a task, made of parts in the same way as a message.
The client sends messages with the SendMessage method. A streaming version, SendStreamingMessage,
sends back updates as they happen. The specification defines three transport bindings (ways of
carrying these calls over the network): JSON-RPC, gRPC and HTTP+JSON. Our agents use JSON-RPC.
How does this apply to Acme? Here are all the building blocks in one exchange, with the router as the client hiring the supply agent as the contractor. Maria, who works in marketing, asks “How did Acme Pure’s deliveries hold up in Western Europe in May?” and the router sends it to the supply agent. This is simplified to the main fields, not the exact wire format, and the user ID is illustrative:
Router to supply agent (SendMessage)
message:
role: user
parts: [ text: "How did Acme Pure's deliveries hold up in Western Europe in May?" ]
metadata: { user_id: "u-maria", home_team: "marketing" }
Supply agent back to router
task:
id: a new task ID, created by the supply agent
status: completed
artifacts:
parts: [ text: the findings, e.g. "OTIF fell from 89.18% to 86.89% over three weeks ...",
file part, media type application/vnd.acb.result+json: the rows and the SQL ]
The message is Maria’s one turn, split into a text part the model reads and metadata only code reads. The task is the supply agent’s record of the job, and the artifact is what it hands back: one part for people and the writer, one part for code that checks the numbers.
In Acme’s build, Part 6 turns this
into running code: the supply agent’s real agent card, the router’s RemoteA2aAgent, the metadata that
carries Maria’s identity, and the structured result the writer checks.
When do I use A2A, and when MCP?
The bottom line: use MCP when the other side does one well-defined thing, and A2A when the other side has judgement of its own. In the café example from the start, the till is MCP and the head chef is A2A.
MCP stands for Model Context Protocol. It is the standard way for an agent to use a tool or data source on another server: list the available tools, call one and get the result. It does a different job from A2A:
A2A or MCP: A2A connects an agent to another agent that has its own model, rules and permissions; MCP connects an agent to a tool; most systems use both
The A2A project sums up the difference in one line: A2A connects agents to each other, and MCP connects each agent to its own tools. Choose A2A when the other side is an agent with its own model, instructions and permissions, and the work may take a long time or need more input. Choose MCP when the other side offers a single, well-defined operation, such as “get campaign spend for this brand”.
In this system, the router calls the specialists over A2A, because each one reasons over its own data under its own rules. The specialists’ tools are Python functions inside each agent (Part 2), so they need no protocol. If another team wanted to reuse one of those tools, MCP would be the right way to offer it.
At Acme, two requests show the rule at work:
- “The pricing team wants to reuse
get_spend.” That is one well-defined operation (a brand in, a EUR figure out) with no reasoning on the other side, so offer it over MCP. - “The supply agent should ask a retailer’s replenishment agent about next week’s orders.” The other side is an agent that reasons over its own data under its own rules, so that is A2A.
A third name now comes up in the same conversation: UTCP, the Universal Tool Calling Protocol. It does MCP’s job (agent to tool), not A2A’s, but without a wrapper server: the tool’s owner publishes a manual describing how to call the existing API directly. And the MCP you read about may be the 2026-07-28 revision that people call “MCP 2.0”, which made MCP stateless. The MCP section above covers both, with an Acme example.
A quick test on six requests, with the answers:
1. "The marketing agent needs to run a read-only BigQuery query." MCP (Google's managed server)
2. "The router needs the supply team's view of a stockout." A2A (an agent with its own rules)
3. "The pricing team wants to reuse get_spend in their own agent." MCP (one operation, no reasoning)
4. "Supply should ask a retailer's replenishment agent about next week." A2A (another company's agent)
5. "Trade marketing has a clean REST API with an OpenAPI spec." direct: OpenAPI tools or UTCP
6. "Gemini Enterprise should be able to call Acme's router." A2A or ADK registration (Part 12)
When a new connection comes up, ask these questions in order, cheapest first. Each step adds something to run and govern, so stop at the first yes:
1. Is the tool your own code, used by one agent? a plain function tool, no protocol (Part 2)
2. Is it an existing API with a clean spec? call it directly: OpenAPI tools (or UTCP)
3. Is it one well-defined operation that several MCP: a managed server if one exists,
agents or teams should share? otherwise your own, filtered narrow
4. Does the other side reason under its own rules, A2A
take time, or need to ask questions?
Try it: classify your own connections
Take one agent you are building, or Acme’s marketing agent, and list every arrow that leaves it. For each one, write down three things:
- What is on the other side? A tool, an agent, or an existing API.
- Who should hold the control point? A server you run (MCP), the other agent’s owner (A2A), or the API itself plus your gateway (a direct call).
- What must never cross it? Tokens, secrets and raw rows the answer does not need.
For Acme’s marketing agent, the answers are: BigQuery through the managed MCP server, filtered to one read-only tool; no agent-to-agent calls of its own (the router makes those); and Maria’s identity in metadata, never in a prompt. If your list has an arrow you cannot classify, that arrow is usually a design question waiting to be asked.
What can go wrong
- Giving an agent a whole MCP server. A general server exposes many tools; an unfiltered toolset breaks the narrow-tools rule. Filter to the tools the job needs.
- Letting the model answer an MCP server’s questions. If an elicitation callback lets the model fill in the form, a server can put questions to your model instead of your user. Decline by default and send safe questions to a person. A server that asks for a password or key in a form is breaking the specification; decline it.
- Trusting identity in a prompt. Over A2A, who is asking travels in metadata that code reads, never in text the model reads (Part 6 shows the defence).
- Assuming sessions still exist. Under the 2026-07-28 MCP spec there is no protocol session; state that a server needs travels as an explicit handle. Check servers you depend on before upgrading.
- Using A2A for a simple tool. An agent on the other side adds a model call, latency and a second set of rules. If the other side has no judgement to apply, it should be a tool.
- A wrapper server that adds nothing. If an MCP server only forwards calls to a clean API, a direct call (OpenAPI tools or UTCP) may be simpler. Keep the server when it filters, logs or enforces policy.
What code did we write in this part?
Every part ends with the same map of Acme’s build: which files exist so far, and the code this part added, explained line by line. The map grows with each part.
This part adds no code to Acme’s build. It explains MCP, A2A and UTCP with wire messages and illustrative snippets (an McpToolset connection to the BigQuery MCP server and an elicitation callback), none of which are part of Acme’s build; the real A2A wiring arrives in Part 6.
Where does it live?
marketing/ the marketing specialist, on BigQuery
app/agent.py P1 marketing Agent with its tools, wrapped in an App
app/tools.py P1 query_marketing_measures, the marketing tool on BigQuery
shared/ code all three services share
routing.py P1 classify: picks the specialist in code, no model
semantic.yaml P1 semantic registry: each measure, its owner and access
+ added in this part ~ changed in this part Pn = the parts that touched the file
What did we learn?
- There are three kinds of connection: agent to tool (MCP), agent to agent (A2A), and agent to an existing API directly (UTCP or OpenAPI tools). What is on the other side decides which.
- MCP has three primitives, sorted by who triggers them: prompts (the user), resources (the
application) and tools (the model). ADK consumes tools with
McpToolset, resources withuse_mcp_resourcesorread_resource, and prompts withMcpInstructionProvider. tool_filterkeeps an MCP server’s broad tool list narrow, so the model sees only what its job needs.- An MCP server can ask questions back (elicitation). In a headless agent, code decides: decline by default, put safe questions to a person, and send secrets only through URL mode to the person.
- The 2026-07-28 MCP spec (often called “MCP 2.0”) is stateless: every request stands alone and carries routing headers. UTCP calls existing APIs directly from a published manual, with no wrapper server.
- A2A lets one agent hand work to another through agent cards, messages, tasks and artifacts, across frameworks and companies.
What comes next?
With the connections named, we start building. Production-ready agents, Part 2: Your first agent: one job, narrow tools builds Acme’s marketing specialist in ADK, including its connection to the BigQuery MCP server from this part.
Sources
- IETF RFC 9110: HTTP Semantics
- JSON-RPC 2.0 specification
- JSON Schema
- Model Context Protocol (MCP)
- MCP Apps overview
- MCP specification: elicitation
- MCP specification: sampling
- MCP specification: roots
- MCP specification: key changes in 2026-07-28
- MCP specification: tools
- MCP specification: resources
- GitHub MCP server (README, tools, read-only and lockdown modes)
- MCP specification: prompts
- MCP blog: The 2026-07-28 specification
- Cloudflare blog: The next generation of MCP
- Microsoft .NET blog: Announcing v2.0 of the official MCP C# SDK
- BigQuery MCP Server
- ADK source: McpToolset
- ADK source: McpInstructionProvider
- A2A protocol specification
- What’s new in A2A v1.0
- A2A and MCP
- Announcing the Agent2Agent Protocol (A2A)
- Linux Foundation launches the Agent2Agent Protocol project
- ADK: A2A protocol
- Universal Tool Calling Protocol (UTCP)
- UTCP on GitHub
Opinions are my own. Every fact in this post comes from the public documentation linked above. Acme Consumer Brands and its data are fictional.