A protocol for crossing a boundary you don't control gets reached
for by default now, even when there's no boundary to cross.
Platform & Architecture·29 Aug 2026·11 min read·references 3 ADR proposals
Every agent we've shipped this year has needed to call at least
one tool that already lives in our own codebase. The reflexive
answer for "how does it call that tool" has increasingly been MCP
— and increasingly, that reflex has been wrong. Not because
MCP is bad, but because it solves a narrower problem than the
reflex assumes: crossing an ownership boundary, a different team,
a different company, a different runtime. Most of our agents never
cross one.
This post walks through two decisions that follow from that
observation, and how they relate. The first draws the line for
when MCP is actually worth its cost. The second decides what
should replace it when it isn't — which turns out to already
exist in how every other caller reaches our systems today.
The first decision, formalized below, draws the boundary line
explicitly — when a tool is worth wrapping in a protocol
server, and when it isn't. It's since grown a second and third
decision underneath it. Use the selector to move between them;
each card notes what it depends on, so jumping straight to a later
one still makes sense on its own.
Agents are treated as a category that always needs MCP, even
when the tool they're calling already lives in the same
codebase, language, and deploy as the agent itself.
Solution
Call internally-owned tools directly through the existing
atlasboxpy_controller call path — the
same controller class REST routes and workers already use
— registered in a queryable
tool_dictionary. Reserve MCP for tools that
genuinely cross an ownership boundary — a different
team, a different company, or multiple independent clients.
improves
trade-off to watch
Cognitive Load
Lower for the common case: one call path, typed response,
instead of holding a protocol schema and a function
signature in sync. Rises only if a tool later needs external
discovery — added deliberately, not by default.
Performance
Removes a serialization and IPC hop from the agent's
reasoning loop. A tool call behaves like any other function
call in-process — no round trip, no JSON-RPC overhead,
no server startup latency on first use.
Reliability
Neutral to positive, conditional on discipline. Calls
inherit the same shared error taxonomy and structured
retryable signal every other caller gets,
instead of per-agent retry logic invented from scratch.
Regresses only if a team bypasses the controller for an
undisciplined direct call into a service.
Evolvability
Deferred, and cheaper than it looks: internal tools are
registered in a queryable tool_dictionary from the start,
giving developers visibility and control without MCP. When a
boundary does appear, an MCP server reads its schema from
that same registry instead of being authored from scratch.
Decision Rule
✓
Same codebase, runtime, and owner →
call it directly through the controller. No server, no
schema, no protocol.
✗
Different team, company, or multiple independent
clients
need the same tool → MCP is earning its keep. Build the
server.
Rough secondary signal: fewer than about three independent
integrations sharing a tool means the N-to-M problem MCP
exists to solve hasn't shown up yet.
AContext›
Agents are now a normal category of caller into our systems,
alongside REST clients and background workers. The default
answer for "how does an agent reach a tool" has become MCP
— stand up a server, describe the tool as a schema,
give the agent a client.
That default isn't wrong in general. MCP solves a specific
problem: letting a client discover and invoke a tool it
doesn't own, across a boundary a standardized protocol
genuinely helps cross. The friction shows up when that
reflex gets applied uniformly, including to tools that were
never on the other side of any such boundary.
BRationale & Evidence›
The cost side isn't speculative. Independent write-ups
across the industry converge on the same short list of costs
when MCP is applied where no ownership boundary exists:
added latency from serialization and IPC now sitting inside
the reasoning loop, added complexity from turning a function
call into a network call, reduced observability, and ongoing
operational toil from every server becoming another artifact
to keep alive.
We're not the only ones drawing this line. AMD's robotics
research team published a physical-AI agent architecture on
25 Aug 2026, calling the robot's existing ROS 2 perception,
planning, and control capabilities directly as tools —
not through an added protocol layer, justified by
interpretability and on-device latency. They take no
position on MCP either way; this is a production team's
revealed preference, not an opinion.
Read the source →
CConsequences›
What gets easier
No new server to deploy or keep alive for tools we
already own.
Failures surface in the same stack trace and error
taxonomy for every caller.
Agents get the same structured
retryable signal every other caller gets,
for free — deciding what to do with it stays the
agent's own job.
Developers get a queryable answer to "what internal
tools exist and who owns them" via the
tool_dictionary, today — not a question that
only gets answered once an external consumer forces
it.
What we give up
No protocol-level runtime discovery —
an external client still can't ask "what tools do you
have" over the wire. The tool_dictionary gives us our
own visibility, not a stranger's client's visibility.
Only sound if the controller is actually used, not
bypassed for a direct call into a service.
DAlternatives Considered›
MCP by default for all agent tool calls.
Rejected — pays the full protocol cost on tools that
never cross a boundary.
Undisciplined direct calls, no shared
infrastructure.
Rejected — reintroduces inconsistent error handling.
Direct calls through the controller; MCP reserved for
genuine boundaries. (Chosen.)
EWhen to Revisit›
A tool used only by our agents needs a second, independent
consumer — wrap the controller in an MCP server
without touching the implementation.
Independent clients needing a given tool cross roughly
three.
FReferences›
AMD ROCm Blog, "Enabling Physical AI Agents with
Lemonade," 25 Aug 2026 —
rocm.blogs.amd.com
tool_dictionary schema and query interface
(internal) — the agent-callable registry AD-014.1.1
introduces for deprecation-window enforcement is an
instance of this same registry, not a separate system.
ADRAD-014.1
Make the Controller the Default In-Process Call Path for Agents
main @ a3f9c1eNeeds Discussion
↪ extends
AD-014
— decides what replaces MCP once you've opted out of it
for a tool.
Problem
Opting out of MCP for internal tools only helps if "call it
directly" doesn't quietly become "call it however each
agent's author feels like" — inconsistent error
handling, no shared observability, no retry logic.
Solution
Any in-process agent imports the same controller class a
REST route or worker would use, and calls its methods
directly. The controller's own base class already wraps
every public method — there's no separate object to
construct and no unwrapped, raw method an agent could
accidentally call instead.
improves
trade-off to watch
Cognitive Load
Zero new concept for engineers who already know the
controller from REST and worker call sites — an agent
is just another caller, not a new abstraction to learn.
Performance
No overhead beyond the controller's own try/except and
formatting logic, already in the hot path for every other
caller today.
Reliability
Agent-originated failures come back with the same
structured, typed
retryable signal every other caller gets
— important since agents run unattended and won't
notice a silent failure. The retry/backoff decision itself
stays the agent's own code, deliberately, rather than a
policy engine's.
Evolvability
Agent-callable methods register in the tool_dictionary at
call time, so if one later needs to become a publicly
discoverable capability, the same controller wraps in an MCP
server reading from that existing entry — not a
rewrite.
Decision Rule
✓
Agent calls a controller it owns → import the
controller class directly and call its methods. No gateway
to construct, no recovery engine to attach — the base
class it already inherits from handles both concerns.
✗
Agent reaches around the controller into a service directly
and wraps it in bespoke error handling → not compliant
with this decision.
AConsequences›
Every caller — REST route, worker, or agent —
now produces identical, typed success and error responses
for the same controller. Debugging an agent failure means
reading the same error taxonomy an engineer already knows
from the REST logs, not a separate agent-specific format.
Require a Deprecation Window Before Changing Any Agent-Callable
Controller Method
agent-runtime @ f6b21d0New Proposal
↪↪ extends
AD-014.1
— closes a narrower gap than it might first appear. Most
structural signature breaks already surface at two layers:
Pydantic raises the validation error the moment a call stops
matching the expected shape, and a proper
atlasboxpy_controller pytest suite is what actually
exercises that call path and asserts the req/res relationships
and spied call chains needed to catch it before merge. What
neither layer reaches — a change that stays valid at both,
or a call path the suite doesn't cover — is what actually
surfaced once the agent-runtime fork started
iterating on controller interfaces faster than its own test
coverage kept up.
Problem
Direct in-process calls (AD-014.1) give agents no
protocol-level interface negotiation the way MCP's schema
discovery would. Pydantic raises a validation error on a
structural break, but only if something exercises that call
path — a atlasboxpy_controller pytest
suite asserting the expected req/res shapes and call chains
is what actually surfaces it before merge. What still lands
unannounced is a change that stays valid at both layers, or
a call path the suite doesn't cover.
Solution
Any controller method reachable by an agent must carry a
minimum deprecation window — old and new signatures
coexist for at least one release cycle — enforced via
the tool_dictionary's agent-callable registry,
checked at review time, not a runtime protocol.
improves
trade-off to watch
Cognitive Load
Largely automated, at two layers: Pydantic throws the
validation error on a structural break; the
atlasboxpy_controller pytest suite exercises the call and
asserts req/res shapes and call chains to actually catch it.
Standard practice, not a new habit. The registry covers
behavioral changes and untested paths.
Performance
No runtime cost — the registry check happens at review
and CI time, not on the call path. Agents keep the zero-hop
performance AD-014.1 already established.
Reliability
Closes the gap neither layer reaches: changes that stay
valid through Pydantic and the suite's own req/res and
call-chain assertions, and call paths the suite doesn't
cover. Structural breaks were already caught pre-merge by
Pydantic's error plus the test that exercised it; this adds
a checkpoint for what wasn't.
Evolvability
Slightly slower to remove old controller methods than an
unconstrained codebase would allow, in exchange for closing
the gap left by both layers together — behavioral
changes and untested paths, not the structural breaks
Pydantic and a atlasboxpy_controller pytest suite already
catch between them.
Decision Rule
✓
Changing a controller method's signature and it appears in
the agent-callable registry → deprecate first, remove
next release cycle.
✗
Method isn't in the registry → normal change process,
no window required.
AConsequences›
Every agent-callable controller method now has exactly one
place its callers can be checked before a breaking change
ships — the registry — as a deliberate backstop
alongside the existing two-layer defense: Pydantic raises
the validation error, and the atlasboxpy_controller pytest
suite is what actually exercises the call and asserts the
req/res shapes and call chains needed to catch it before
merge. The registry covers what neither layer reaches:
behavioral changes, and call paths the suite doesn't
exercise.
BReferences›
Depends on AD-014.1 (this document extends it).
Agent-runtime fork RFC (internal).
atlasboxpy_controller
package (formerly validator_gateway), Phase 8 — Test
Suite Completion (internal): the pytest coverage
requirements this decision assumes are already in place.
All References (3)
›
AMD ROCm Blog, "Enabling Physical AI Agents with Lemonade," 25
Aug 2026 —
rocm.blogs.amd.com— cited in AD-014
atlasboxpy_controller
package, formerly validator_gateway— cited in AD-014, AD-014.1
Agent-runtime fork RFC (internal)
— cited in AD-014.1.1
Keep it strategically simple
Skipping the protocol layer isn't the lazy choice, it's the
considered one — made after checking whether the boundary
the protocol exists to solve is actually there. Most of the time,
for most of our agents, it isn't. Reading one decision at a time,
rather than scrolling past all three, is deliberate too: the
selector defaults to whichever decision is currently Accepted
— AD-014 today — not just whichever one happens to be
the root, so a reader always lands on the settled state of the
system rather than an artifact of how the tree is drawn. A reader
who jumps straight to AD-014.1.1 still finds, on the card itself,
that it extends AD-014.1, which extends AD-014 — without
having had to read both first to make sense of it.
Platform & Architecture · this post embeds AD-014,
AD-014.1, and AD-014.1.1 · prototype layout, static HTML/CSS