서버 도구
서버 도구 (Server tools)
서버에서 실행되는 도구들은 공통된 동작 방식을 공유해요. server_tool_use 블록, pause_turn 이어짐, 서버·클라이언트 도구가 섞인 턴, ZDR(Zero Data Retention) 자격, 도메인 필터링이 바로 그것이에요. 각 도구에 대한 자세한 내용은 도구 레퍼런스를 보세요.
출처: 문서
본문
서버에서 실행되는 도구들은 공통된 동작 방식을 공유해요. 바로 server_tool_use 블록, pause_turn 이어짐, 서버·클라이언트 도구가 섞인 턴, ZDR(Zero Data Retention) 자격, 도메인 필터링이에요. 각 도구에 대해서는 도구 레퍼런스를 참고하세요.
server_tool_use 블록 (The server_tool_use block)
서버에서 실행되는 도구가 동작하면 server_tool_use 블록이 Claude의 응답에 나타나요. 그 id 필드는 srvtoolu_ 접두사를 써서 클라이언트 도구 호출과 구분돼요:
{
"type": "server_tool_use",
"id": "srvtoolu_01A2B3C4D5E6F7G8H9",
"name": "web_search",
"input": { "query": "latest quantum computing breakthroughs" }
}
API가 내부적으로 도구를 실행해요. 우리는 호출과 결과를 응답에서 보지만 실행을 처리하지는 않아요. 클라이언트 tool_use 블록과 달리 tool_result로 응답할 필요가 없어요. 도구의 결과 블록(예: 웹 검색의 web_search_tool_result)은 같은 어시스턴트 턴에서 server_tool_use 블록 뒤에 이어지고, tool_use_id로 짝지어져요. 만약 Claude가 동시에 우리 클라이언트 도구 중 하나를 호출하면 server_tool_use 블록은 결과 없이 나타나고, 응답은 stop_reason: "tool_use"로 끝나요. API는 다음 요청에서 클라이언트 tool_result 블록을 반환할 때 그 도구를 실행해요.
서버 측 루프와 pause_turn (The server-side loop and pause_turn)
웹 검색 같은 서버 도구를 사용하면 API가 서버 측 에이전틱 루프에서 도구 호출을 실행해요. 오래 돌아가는 턴에서는 API가 그 루프를 멈추고 pause_turn 정지 이유를 반환할 수 있어요.
pause_turn 정지 이유를 처리하는 방법:
# Initial request. If "stop_reason" in the output is "pause_turn", re-run with
# the assistant content appended to messages (see the SDK tabs).
ant messages create <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
tools:
- {type: web_search_20250305, name: web_search, max_uses: 10}
messages:
- {role: user, content: "Search for comprehensive information about quantum computing breakthroughs in 2025"}
YAML
client = anthropic.Anthropic()
# Initial request with web search
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Search for comprehensive information about quantum computing breakthroughs in 2025",
}
],
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
)
# Check if the response has pause_turn stop reason
if response.stop_reason == "pause_turn":
# Continue the conversation with the paused content
messages = [
{
"role": "user",
"content": "Search for comprehensive information about quantum computing breakthroughs in 2025",
},
{"role": "assistant", "content": response.content},
]
# Send the continuation request
continuation = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=messages,
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
)
print(continuation)
else:
print(response)
const client = new Anthropic();
// Initial request with web search
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [
{
role: "user",
content:
"Search for comprehensive information about quantum computing breakthroughs in 2025"
}
],
tools: [
{
type: "web_search_20250305",
name: "web_search",
max_uses: 10
}
]
});
// Check if the response has pause_turn stop reason
if (response.stop_reason === "pause_turn") {
// Continue the conversation with the paused content
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Search for comprehensive information about quantum computing breakthroughs in 2025"
},
{ role: "assistant", content: response.content }
];
// Send the continuation request
const continuation = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages,
tools: [
{
type: "web_search_20250305",
name: "web_search",
max_uses: 10
}
]
});
console.log(continuation);
} else {
console.log(response);
}
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [
new() {
Role = Role.User,
Content = "Search for comprehensive information about quantum computing breakthroughs in 2025"
}
],
Tools = [new ToolUnion(new WebSearchTool20250305 { MaxUses = 10 })]
};
var response = await client.Messages.Create(parameters);
if (response.StopReason?.Value() == StopReason.PauseTurn)
{
// Continue the conversation with the paused content
var continuationParams = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [
new() {
Role = Role.User,
Content = "Search for comprehensive information about quantum computing breakthroughs in 2025"
},
new() {
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList()
}
],
Tools = [new ToolUnion(new WebSearchTool20250305 { MaxUses = 10 })]
};
var continuation = await client.Messages.Create(continuationParams);
Console.WriteLine(continuation);
}
else
{
Console.WriteLine(response);
}
client := anthropic.NewClient()
webSearchTool := []anthropic.ToolUnionParam{
{OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{
MaxUses: anthropic.Int(10),
}},
}
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Search for comprehensive information about quantum computing breakthroughs in 2025")),
},
Tools: webSearchTool,
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == anthropic.StopReasonPauseTurn {
// Pass the paused response back as-is so Claude can continue the turn
continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Search for comprehensive information about quantum computing breakthroughs in 2025")),
response.ToParam(),
},
Tools: webSearchTool,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(continuation)
} else {
fmt.Println(response)
}
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.WebSearchTool20250305;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("Search for comprehensive information about quantum computing breakthroughs in 2025")
.addTool(WebSearchTool20250305.builder()
.maxUses(10L)
.build())
.build();
Message response = client.messages().create(params);
if (response.stopReason().isPresent()
&& response.stopReason().get().equals(StopReason.PAUSE_TURN)) {
MessageCreateParams continuationParams = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("Search for comprehensive information about quantum computing breakthroughs in 2025")
.addMessage(response)
.addTool(WebSearchTool20250305.builder()
.maxUses(10L)
.build())
.build();
Message continuation = client.messages().create(continuationParams);
IO.println(continuation);
} else {
IO.println(response);
}
}
$client = new Client();
$response = $client->messages->create(
maxTokens: 1024,
messages: [
[
'role' => 'user',
'content' => 'Search for comprehensive information about quantum computing breakthroughs in 2025'
]
],
model: 'claude-opus-5-5',
tools: [
[
'type' => 'web_search_20250305',
'name' => 'web_search',
'max_uses' => 10
]
],
);
if ($response->stopReason === 'pause_turn') {
$messages = [
[
'role' => 'user',
'content' => 'Search for comprehensive information about quantum computing breakthroughs in 2025'
],
[
'role' => 'assistant',
'content' => $response->content
]
];
$continuation = $client->messages->create(
maxTokens: 1024,
messages: $messages,
model: 'claude-opus-5-5',
tools: [
[
'type' => 'web_search_20250305',
'name' => 'web_search',
'max_uses' => 10
]
],
);
echo $continuation;
} else {
echo $response;
}
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [
{
role: "user",
content:
"Search for comprehensive information about quantum computing breakthroughs in 2025"
}
],
tools: [
{
type: "web_search_20250305",
name: "web_search",
max_uses: 10
}
]
)
if response.stop_reason == :pause_turn
messages = [
{
role: "user",
content: "Search for comprehensive information about quantum computing breakthroughs in 2025"
},
{
role: "assistant",
content: response.content
}
]
continuation = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages,
tools: [
{
type: "web_search_20250305",
name: "web_search",
max_uses: 10
}
]
)
puts continuation
else
puts response
end
pause_turn을 처리할 때:
- 대화를 이어가기: 멈춘 응답을 그대로 다음 요청에 전달해 Claude가 턴을 이어가게 하세요.
- 도구 상태 보존: 이어짐 요청에 같은 도구를 포함하세요. 멈춘 턴은 아직 실행되지 않은 도구의
server_tool_use블록으로 끝날 수 있는데, 그 도구가 이어짐 요청에 없으면 API가 검증 오류를 반환해요. - 필요하면 반복: 이어간 턴이 다시 멈출 수 있어요. 각 응답의
stop_reason을 확인하고 다른 정지 이유가 나올 때까지 계속하되, 재시도 루프처럼 이어짐 횟수에 상한을 두세요.
다른 stop_reason 값과 일반적인 처리 패턴은 정지 이유와 폴백을 보세요.
한 턴에서 서버 도구와 클라이언트 도구 섞기 (Mixing server tools and client tools in one turn)
Claude는 같은 병렬 도구 호출 그룹에서 서버 도구와 클라이언트 도구를 호출할 수 있어요. 예를 들어 web_fetch를 사용자 정의 도구와 함께 호출할 수 있죠. 클라이언트 도구는 우리 코드가 실행하고 tool_use 블록을 만드는 모든 도구예요. 사용자 정의든 Bash 도구 같은 Anthropic 스키마 클라이언트 도구든 상관없어요. 그런 일이 일어나면 API는 서버 도구를 실행하지 않아요. 클라이언트 도구를 먼저 실행할 수 있도록 즉시 반환해요:
stop_reason이"pause_turn"이 아니라"tool_use"예요.content에server_tool_use블록과 클라이언트tool_use블록이 담기지만 서버 도구의 결과 블록은 없어요. 그 호출은 아직 끝나지 않았어요.- 다른 표시는 없어요.
id에 응답의 결과 블록과 짝지어지지 않는server_tool_use블록을 찾아서 이 상태를 감지하세요. MCP 커넥터의mcp_tool_use블록도 똑같이 동작해요. 이미 같은 응답에 결과 블록이 있는 서버 도구 호출은 완료된 것이고 우리가 할 일이 없어요.
{
"stop_reason": "tool_use",
"content": [
{
"type": "text",
"text": "I'll fetch the article and check your system at the same time."
},
{
"type": "server_tool_use",
"id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA",
"name": "web_fetch",
"input": { "url": "https://example.com/article" }
},
{
"type": "tool_use",
"id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
"name": "run_command",
"input": { "command": "uname -a" }
}
]
}
턴을 이어가려면 클라이언트 도구들을 실행하고, 그 응답의 각 tool_use 블록에 대한 tool_result 블록만 담긴 사용자 메시지를 보내세요. tools 배열을 유지하세요. 대기 중인 서버 도구가 더 이상 정의되지 않은 재개 요청은 메시지가 but no `web_fetch` tool was provided로 끝나는 400으로 실패해요.
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
"content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
}
]
}
API는 우리 결과를 여전히 열려 있는 어시스턴트 턴에 붙이고, 지연된 서버 도구를 실행하며(멈춘 코드 실행의 경우 재개), Claude가 이어질 수 있게 해요. Claude가 직접 호출한 서버 도구의 경우 다음 응답은 이전 응답의 server_tool_use id에 답하는 결과 블록으로 시작하고, 그 뒤에 새로 생성된 콘텐츠와 새로운 stop_reason이 이어져요:
{
"stop_reason": "end_turn",
"content": [
{
"type": "web_fetch_tool_result",
"tool_use_id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA",
"content": {
"type": "web_fetch_result",
"url": "https://example.com/article",
"content": {
"type": "document",
"source": {
"type": "text",
"media_type": "text/plain",
"data": "Full text content of the article..."
}
}
}
},
{
"type": "text",
"text": "The article argues that... and your machine is running Linux..."
}
]
}
server_tool_use 블록과 그 결과 블록은 위치가 아니라 tool_use_id로 짝지어져요. 이 흐름에서 둘은 서로 다른 두 응답에 도착하고, server_tool_use 블록은 두 번째 응답에서 반복되지 않아요. 이후 요청에서는 이 전체 교환을 messages 배열에 순서대로 유지하세요. 첫 응답을 assistant 메시지로, tool_result 사용자 메시지를, 그리고 다음 응답을 또 다른 assistant 메시지로 — 다른 도구 사용 교환을 쌓는 것과 같은 방식이에요.
`web_fetch` tool use with id `srvtoolu_01HxbWnMRmbWyMfUtJKC45rA` was found without a corresponding `web_fetch_tool_result` block
결과보다 앞서 콘텐츠를 넣거나, 클라이언트 tool_use ID의 일부에만 답하거나, tool_result 블록을 전혀 포함하지 않는 후속 요청은 도구 호출 처리하기에 설명된 클라이언트 도구 오류로 더 일찍 실패해요:
`tool_use` ids were found without `tool_result` blocks immediately after: toolu_01PjgRJLbXrXEMZwDNYLnBqk. Each `tool_use` block must have a corresponding `tool_result` block in the next message.
Claude에게 더 많은 입력을 주려면 턴이 끝난 후에 별도의 사용자 메시지로 보내세요.
이것이 pause_turn과 다른 점: pause_turn 응답도 아직 실행되지 않은 server_tool_use 블록으로 끝날 수 있어요. 하지만 그런 경우 클라이언트 tool_use 블록을 우리가 기다리게 만들지 않으므로, 어시스턴트 콘텐츠를 그대로 다시 보내면 되요. 클라이언트 tool_use 블록을 우리가 기다리게 만드는 응답은 pause_turn의 stop_reason을 갖지 않아요. Claude가 도구를 호출하려 멈추면 stop_reason은 tool_use이고, 응답을 다시 보내는 대신 클라이언트 tool_result 블록을 보내서 이어가요. 두 경우 모두 API는 다음 요청 시작에 대기 중인 서버 도구를 실행해요.
다음 예시는 웹 페치를 사용자 정의 run_command 도구와 함께 활성화하고 섞인 응답을 처리해요:
# If "stop_reason" is "tool_use" and a server_tool_use block has no matching
# result block, run the client tools and re-run with a user message of only
# their tool_result blocks appended (see the SDK tabs).
ant messages create <<'YAML'
model: claude-opus-4-8
max_tokens: 1024
messages:
- role: user
content: "Summarize https://example.com/article and run uname -a to tell me what system this is on."
tools:
- {type: web_fetch_20250910, name: web_fetch, max_uses: 5}
- name: run_command
description: Run a shell command on this computer and return its output.
input_schema:
type: object
properties:
command: {type: string, description: The command to run}
required: [command]
YAML
client = anthropic.Anthropic()
tools = [
{"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5},
{
"name": "run_command",
"description": "Run a shell command on this computer and return its output.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "The command to run"}
},
"required": ["command"],
},
},
]
messages = [
{
"role": "user",
"content": "Summarize https://example.com/article and run uname -a to tell me what system this is on.",
}
]
response = client.messages.create(
model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages
)
tool_results = [
{
"type": "tool_result",
"tool_use_id": block.id,
# Run your tool here. This example returns a fixed string.
"content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux",
}
for block in response.content
if block.type == "tool_use"
]
if response.stop_reason == "tool_use" and tool_results:
# A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
# Send back only the client tool_result blocks, with the same tools.
continuation = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=[
*messages,
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results},
],
)
# If a web_fetch was deferred, it runs on this request and its
# web_fetch_tool_result is the first block of continuation.content.
print(continuation)
else:
print(response)
const client = new Anthropic();
const webFetchTool = {
type: "web_fetch_20250910",
name: "web_fetch",
max_uses: 5
} as const;
const runCommandTool: Anthropic.Tool = {
name: "run_command",
description: "Run a shell command on this computer and return its output.",
input_schema: {
type: "object" as const,
properties: {
command: { type: "string", description: "The command to run" }
},
required: ["command"]
}
};
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Summarize https://example.com/article and run uname -a to tell me what system this is on."
}
];
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
tools: [webFetchTool, runCommandTool],
messages
});
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
// Run your tool here. This example returns a fixed string.
content: "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
});
}
}
if (response.stop_reason === "tool_use" && toolResults.length > 0) {
// A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
// Send back only the client tool_result blocks, with the same tools.
const continuation = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
tools: [webFetchTool, runCommandTool],
messages: [
...messages,
{ role: "assistant", content: response.content },
{ role: "user", content: toolResults }
]
});
// If a web_fetch was deferred, it runs on this request and its
// web_fetch_tool_result is the first block of continuation.content.
console.log(continuation);
} else {
console.log(response);
}
AnthropicClient client = new();
List<ToolUnion> tools =
[
new ToolUnion(new WebFetchTool20250910() { MaxUses = 5 }),
new ToolUnion(new Tool()
{
Name = "run_command",
Description = "Run a shell command on this computer and return its output.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["command"] = JsonSerializer.SerializeToElement(
new { type = "string", description = "The command to run" }
),
},
Required = ["command"],
},
}),
];
MessageParam userMessage = new()
{
Role = Role.User,
Content = "Summarize https://example.com/article and run uname -a to tell me what system this is on."
};
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 1024,
Tools = tools,
Messages = [userMessage]
});
var toolResults = new List<ContentBlockParam>();
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse))
{
toolResults.Add(new ContentBlockParam(new ToolResultBlockParam()
{
ToolUseID = toolUse.ID,
// Run your tool here. This example returns a fixed string.
Content = "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux",
}));
}
}
if (response.StopReason?.Value() == StopReason.ToolUse && toolResults.Count > 0)
{
// A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
// Send back only the client tool_result blocks, with the same tools.
var continuation = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus4_8,
MaxTokens = 1024,
Tools = tools,
Messages =
[
userMessage,
new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList()
},
new() { Role = Role.User, Content = new MessageParamContent(toolResults) }
]
});
// If a web_fetch was deferred, it runs on this request and its
// web_fetch_tool_result is the first block of continuation.Content.
Console.WriteLine(continuation);
}
else
{
Console.WriteLine(response);
}
client := anthropic.NewClient()
tools := []anthropic.ToolUnionParam{
{OfWebFetchTool20250910: &anthropic.WebFetchTool20250910Param{
MaxUses: anthropic.Int(5),
}},
{OfTool: &anthropic.ToolParam{
Name: "run_command",
Description: anthropic.String("Run a shell command on this computer and return its output."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"command": map[string]any{
"type": "string",
"description": "The command to run",
},
},
Required: []string{"command"},
},
}},
}
userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize https://example.com/article and run uname -a to tell me what system this is on."))
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 1024,
Tools: tools,
Messages: []anthropic.MessageParam{userMessage},
})
if err != nil {
log.Fatal(err)
}
var toolResults []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
// Run your tool here. This example returns a fixed string.
output := "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, false))
}
}
if response.StopReason == anthropic.StopReasonToolUse && len(toolResults) > 0 {
// A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
// Send back only the client tool_result blocks, with the same tools.
continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_8,
MaxTokens: 1024,
Tools: tools,
Messages: []anthropic.MessageParam{
userMessage,
response.ToParam(),
anthropic.NewUserMessage(toolResults...),
},
})
if err != nil {
log.Fatal(err)
}
// If a web_fetch was deferred, it runs on this request and its
// web_fetch_tool_result is the first block of continuation.Content.
fmt.Println(continuation)
} else {
fmt.Println(response)
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool runCommandTool = Tool.builder()
.name("run_command")
.description("Run a shell command on this computer and return its output.")
.inputSchema(Tool.InputSchema.builder()
.properties(JsonValue.from(Map.of(
"command", Map.of("type", "string", "description", "The command to run")
)))
.putAdditionalProperty("required", JsonValue.from(List.of("command")))
.build())
.build();
String prompt = "Summarize https://example.com/article and run uname -a to tell me what system this is on.";
Message response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(1024L)
.addTool(WebFetchTool20250910.builder().maxUses(5L).build())
.addTool(runCommandTool)
.addUserMessage(prompt)
.build());
List<ContentBlockParam> toolResults = new ArrayList<>();
for (ContentBlock block : response.content()) {
block.toolUse().ifPresent(toolUse -> toolResults.add(ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
// Run your tool here. This example returns a fixed string.
.content("Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux")
.build()
)));
}
boolean isToolUse = response.stopReason()
.map(StopReason.TOOL_USE::equals)
.orElse(false);
if (isToolUse && !toolResults.isEmpty()) {
// A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
// Send back only the client tool_result blocks, with the same tools.
Message continuation = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_4_8)
.maxTokens(1024L)
.addTool(WebFetchTool20250910.builder().maxUses(5L).build())
.addTool(runCommandTool)
.addUserMessage(prompt)
.addMessage(response)
.addUserMessageOfBlockParams(toolResults)
.build());
// If a web_fetch was deferred, it runs on this request and its
// web_fetch_tool_result is the first block of continuation.content().
IO.println(continuation);
} else {
IO.println(response);
}
}
$client = new Client();
$tools = [
['type' => 'web_fetch_20250910', 'name' => 'web_fetch', 'max_uses' => 5],
[
'name' => 'run_command',
'description' => 'Run a shell command on this computer and return its output.',
'input_schema' => [
'type' => 'object',
'properties' => [
'command' => ['type' => 'string', 'description' => 'The command to run']
],
'required' => ['command']
]
]
];
$userMessage = ['role' => 'user', 'content' => 'Summarize https://example.com/article and run uname -a to tell me what system this is on.'];
$response = $client->messages->create(
maxTokens: 1024,
messages: [$userMessage],
model: 'claude-opus-4-8',
tools: $tools,
);
$toolResults = [];
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block->id,
// Run your tool here. This example returns a fixed string.
'content' => 'Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux'
];
}
}
if ($response->stopReason === 'tool_use' && count($toolResults) > 0) {
// A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
// Send back only the client tool_result blocks, with the same tools.
$continuation = $client->messages->create(
maxTokens: 1024,
messages: [
$userMessage,
['role' => 'assistant', 'content' => $response->content],
['role' => 'user', 'content' => $toolResults],
],
model: 'claude-opus-4-8',
tools: $tools,
);
// If a web_fetch was deferred, it runs on this request and its
// web_fetch_tool_result is the first block of $continuation->content.
echo $continuation;
} else {
echo $response;
}
client = Anthropic::Client.new
tools = [
{ type: "web_fetch_20250910", name: "web_fetch", max_uses: 5 },
{
name: "run_command",
description: "Run a shell command on this computer and return its output.",
input_schema: {
type: "object",
properties: {
command: { type: "string", description: "The command to run" }
},
required: ["command"]
}
}
]
user_message = {
role: "user",
content: "Summarize https://example.com/article and run uname -a to tell me what system this is on."
}
response = client.messages.create(
model: "claude-opus-4-8",
max_tokens: 1024,
tools: tools,
messages: [user_message]
)
tool_results = []
response.content.each do |block|
next unless block.type == :tool_use
tool_results << {
type: "tool_result",
tool_use_id: block.id,
# Run your tool here. This example returns a fixed string.
content: "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
}
end
if response.stop_reason == :tool_use && !tool_results.empty?
# A server_tool_use block with no result block in this response is not finished; its result arrives in a later response.
# Send back only the client tool_result blocks, with the same tools.
continuation = client.messages.create(
model: "claude-opus-4-8",
max_tokens: 1024,
tools: tools,
messages: [
user_message,
{ role: "assistant", content: response.content },
{ role: "user", content: tool_results }
]
)
# If a web_fetch was deferred, it runs on this request and its
# web_fetch_tool_result is the first block of continuation.content.
puts continuation
else
puts response
end
이 코드는 Claude가 두 종류의 호출을 섞지 않을 때도 올바르게 동작해요. 클라이언트 tool_use 블록만 있는 턴도 같은 이어짐 경로를 타고, 서버 도구 호출만 있는 턴은 우리의 클라이언트 tool_result 블록이 필요 없어요. 그 결과 블록은 보통 이미 있고, pause_turn 응답처럼 멈춰서 돌아오는 것은 그대로 다시 보내면 되거든요.
ZDR과 allowed_callers (ZDR and allowed_callers)
웹 검색(web_search_20250305)과 웹 페치(web_fetch_20250910)의 기본 버전은 Zero Data Retention(ZDR) 자격이 있어요.
동적 필터링이 있는 _20260209 이후 버전은 내부적으로 코드 실행에 의존하므로 기본적으로 ZDR 자격이 없어요.
_20260209 이후 버전의 서버 도구를 ZDR과 함께 쓰려면 도구에 "allowed_callers": ["direct"]를 설정해 동적 필터링을 비활성화하세요:
{
"type": "web_search_20260209",
"name": "web_search",
"allowed_callers": ["direct"]
}
이렇게 하면 도구가 직접 호출로만 한정되고 내부 코드 실행 단계를 우회해요.
allowed_callers는 도구를 어떻게 호출할 수 있는지 제어해요. Claude가 직접("direct"), 코드 실행 컨테이너 안에서(예: "code_execution_20260120"), 또는 둘 다로요. 웹 도구의 _20260209 버전은 기본적으로 코드 실행 호출자만 사용하고, 이전 버전은 기본적으로 ["direct"]예요. 프로그래매틱 도구 호출을 지원하지 않는 모델에서는 이 버전들이 allowed_callers: ["direct"]를 요구해요. 없으면 API가 설정하라는 검증 오류를 반환해요.
도메인 필터링 (Domain filtering)
웹에 접근하는 서버 도구는 Claude가 닿을 수 있는 도메인을 제어하는 allowed_domains와 blocked_domains 매개변수를 받아요. 둘 다 도구 객체의 필드예요:
{
"type": "web_search_20250305",
"name": "web_search",
"allowed_domains": ["example.com", "docs.python.org"]
}
도메인 필터를 사용할 때:
- 도메인에 HTTP/HTTPS 스킴을 포함하지 마세요(
https://example.com대신example.com을 쓰세요). - 서브도메인은 자동으로 포함돼요(
example.com은docs.example.com을 포함해요). - 특정 서브도메인은 그 서브도메인으로만 결과를 제한해요(
docs.example.com은example.com이나api.example.com이 아니라 그 서브도메인의 결과만 반환해요). - 웹 검색은 하위 경로를 지원하고 경로 뒤의 모든 것을 매칭해요(
example.com/blog는example.com/blog/post-1과 매칭돼요). - 웹 페치는 도메인에서만 매칭해요. 경로가 포함된 항목은 웹 페치 URL과 절대 매칭되지 않아요.
allowed_domains나blocked_domains중 하나는 쓸 수 있지만, 같은 요청에 둘 다는 못 써요.
와일드카드 지원:
- 와일드카드(
*)는 도메인 자체에는 허용되지 않고, 그 뒤의 경로에만 허용돼요. - 유효:
example.com/*,example.com/*/articles - 무효:
*.example.com,ex*.com
무효한 도메인 형식은 요청 시점에 400 invalid_request_error로 거부돼요.
Claude Managed Agents는 에이전트 툴셋의 web_search와 web_fetch 항목에서 같은 allowed_domains와 blocked_domains 필드를 사용해요. Managed Agents에서 각 목록은 최대 64개 항목을 보유하고, web_fetch에 나열된 도메인은 경로를 포함할 수 없으며, max_uses, citations, cache_control 같은 Messages API 도구 전용 필드는 사용할 수 없어요. 전체 규칙은 웹 검색·웹 페치 도메인 제한을 보세요.
Claude 콘솔의 조직 수준 웹 검색·웹 페치 설정은 Messages API 요청에만 적용돼요. 에이전트 툴셋의 도구별 목록만 사용하는 Managed Agents 세션에는 적용되지 않아요.
코드 실행이 포함된 동적 필터링 (Dynamic filtering with code execution)
웹 검색·웹 페치의 _20260209 이후 버전은 검색 결과에 동적 필터를 적용하기 위해 내부적으로 코드 실행을 사용해요.
서버 도구 이벤트 스트리밍 (Streaming server-tool events)
서버 도구 이벤트는 일반적인 서버 전송 이벤트(SSE) 흐름의 일부로 스트리밍돼요. Claude가 직접 호출하는 server_tool_use 블록은 클라이언트 tool_use 블록처럼 스트리밍돼요. content_block_start 이벤트 뒤에 input_json_delta 이벤트가 따르는 형태예요. 결과 블록은 델타 없이 단일 content_block_start 이벤트로 통째로 도착해요.
전체 이벤트 레퍼런스는 스트리밍을 보세요. 각 도구 페이지는 다를 때 도구별 이벤트 이름을 문서화해요.
배치 요청 (Batch requests)
모든 서버 도구가 배치 처리를 지원해요. 배치에서는 에이전틱 루프가 동기 요청과 마찬가지로 더 높은 턴당 반복 제한으로 실행돼요. 루프가 그 한계에 도달하면 응답이 stop_reason: "pause_turn"으로 끝나요. 반환된 콘텐츠로 후속 요청을 제출해 이어갈 수 있어요. 자세한 내용은 서버 도구와 에이전틱 루프를 보세요.
흔한 배치 워크로드에는 웹의 정보로 데이터셋을 보강하기, 많은 문서 세트를 최신 소스와 대조하기, 많은 파일에 걸쳐 분석 코드 실행하기 등이 있어요.