Integration Protocols, Auth & Observability
Domain 3 is the heaviest domain on the exam: twelve of the sixty-three items. It is where an architecture stops being a diagram and starts touching other people's systems, other people's data, and other people's blast radius. The questions here are not "can you wire up MCP." They are "given four ways to reduce this risk, which one is actually the control, and which three only watch it happen." This is the second of Domain 3's two labs.
- Evaluate tool/agent configuration for capability bloat
- Analyze authentication and authorization requirements to identify security gaps
- Evaluate accuracy-latency trade-offs and justify configuration decisions
- Analyze observability challenges and select monitoring strategies at scale
- Evaluate connection protocols and select the appropriate integration mechanism (MCP, API/CLI, agent-to-agent)
1. Capability Bloat and Least Privilege
The exam guide's own Sample 1 is worth teaching verbatim, because it encodes the entire mental model for this domain in a single item:
| The guide's Sample 1 |
|---|
| A customer-support agent can read tickets, draft replies, issue refunds, and delete user accounts. Support staff only ever need to read tickets and draft replies. Applying least privilege, which change best reduces risk? |
The correct answer is to remove the refund and delete tools from the agent's configuration entirely. Not to log their use. Not to add a confirmation prompt. Not to move to a more capable model that "makes fewer mistakes." Sit with why each of those is wrong, because the distractors are the lesson:
| Option | Control type | Why it loses |
|---|---|---|
| Remove the two tools | Preventive — least privilege | The capability no longer exists. Nothing to exploit, misfire, or be socially-engineered into. |
| Log every refund and deletion | Detective | You find out after the account is gone. Logging is how you learn you had the wrong architecture. |
| Add a human confirmation prompt | Compensating | Better than nothing, but the capability is still wired in, one prompt-injection or fatigued click away from firing. |
| Use a more capable model | Unrelated | Model quality has nothing to do with authorization scope. A smarter model with the delete tool can still delete. |
Generalize the pattern: when one option removes a capability and the others monitor it, the removing option wins. The staff never needed refund or delete, so neither does the agent acting on their behalf. Grant is the exception that must be justified; the default is deny.
Bloat is a correctness problem, not only a security one
There is a second reason the exam keeps deleting tools, and candidates who think of this purely as a security concern miss it: more tools degrade tool-selection accuracy. Every tool you expose is another option the model weighs on every turn, another schema competing for attention, another chance to pick the plausible-but-wrong action. A twenty-tool agent is not just twenty tools' worth of attack surface; it is measurably worse at choosing among them than a five-tool agent. Pruning the surface makes the agent both safer and more correct. When an item asks how to improve a flailing agent's reliability, "give it fewer, sharper tools" is frequently the answer, not "give it more."
2. Authentication vs. Authorization: The Gap Analysis
The objective is phrased as "analyze authentication and authorization requirements to identify security gaps." The exam is testing whether you can tell the two apart and spot where a design conflates them. Authentication answers who is calling. Authorization answers what this caller is allowed to do. An agent that authenticates flawlessly and authorizes nothing is the most common failure in the domain.
The governing rule, and the sentence to carry into the exam:
| Common gap | What it looks like | The fix |
|---|---|---|
| Confused deputy | Agent runs as a service account holding the union of all users' permissions; per-user scoping lives only in the prompt | Propagate the end user's identity; authorize each tool call against that user's grants |
| No per-call authorization | Access is checked at login, never again; the agent then calls any tool with any arguments | Check authorization inside each tool, on the actual arguments, every call |
| Unbounded tokens | Tokens with no audience and no expiry: a stolen token works forever, anywhere | Short-lived, audience-scoped tokens; rotate; verify audience on receipt |
| Secrets in the prompt | API keys or tokens pasted into the system prompt or a message | Never. Prompts are logged, cached, and replayed. Inject at the transport layer, never the context |
| Transitive MCP trust | You trust an MCP server, so you trust everything it returns and every tool it exposes | Treat MCP tool output as untrusted input; scope credentials per server; don't let one server's content drive privileged calls |
The last row is where prompt injection and authorization meet. An MCP server returns content; that content can contain instructions; if your agent holds broad privilege and treats retrieved text as trusted, a malicious document becomes a command. The defense is the same least-privilege posture as Section 1: the agent should not hold the capability that the injected instruction wants to invoke.
Authorization belongs in the tool, not the prompt
Candidates reach for "add a system-prompt rule: only access records belonging to the current user." That is not an authorization control, it is a request. The model can be talked out of it, and a prompt injection routinely will be. The check must live in code the model cannot reason around, on the real arguments, using the real user's identity:
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
# The requesting user's identity is bound at request-assembly time,
# NOT supplied by the model. The model cannot forge or override it.
def make_tools(user):
@beta_tool
def get_invoice(invoice_id: str) -> str:
"""Fetch a single invoice by ID.
Args:
invoice_id: The invoice identifier to retrieve.
"""
invoice = db.invoices.get(invoice_id)
# Authorization is enforced HERE, in code, on the real arguments -
# against the requesting user's grants, not the app's.
if invoice is None or invoice.owner_id != user.id:
# Do not leak existence. Same answer for "missing" and "forbidden".
return "No invoice with that ID is available to you."
return invoice.render()
return [get_invoice]
# The agent authenticates and ACTS AS `user`, never as an omnipotent
# service account. Least privilege is enforced by what make_tools() grants.
runner = client.beta.messages.tool_runner(
model="claude-opus-4-8",
max_tokens=4096,
tools=make_tools(current_user),
messages=[{"role": "user", "content": user_question}],
)
The invariant: the model chooses which tool and what arguments; your code decides whether this user may have that result. Never the other way around.
3. Connection Protocols: MCP, API/CLI, Agent-to-Agent
The objective names three integration mechanisms and asks you to select among them. They are not interchangeable, and the exam rewards the architect who can say why one boundary fits and the others do not.
| Mechanism | What it is | Reach for it when | What it costs |
|---|---|---|---|
| MCP standardized server |
A server exposing tools, resources, and prompts with discovery, over a standard protocol | A capability surface is reused across many clients; N-to-M integration; a team owns a tool set others consume | A protocol and a server to run and secure; transitive-trust surface |
| Direct API / CLI bespoke call |
Your code calls the service's own API or shells out to its CLI | One bespoke integration you own end to end; you want the tightest control and the smallest surface | No discovery, no reuse: every new client re-integrates by hand |
| Agent-to-agent delegation |
One agent delegates a task to another agent across a trust or team boundary | The work crosses an org, team, or trust boundary and the far side is itself agentic | A coordination protocol to agree on, and an audit trail that now spans two autonomous systems |
The selection criteria that discriminate exam answers: reuse across clients (many consumers → MCP), who owns the tool (you own it and only you call it → direct API/CLI), trust boundary (crossing one → agent-to-agent, and now you own an audit problem), and discoverability (need it → MCP; don't → the discovery machinery is overhead). A single bespoke integration wrapped in an MCP server is over-engineering; a capability that ten teams re-implement by hand should have been an MCP server.
The MCP connector: both halves are required
When you connect Claude directly to a remote MCP server from the Messages API, the classic bug is supplying only one of the two required parameters. You must declare the server in mcp_servers and reference it by name in a mcp_toolset entry inside tools. Supplying mcp_servers alone is rejected as a validation error; every declared server must be referenced by exactly one toolset. The beta header mcp-client-2025-11-20 is required.
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
betas=["mcp-client-2025-11-20"],
# HALF ONE: declare the server. Note there is NO credential here -
# auth is injected at the transport layer, never in the prompt.
mcp_servers=[
{
"type": "url",
"name": "issue-tracker",
"url": "https://mcp.internal.example.com/sse",
}
],
# HALF TWO: reference it by name. Omitting this - or letting the
# names disagree - is the classic 400. Every declared server must
# be referenced by exactly one mcp_toolset.
tools=[
{"type": "mcp_toolset", "mcp_server_name": "issue-tracker"}
],
messages=[{"role": "user", "content": "Summarize open P1 issues."}],
)
4. Accuracy–Latency Trade-offs, and Justifying the Config
The objective is "evaluate accuracy-latency trade-offs and justify configuration decisions." The verb justify is the whole point: the exam does not want you to know that a lever exists, it wants you to state the SLO, name the lever, and name the cost. An architect who says "use caching" without saying which metric it moves and what it costs has not justified anything.
| Lever | Moves | The cost / the catch |
|---|---|---|
| Model tier (Opus → Haiku) | Latency down, cost down | Quality down. The trade nobody can escape |
output_config.effort | Quality up at higher effort | Latency and tokens up. Tune per route |
| Adaptive thinking | Quality up on hard turns | Latency up when it engages; the model decides when |
| Streaming | Perceived latency (time-to-first-token) down | Does not reduce total cost or total latency: only when the user first sees output |
| Prompt caching | Cost AND latency together | The rare two-for-one; requires a stable prefix. When an item says "improves both," look here |
| Batching | Cost down ~50% | Latency destroyed (up to 24h window); single-turn only; results are UNORDERED: key by custom_id |
| Reranking depth / retrieval top-k | Accuracy up with more candidates | Latency and tokens up; diminishing returns past a point |
| Parallel fan-out | Wall-clock latency down for independent work | Cost unchanged (same total tokens); coordination complexity up |
Two rows are exam magnets. Streaming moves perceived latency only. If an item asks to lower cost or total latency, streaming is a distractor. Prompt caching moves cost and latency together: if an item asks for both, it is almost always the answer. And batching is the trap that pairs a true fact ("50% cheaper") with the wrong requirement (anything a user is waiting on).
custom_id, never by position. Code that zips the results list against the requests list will silently mismatch every row the moment two requests finish out of order, which they will. The custom_id you set on each request is the only reliable join key, and the exam tests whether you know that.
Justifying a config is a three-part sentence. The SLO is p95 < 3s and cost < $0.02/ticket. The lever is prompt caching on the 11k-token policy corpus. The cost is a stable prefix: the corpus must sit before anything that varies, or it caches nothing. That is a defensible decision. "Use caching" is not.
5. Observability at Scale
The objective is "analyze observability challenges and select monitoring strategies at scale." The word scale is doing the work: at ten requests a day you can log everything; at ten million you cannot, and the exam is testing whether you know what to keep and what to drop.
What to emit
| Signal | Why it matters |
|---|---|
| Trace / span per turn, tool call, retrieval | Localizes where a slow or wrong response was born: model, tool, or retrieval |
request_id on every response | response._request_id is public despite the underscore; it is what Anthropic needs to trace a failure. Log it always |
| Token usage, split out | cache_read_input_tokens vs input_tokens: see the trap below |
| Stop reasons | end_turn, max_tokens, tool_use, refusal: a spike in max_tokens means truncation; a spike in refusal means a content or prompt problem |
| Tool error rates | A tool failing 5% of calls is invisible in aggregate latency but corrodes correctness |
| Per-tenant cost | The number the CFO asks for, and the only way to catch one tenant's runaway loop |
input_tokens excludes cache reads, so naive cost math undercounts, and naive savings math over-counts. Total prompt size is input_tokens + cache_creation_input_tokens + cache_read_input_tokens. The input_tokens field is the uncached remainder only. An agent that ran for an hour but shows input_tokens of 4k did not process 4k tokens: the rest was served from cache. Sum the three fields, or your dashboard lies in whichever direction is most flattering.
Emitting the trace record is the concrete skill. The usage split is not optional detail, it is the difference between a cost dashboard that reconciles and one that does not:
import json, logging
logger = logging.getLogger("agent.trace")
def emit_trace(response, tenant_id, turn_id):
u = response.usage
record = {
"turn_id": turn_id,
"tenant_id": tenant_id,
# Public despite the underscore - this is what Anthropic traces on.
"request_id": response._request_id,
"model": response.model,
"stop_reason": response.stop_reason,
# Split the usage out. input_tokens is the UNCACHED remainder only -
# summing all three gives the true prompt size and the true cost.
"input_tokens": u.input_tokens,
"cache_read_input_tokens": u.cache_read_input_tokens,
"cache_creation_input_tokens": u.cache_creation_input_tokens,
"output_tokens": u.output_tokens,
}
# A metric (cheap, aggregate) - NOT the full prompt (PII + volume).
logger.info(json.dumps(record))
The scale problem, and the three tiers of telemetry
You cannot log full prompts and responses for every request: that is PII you now have to protect and a volume you cannot afford to store or search. So you don't. You sample traces, redact, aggregate the metrics, and keep exemplars of the interesting cases. The discipline is knowing which tier each question belongs to:
| Tier | Cost | Coverage | Answers |
|---|---|---|---|
| Metrics | Cheap | Every request, aggregated | Is it up? Is it fast? Is it affordable? (and alerting) |
| Traces | Expensive | Sampled + redacted, plus exemplars | Why did this request behave this way? (debugging) |
| Evals | Offline | Labelled dataset, run deliberately | Is it correct? (Lab 6) |
Metrics tell you something broke. Traces tell you where. Evals tell you whether the fix actually improved correctness. Confusing the tiers (trying to answer "is it correct?" from production metrics, or "is it up?" from an offline eval set) is a recurring wrong answer in this domain.
6. The Three Questions Observability Must Answer
Strip everything above to its intent and observability exists to answer three questions, in order:
| Question | Signal that answers it | Who usually instruments it |
|---|---|---|
| Is it up? | Error rate, latency, availability metrics | Everyone. This is table stakes |
| Is it correct? | Evals, tool error rates, human-override signals, refusal spikes | Almost no one, and it is the one that matters most |
| Is it affordable? | Per-tenant token cost, cache-hit ratio, model mix | Finance notices when it's too late |
7. Design Exercise
Self-driven lab Lab5_Self_Driven_Lab.ipynbObjective: harden a multi-tenant agent platform against the four failure modes this lab covered: bloat, broken authorization, wrong protocol, and blind observability. About 60 minutes.
Scenario. You run a multi-tenant agent platform. Each tenant's agent needs three integrations: read from the tenant's CRM, write to the tenant's calendar, and issue invoices against the tenant's billing account. Today every tenant's agent runs as one shared service account, holds all three capabilities plus a dozen more inherited from a template, authenticates once at session start, and logs nothing but HTTP status codes. Volume is growing past what full-prompt logging can sustain.
- Run a capability-bloat audit. List every tool on the agent. For each, name the tenant task it serves. Delete the ones you cannot name, and justify each deletion as least privilege, not cleanup. Which of the twelve inherited tools survive? Write the sentence that kills the rest.
- Design per-user authorization. The shared service account is a confused deputy: any tenant's agent can reach any tenant's data. Redesign so each agent acts with the requesting tenant's permissions, and each of the three tool calls is authorized against that tenant's grants on the real arguments, in code, not in the prompt. Where exactly does the check live for the invoice tool?
- Choose a protocol per integration. CRM read, calendar write, invoice issuance: pick MCP, direct API/CLI, or agent-to-agent for each, and defend it with the four criteria (reuse, ownership, trust boundary, discoverability). Which one, if any, actually crosses a trust boundary? Which is a bespoke integration that an MCP server would over-engineer?
- Reconcile the SLO. Invoice issuance must feel instant to the user but need not be cheap; the nightly CRM sync must be cheap but no one is waiting on it. Assign a lever to each, and name the one you would not use for the interactive path and why (hint: it is 50% cheaper and completely wrong here).
- Specify the trace schema. Write the exact fields you emit per turn: include
request_id, the three-way token split, stop reason, per-tenant cost, and tool error flag. State which of the three questions (up / correct / affordable) each field serves. - Specify the sampling policy. You cannot log full prompts at this volume. Define what you sample, what you redact, what you aggregate, and which exemplars you always keep (hint: keep every refusal and every tool error, sample the rest). State what you will be unable to answer as a result, and why that is an acceptable trade.
Step 2 is the one the exam tests hardest. Step 6 is the one that keeps the platform legal.
Check Yourself
Exam-style items. Commit to an answer before expanding.
An agent for an internal analytics tool exposes 18 tools. Users complain it frequently calls the wrong one: running a report when they asked to export data, and vice versa. Availability and latency are fine. Which change most directly improves correctness?
Correct: C. More tools degrade tool-selection accuracy: every exposed tool is another option competing for the model's attention on every turn. Eighteen tools where users touch a handful is a bloated surface; pruning it to the tools that serve real tasks makes the model measurably better at choosing among what remains. Bloat is a correctness problem, not only a security one.
A treats a selection problem as a capability problem: a smarter model still has to disambiguate 18 options, and the guide's default answer is "less," not "more model." B is a request the model can reason around, and it grows the prompt that is already overloaded. D is detective: it tells you the agent chose wrong after it did, which is how you diagnose the surface you should have pruned, not how you fix it.
A support agent authenticates as a single service account that holds read access to every customer's tickets. Per-customer scoping is enforced by a system-prompt instruction: "only discuss tickets belonging to the authenticated user." A security review flags this. What is the correct characterization, and the fix?
Correct: B. The agent authenticates as an omnipotent service account and lets the model, via a prompt instruction, decide who sees what. That is the textbook confused deputy: the identity layer authorizes nothing, so any prompt injection or model slip crosses customer boundaries. The rule is that the agent must act with the permissions of the requesting user, and the check must live in code on the real arguments, not in the prompt.
A mistakes a request for a control: a prompt instruction is precisely what an injection overrides. C is detective: logging tells you a boundary was crossed after the data leaked. D addresses credential hygiene, which is real but orthogonal: a freshly rotated omnipotent token is still omnipotent, and the authorization gap is untouched.
A team wants two things from its Claude ticketing agent: lower cost per ticket, and a faster time-to-first-response for the user. Each request sends an 11,000-token policy corpus followed by a short question. Which two changes serve these goals? (Choose two.)
Correct: A and C. Caching a stable 11k-token prefix moves cost and latency together: reads bill at roughly a tenth of input, and the prefix is not reprocessed, cutting time-to-first-token. That is the rare two-for-one lever. Streaming does not reduce cost or total latency, but the stated goal is a faster first response, and streaming is exactly what lowers perceived time-to-first-token.
B halves cost but destroys latency: batches run in a window up to 24 hours; no user is waiting on that. It is the trap: a true fact ("50% cheaper") attached to the wrong requirement. D raises quality at the cost of both latency and tokens, the opposite of both goals. E cuts cost by discarding policy the agent needs to answer correctly: that is not optimization, it is changing the product.
A Claude platform passes every infrastructure health check (99.98% uptime, sub-second p95, on budget), yet a growing number of users report that answers are subtly wrong. The team keeps adding latency and error-rate dashboards and finds nothing. What is the gap?
Correct: B. Observability answers three questions: is it up, is it correct, is it affordable. Uptime, latency, and cost all answer the first and third; none of them can see correctness, which is invisible to infrastructure monitoring by construction. A system that is fast, available, and quietly wrong 15% of the time is the canonical symptom. Correctness requires offline evals (Lab 6), tool error rates, refusal spikes, and human-override signals, not another latency chart.
A and C both propose more of the same up-time instrumentation, which by definition cannot surface a correctness defect. D might change quality, but with no eval you have no way to know whether it helped, hurt, or did nothing: you would be guessing, which is precisely the blindness the item describes.
Key Takeaways
- Least privilege removes the capability; it does not guard it. When one option deletes a tool and the others log, prompt, or upgrade around it, deleting wins: the guide's own Sample 1 deletes two tools.
- Capability bloat is a correctness problem, not only a security one: more tools measurably degrade tool-selection accuracy. Fewer, sharper tools is often the reliability fix.
- The agent must act with the permissions of the requesting user, not the application. A shared omnipotent service account is a confused deputy; authorize each tool call in code, on the real arguments, against that user's grants.
- Authorization belongs in the tool implementation, never the prompt. A prompt rule is a request an injection overrides; a code check is a control it cannot.
- Protocol selection: MCP for reusable, discoverable, N-to-M surfaces; direct API/CLI for a single bespoke integration you own; agent-to-agent only when a real trust boundary is crossed, and now you own two audit trails. The MCP connector needs both
mcp_serversandmcp_toolset. - Justify configs as SLO + lever + cost. Streaming moves perceived latency only; caching moves cost and latency together; batching is 50% cheaper, latency-destroying, and returns results UNORDERED: key by
custom_id. input_tokensis the uncached remainder only: sum it with cache-read and cache-creation tokens or your cost math is wrong. Always logresponse._request_id(public despite the underscore).- At scale you cannot log everything: sample traces, redact, aggregate metrics, keep exemplars. Distinguish metrics (cheap, aggregate, alerting) from traces (expensive, sampled, debugging) from evals (offline, labelled, correctness). Most teams answer "is it up?" and are blind to "is it correct?"