서버 도구

서버 도구 (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 정지 이유를 처리하는 방법:

```bash cURL # Initial request. If "stop_reason" in the response is "pause_turn", continue # the turn by re-sending the request with the assistant content appended to messages. curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "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}] }' ```
# 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"예요.
  • contentserver_tool_use 블록과 클라이언트 tool_use 블록이 담기지만 서버 도구의 결과 블록은 없어요. 그 호출은 아직 끝나지 않았어요.
  • 다른 표시는 없어요. id에 응답의 결과 블록과 짝지어지지 않는 server_tool_use 블록을 찾아서 이 상태를 감지하세요. MCP 커넥터mcp_tool_use 블록도 똑같이 동작해요. 이미 같은 응답에 결과 블록이 있는 서버 도구 호출은 완료된 것이고 우리가 할 일이 없어요.
[프로그래매틱 도구 호출](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling)에서는 같은 응답 형태가 다른 의미를 가져요. 클라이언트 `tool_use` 블록이 Claude에서 직접 온 게 아니라 `code_execution` 도구 안에서 실행 중인 코드에서 오고, 그 `caller` 필드가 호출한 `code_execution` 블록을 가리켜요. 그 코드는 이미 시작됐고, 지연된 도구를 시작하는 대신 재개되기를 기다리며 우리 `tool_result` 블록을 기다리는 중이에요. 코드가 완료되면 `code_execution` 블록 자체의 결과 블록이 도착하는데, 이는 도구 결과를 여러 번 왕복할 수 있어요. 후속 사용자 메시지 자체는 두 경우 모두 같아요. 프로그래매틱 도구 호출에서는 그 페이지에서 보여주듯 응답의 `container` 필드의 `id`도 함께 돌려보내야 해요.
{
  "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 메시지로 — 다른 도구 사용 교환을 쌓는 것과 같은 방식이에요.

후속 사용자 메시지는 `tool_result` 블록 외에는 아무것도 포함하면 안 돼요. 결과 뒤에 추가된 텍스트 같은 블록은 API에게 어시스턴트 턴이 끝났다고 알려 줘요. Claude가 직접 호출한 서버 도구의 경우 턴이 해결되지 않은 서버 도구 호출로 남게 되고, 요청이 400 `invalid_request_error`로 실패해요:
`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_turnstop_reason을 갖지 않아요. Claude가 도구를 호출하려 멈추면 stop_reasontool_use이고, 응답을 다시 보내는 대신 클라이언트 tool_result 블록을 보내서 이어가요. 두 경우 모두 API는 다음 요청 시작에 대기 중인 서버 도구를 실행해요.

다음 예시는 웹 페치를 사용자 정의 run_command 도구와 함께 활성화하고 섞인 응답을 처리해요:

```bash cURL # If "stop_reason" is "tool_use" and a server_tool_use block has no matching # result block, that call is not finished. Run the client tools, then POST # again with one more user message containing only their tool_result blocks # and the same tools array (see the SDK tabs). curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "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"] } } ] }' ```
# 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가 설정하라는 검증 오류를 반환해요.

웹 페치를 ZDR 자격 구성으로 사용하더라도, Claude가 사이트에서 콘텐츠를 가져오면 웹사이트 게시자가 URL에 전달된 매개변수를 보관할 수 있어요.

도메인 필터링 (Domain filtering)

웹에 접근하는 서버 도구는 Claude가 닿을 수 있는 도메인을 제어하는 allowed_domainsblocked_domains 매개변수를 받아요. 둘 다 도구 객체의 필드예요:

{
  "type": "web_search_20250305",
  "name": "web_search",
  "allowed_domains": ["example.com", "docs.python.org"]
}

도메인 필터를 사용할 때:

  • 도메인에 HTTP/HTTPS 스킴을 포함하지 마세요(https://example.com 대신 example.com을 쓰세요).
  • 서브도메인은 자동으로 포함돼요(example.comdocs.example.com을 포함해요).
  • 특정 서브도메인은 그 서브도메인으로만 결과를 제한해요(docs.example.comexample.com이나 api.example.com이 아니라 그 서브도메인의 결과만 반환해요).
  • 웹 검색은 하위 경로를 지원하고 경로 뒤의 모든 것을 매칭해요(example.com/blogexample.com/blog/post-1과 매칭돼요).
  • 웹 페치는 도메인에서만 매칭해요. 경로가 포함된 항목은 웹 페치 URL과 절대 매칭되지 않아요.
  • allowed_domainsblocked_domains 중 하나는 쓸 수 있지만, 같은 요청에 둘 다는 못 써요.

와일드카드 지원:

  • 와일드카드(*)는 도메인 자체에는 허용되지 않고, 그 뒤의 경로에만 허용돼요.
  • 유효: example.com/*, example.com/*/articles
  • 무효: *.example.com, ex*.com

무효한 도메인 형식은 요청 시점에 400 invalid_request_error로 거부돼요.

요청 수준 도메인 제한은 Claude 콘솔에서 구성한 조직 수준 도메인 제한과 함께 동작해요. 요청 수준 `allowed_domains`는 조직 수준 허용 목록의 부분 집합이어야 해요. 범위 밖의 항목은 API가 검증 오류를 반환하게 해요. 조직이 차단한 도메인을 포함하는 요청 수준 허용 목록은 충돌하는 항목을 이름으로 알려 주는 `400` 오류로 거부돼요. 도메인 이름의 유니코드 문자는 호모그래프 공격으로 도메인 필터를 우회할 수 있어요. `аmazon.com`(키릴 문자 `а` 사용)은 `amazon.com`과 똑같이 보이지만 다른 도메인이에요. 허용·차단 목록에는 ASCII 전용 도메인 이름을 쓰고, 기존 항목에 비-ASCII 문자가 있는지 감사하세요.

Claude Managed Agents는 에이전트 툴셋의 web_searchweb_fetch 항목에서 같은 allowed_domainsblocked_domains 필드를 사용해요. Managed Agents에서 각 목록은 최대 64개 항목을 보유하고, web_fetch에 나열된 도메인은 경로를 포함할 수 없으며, max_uses, citations, cache_control 같은 Messages API 도구 전용 필드는 사용할 수 없어요. 전체 규칙은 웹 검색·웹 페치 도메인 제한을 보세요.

Claude 콘솔의 조직 수준 웹 검색·웹 페치 설정은 Messages API 요청에만 적용돼요. 에이전트 툴셋의 도구별 목록만 사용하는 Managed Agents 세션에는 적용되지 않아요.

코드 실행이 포함된 동적 필터링 (Dynamic filtering with code execution)

웹 검색·웹 페치의 _20260209 이후 버전은 검색 결과에 동적 필터를 적용하기 위해 내부적으로 코드 실행을 사용해요.

이 버전들에 `code_execution` 도구를 추가할 필요는 없어요. 동적 필터링이 실행될 때 API가 요청에 코드 실행을 자동으로 프로비저닝하고, 두 도구가 단일 실행 컨테이너를 공유해요. 포함한다면 `code_execution_20260120` 이상을 쓰세요. API는 이 웹 도구 버전들과 함께 이전 코드 실행 버전을 거부해요.

서버 도구 이벤트 스트리밍 (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"으로 끝나요. 반환된 콘텐츠로 후속 요청을 제출해 이어갈 수 있어요. 자세한 내용은 서버 도구와 에이전틱 루프를 보세요.

흔한 배치 워크로드에는 웹의 정보로 데이터셋을 보강하기, 많은 문서 세트를 최신 소스와 대조하기, 많은 파일에 걸쳐 분석 코드 실행하기 등이 있어요.

더 알아보기 (Learn more)