Tool use, MCP и потоковая передача в API

В предыдущей статье мы разобрали базовый Messages API — как отправлять сообщения, читать ответы и считать токены. Теперь переходим к трём механизмам, которые превращают простой клиент в агента: инструменты (tool use), подключение MCP-серверов напрямую из API и стриминг.


Клиентские инструменты: описание через JSON Schema

Инструмент — это функция вашего кода, которую модель может вызвать. Описывается в параметре tools запроса тремя обязательными полями:

{
  "name": "get_file_contents",
  "description": "Читает содержимое текстового файла по заданному пути. Использовать, когда нужно получить исходный код, конфиг или данные из файловой системы. Возвращает строку. Не работает с бинарными файлами и URL.",
  "input_schema": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "Абсолютный или относительный путь к файлу"
      }
    },
    "required": ["path"]
  }
}

Самое важное поле — description. Именно по нему модель решает, когда и как использовать инструмент. Пишите конкретно: что делает, при каких запросах вызывать, что возвращает, чего не умеет. Два слова хуже четырёх предложений.

Для инструментов со сложными параметрами или форматными ограничениями добавьте input_examples — массив примерных аргументов. Каждый пример должен соответствовать input_schema — невалидный пример API отклонит ошибкой 400.

Проверь себя
Инструмент описан одной строкой: `"description": "Поиск по файлам"`. Назовите конкретную проблему, которая из этого вытекает.

Быстрое повторение
Какие три обязательных поля нужны для описания инструмента в Messages API?

Цикл tool use: запрос → выполнение → результат

Когда модель решает вызвать инструмент, ответ приходит со stop_reason: "tool_use". В content — блок tool_use:

{
  "type": "tool_use",
  "id": "toolu_01AbCdEf",
  "name": "get_file_contents",
  "input": {"path": "src/main.py"}
}

Три поля: id (нужен для tool_result), name (какую функцию запустить), input (аргументы). Ваш код выполняет операцию, затем отправляет результат в новом user-сообщении:

{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01AbCdEf",
      "content": "import anthropic\n\ndef main():\n    ..."
    }
  ]
}

Перед этим — обязательно дописать ассистентский ответ с блоком tool_use в историю messages. API stateless, состояние полностью в ваших руках. Если инструмент завершился ошибкой — передайте "is_error": true с текстом ошибки в content, и модель скорректирует подход.

> Важно: в user-сообщении с результатами блоки tool_result должны идти первыми — до любого текстового контента. Нарушение этого порядка вернёт ошибку 400.

После получения tool_result модель продолжает генерацию. Если нужен ещё один инструмент — stop_reason снова будет "tool_use". Цикл повторяется, пока не придёт "end_turn".

sequenceDiagram participant App as Ваше приложение participant API as Claude API App->>API: messages + tools API-->>App: stop_reason tool_use Note over App: Выполнить name(input) App->>API: messages + tool_result API-->>App: stop_reason end_turn Note over App: Или снова tool_use — цикл
sequenceDiagram
    participant App as Ваше приложение
    participant API as Claude API
    App->>API: messages + tools
    API-->>App: stop_reason tool_use
    Note over App: Выполнить name(input)
    App->>API: messages + tool_result
    API-->>App: stop_reason end_turn
    Note over App: Или снова tool_use — цикл
Агентный цикл tool use: запрос, выполнение инструмента, возврат результата
Проверь себя
Вы отправили `tool_result` и получили новый ответ. `stop_reason` снова `"tool_use"`. Что происходит и что делать?

Быстрое повторение
Какое значение stop_reason указывает, что модель вызвала инструмент?

Контроль вызовов: tool_choice и strict

По умолчанию tool_choice: {"type": "auto"} — модель сама решает, вызывать инструмент или отвечать текстом. Три дополнительных варианта:

tool_choiceПоведение
{"type": "any"}Обязательно вызвать хотя бы один инструмент
{"type": "tool", "name": "..."}Только этот конкретный инструмент
{"type": "none"}Инструменты заблокированы в этом ответе

При any и tool модель не пишет текст перед вызовом — сразу переходит к tool_use. Добавьте "strict": true в определение инструмента, чтобы гарантировать точное соответствие аргументов схеме без лишних полей и без пропуска required-параметров.

Помимо клиентских инструментов есть серверные — их выполняет инфраструктура Anthropic, tool_result слать не нужно. Подключаются через тот же tools, но с именованным типом:

tools=[{"type": "web_search_20260209", "name": "web_search"}]

Быстрое повторение
Какое значение tool_choice обязывает модель вызвать хотя бы один инструмент?

MCP-коннектор: внешние серверы из API

В Claude Code MCP-серверы добавляются командой claude mcp add. В Messages API — параметр mcp_servers (бета-функция, заголовок mcp-client-2025-11-20):

response = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=1000,
    messages=[{"role": "user", "content": "Покажи открытые PR"}],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://github-mcp.example.com/sse",
            "name": "github",
            "authorization_token": "ghp_..."
        }
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "github"}],
    betas=["mcp-client-2025-11-20"],
)

Сервер должен быть публично доступен по HTTPS (SSE или Streamable HTTP) — stdio-серверы через этот механизм не работают. Параметр mcp_servers описывает соединение; tools с типом mcp_toolset — какие инструменты включить (по умолчанию все).

Официальная архитектурная схема MCP: приложение обращается к Claude API, который по HTTPS подключается к удалённому MCP-серверу и получает его инструменты.Источник: https://www.whatismcp.com/ru

Для блокировки опасных инструментов (denylist):

{
  "type": "mcp_toolset",
  "mcp_server_name": "github",
  "configs": {"delete_repository": {"enabled": false}}
}

Для allowlist — задайте "default_config": {"enabled": false}, затем включите нужные через configs поимённо. Ответ содержит блоки mcp_tool_use и mcp_tool_result — аналогичны обычным, плюс поле server_name.


Стриминг

По умолчанию API ждёт полного ответа и отдаёт его разом. Для длинных генераций — несколько секунд молчания перед первым символом. stream: true переключает на Server-Sent Events.

В Python SDK — метод .stream():

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Объясни GC в CPython"}],
) as stream:
    for chunk in stream.text_stream:
        print(chunk, end="", flush=True)

final = stream.get_final_message()
print(f"\nТокенов: {final.usage.input_tokens} in / {final.usage.output_tokens} out")

В TypeScript:

await client.messages
  .stream({
    model: "claude-sonnet-4-6",
    max_tokens: 2048,
    messages: [{ role: "user", content: "Объясни GC в CPython" }],
  })
  .on("text", (t) => process.stdout.write(t));

Стриминг с инструментами. Когда модель стримит вызов инструмента, вместо text_delta приходят события input_json_delta с частичным JSON аргументов:

event: content_block_delta
data: {"type":"content_block_delta","index":1,
       "delta":{"type":"input_json_delta","partial_json":"{\"path\": \"/src/"}}

Склеивайте строки partial_json до события content_block_stop, потом парсите JSON. SDK делает это автоматически — в text_stream данные инструментов не попадают. Следите за финальным stop_reason в событии message_delta: если "tool_use" — нужна следующая итерация цикла.

Проверь себя
При стриминге с инструментами приходит событие `content_block_delta` с `"type": "input_json_delta"` и `"partial_json": "{\"path\": \"/src/"`. Когда можно распарсить это как полноценный JSON?

See also

Источники

  1. Tool use with Claude — Overview
  2. Define tools — Anthropic Docs
  3. Handle tool calls — Anthropic Docs
  4. MCP connector — Anthropic Docs
  5. Streaming messages — Anthropic Docs