Skip to content
abhinavbhatia
Sheet 02 / 05 — Blogs← Archive

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.

ACME ARCHITECTURE · PART 1.9 LOCAL FIRST: ADK WEB · AGENT RUNTIME IN PART 11 each agent is its own engine with its own identity Shopper Acme's customers P11.75 GE for CX customer chat, voice, reps P11.75 Legal edition industry skills, connectors P11.75 User Maria in marketing P1 Gemini Enterprise where people use agents P12 Other front doors own app (AG-UI), Chat, Slack P12.5 Plugins + Model Armor · checks around every step P9 Router agent Workflow: extract, route, write P1 Supply agent Databricks specialist P3 Marketing agent BigQuery specialist P2 Code sandbox runs generated code P3.5 Skills versioned in Agent Registry P4 Sessions + Memory Bank what it remembers about each user P8 AGENT GATEWAY · EGRESS (FORWARD PROXY) · P12 Gemini models the reasoning engine P1 Other models Model Garden P13 Knowledge RAG and search P14 BigQuery marketing data P2 Databricks supply data P3 Market watch managed agent, public web P1.75 Agent Identity one per agent P7 Auth manager acts for the user P7 Secret Manager no secrets in code P11 Human approval for the one write P9 Cloud Build CI on every change P11 Eval gate blocks a worse agent P10 Cloud Trace one per question P11 Cloud Logging redacted events P11 Agent Analytics BigQuery plugin P9 IDENTITY AND SAFETY SHIPPING MODELS, KNOWLEDGE, DATA OBSERVABILITY deploy this part earlier parts later parts Pn = the part that adds it

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=Verde reads campaigns, POST /orders creates 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 Your agent the client in every connection below Acme: router or specialist MCP list tools, call one A2A send a task, get artifacts UTCP / OpenAPI read a manual, call directly MCP server, then a tool one operation, no judgement of its own Another agent its own model, rules and permissions An existing API built for programs, no wrapper server

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, params on the way in, jsonrpc, id and result (or error) 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 says a2a.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.

