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
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, 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
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
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).
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
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.
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
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).
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
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.
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 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.
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
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.
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).