Part 4 — Tools and MCP
How do agents interact with the real world — and what security problem came with the answer?
10 min · Updated June 2026
A model that can only talk is a curiosity. A model that can query your claims database, file a ticket, send an email, or read a spreadsheet is a worker. Tools are how that happens, and in 2026 the way tools are integrated has been substantially standardised by the Model Context Protocol (MCP).
Everyone’s telling me to “adopt MCP” — what problem does it actually solve?
MCP is an open protocol — originally from Anthropic, donated to the Linux Foundation in December 2025 and now co-stewarded by Anthropic, OpenAI, Google, Microsoft, AWS, and Cloudflare — that standardises how an agent connects to tools and data sources.
The mental model people reach for is “USB-C for AI agents”: instead of writing a bespoke integration for every tool-and-model combination, you build an MCP server once (wrapping your database, your API, your file store) and any MCP-compatible client — any agent, any IDE — can use it.
Adoption has been fast. By spring 2026 the protocol is supported natively across every major lab and IDE, there are many thousands of public MCP servers in registries, and enterprise adoption is well into majority territory. For a vertical agent builder, MCP gives you portability, discoverability through registries, and vendor independence.
Do I wrap everything in MCP, or just define tools in my own code?
You do not have to use MCP. The older approach — defining tools directly in your code as functions the model can call — is still completely valid and is often better for tools that live inside your own application.
- Function calling wins on performance (no network hop, no protocol overhead) and on fine-grained control. Best for tools tightly coupled to your app.
- MCP wins on portability, reuse across teams and clients, and integration with third-party tools. Best as the boundary between your agent and shared or external systems.
A reasonable rule: internal, tightly-coupled tools as native functions; anything shared, external, or reused across multiple agents as MCP servers.
In ADK both live behind the same tools=[...]list, which makes the “internal vs boundary” decision concrete. A native function tool is just a plain Python function — ADK reads its signature and docstring to build the schema, no network hop:
from google.adk.agents import LlmAgent
# Native function tool: internal, tightly coupled, no protocol overhead.
def get_claim_status(claim_id: str) -> dict:
"""Look up the current status of an insurance claim by its ID.
Args:
claim_id: The claim reference, e.g. "CLM-881".
Returns:
A dict with keys 'status' and 'last_updated'.
"""
... # your own DB call, running in-processA shared or external capability comes in through MCPToolset, which connects to an MCP server, discovers its tools, and proxies calls — the same agent, the two integration styles side by side:
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from google.adk.tools.mcp_tool import StreamableHTTPConnectionParams
agent = LlmAgent(
model="gemini-flash-latest",
name="ClaimsAgent",
tools=[
get_claim_status, # internal → native function
MCPToolset( # external/shared → MCP boundary
connection_params=StreamableHTTPConnectionParams(url="http://localhost:8788/mcp"),
),
],
)The decision isn’t framework-specific, but seeing both in one tools list is the clearest way to internalise the rule: reach for a function when you own the code, reach for MCPToolsetwhen you’re crossing a boundary.
My agent keeps mis-calling tools or drowning in tool output — how do I design tools it can actually use?
Two patterns dominate good tool design in 2026, both driven by the same problem — too many tools and too much tool output drowning the context.
Tool examples beat tool schemas. A JSON schema tells the model the shape of a tool’s arguments but not how to use it well. Adding a few concrete usage examples has been shown to lift parameter-handling accuracy substantially — one report cited an improvement from around 72% to around 90% on complex parameters. Treat tool definitions like documentation for a junior colleague: show, don’t just specify.
In ADK this lands in a very practical place: because the model reads a function tool’s docstring as its instructions, the docstring is your tool documentation. A bare signature underperforms a docstring that shows the argument format and gives an example:
def file_refund(claim_id: str, amount_cents: int, reason: str) -> dict:
"""File a refund against a claim. Prefer this over closing a claim manually.
Args:
claim_id: Claim reference in the form "CLM-####", e.g. "CLM-0881".
amount_cents: Whole cents, never dollars. $49.99 is 4999, not 49.99.
reason: One of "duplicate", "overcharge", "goodwill".
Example:
To refund $49.99 on claim CLM-0881 for a duplicate charge:
file_refund(claim_id="CLM-0881", amount_cents=4999, reason="duplicate")
"""
...The amount_centsnote and the worked example are exactly the kind of detail that moves parameter accuracy — the model stops guessing units and format because you showed it.
Let the agent call tools as code, not as round-trips. Instead of the classic loop — model emits one tool call, waits for the result, emits the next — newer approaches let the model write a small program that orchestrates many tools at once in a sandbox:
- Anthropic’s Programmatic Tool Calling (generally available with Sonnet 4.6 as of February 2026): the model writes Python that runs in a managed container, calling tools as functions and only surfacing the final result to its context. Reported token reductions of around 37% on multi-tool workflows.
- Cloudflare’s Code Mode: generate code-level interfaces from MCP tool schemas and let the model write JavaScript against them in a sandboxed isolate. On Cloudflare’s own API this collapsed the token cost from over a million tokens to around a thousand — a roughly 99.9% reduction.
For tool-heavy agents, generating code that calls tools is dramatically more efficient than chatting one tool call at a time. Once your agent crosses roughly twenty tools, this stops being optional.
There is also parallel tool calling— having the agent fire several independent tool calls at once. It is faster for broad search-style tasks, but be aware that the parallelism can burn on the order of 15× the tokens of a single conversation. Use it where the task value justifies the spend, not reflexively.
I have deep domain procedures — how do I give them to the agent without bloating its context?
An Agent Skill is an open standard where a capability is packaged as a folder containing a SKILL.md instructions file plus any scripts and resources. Only a few dozen summary tokens load into context until the agent actually needs the skill, at which point the full detail loads.
It is a clean way to give an agent deep, domain-specific procedures — how your firm drafts a particular contract type, how your hospital codes a particular encounter — without permanently bloating its context. Think of it as progressive disclosure applied to expertise.
This is the same just-in-time principle from Part 2, now applied to procedures rather than data: the agent carries a one-line summary of the skill and pays the full token cost only at the moment it decides to use it.
I’m wiring tools into a regulated system — what’s the security exposure and how do I contain it?
Security Exposure
The rapid, sprawling adoption of MCP has created a real and active security problem. If you are building in a regulated vertical, you cannot treat this as a footnote.
Through late 2025 and into 2026, security researchers documented a steady stream of serious vulnerabilities: an architectural flaw in the official SDKs exposing large numbers of servers to remote code execution; server-side request forgery flaws; DNS-rebinding issues in official SDKs; and at least one real supply-chain attack where a backdoored server was published to a public registry.
The OWASP Top 10 for Agentic Applications (released late 2025) names agent goal hijacking — manipulating an agent into pursuing an attacker’s objective, often via prompt injection delivered through tool output or retrieved content — as the top risk class.
The defensive posture that has emerged:
- Never expose raw MCP servers directly to the model client. Front everything with an MCP gateway or portal that provides single sign-on, per-tool access curation, audit logging, and data-loss-prevention scanning. The point is a controlled front door.
- Use OAuth, not static API keys, and watch the protocol’s move toward short-lived, federated workload identities.
- Treat locally-installed, unvetted MCP servers as a liability, not a convenience. Pin versions, track the CVE feed, patch promptly.
- Sandbox anything that executes. Run tools in an isolated worker with no host filesystem access by default and tight egress controls — never in your main application process. WebAssembly-based per-call sandboxing is emerging as a strong answer here.
- Guard the inputs to tools and the outputs from retrieval, because that is where prompt injection rides in.
An agent with tools is an agent with an attack surface. Design for that from day one.
Two of these controls have direct hooks in ADK worth knowing, because they’re where “design for it from day one” becomes a line of code rather than a policy document. First, per-tool access curation — don’t expose an MCP server’s entire surface just because it’s connected. tool_filter narrows a toolset to an explicit allow-list:
MCPToolset(
connection_params=StreamableHTTPConnectionParams(url="http://localhost:8788/mcp"),
tool_filter=["search_records", "get_record"], # read-only subset; no delete/update exposed
)Second, guarding tool inputs — a before_tool_callback runs before any tool executes and can inspect the arguments and block the call, returning an error result instead. This is the enforcement point for policy the model must never override (spend limits, blocked entities, out-of-scope actions):
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.tool_context import ToolContext
from typing import Optional, Dict, Any
def refund_guardrail(tool: BaseTool, args: Dict[str, Any],
tool_context: ToolContext) -> Optional[Dict]:
"""Hard policy limit enforced in code, not left to the model's judgement."""
if tool.name == "file_refund" and args.get("amount_cents", 0) > 50000: # > $500
return { # returning a dict SKIPS the tool and becomes its result
"status": "error",
"error_message": "Refunds over $500 require human approval (policy)."
}
return None # None = allow the tool to run
agent = LlmAgent(
model="gemini-flash-latest",
name="ClaimsAgent",
tools=[file_refund],
before_tool_callback=refund_guardrail, # runs on every tool call
)The important idea, not the specific API: a guardrail enforced in your code around the tool holds even if the model is successfully prompt-injected, because the check sits outsidethe model’s control. That is the whole point of the “controlled front door” — the model can be talked into asking, but not into bypassing the gate.
What if the system I need to drive has no API at all?
Some business workflows have no API — legacy ERPs, insurance claims systems, government portals. For these, agents can now drive a screen directly via Anthropic’s Computer Use tool or Playwright-based MCP servers. This is powerful, but it dramatically amplifies the prompt-injection surface — every page the agent reads is untrusted input — so it demands the strictest guardrails and human checkpoints.
The before_tool_callbackpattern above matters even more here: when the “tool output” is a whole web page the agent didn’t write, treating that output as untrusted — and gating any consequential action behind a code-enforced check and a human checkpoint — is the difference between automation and a liability.
Libraries and frameworks referenced on this page
- Google Agent Development Kit (ADK) —
google-adk(Python) — used for all code samples. Native function tools are plain Python functions passed toLlmAgent(tools=[...]), with the docstring serving as the tool’s documentation. MCP integration is viaMCPToolsetwithStreamableHTTPConnectionParams. Security controls shown:tool_filterfor per-tool access curation andbefore_tool_callback(usingBaseTool/ToolContext) as a pre-execution input guard. Model shown:gemini-flash-latest. - Model Context Protocol (MCP) — the open, multi-vendor tool-integration standard; ADK acts as an MCP client here via
MCPToolset. Public MCP servers are the “external/shared” side of the function-vs-MCP decision. - Anthropic Programmatic Tool Calling / Cloudflare Code Mode — the “tools-as-code” efficiency patterns for tool-heavy agents; both are model/runtime features rather than ADK APIs, cited conceptually.
- Agent Skills (
SKILL.mdstandard) — progressive-disclosure packaging of domain procedures; an open standard, cited conceptually. - Anthropic Computer Use / Playwright MCP servers — screen-driving tools for no-API systems; cited conceptually, with the security caveat that page content is untrusted input.