Server-side tools
Some tools Anthropic runs for you. Learn the built-in, server-executed tools — web search and code execution — how they differ from the client-side tool loop you built in C2 (no manual loop: the result is already in the response), how to switch them on in tools=[...], how to read their results and citations, and how to reason about their cost and safety.
Learning objectives
- Say what a server-side tool is and how Anthropic runs it for you.
- Contrast client-side tool use (your manual loop) with server-side tools (no loop — the result is already in the response).
- Enable the web search tool and read its results and citations.
- Enable the code execution tool and read its stdout/stderr.
- Reason about cost, safety/permissions, and when to pick server-side vs your own tools.
1 · What a server-side tool is essential
Picture the two ways a restaurant can get you a glass of water. In the first, the waiter brings you an empty glass and a note: "the tap is down the hall, third door, mind the step" — and you get up, walk down, fill it, and carry it back. In the second, you just ask, and a full glass arrives at your table; the fetching happened in the kitchen and you never saw it. Client-side tools are the first restaurant. Server-side tools are the second. That single difference — who gets up to do the work — is what this whole lesson turns on, and getting it wrong is the most common reason people either write a pointless while loop around a server tool or wait forever for a "tool call" that already happened.
Here's why the distinction earns its own section instead of a footnote. When you first meet tools, everything looks like the first restaurant: the model asks, you run, you reply. So your instinct — a good instinct, honestly — is to reach for the loop every time. But some capabilities aren't yours to run at all; there's no tap down your hall. Live web search needs a search engine you don't host; a Python sandbox needs an execution environment you'd have to build and secure. Anthropic already runs those, so you don't write the function, you don't spin up the sandbox, and you don't drive a loop — you just tick a box in your request. The trap is treating a server tool like a client tool and hunting for a tool_use block to answer; there isn't one, and the code sits waiting on a reply that will never come.
A tool is a capability the model can invoke instead of just writing text. In C2 you built client-side tools: you describe a function, the model asks to call it, and your code runs it and feeds the result back in a loop. A server-side tool is different — it's a capability Anthropic has already built and runs on its own infrastructure. You just switch it on; the model calls it and the result comes back inside the same response. There is no function for you to write and no loop for you to run.
Two of the most useful ones ship today: web search (the model searches the live web and cites its sources) and code execution (the model writes Python and runs it in a sandbox). A close cousin, web fetch, pulls the full text of a specific URL. All three execute on Anthropic's side — you never see the search engine or the sandbox, only the results.
tools=[...] list; the only question is whether your code executes it (client-side) or Anthropic's infrastructure does (server-side).
Read it top-down: one request that names a tool, then the fork that decides who actually does the work.
- The top box is the model asking for a capability — a tool it wants to invoke instead of just writing text. You always declare tools the same way, in
tools=[...]. - The two arrows are the fork: the same request can route to either kind of tool.
- The left amber box is client-side (from C2): your code runs the function and you drive a loop. The right green box is server-side: Anthropic runs the built-in tool (web search, code execution, web fetch) and hands back the result — nothing for you to write or loop.
In short: Same declaration, opposite runtime — client-side means you execute it, server-side means Anthropic does.
tools=[...] list, but the mechanics are opposite. For your tools you get a tool_use block and must reply with a tool_result. For a server tool, Anthropic runs it and the result block is already sitting in response.content when the call returns.2 · Client-side vs server-side: the two loops essential
If you remember one thing from this lesson, make it this: who owns the loop. Everything else — how you read results, how errors surface, how you budget cost — falls out of that one fact. People who skip this section end up writing code that either double-loops a server tool (harmless but confused) or, worse, tries to intercept a server tool that already ran and can't be stopped. The bug isn't a typo; it's a wrong mental model, and it's expensive to debug because the code looks reasonable.
The cleanest way to feel the difference is to think about who is holding the phone during the call. With a client-side tool, the model calls you, says "I need the weather in Paris — go find it," and then holds the line while you look it up; nothing continues until you speak the answer back. You are the runtime, and the conversation is a series of these back-and-forths. With a server-side tool, the model makes the call, does the lookup itself in the back office, and only rings you once — when the whole thing is finished and the answer is already written down. One conversation, one hang-up. The word "loop" is literal in the first case (you genuinely write a while that keeps going) and absent in the second.
The single most important idea in this lesson is the difference in control flow. With a client-side tool, you are the runtime: the model pauses, hands you a request, and waits for you to run the function and send the answer back — round-trip after round-trip until it's done (the while True loop from C2). With a server-side tool, Anthropic is the runtime: it runs the tool for you and only returns when the answer is already folded into the response.
This is the tool loop you built in C2. You are the runtime — the model can't run anything itself, so it hands the work back to you and waits.
- You send request — one API call, with your tool's JSON schema in
tools. - Model asks: tool_use — the model stops and returns a
tool_useblock saying which tool it wants and with what arguments.stop_reasonis"tool_use". - YOUR code runs it — you execute the function on your machine. Anthropic never sees it happen.
- You send tool_result — you package the answer as a
tool_resultblock and send the whole conversation back. - Model answers — now it can reply, or ask for another tool, which loops you back to the start.
In short: The loop is yours to drive. That's control — you can log, gate, or refuse each call — but it's also work: you build the execution environment and the while loop.
Same picture, but Anthropic is the runtime. The whole middle of the client-side loop collapses into one server-side step you never see.
- You send request — one API call. The only difference from a plain call is a tool
type(likeweb_search_20260209) intools. - Anthropic runs the tool — the search or the sandbox runs on Anthropic's infrastructure. There is no
tool_usehanded to you and nothing for you to execute. - Result already in response — when the single call returns, the tool has already run and its result blocks are sitting in
response.content.
In short: No while loop, no tool_result to send. You gave up the ability to intercept the tool, and in exchange you skip building and running it entirely.
So the client-side path is request → tool_use → you run it → tool_result → answer, looping as many times as the task needs. The server-side path collapses to request → answer (with the tool already run). You trade control (you can't intercept or gate a server tool) for simplicity (no loop, no execution environment to build).
| Client-side tool (C2) | Server-side tool (this lesson) | |
|---|---|---|
| Who executes it | your code | Anthropic's servers |
| You write the function? | yes | no — it's built in |
| Manual loop needed? | yes (while loop) | no |
| How you enable it | your JSON schema in tools | the tool type in tools |
| Result location | you build the tool_result | already in response.content |
| Can you gate/approve it? | yes | no (runs automatically) |
3 · Enabling the web search tool intermediate
A model's knowledge has a freeze date, and that gap is exactly where confident-sounding wrong answers come from. Ask it about a release from last week and it will either admit it doesn't know or, worse, invent something plausible. Web search closes that gap: the model runs a live search, reads real pages, and cites them — so instead of guessing from stale memory it answers from what's on the web right now. Turning it on is almost insultingly easy, which is the point of this section — the hard part isn't enabling it, it's remembering that you don't then write a loop.
Think of enabling a server tool as flipping a switch on a request rather than installing a machine. For your own tools you hand over a full instruction manual — a name, a description, a JSON input_schema spelling out every argument — because your code has to know how to run the function. A server tool needs none of that; Anthropic already knows how to run it. So you just name the kind of tool with a type string (plus its fixed name), and that's the entire declaration. The one wrinkle that trips people is the version suffix: the type is dated (web_search_20260209), the right date depends on the model, and pasting an old one against a new model — or a new one against an old model — is a quiet mismatch. Pin it, and re-check the docs when you change models.
You enable a server-side tool by adding its spec to the tools=[...] list — the same list you'd use for your own tools, but instead of a name + schema you give a type (and the tool's fixed name). For web search the current dynamic-filtering variant is web_search_20260209 on Opus 4.8/4.7/4.6 and Sonnet 4.6; older models use the basic web_search_20250305. Optional keys let you cap uses or restrict domains.
The block below is real anthropic SDK code. It needs an API key and network — and web search has its own per-search pricing on top of tokens (see §6).
input_schema; a server tool needs only a dated type and its fixed name.
Compare the two columns side by side — left is what you must supply for a client-side tool, right is all a server tool needs.
- The left amber stack is the full declaration your own function requires: a
name, adescription, and a JSONinput_schemaspelling out every argument — because your code has to know how to run it. - The right green pair is the entire server-tool declaration: a dated
typestring (web_search_20260209) plus its fixedname. No schema, because Anthropic already knows how to run it. - The note under the right column is the one wrinkle: the
typeis version-dated and the right date depends on the model — pin it and re-check when you switch models.
In short: Client-side = a full instruction manual; server-side = a dated type and a name, nothing more.
web_search.py# needs: pip install anthropic + ANTHROPIC_API_KEY; server tools have their own pricing
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user",
"content": "What did Anthropic announce most recently? Cite your sources."}],
tools=[{
# Server-side tool: a `type` + fixed `name`, NOT a custom input_schema.
# web_search_20260209 = dynamic-filtering variant (Opus 4.8/4.7/4.6, Sonnet 4.6).
# Older models use "web_search_20250305".
# check the current tool type/version in the docs before shipping.
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 3, # optional: cap how many searches
# "allowed_domains": ["anthropic.com"], # optional allow/block lists
}],
)
# You do NOT run a tool loop. Anthropic already searched and folded the
# results into this one response. Just read stop_reason + content.
print("stop_reason:", resp.stop_reason)
for block in resp.content:
if block.type == "text":
print(block.text)
This enables a server-side tool and — crucially — has no loop. Compare it to the manual client-side loop in C2: there you had to detect tool_use, run a function, and re-send. Here you just make one call and read the answer.
tools=[{...}]is the same list you'd use for your own tools, but a server tool is declared by itstypeand fixedname— not a custominput_schema. Thattypestring is how you switch the built-in capability on.web_search_20260209is the current dynamic-filtering variant (Opus 4.8/4.7/4.6, Sonnet 4.6); older models useweb_search_20250305. The version depends on the model — check the docs when you upgrade.max_usescaps how many searches the model may run (you're billed per search — see §6).allowed_domains/blocked_domainsfence where it can look.- After the call there is no tool loop: Anthropic already searched. You just read
resp.stop_reasonand loop overresp.contentfor the text blocks.
What the output means: The model's answer prints, already grounded in a live web search that happened server-side — no while True and no tool_result anywhere.
Try this: This block needs an API key and network (and web search has its own per-search price). Put the same request through the C2 client-side loop in your head — you'd need three more steps. That gap is the whole lesson.
while True and no tool_result being sent back. Compare this to the manual client-side loop in C2: there you had to detect stop_reason == "tool_use", run the function, and re-send. Here the search already happened server-side.4 · Reading search results & citations intermediate
A response with a server tool in it is not a flat string — it's a small transcript, and if you only read the text you throw away half the value. The citations that let you fact-check the answer, the URLs a user might click, the very query the model chose to run: all of that lives in extra blocks alongside the prose. Teams that treat the response as "just .text" ship agents that sound grounded but can't show their work, and then can't explain to a reviewer where a claim came from. Reading the whole transcript is what turns web search from a party trick into something you can defend.
The mental model is a reporter's notebook, not a finished article. As the model works, it jots things down in order: "I searched for this" (a server_tool_use block), "here's what came back" (a web_search_tool_result block holding the list of hits), "here's my sentence" (a text block) — and pinned to that sentence, "…and I got it from these pages" (a citations array). Your job when the call returns is simply to walk that notebook page by page and pick out what you need, branching on each block's type. There's no loop and no callback — the notebook is already complete when you get it; you're a reader, not a participant. The common mistake here is assuming the blocks come in a tidy fixed order and indexing blindly; instead, iterate and switch on type, because text and tool blocks interleave however the model reasoned.
When a server tool runs, its output arrives as extra content blocks in the response, interleaved with the model's text. For web search you'll see a server_tool_use block (the query the model chose) followed by a web_search_tool_result block whose content is a list of results. The model's answer text then arrives in normal text blocks, and cited sentences carry a citations array pointing back at the sources it used.
The block below is real anthropic SDK code that walks the response and prints the results and citations. It needs an API key and network.
type: the query, the list of hits, the answer text, and the citations pinned to each sentence all arrive as separate blocks.
Follow the arrows left to right — this is the order the model jotted things into its notebook as it worked.
server_tool_userecords the query the model chose to run — you didn't pick it.web_search_tool_resultholds what came back: on success its.contentis a list of hits (title + url).textis the model's answer sentence, and the green box hanging off it is thecitationsarray — the sources pinned to that sentence so you can fact-check it.- The blocks interleave however the model reasoned, so you iterate and switch on
typerather than indexing a fixed position.
In short: Read the whole notebook — query, hits, text, citations are four block types you pick out by .type, not one flat string.
read_results.py# needs: pip install anthropic + ANTHROPIC_API_KEY; server tools have their own pricing
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Summarize the latest on X. Cite sources."}],
tools=[{"type": "web_search_20260209", "name": "web_search"}],
)
for block in resp.content:
if block.type == "server_tool_use":
# The query the model chose to run, server-side.
print("SEARCHED:", block.input.get("query"))
elif block.type == "web_search_tool_result":
# On success .content is a LIST of results; on error it is a single
# error object (e.g. {"error_code": "max_uses_exceeded"}) — branch on that.
results = block.content
if isinstance(results, list):
for r in results:
print(" -", r.title, "->", r.url)
else:
print(" search error:", getattr(results, "error_code", results))
elif block.type == "text":
print(block.text)
# Cited sentences carry a citations array pointing at the sources used.
for c in (block.citations or []):
print(" cited:", getattr(c, "url", None), getattr(c, "title", None))
When a server tool runs, its output arrives as extra content blocks mixed in with the model's text. This walks the response and pulls out the query, the results, and the citations — the loop-free equivalent of reading a client-side tool_result.
server_tool_use— the query the model chose to run. You didn't pick it; the model did, server-side.block.input.get("query")shows what it searched for.web_search_tool_result— on success its.contentis a list of results (title + url). On error it's a single error object (e.g.max_uses_exceeded). Theisinstance(results, list)check is why: server tools don't raise, so you branch on the block, not atry/except.textblocks carry the model's answer, and cited sentences have acitationsarray pointing back at the sources — that's how web search shows its work.
What the output means: You see the query the model ran, each result's title and URL, the answer text, and the sources cited for each claim.
Try this: Force an error (set max_uses: 0) and confirm the code prints "search error" instead of crashing — proof you understood that a server-tool failure is a result block, not an exception.
web_search_tool_result block as an error object, not as a Python exception. For web search a success content is a list and an error content is an object — check which before you index it.5 · The code execution tool advanced
Language models are famously bad at arithmetic — ask one to average eight numbers and it may fudge the last digit. Not because it's dumb, but because it's predicting the answer, not computing it. Code execution fixes this by letting the model stop guessing and actually run the numbers: it writes real Python, a real interpreter runs it, and the real output comes back. The same trick turns "I think the CSV has about a thousand rows" into a counted, exact answer. If web search grounds the model in facts, code execution grounds it in computation.
The instinct that makes this section feel hard is a false alarm: "surely running code needs a callback — I have to hand the program somewhere and get output back?" You don't. The sandbox is a pop-up kitchen Anthropic sets up and tears down — the model writes the recipe and cooks it in there, and you're only handed the finished plate. Nothing runs on your machine, so there's no container to build, no dependency to install (data-science libraries are pre-loaded), and no security surface of your own to worry about. The flip side, and the thing to internalize: because it's their kitchen with no internet, the code can't reach your files, your database, or the web unless you explicitly upload data — which is exactly why it's safe to let run automatically. The one habit to build is reading the result like a shell command: check return_code first (0 means success, read stdout; anything else means it errored, read stderr) instead of assuming the run worked.
Code execution lets the model write Python and run it in an Anthropic-hosted sandbox (no internet, data-science libraries pre-installed). You don't run anything — you declare the tool and the model's code, its stdout/stderr, and any files it produced come back in the response. The current type is code_execution_20250825; its result arrives as a bash_code_execution_tool_result block with stdout / stderr / return_code.
The block below is real anthropic SDK code. It needs an API key and network, and code execution is billed by sandbox time (see §6).
return_code like a shell command — 0 is stdout, anything else is stderr.
Follow the top row left to right, then take the branch at the end.
- Model writes Python — a
server_tool_useblock; the model both writes and owns the code. - Anthropic sandbox runs it — a pop-up kitchen with no internet and data-science libraries preloaded; nothing touches your machine, which is why it's safe to run automatically.
- result block — the finished plate; you check
return_codefirst, like reading a shell command. - The green branch (
return_code == 0) means success — readstdout. The red branch (anything else) means it errored — readstderr.
In short: Model writes it, sandbox runs it, you branch on return_code: 0 → stdout, else → stderr.
code_execution.py# needs: pip install anthropic + ANTHROPIC_API_KEY; server tools have their own pricing
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=2048,
messages=[{"role": "user",
"content": "Compute the mean and standard deviation of "
"[2, 4, 4, 4, 5, 5, 7, 9] with numpy."}],
tools=[{
# Server-side sandbox — the model writes AND runs the code.
# check the current tool type/version in the docs before shipping.
"type": "code_execution_20250825",
"name": "code_execution",
}],
)
for block in resp.content:
if block.type == "text":
print(block.text) # the model's explanation
elif block.type == "server_tool_use":
print("RUNNING:", block.name) # what the model chose to do
elif block.type == "bash_code_execution_tool_result":
result = block.content
if getattr(result, "return_code", 0) == 0:
print("stdout:", result.stdout) # the program's output
else:
print("stderr:", result.stderr) # it errored — read stderr
Code execution is the second big server-side tool: the model writes Python and runs it in an Anthropic-hosted sandbox. You don't run the code — you enable the tool and read the output that comes back.
- The tool is enabled the same way — a
type(code_execution_20250825) andnameintools. No schema, because the model writes the code itself. server_tool_usetells you the model chose to run code; the actual program runs in the sandbox, not on your machine.bash_code_execution_tool_resultcarries the run: checkreturn_code—0means readresult.stdout(the output), anything else means readresult.stderr(it errored).- As with web search, there's no callback and no loop — the output is already in the response when the call returns.
What the output means: The model's explanation plus the numeric result the sandbox computed with numpy — mean and standard deviation — printed from stdout.
Try this: This needs an API key and network, and code execution is billed by sandbox time. If the server loop hits its cap you'll see stop_reason == "pause_turn"; re-send the messages to resume — you still never run the code yourself.
stop_reason == "pause_turn" — you re-send the messages to let it resume, but you never execute the code yourself.6 · Cost & safety advanced
Here's the line item that ends up in a surprised postmortem: the model, not you, decides how many times to spend. With ordinary token billing you can reason about cost from the size of your prompt. With server tools that intuition breaks, because a single request can trigger three searches, or ten, or a two-minute sandbox session — and each of those has a price on top of tokens. An agent told to "research this thoroughly" and left uncapped is a metaphorical taxi with the meter running and no destination. This section is the seatbelt.
The mental shift is from a fixed-price ticket to a metered ride. Tokens are the ticket — priced by length, roughly predictable. Server tools are the meter — priced by use, and the model is driving. Web search bills per search; code execution bills by sandbox time (with a monthly free allowance). The fix is to bound what the driver can do: max_uses caps how many searches per request, max_tokens caps the length, and allow/block domain lists fence where web search may go. The safety story rhymes with the cost story — the two things that leave your control are the same two you should bound: web search reaches the live internet (fence it with domains) and code execution runs model-written code (but in Anthropic's sandbox, so it can't touch your machine). The common mistake is enabling a tool, watching it work beautifully on one demo query, and shipping without a cap — then discovering the bill after a week of real traffic. Estimate first; the helper below lets you do exactly that, offline, before you spend a cent.
Server-side tools have their own pricing on top of tokens. Web search is billed per search; code execution is billed by sandbox time (with a monthly free allowance). Because the model decides how many times to invoke a tool, an unbounded task can run several searches or long sandbox sessions — so cap it. The offline helper below (pure stdlib, runs offline) estimates a per-request bill from your assumptions so you can sanity-check before enabling a tool in production.
max_uses, max_tokens, and domain lists.
Read the left column top-down as a running total, then the right column as the levers that cap it.
- The left stack adds up one request's bill: ordinary
tokens(blue — priced by length, predictable), plus per-search fees and sandbox minutes (amber — priced by use, and the model decides how many), summing tototal_usd. - The amber rows are the surprise: unlike tokens, you don't set them directly — a request told to "research thoroughly" can fire many searches.
- The right green boxes are the fixes:
max_usescaps searches,max_tokenscaps length, and allow/block domain lists fence where search may go — bounding both the cost and the safety surface.
In short: The meter runs on searches and sandbox time the model chooses — cap it with max_uses, max_tokens, and domain lists before you ship.
server_cost.pydef server_tool_cost(searches, search_price, sandbox_minutes,
sandbox_price_per_min, tokens, token_price_per_1k):
"""Estimate one request's bill: search fees + sandbox time + tokens."""
search_cost = searches * search_price
sandbox_cost = sandbox_minutes * sandbox_price_per_min
token_cost = tokens / 1000 * token_price_per_1k
total = search_cost + sandbox_cost + token_cost
return {
"search_cost": round(search_cost, 4),
"sandbox_cost": round(sandbox_cost, 4),
"token_cost": round(token_cost, 4),
"total_usd": round(total, 4),
}
print(server_tool_cost(searches=3, search_price=0.01,
sandbox_minutes=0.5, sandbox_price_per_min=0.05,
tokens=4000, token_price_per_1k=0.005))
{'search_cost': 0.03, 'sandbox_cost': 0.025, 'token_cost': 0.02, 'total_usd': 0.075}
Server tools cost money on top of tokens — per search for web search, per sandbox minute for code execution. This helper is pure stdlib and runs offline: it estimates one request's bill so you can sanity-check before turning a tool on.
search_cost= number of searches × the per-search price. Because the model decides how many searches to run, this is the line that surprises people.sandbox_cost= sandbox minutes × per-minute price — code execution is billed by time in the sandbox, not by tokens.token_costis the ordinary token bill; the total sums all three so you see the true per-request cost, not just the token slice.
What the output means: A breakdown: 3 searches cost $0.03, half a sandbox minute $0.025, the tokens $0.02 — $0.075 total for the request. Change the inputs to model your own volume.
Try this: Bump searches to 10 and watch total_usd jump — that's why you cap the model with max_uses. This helper runs with a plain python server_cost.py; no key needed.
max_uses on web search, and set a sensible max_tokens. For safety: web search reaches the live internet (use allowed_domains/blocked_domains to fence it), and code execution runs model-written code — but in Anthropic's sandbox, not yours, so it can't touch your machine or your data unless you upload it.7 · When to use server-side vs your own tools professional
The wrong split here doesn't cause a bug — it causes an incident. Route "charge the customer's card" to a tool that runs automatically with no veto from your code, and one confused model turn can move real money before a human ever sees it. Route "summarize today's news" to a hand-built client-side tool, and you've spent a sprint reimplementing a search engine Anthropic would have run for free. Both mistakes come from the same root: not asking the one question that actually decides it.
That question is whose house does the action happen in? Think of it as a trust boundary drawn around your systems, your credentials, and your data. Anything that stays outside that boundary — looking something up on the public web, doing a throwaway calculation, fetching a page anyone could fetch — is safe to let Anthropic run automatically, so it wins as a server-side tool: generic, no infra, nothing of yours at risk. Anything that reaches inside the boundary — your database, your billing API, an email sent as you, anything needing your secrets or a human's approval — must stay a client-side tool, because only your code can inject the credentials, gate the call, and log it for audit. When in doubt, keep it client-side: it's easy to move a safe capability out to a server tool later, but you can't un-spend money a server tool already spent. The mistake to avoid is deciding by convenience ("server tools are less work, so use them") instead of by boundary — the extra work of a client-side tool is the control you're paying for.
The choice is about who should run the action. Reach for a server-side tool when the capability is generic, safe to run automatically, and you'd rather not build it: live web info, ad-hoc computation, fetching a public URL. Reach for a client-side tool when the action touches your systems (your database, your API, sending an email), needs your credentials, or must be gated/approved/audited before it runs.
The helper below (pure stdlib, runs offline) encodes that decision so you can defend the choice by criteria rather than by taste.
The big box is the line drawn around your systems, credentials, and data. Sort each capability by which side of it the action happens.
- The green boxes outside the boundary — web search, a throwaway calculation, fetching a public URL — touch nothing of yours, so they're safe to run automatically as server-side tools.
- The red boxes inside — your database, billing API, sending email as you, anything needing a human's approval — reach your credentials and data, so only your code can inject secrets, gate the call, and log it: client-side.
- The rule of thumb: when in doubt, keep it client-side — you can promote a safe capability to a server tool later, but you can't un-spend what a server tool already did.
In short: Outside your boundary → server-side; inside it (data, creds, approval) → client-side.
choose_side.pydef choose_tool_side(touches_your_systems, needs_your_credentials,
must_gate_or_audit, is_generic_capability):
"""Decide whether an action belongs in a client-side or server-side tool."""
if touches_your_systems or needs_your_credentials or must_gate_or_audit:
return "client-side (your code runs it: control, credentials, approval)"
if is_generic_capability:
return "server-side (Anthropic runs it: web search / code exec, no loop)"
return "client-side (default: keep control unless it's clearly generic)"
# a) live web info, nothing of yours involved -> server-side
print(choose_tool_side(False, False, False, True))
# b) charge a customer's card -> client-side (credentials + must gate)
print(choose_tool_side(True, True, True, False))
# c) query YOUR order database -> client-side (your systems)
print(choose_tool_side(True, False, False, False))
server-side (Anthropic runs it: web search / code exec, no loop)
client-side (your code runs it: control, credentials, approval)
client-side (your code runs it: control, credentials, approval)
The real design question is who should run the action. This helper (pure stdlib, runs offline) encodes the rule so you can defend the choice by criteria instead of by taste.
- If the action touches your systems, needs your credentials, or must be gated/audited, it belongs in a client-side tool — you keep control, hold the secrets, and can approve it before it runs.
- Otherwise, if it's a generic capability (live web info, ad-hoc computation, fetching a public URL), a server-side tool is the win: Anthropic runs it, no loop, nothing to build.
- The default when nothing clearly applies is client-side — keep control unless the capability is obviously generic and safe to run automatically.
What the output means: (a) news summary → server-side; (b) charging a card → client-side (credentials + must gate); (c) querying your order DB → client-side (your systems).
Try this: Run it on an action you're building. If any of the first three flags is true, the answer is client-side — because a server tool runs automatically with no veto from your code.
tools=[...] list can hold both kinds at once — e.g. a server-side web_search alongside your own client-side place_order. The model picks the right one per step; you only run the client-side ones.8 · Tech-lead — owning server-side tools in production tech-lead
An individual contributor asks "does the tool work?"; a lead asks "what happens when it works ten thousand times a day, and what happens the day the tool version changes underneath us?" Server-side tools are seductive precisely because they're so easy to turn on — one type string and you're live — and that ease is exactly what makes them easy to get wrong at scale. The convenience hides real ownership: a cost meter you don't directly control, a data path that leaves your walls, an error model that never raises, and a version pin that silently depends on the model. None of these bite in a demo; all of them bite in production. The lead's job is to make each one a deliberate decision written down somewhere, not an accident discovered in an incident channel.
A lead owns four decisions. Cost governance: cap invocations (max_uses, max_tokens) and monitor the per-request server-tool bill, not just tokens — an agent that searches on every turn is a budget leak. Data boundary: web search reaches the public internet and code execution runs in Anthropic's sandbox — know what leaves and fence it with allow/block domain lists. Correct result-reading: server tools never raise; errors are result blocks, and success vs error content can be a list vs an object — the response-parsing code must branch on that or it breaks in production. The client/server split: keep anything that touches your systems or needs approval on the client side; let generic, safe capabilities run server-side. Pin the exact tool type/version and re-check it against the docs when you upgrade models, since the available variant depends on the model.
type switch hides — make each a deliberate call.
Start at the top box — the deceptively simple "turn it on" — then follow the four connectors down to what a lead actually owns.
- Cost governance — cap invocations with
max_uses/max_tokensand monitor the per-request tool bill, not just tokens. - Data boundary — web search reaches the public internet and code runs in Anthropic's sandbox; know what leaves and fence it with allow/block domain lists.
- Correct result-reading — server tools never raise; errors are result blocks and success vs error content differs in shape, so the parser must branch.
- The client/server split — the trust boundary decides placement, and you pin the exact tool
type/version because the available variant depends on the model.
In short: The one-line switch hides four production decisions — cost, data boundary, result-reading, and the split — so write each one down deliberately.
Exercise AP5.1 — Read a server-tool response
Context: Reading a server-tool response cleanly means two things: pulling every result's title/URL and citation out of the result block, and proving you understood that a failed search returns an error block at HTTP 200 rather than raising.
Your task: Using web_search.py and read_results.py as templates, enable web search and write the loop-free parser that prints each result's title/URL and every citation, then deliberately trigger an error and show your code handles the error result block without crashing.
Requirements:
- Declare the
web_searchserver tool and iterateresp.content - Print each result's
title/urland any citations from theweb_search_tool_resultblock - Force an error (e.g.
max_uses: 0or a bogus domain) and read the error result'serror_code - Handle the error result block without a crash — server tools don't raise
- Requires an API key to run
💡 Hint: Branch on whether block.content is a list (success) or an object (error) before you index into it — that single check is what keeps the error path from crashing.
Exercise AP5.2 — Decide the split, then budget it
Context: The server-vs-client choice and its cost are two halves of one decision: place each capability by its trust boundary, then size the bill from how many server-tool uses each request triggers — and cap that with max_uses.
Your task: For three actions — summarize today's news, refund a customer, run an ad-hoc numpy calculation — use choose_side.py to place each on the client or server side and justify it, then use server_cost.py to estimate the monthly bill if the news summary runs 3 searches per request across your volume.
Requirements:
- Place news summary and the numpy calc on server tools (
web_search,code_execution) and the refund on a client-defined tool - Justify each placement by its trust/data boundary
- Estimate monthly cost assuming 3 searches per news-summary request at your expected volume
- State the
max_usescap you'd set to bound that cost - Run offline — routing and cost estimation only
💡 Hint: The refund touches your money and auth, so it can't be a server tool; for the estimate, multiply searches-per-request by request volume before applying the per-search price.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Server tools run on Anthropic's own infrastructure, so unlike a client-defined tool you never write an execution loop — you just declare the tool and Claude does the query, the fetch, and the citation for you in a single response.
Your task: Add the web search server tool so Claude can answer with current information, with no client-side execution loop.
Requirements:
- Pass
tools=[{"type": "web_search_20260209", "name": "web_search"}]tomessages.create - Do not implement any tool-execution loop — Anthropic runs the search
- Iterate
resp.contentand print blocks whereblock.type == "text" - Note that the results and citations arrive in the same response
- Requires an API key to run
💡 Hint: The dated _20260209 suffix selects the current web-search version with dynamic model filtering — use the version string exactly.
Show solution
Server tools run on Anthropic's infra — just declare them:
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=[{"role": "user",
"content": "What are the latest Claude model releases?"}],
)
for block in resp.content:
if block.type == "text":
print(block.text)
Unlike a client-defined tool, you never execute anything — Claude issues the query, Anthropic runs the search, and the results plus citations come back in the same response. The _20260209 version adds dynamic filtering (Opus 4.8 / 4.7 / 4.6, Sonnet 4.6).
Context: A web-search response interleaves the answer text with a result block, and a critical gotcha lurks in the error path: server tools don't raise — a failed search returns HTTP 200 with an error object inside the result block.
Your task: Iterate a web-search response, distinguishing the web_search_tool_result block from the answer text, and handle the fact that a successful result is a list while an error result is a single object.
Requirements:
- Branch on
block.typeto separatetextfromweb_search_tool_result - For a success,
block.contentis a list — iterate it and read each result'stitle/url - For an error,
block.contentis a single object — read itserror_code - Guard the shape with an
isinstance(content, list)check before indexing - Requires an API key to run
💡 Hint: Because errors come back at HTTP 200, a try/except won't catch them — inspect the block's content shape instead.
Show solution
Branch on block type; guard the error shape:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=[{"role": "user", "content": "Who won the 2024 Turing Award?"}],
)
for block in resp.content:
if block.type == "text":
print("ANSWER:", block.text)
elif block.type == "web_search_tool_result":
content = block.content
if isinstance(content, list): # success -> list of results
for r in content:
print("SRC:", r.title, r.url)
else: # error -> single object
print("SEARCH ERROR:", content.error_code)
Server-tool errors do not raise — they return HTTP 200 with an error inside the result block. A successful content is a list of results; an error content is a single object with error_code. Branch on that before indexing.
Context: Code execution is another fully server-side tool: Claude writes and runs code in an Anthropic-hosted sandbox and hands you back stdout/stderr/return-code — no container for you to manage, but the result block type is versioned and easy to mismatch.
Your task: Add the code execution server tool and read its output block, matching on the bash_code_execution_tool_result type rather than the bare tool name.
Requirements:
- Pass
tools=[{"type": "code_execution_20260120", "name": "code_execution"}] - Iterate
resp.contentand printtextblocks - Match the result on
bash_code_execution_tool_result, not the legacycode_execution_tool_resultname - On success read
stdoutandreturn_code; on failure readerror_code - Requires an API key to run
💡 Hint: The _20260120 version emits the bash_-prefixed result type; check the inner content.type to tell a result from an error.
Show solution
Code execution is fully server-side — declare it, read the result block:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=2048,
tools=[{"type": "code_execution_20260120", "name": "code_execution"}],
messages=[{"role": "user",
"content": "Compute the mean and stdev of [1,2,3,4,5,6,7,8,9,10]."}],
)
for block in resp.content:
if block.type == "text":
print(block.text)
elif block.type == "bash_code_execution_tool_result":
r = block.content
if r.type == "bash_code_execution_result":
print("stdout:", r.stdout, "rc:", r.return_code)
else:
print("exec error:", r.error_code)
Claude writes and runs the code in a sandboxed container; you get stdout/stderr/return_code back. Match on bash_code_execution_tool_result — the legacy bare code_execution_tool_result name is not what _20260120 emits.
Context: Server tools run their own internal sampling loop that is capped at roughly ten iterations; when it hits that cap it returns stop_reason == "pause_turn" — not an error, just a checkpoint you resume by re-sending the turn.
Your task: Handle pause_turn in the server-tool loop by resuming correctly: re-send the same turn without appending a literal 'Continue.' message.
Requirements:
- Loop on
messages.create, breaking whenresp.stop_reason != "pause_turn" - To resume, re-send the original user turn plus an assistant message carrying
resp.content - Do not append a 'Continue.' user message — the server detects the trailing server-tool block itself
- Cap the number of continuations so a stuck loop can't run forever
- Requires an API key to run
💡 Hint: Rebuild messages as [original_user_turn, {"role": "assistant", "content": resp.content}] and call again; the server picks up where it paused.
Show solution
Resume by re-sending the same turn; the server detects the trailing server-tool block:
messages = [{"role": "user", "content": "Research X thoroughly and summarize."}]
for _ in range(5): # cap continuations
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=2048,
tools=[{"type": "web_search_20260209", "name": "web_search"}],
messages=messages,
)
if resp.stop_reason != "pause_turn":
break
# Re-send: user query + assistant response. Do NOT append "Continue."
messages = [messages[0], {"role": "assistant", "content": resp.content}]
print(next(b.text for b in resp.content if b.type == "text"))
pause_turn means the server-side sampling loop hit its iteration limit, not that anything failed. Re-send the user turn plus the assistant's partial response and the server resumes automatically — adding a literal "Continue." message would confuse it. Always cap continuations so a stuck loop can't run forever.
Context: Left unbounded, a web-search tool can loop, run up cost, and fetch from anywhere; max_uses and domain scoping turn it into a predictable, auditable capability. There's also a subtle interaction: the current web tools already run code internally, so stacking a second execution environment backfires.
Your task: Limit web search with max_uses and an allowed-domain list, and explain why you should not also declare code_execution alongside the _20260209 web tools.
Requirements:
- Set
max_usesto cap the number of searches per request - Scope fetching with
allowed_domains(orblocked_domains) - Explain that
max_usesbounds cost and stops runaway search loops - State that the
_20260209web tools already run code under the hood, so addingcode_executionconfuses the model - Requires an API key to run
💡 Hint: Both controls are just extra keys on the tool declaration dict — add them next to type and name, not as separate parameters.
Show solution
Bound the tool and scope its domains:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
tools=[{
"type": "web_search_20260209", "name": "web_search",
"max_uses": 3, # cap searches per request
"allowed_domains": ["docs.anthropic.com", "platform.claude.com"],
}],
messages=[{"role": "user", "content": "How do I enable prompt caching?"}],
)
max_uses caps cost and stops runaway search loops; allowed_domains (or blocked_domains) scopes what the model may fetch. Do not separately declare code_execution next to the _20260209 web tools — those versions already run code under the hood for dynamic filtering, and a second execution environment confuses the model.
Context: The platform owner's core decision for every capability is server tool vs client-defined tool, and it comes down to trust and hosting: Anthropic-hosted tools need zero infra, but anything touching your private data, auth, or security boundary must run in your own harness.
Your task: Write an offline selector that, for four needs — current web info, running untrusted code, calling your internal billing API, and querying your private DB — chooses a server tool or a client-defined tool with reasoning.
Requirements:
- Route current web info to the
web_searchserver tool - Route sandboxed/untrusted code to the
code_executionserver tool - Route the internal billing API and the private DB to client-defined tools
- Attach a one-line justification per choice (hosting, citations, or data/auth boundary)
- Run entirely offline — pure routing logic, no API key
💡 Hint: The dividing question is 'can Anthropic host this safely?' — if it touches your credentials or private data, only your code can gate the call, so it must be client-defined.
Show solution
Route each need to the tool type that fits its trust and hosting model (pure logic):
def choose_tool(need):
table = {
"current web info": ("server: web_search",
"Anthropic hosts search; no infra, citations included"),
"run sandboxed code": ("server: code_execution",
"Anthropic-hosted sandbox; no container to manage"),
"internal billing API":("client-defined tool",
"your data/auth boundary — you must execute & gate it"),
"private database": ("client-defined tool",
"private data never leaves your infra; you run the query"),
}
return table.get(need, ("client-defined tool", "default: you own execution"))
for n in ["current web info", "run sandboxed code",
"internal billing API", "private database"]:
tool, why = choose_tool(n)
print(f"{n:22} -> {tool:22} ({why})")
Server tools win when Anthropic can host the capability (web, sandboxed code) — zero infra, built-in citations/sandboxing. Anything touching your private data, auth, or security boundary must be a client-defined tool: only your harness can gate the call, inject credentials, and keep the data on your side.
✓ Checkpoint — you can move on when you can…
- Explain what a server-side tool is and that Anthropic executes it for you.
- Contrast the client-side manual loop with the loop-free server-side path.
- Enable web search via the tool
typeand read its results + citations. - Enable code execution and read stdout/stderr from the result block.
- Reason about cost, safety/permissions, and when to pick server- vs client-side.
Knowledge check check yourself
How does control flow differ between a server-side tool and a client-side tool, and what do you trade for that difference?
Show answer
tool_use block, you run it, return a tool_result, then get the answer. A server-side tool runs inside Anthropic's call, so the result is already in response.content when it returns; you trade control (can't gate or intercept the call) for simplicity (no loop, no sandbox to run).Why can't you branch on try/except to handle a failed server-side tool call, and what must you inspect instead?
Show answer
error_code; for code execution you check return_code (0 = read stdout, else stderr).