WHAT GOES OVER THE WIRE, LAYER BY LAYER (AS OF OCTOBER 2026) MCP agent to tool, via a server A2A agent to agent UTCP agent to existing API HTTP POST /mcp headers: MCP-Protocol-Version, Mcp-Method, Mcp-Name GET /.well-known/agent-card.json then POST to the url in the card header: A2A-Version GET /utcp (the manual) then the API's own call, e.g. GET /promotions?brand=... JSON-RPC 2.0 ENVELOPE jsonrpc, id, method, params method: tools/list, tools/call reply: jsonrpc, id, result jsonrpc, id, method, params method: SendMessage (default binding; also gRPC, HTTP+JSON) none no envelope: the manual is plain JSON, the call is the API's own PROTOCOL FIELDS tools[]: name, title, description result: resultType, content[], isError; lists: ttlMs, cacheScope AgentCard: supportedInterfaces, skills Message: role, parts, messageId, metadata; Task: status, artifacts manual_version, utcp_version, tools[]: name, description, tool_call_template JSON SCHEMA (ARGUMENTS) inputSchema on every tool; tools/call arguments must match (optional outputSchema) none for skills: id, name, description, tags, examples; the request is text and parts inputs on every tool; the agent fills them, then the call template places them Defined by: HTTP (RFC 9110), JSON-RPC 2.0 (jsonrpc.org), JSON Schema (json-schema.org); MCP fields by the MCP spec (schema.ts), A2A fields by a2a.proto (serialised to JSON), UTCP fields by the UTCP spec.

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, BY WHO PULLS THE TRIGGER Prompts the user decides like a slash command /weekly_spend_summary brand: Acme Pure server returns a ready-made prompt template, filled in prompts/list, prompts/get ADK: McpInstructionProvider Resources the application decides like @-mention or a file schema of campaign_performance read-only data with a URI, put into context by the app resources/list, resources/read ADK: use_mcp_resources=True Tools the model decides like a function call execute_sql_readonly( sql="SELECT ...") actions: search, read, write; the model chooses tools/list, tools/call ADK: McpToolset Most agents only use the third column. The other two exist because a person or an application, not the model, is often the right one to decide what goes into the context.

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 a subscriptions/listen request and receives notifications/resources/updated when 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/get with 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 MCP server (github/github-mcp-server), signed in as octocat; repo octocat/payment-service Prompts: the user picks a built-in prompt, for example issue_to_fix_workflow owner, repo, title, description, labels returns messages that ask the model to create the issue, assign it to Copilot coding agent and watch for the PR Resources: the app attaches 5 URI templates, read-only repo://{owner}/{repo}/ contents{/path*} refs/heads/{branch}/... sha/{sha}/... refs/tags/{tag}/... refs/pull/{prNumber}/head/... e.g. @README.md on main Tools: the model calls many; these are a few list_issues search_repositories get_file_contents issue_write (create, update) push_files create_pull_request only tools change GitHub Names checked against the server's README and source, November 2026. Started with --read-only, the server offers only its read-only tools.

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_issues and search_repositories are reads with filters the model chooses during the task, such as open issues labelled bug. That is why they are tools rather than resources: the model, not the user, knows the filter.
  • push_files commits one or more files to a branch in a single call. Its arguments, from the README, are owner, repo, branch, files (each with a path and content) and message:
{
  "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 (with method set to create or update) and create_pull_request change 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=True hands the choice to the model. It adds a load_mcp_resource tool, so the model decides which resource to load. Treat what it loads like any other tool result. If your code should decide, call read_resource yourself and put the result in the context.
  • McpInstructionProvider turns an MCP prompt into an instruction. You pass it the server’s connection details and a prompt name, and use it as the agent’s instruction. 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.

  1. 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 npx for 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.
  2. 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.

HOW AN AGENT CONNECTS TO AN MCP SERVER ADK Agent McpToolset StreamableHTTPConnection tool_filter= [execute_sql_readonly] BigQuery MCP Server Tools exposed: list_dataset_ids, get_table_info, execute_sql_readonly

HTTP / SSE Tool Results

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 a SELECT statement.

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, then list_table_ids, then get_table_info on campaign_performance, and only then execute_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-25 version 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.

WHEN THE SERVER ASKS A QUESTION BACK Marketing agent McpToolset elicitation_callback (code decides) Agency MCP server get_bookings needs: which account? 1. tool call 2. input required: a question Ask a person form, via front door Decline the default Pass the link URL mode, to Maria 3. retry with the answer Never: the model invents the answer and accepts

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 initialize exchange and the Mcp-Session-Id header are gone. “Each request now travels on its own, carrying its protocol version, client identity, and client capabilities”, and an optional server/discover call 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-Method and Mcp-Name HTTP 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 ttlMs and cacheScope and 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_filter above), log, and now route on the Mcp-Name header. 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:

Router A2A client asks Supply agent A2A server (remote agent) answers Agent card who it is, its skills, its URL 1 Message (role: user) text part: question metadata: user_id one turn of the conversation 2 Task status: submitted, working, completed Artifact: the result a text part + a data part 3 A message carries one turn. A task tracks the work. Artifacts hold what comes back.

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 (user for the client, agent for the server), a list of parts and optional metadata.
  • 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: AGENT TO AGENT MCP: AGENT TO TOOL Router Supply agent An agent Tool server the other side reasons: own model, the other side is a function: own instructions, own permissions a name, arguments, a result work is a task: it can take long, one call, one answer ask for input, then complete found through its agent card found through its tool list ACB: router to marketing and supply ACB: tools are plain functions (Part 2) Ask: does the other side need to think, own data and keep its own rules? Then A2A. If it is one well-defined action, it is a tool. Expose it with MCP or a plain function.

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:

  1. What is on the other side? A tool, an agent, or an existing API.
  2. 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).
  3. 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 with use_mcp_resources or read_resource, and prompts with McpInstructionProvider.
  • tool_filter keeps 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

Opinions are my own. Every fact in this post comes from the public documentation linked above. Acme Consumer Brands and its data are fictional.