agent-sdk/custom-tools.md +833 −0 created
1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Предоставьте Claude пользовательские инструменты
6
7> Определите пользовательские инструменты с помощью встроенного MCP-сервера Agent SDK, чтобы Claude мог вызывать ваши функции, обращаться к вашим API и выполнять операции, специфичные для вашей области.
8
9Пользовательские инструменты расширяют Agent SDK, позволяя вам определять собственные функции, которые Claude может вызывать во время разговора. Используя встроенный MCP-сервер SDK, вы можете предоставить Claude доступ к базам данных, внешним API, логике, специфичной для вашей области, или любым другим возможностям, которые требует ваше приложение.
10
11В этом руководстве рассказывается, как определять инструменты с входными схемами и обработчиками, объединять их в MCP-сервер, передавать их в `query` и контролировать, к каким инструментам Claude может получить доступ. Оно также охватывает обработку ошибок, аннотации инструментов и возврат нетекстового содержимого, такого как изображения.
12
13## Краткая справка
14
15| Если вы хотите... | Сделайте это |
16| :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
17| Определить инструмент | Используйте [`@tool`](/ru/agent-sdk/python#tool) (Python) или [`tool()`](/ru/agent-sdk/typescript#tool) (TypeScript) с именем, описанием, схемой и обработчиком. См. [Создание пользовательского инструмента](#create-a-custom-tool). |
18| Зарегистрировать инструмент с Claude | Оберните в `create_sdk_mcp_server` / `createSdkMcpServer` и передайте в `mcpServers` в `query()`. См. [Вызов пользовательского инструмента](#call-a-custom-tool). |
19| Предварительно одобрить инструмент | Добавьте в разрешённые инструменты. См. [Настройка разрешённых инструментов](#configure-allowed-tools). |
20| Удалить встроенный инструмент из контекста Claude | Передайте массив `tools`, содержащий только встроенные инструменты, которые вы хотите. См. [Настройка разрешённых инструментов](#configure-allowed-tools). |
21| Позволить Claude вызывать инструменты параллельно | Установите `readOnlyHint: true` на инструментах без побочных эффектов. См. [Добавление аннотаций инструментов](#add-tool-annotations). |
22| Обработать ошибки без остановки цикла | Верните `isError: true` вместо выброса исключения. См. [Обработка ошибок](#handle-errors). |
23| Вернуть изображения или файлы | Используйте блоки `image` или `resource` в массиве содержимого. См. [Возврат изображений и ресурсов](#return-images-and-resources). |
24| Вернуть результат в формате машиночитаемого JSON | Установите `structuredContent` на результат. См. [Возврат структурированных данных](#return-structured-data). |
25| Масштабировать до множества инструментов | Используйте [поиск инструментов](/ru/agent-sdk/tool-search) для загрузки инструментов по требованию. |
26
27## Создание пользовательского инструмента
28
29Инструмент определяется четырьмя частями, передаваемыми в качестве аргументов вспомогательной функции [`tool()`](/ru/agent-sdk/typescript#tool) в TypeScript или декоратору [`@tool`](/ru/agent-sdk/python#tool) в Python:
30
31* **Имя:** уникальный идентификатор, который Claude использует для вызова инструмента.
32* **Описание:** что делает инструмент. Claude читает это, чтобы решить, когда его вызывать.
33* **Входная схема:** аргументы, которые должен предоставить Claude. В TypeScript это всегда [схема Zod](https://zod.dev/), и типы `args` обработчика автоматически выводятся из неё. В Python это словарь, отображающий имена на типы, например `{"latitude": float}`, который SDK преобразует в JSON Schema для вас. Декоратор Python также принимает полный словарь [JSON Schema](https://json-schema.org/understanding-json-schema/about) непосредственно, когда вам нужны перечисления, диапазоны, необязательные поля или вложенные объекты.
34* **Обработчик:** асинхронная функция, которая запускается, когда Claude вызывает инструмент. Она получает проверенные аргументы и должна вернуть объект с:
35 * `content` (обязательно): массив блоков результатов, каждый с типом `"text"`, `"image"` или `"resource"`. См. [Возврат изображений и ресурсов](#return-images-and-resources) для нетекстовых блоков.
36 * `structuredContent` (необязательно): объект JSON, содержащий результат как машиночитаемые данные, возвращаемые вместе с `content`. См. [Возврат структурированных данных](#return-structured-data).
37 * `isError` (необязательно): установите на `true`, чтобы сигнализировать об ошибке инструмента, чтобы Claude мог на неё реагировать. См. [Обработка ошибок](#handle-errors).
38
39После определения инструмента оберните его в сервер с помощью [`createSdkMcpServer`](/ru/agent-sdk/typescript#createsdkmcpserver) (TypeScript) или [`create_sdk_mcp_server`](/ru/agent-sdk/python#create_sdk_mcp_server) (Python). Сервер работает встроенным образом внутри вашего приложения, а не как отдельный процесс.
40
41### Пример инструмента погоды
42
43Этот пример определяет инструмент `get_temperature` и оборачивает его в MCP-сервер. Он только настраивает инструмент; чтобы передать его в `query` и запустить его, см. [Вызов пользовательского инструмента](#call-a-custom-tool) ниже.
44
45<CodeGroup>
46 ```python Python theme={null}
47 from typing import Any
48 import httpx
49 from claude_agent_sdk import tool, create_sdk_mcp_server
50
51
52 # Define a tool: name, description, input schema, handler
53 @tool(
54 "get_temperature",
55 "Get the current temperature at a location",
56 {"latitude": float, "longitude": float},
57 )
58 async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
59 async with httpx.AsyncClient() as client:
60 response = await client.get(
61 "https://api.open-meteo.com/v1/forecast",
62 params={
63 "latitude": args["latitude"],
64 "longitude": args["longitude"],
65 "current": "temperature_2m",
66 "temperature_unit": "fahrenheit",
67 },
68 )
69 data = response.json()
70
71 # Return a content array - Claude sees this as the tool result
72 return {
73 "content": [
74 {
75 "type": "text",
76 "text": f"Temperature: {data['current']['temperature_2m']}°F",
77 }
78 ]
79 }
80
81
82 # Wrap the tool in an in-process MCP server
83 weather_server = create_sdk_mcp_server(
84 name="weather",
85 version="1.0.0",
86 tools=[get_temperature],
87 )
88 ```
89
90 ```typescript TypeScript theme={null}
91 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
92 import { z } from "zod";
93
94 // Define a tool: name, description, input schema, handler
95 const getTemperature = tool(
96 "get_temperature",
97 "Get the current temperature at a location",
98 {
99 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
100 longitude: z.number().describe("Longitude coordinate")
101 },
102 async (args) => {
103 // args is typed from the schema: { latitude: number; longitude: number }
104 const response = await fetch(
105 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}¤t=temperature_2m&temperature_unit=fahrenheit`
106 );
107 const data: any = await response.json();
108
109 // Return a content array - Claude sees this as the tool result
110 return {
111 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
112 };
113 }
114 );
115
116 // Wrap the tool in an in-process MCP server
117 const weatherServer = createSdkMcpServer({
118 name: "weather",
119 version: "1.0.0",
120 tools: [getTemperature]
121 });
122 ```
123</CodeGroup>
124
125См. справку [`tool()`](/ru/agent-sdk/typescript#tool) TypeScript или справку [`@tool`](/ru/agent-sdk/python#tool) Python для полных деталей параметров, включая форматы входных JSON Schema и структуру возвращаемого значения.
126
127<Tip>
128 Чтобы сделать параметр необязательным: в TypeScript добавьте `.default()` к полю Zod. В Python словарь схемы рассматривает каждый ключ как обязательный, поэтому оставьте параметр вне схемы, упомяните его в строке описания и читайте его с помощью `args.get()` в обработчике. Инструмент [`get_precipitation_chance` ниже](#add-more-tools) показывает оба паттерна.
129</Tip>
130
131### Вызов пользовательского инструмента
132
133Передайте созданный MCP-сервер в `query` через опцию `mcpServers`. Ключ в `mcpServers` становится сегментом `{server_name}` в полностью квалифицированном имени каждого инструмента: `mcp__{server_name}__{tool_name}`. Перечислите это имя в `allowedTools`, чтобы инструмент работал без запроса разрешения.
134
135Эти фрагменты повторно используют `weatherServer` из [примера выше](#weather-tool-example), чтобы спросить Claude о погоде в определённом месте.
136
137<CodeGroup>
138 ```python Python theme={null}
139 import asyncio
140 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
141
142
143 async def main():
144 options = ClaudeAgentOptions(
145 mcp_servers={"weather": weather_server},
146 allowed_tools=["mcp__weather__get_temperature"],
147 )
148
149 async for message in query(
150 prompt="What's the temperature in San Francisco?",
151 options=options,
152 ):
153 # ResultMessage is the final message after all tool calls complete
154 if isinstance(message, ResultMessage) and message.subtype == "success":
155 print(message.result)
156
157
158 asyncio.run(main())
159 ```
160
161 ```typescript TypeScript theme={null}
162 import { query } from "@anthropic-ai/claude-agent-sdk";
163
164 for await (const message of query({
165 prompt: "What's the temperature in San Francisco?",
166 options: {
167 mcpServers: { weather: weatherServer },
168 allowedTools: ["mcp__weather__get_temperature"]
169 }
170 })) {
171 // "result" is the final message after all tool calls complete
172 if (message.type === "result" && message.subtype === "success") {
173 console.log(message.result);
174 }
175 }
176 ```
177</CodeGroup>
178
179### Добавление дополнительных инструментов
180
181Сервер содержит столько инструментов, сколько вы перечислите в его массиве `tools`. Если на сервере более одного инструмента, вы можете перечислить каждый в `allowedTools` отдельно или использовать подстановочный знак `mcp__weather__*`, чтобы охватить каждый инструмент, который сервер предоставляет.
182
183Пример ниже добавляет второй инструмент, `get_precipitation_chance`, к `weatherServer` из [примера инструмента погоды](#weather-tool-example) и перестраивает его с обоими инструментами в массиве.
184
185<CodeGroup>
186 ```python Python theme={null}
187 # Define a second tool for the same server
188 @tool(
189 "get_precipitation_chance",
190 "Get the hourly precipitation probability for a location. "
191 "Optionally pass 'hours' (1-24) to control how many hours to return.",
192 {"latitude": float, "longitude": float},
193 )
194 async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
195 # 'hours' isn't in the schema - read it with .get() to make it optional
196 hours = args.get("hours", 12)
197 async with httpx.AsyncClient() as client:
198 response = await client.get(
199 "https://api.open-meteo.com/v1/forecast",
200 params={
201 "latitude": args["latitude"],
202 "longitude": args["longitude"],
203 "hourly": "precipitation_probability",
204 "forecast_days": 1,
205 },
206 )
207 data = response.json()
208 chances = data["hourly"]["precipitation_probability"][:hours]
209
210 return {
211 "content": [
212 {
213 "type": "text",
214 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
215 }
216 ]
217 }
218
219
220 # Rebuild the server with both tools in the array
221 weather_server = create_sdk_mcp_server(
222 name="weather",
223 version="1.0.0",
224 tools=[get_temperature, get_precipitation_chance],
225 )
226 ```
227
228 ```typescript TypeScript theme={null}
229 // Define a second tool for the same server
230 const getPrecipitationChance = tool(
231 "get_precipitation_chance",
232 "Get the hourly precipitation probability for a location",
233 {
234 latitude: z.number(),
235 longitude: z.number(),
236 hours: z
237 .number()
238 .int()
239 .min(1)
240 .max(24)
241 .default(12) // .default() makes the parameter optional
242 .describe("How many hours of forecast to return")
243 },
244 async (args) => {
245 const response = await fetch(
246 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
247 );
248 const data: any = await response.json();
249 const chances = data.hourly.precipitation_probability.slice(0, args.hours);
250
251 return {
252 content: [{ type: "text", text: `Next ${args.hours} hours: ${chances.join("%, ")}%` }]
253 };
254 }
255 );
256
257 // Rebuild the server with both tools in the array
258 const weatherServer = createSdkMcpServer({
259 name: "weather",
260 version: "1.0.0",
261 tools: [getTemperature, getPrecipitationChance]
262 });
263 ```
264</CodeGroup>
265
266Каждый инструмент в этом массиве потребляет пространство контекстного окна на каждом ходу. Если вы определяете десятки инструментов, см. [поиск инструментов](/ru/agent-sdk/tool-search) для загрузки их по требованию.
267
268### Добавление аннотаций инструментов
269
270[Аннотации инструментов](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations) — это необязательные метаданные, описывающие поведение инструмента. Передайте их в качестве пятого аргумента вспомогательной функции `tool()` в TypeScript или через аргумент ключевого слова `annotations` для декоратора `@tool` в Python. Все поля подсказок являются логическими значениями.
271
272| Поле | По умолчанию | Значение |
273| :---------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------- |
274| `readOnlyHint` | `false` | Инструмент не изменяет свою среду. Контролирует, может ли инструмент вызываться параллельно с другими инструментами только для чтения. |
275| `destructiveHint` | `true` | Инструмент может выполнять деструктивные обновления. Только информационное. |
276| `idempotentHint` | `false` | Повторные вызовы с одинаковыми аргументами не имеют дополнительного эффекта. Только информационное. |
277| `openWorldHint` | `true` | Инструмент обращается к системам вне вашего процесса. Только информационное. |
278
279Аннотации — это метаданные, а не принуждение. Инструмент, отмеченный как `readOnlyHint: true`, всё ещё может писать на диск, если это то, что делает обработчик. Держите аннотацию точной для обработчика.
280
281Этот пример добавляет `readOnlyHint` к инструменту `get_temperature` из [примера инструмента погоды](#weather-tool-example).
282
283<CodeGroup>
284 ```python Python theme={null}
285 from claude_agent_sdk import tool, ToolAnnotations
286
287
288 @tool(
289 "get_temperature",
290 "Get the current temperature at a location",
291 {"latitude": float, "longitude": float},
292 annotations=ToolAnnotations(
293 readOnlyHint=True
294 ), # Lets Claude batch this with other read-only calls
295 )
296 async def get_temperature(args):
297 return {"content": [{"type": "text", "text": "..."}]}
298 ```
299
300 ```typescript TypeScript theme={null}
301 tool(
302 "get_temperature",
303 "Get the current temperature at a location",
304 { latitude: z.number(), longitude: z.number() },
305 async (args) => ({ content: [{ type: "text", text: `...` }] }),
306 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
307 );
308 ```
309</CodeGroup>
310
311См. `ToolAnnotations` в справке [TypeScript](/ru/agent-sdk/typescript#toolannotations) или [Python](/ru/agent-sdk/python#toolannotations).
312
313## Контроль доступа к инструментам
314
315[Пример инструмента погоды](#weather-tool-example) зарегистрировал сервер и перечислил инструменты в `allowedTools`. Этот раздел охватывает, как конструируются имена инструментов и как ограничить доступ, когда у вас есть несколько инструментов или вы хотите ограничить встроенные инструменты.
316
317### Формат имени инструмента
318
319Когда инструменты MCP предоставляются Claude, их имена следуют определённому формату:
320
321* Паттерн: `mcp__{server_name}__{tool_name}`
322* Пример: инструмент с именем `get_temperature` на сервере `weather` становится `mcp__weather__get_temperature`
323
324### Настройка разрешённых инструментов
325
326Опция `tools` и списки разрешённых/запрещённых инструментов работают на отдельных уровнях. `tools` контролирует, какие встроенные инструменты появляются в контексте Claude. Списки разрешённых и запрещённых инструментов контролируют, одобрены ли вызовы или отклонены после того, как Claude попытается их выполнить.
327
328| Опция | Уровень | Эффект |
329| :------------------------ | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330| `tools: ["Read", "Grep"]` | Доступность | Только перечисленные встроенные инструменты находятся в контексте Claude. Неперечисленные встроенные инструменты удаляются. Инструменты MCP не затрагиваются. |
331| `tools: []` | Доступность | Все встроенные инструменты удаляются. Claude может использовать только ваши инструменты MCP. |
332| разрешённые инструменты | Разрешение | Перечисленные инструменты работают без запроса разрешения. Неперечисленные инструменты остаются доступными; вызовы проходят через [поток разрешений](/ru/agent-sdk/permissions). |
333| запрещённые инструменты | Разрешение | Каждый вызов перечисленного инструмента отклоняется. Инструмент остаётся в контексте Claude, поэтому Claude может всё ещё попытаться его использовать перед отклонением вызова. |
334
335Чтобы ограничить, какие встроенные инструменты может использовать Claude, предпочитайте `tools` запрещённым инструментам. Пропуск инструмента из `tools` удаляет его из контекста, чтобы Claude никогда не пытался его использовать; перечисление его в `disallowedTools` (Python: `disallowed_tools`) блокирует вызов, но оставляет инструмент видимым, поэтому Claude может потратить ход, пытаясь его использовать. См. [Настройка разрешений](/ru/agent-sdk/permissions) для полного порядка оценки.
336
337## Обработка ошибок
338
339То, как ваш обработчик сообщает об ошибках, определяет, продолжается ли цикл агента или останавливается:
340
341| Что происходит | Результат |
342| :---------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
343| Обработчик выбрасывает неперехваченное исключение | Цикл агента останавливается. Claude никогда не видит ошибку, и вызов `query` завершается ошибкой. |
344| Обработчик перехватывает ошибку и возвращает `isError: true` (TS) / `"is_error": True` (Python) | Цикл агента продолжается. Claude видит ошибку как данные и может повторить попытку, попробовать другой инструмент или объяснить сбой. |
345
346Пример ниже перехватывает два вида сбоев внутри обработчика вместо того, чтобы позволить им выбросить исключение. Статус HTTP, отличный от 200, перехватывается из ответа и возвращается как результат ошибки. Ошибка сети или неверный JSON перехватываются окружающим `try/except` (Python) или `try/catch` (TypeScript) и также возвращаются как результат ошибки. В обоих случаях обработчик возвращается нормально и цикл агента продолжается.
347
348<CodeGroup>
349 ```python Python theme={null}
350 import json
351 import httpx
352 from typing import Any
353
354
355 @tool(
356 "fetch_data",
357 "Fetch data from an API",
358 {"endpoint": str}, # Simple schema
359 )
360 async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
361 try:
362 async with httpx.AsyncClient() as client:
363 response = await client.get(args["endpoint"])
364 if response.status_code != 200:
365 # Return the failure as a tool result so Claude can react to it.
366 # is_error marks this as a failed call rather than odd-looking data.
367 return {
368 "content": [
369 {
370 "type": "text",
371 "text": f"API error: {response.status_code} {response.reason_phrase}",
372 }
373 ],
374 "is_error": True,
375 }
376
377 data = response.json()
378 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
379 except Exception as e:
380 # Catching here keeps the agent loop alive. An uncaught exception
381 # would end the whole query() call.
382 return {
383 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
384 "is_error": True,
385 }
386 ```
387
388 ```typescript TypeScript theme={null}
389 tool(
390 "fetch_data",
391 "Fetch data from an API",
392 {
393 endpoint: z.string().url().describe("API endpoint URL")
394 },
395 async (args) => {
396 try {
397 const response = await fetch(args.endpoint);
398
399 if (!response.ok) {
400 // Return the failure as a tool result so Claude can react to it.
401 // isError marks this as a failed call rather than odd-looking data.
402 return {
403 content: [
404 {
405 type: "text",
406 text: `API error: ${response.status} ${response.statusText}`
407 }
408 ],
409 isError: true
410 };
411 }
412
413 const data = await response.json();
414 return {
415 content: [
416 {
417 type: "text",
418 text: JSON.stringify(data, null, 2)
419 }
420 ]
421 };
422 } catch (error) {
423 // Catching here keeps the agent loop alive. An uncaught throw
424 // would end the whole query() call.
425 return {
426 content: [
427 {
428 type: "text",
429 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
430 }
431 ],
432 isError: true
433 };
434 }
435 }
436 );
437 ```
438</CodeGroup>
439
440## Возврат изображений и ресурсов
441
442Массив `content` в результате инструмента принимает блоки `text`, `image` и `resource`. Вы можете смешивать их в одном ответе.
443
444### Изображения
445
446Блок изображения содержит байты изображения встроенным образом, закодированные как base64. Нет поля URL. Чтобы вернуть изображение, которое находится по URL, получите его в обработчике, прочитайте байты ответа и закодируйте их в base64 перед возвратом. Результат обрабатывается как визуальный ввод.
447
448| Поле | Тип | Примечания |
449| :--------- | :-------- | :----------------------------------------------------------------------------------- |
450| `type` | `"image"` | |
451| `data` | `string` | Байты в кодировке Base64. Только сырой base64, без префикса `data:image/...;base64,` |
452| `mimeType` | `string` | Обязательно. Например `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
453
454<CodeGroup>
455 ```python Python theme={null}
456 import base64
457 import httpx
458
459
460 # Define a tool that fetches an image from a URL and returns it to Claude
461 @tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
462 async def fetch_image(args):
463 async with httpx.AsyncClient() as client: # Fetch the image bytes
464 response = await client.get(args["url"])
465
466 return {
467 "content": [
468 {
469 "type": "image",
470 "data": base64.b64encode(response.content).decode(
471 "ascii"
472 ), # Base64-encode the raw bytes
473 "mimeType": response.headers.get(
474 "content-type", "image/png"
475 ), # Read MIME type from the response
476 }
477 ]
478 }
479 ```
480
481 ```typescript TypeScript theme={null}
482 tool(
483 "fetch_image",
484 "Fetch an image from a URL and return it to Claude",
485 {
486 url: z.string().url()
487 },
488 async (args) => {
489 const response = await fetch(args.url); // Fetch the image bytes
490 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
491 const mimeType = response.headers.get("content-type") ?? "image/png";
492
493 return {
494 content: [
495 {
496 type: "image",
497 data: buffer.toString("base64"), // Base64-encode the raw bytes
498 mimeType
499 }
500 ]
501 };
502 }
503 );
504 ```
505</CodeGroup>
506
507### Ресурсы
508
509Блок ресурса встраивает часть содержимого, идентифицируемую по URI. URI — это метка для Claude, чтобы ссылаться на неё; фактическое содержимое находится в поле `text` или `blob` блока. Используйте это, когда ваш инструмент производит что-то, что имеет смысл адресовать по имени позже, например сгенерированный файл или запись из внешней системы.
510
511| Поле | Тип | Примечания |
512| :------------------ | :----------- | :----------------------------------------------------------------- |
513| `type` | `"resource"` | |
514| `resource.uri` | `string` | Идентификатор содержимого. Любая схема URI |
515| `resource.text` | `string` | Содержимое, если это текст. Предоставьте это или `blob`, но не оба |
516| `resource.blob` | `string` | Содержимое в кодировке base64, если это двоичное |
517| `resource.mimeType` | `string` | Необязательно |
518
519Этот пример показывает блок ресурса, возвращаемый из обработчика инструмента. URI `file:///tmp/report.md` — это метка, на которую Claude может ссылаться позже; SDK не читает из этого пути.
520
521<CodeGroup>
522 ```typescript TypeScript theme={null}
523 return {
524 content: [
525 {
526 type: "resource",
527 resource: {
528 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
529 mimeType: "text/markdown",
530 text: "# Report\n..." // The actual content, inline
531 }
532 }
533 ]
534 };
535 ```
536
537 ```python Python theme={null}
538 return {
539 "content": [
540 {
541 "type": "resource",
542 "resource": {
543 "uri": "file:///tmp/report.md", # Label for Claude to reference, not a path the SDK reads
544 "mimeType": "text/markdown",
545 "text": "# Report\n...", # The actual content, inline
546 },
547 }
548 ]
549 }
550 ```
551</CodeGroup>
552
553Эти формы блоков происходят из типа MCP `CallToolResult`. См. [спецификацию MCP](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result) для полного определения.
554
555## Возврат структурированных данных
556
557`structuredContent` — это необязательный объект JSON на результате, отдельный от массива `content`. Используйте его для возврата сырых значений, которые Claude может читать как точные поля вместо их анализа из текстовой строки или изображения.
558
559Когда установлен `structuredContent`, Claude получает JSON плюс любые блоки изображения или ресурса из `content`. Текстовые блоки в `content` не пересылаются, так как предполагается, что они дублируют структурированные данные. Пример ниже отображает диаграмму как блок изображения и возвращает точки данных позади неё в `structuredContent` из того же обработчика.
560
561```typescript TypeScript theme={null}
562return {
563 content: [
564 {
565 type: "image",
566 data: chartPngBuffer.toString("base64"),
567 mimeType: "image/png"
568 }
569 ],
570 structuredContent: {
571 series: "temperature_2m",
572 unit: "fahrenheit",
573 points: [62.1, 63.4, 65.0, 64.2]
574 }
575};
576```
577
578<Note>
579 Декоратор Python `@tool` пересылает только `content` и `is_error` из словаря возврата обработчика. Чтобы вернуть `structuredContent` из Python, запустите [автономный MCP-сервер](/ru/agent-sdk/mcp) вместо встроенного сервера SDK.
580</Note>
581
582## Пример: конвертер единиц
583
584Этот инструмент преобразует значения между единицами длины, температуры и веса. Пользователь может спросить "преобразовать 100 километров в мили" или "что такое 72°F в Цельсиях", и Claude выбирает правильный тип единицы и единицы из запроса.
585
586Он демонстрирует два паттерна:
587
588* **Схемы перечисления:** `unit_type` ограничен фиксированным набором значений. В TypeScript используйте `z.enum()`. В Python словарь схемы не поддерживает перечисления, поэтому требуется полный словарь JSON Schema.
589* **Обработка неподдерживаемого ввода:** когда пара преобразования не найдена, обработчик возвращает `isError: true`, чтобы Claude мог сказать пользователю, что пошло не так, вместо того чтобы рассматривать сбой как нормальный результат.
590
591<CodeGroup>
592 ```python Python theme={null}
593 from typing import Any
594 from claude_agent_sdk import tool, create_sdk_mcp_server
595
596
597 # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
598 # The dict schema has no equivalent, so full JSON Schema is required.
599 @tool(
600 "convert_units",
601 "Convert a value from one unit to another",
602 {
603 "type": "object",
604 "properties": {
605 "unit_type": {
606 "type": "string",
607 "enum": ["length", "temperature", "weight"],
608 "description": "Category of unit",
609 },
610 "from_unit": {
611 "type": "string",
612 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
613 },
614 "to_unit": {"type": "string", "description": "Unit to convert to"},
615 "value": {"type": "number", "description": "Value to convert"},
616 },
617 "required": ["unit_type", "from_unit", "to_unit", "value"],
618 },
619 )
620 async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
621 conversions = {
622 "length": {
623 "kilometers_to_miles": lambda v: v * 0.621371,
624 "miles_to_kilometers": lambda v: v * 1.60934,
625 "meters_to_feet": lambda v: v * 3.28084,
626 "feet_to_meters": lambda v: v * 0.3048,
627 },
628 "temperature": {
629 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
630 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
631 "celsius_to_kelvin": lambda v: v + 273.15,
632 "kelvin_to_celsius": lambda v: v - 273.15,
633 },
634 "weight": {
635 "kilograms_to_pounds": lambda v: v * 2.20462,
636 "pounds_to_kilograms": lambda v: v * 0.453592,
637 "grams_to_ounces": lambda v: v * 0.035274,
638 "ounces_to_grams": lambda v: v * 28.3495,
639 },
640 }
641
642 key = f"{args['from_unit']}_to_{args['to_unit']}"
643 fn = conversions.get(args["unit_type"], {}).get(key)
644
645 if not fn:
646 return {
647 "content": [
648 {
649 "type": "text",
650 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
651 }
652 ],
653 "is_error": True,
654 }
655
656 result = fn(args["value"])
657 return {
658 "content": [
659 {
660 "type": "text",
661 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
662 }
663 ]
664 }
665
666
667 converter_server = create_sdk_mcp_server(
668 name="converter",
669 version="1.0.0",
670 tools=[convert_units],
671 )
672 ```
673
674 ```typescript TypeScript theme={null}
675 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
676 import { z } from "zod";
677
678 const convert = tool(
679 "convert_units",
680 "Convert a value from one unit to another",
681 {
682 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
683 from_unit: z
684 .string()
685 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
686 to_unit: z.string().describe("Unit to convert to"),
687 value: z.number().describe("Value to convert")
688 },
689 async (args) => {
690 type Conversions = Record<string, Record<string, (v: number) => number>>;
691
692 const conversions: Conversions = {
693 length: {
694 kilometers_to_miles: (v) => v * 0.621371,
695 miles_to_kilometers: (v) => v * 1.60934,
696 meters_to_feet: (v) => v * 3.28084,
697 feet_to_meters: (v) => v * 0.3048
698 },
699 temperature: {
700 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
701 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
702 celsius_to_kelvin: (v) => v + 273.15,
703 kelvin_to_celsius: (v) => v - 273.15
704 },
705 weight: {
706 kilograms_to_pounds: (v) => v * 2.20462,
707 pounds_to_kilograms: (v) => v * 0.453592,
708 grams_to_ounces: (v) => v * 0.035274,
709 ounces_to_grams: (v) => v * 28.3495
710 }
711 };
712
713 const key = `${args.from_unit}_to_${args.to_unit}`;
714 const fn = conversions[args.unit_type]?.[key];
715
716 if (!fn) {
717 return {
718 content: [
719 {
720 type: "text",
721 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
722 }
723 ],
724 isError: true
725 };
726 }
727
728 const result = fn(args.value);
729 return {
730 content: [
731 {
732 type: "text",
733 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
734 }
735 ]
736 };
737 }
738 );
739
740 const converterServer = createSdkMcpServer({
741 name: "converter",
742 version: "1.0.0",
743 tools: [convert]
744 });
745 ```
746</CodeGroup>
747
748После определения сервера передайте его в `query` так же, как в примере с погодой. Этот пример отправляет три разных запроса в цикле, чтобы показать, как один и тот же инструмент обрабатывает разные типы единиц. Для каждого ответа он проверяет объекты `AssistantMessage` (которые содержат вызовы инструментов, которые Claude сделал на этом ходу) и выводит каждый `ToolUseBlock` перед выводом финального текста `ResultMessage`. Это позволяет вам увидеть, когда Claude использует инструмент, а когда отвечает из своих собственных знаний.
749
750<CodeGroup>
751 ```python Python theme={null}
752 import asyncio
753 from claude_agent_sdk import (
754 query,
755 ClaudeAgentOptions,
756 ResultMessage,
757 AssistantMessage,
758 ToolUseBlock,
759 )
760
761
762 async def main():
763 options = ClaudeAgentOptions(
764 mcp_servers={"converter": converter_server},
765 allowed_tools=["mcp__converter__convert_units"],
766 )
767
768 prompts = [
769 "Convert 100 kilometers to miles.",
770 "What is 72°F in Celsius?",
771 "How many pounds is 5 kilograms?",
772 ]
773
774 for prompt in prompts:
775 async for message in query(prompt=prompt, options=options):
776 if isinstance(message, AssistantMessage):
777 for block in message.content:
778 if isinstance(block, ToolUseBlock):
779 print(f"[tool call] {block.name}({block.input})")
780 elif isinstance(message, ResultMessage) and message.subtype == "success":
781 print(f"Q: {prompt}\nA: {message.result}\n")
782
783
784 asyncio.run(main())
785 ```
786
787 ```typescript TypeScript theme={null}
788 import { query } from "@anthropic-ai/claude-agent-sdk";
789
790 const prompts = [
791 "Convert 100 kilometers to miles.",
792 "What is 72°F in Celsius?",
793 "How many pounds is 5 kilograms?"
794 ];
795
796 for (const prompt of prompts) {
797 for await (const message of query({
798 prompt,
799 options: {
800 mcpServers: { converter: converterServer },
801 allowedTools: ["mcp__converter__convert_units"]
802 }
803 })) {
804 if (message.type === "assistant") {
805 for (const block of message.message.content) {
806 if (block.type === "tool_use") {
807 console.log(`[tool call] ${block.name}`, block.input);
808 }
809 }
810 } else if (message.type === "result" && message.subtype === "success") {
811 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
812 }
813 }
814 }
815 ```
816</CodeGroup>
817
818## Следующие шаги
819
820Пользовательские инструменты оборачивают асинхронные функции в стандартный интерфейс. Вы можете смешивать паттерны на этой странице на одном сервере: один сервер может содержать инструмент базы данных, инструмент шлюза API и средство визуализации изображений рядом друг с другом.
821
822Отсюда:
823
824* Если ваш сервер растёт до десятков инструментов, см. [поиск инструментов](/ru/agent-sdk/tool-search) для отложенной загрузки их до того, как Claude их потребует.
825* Чтобы подключиться к внешним MCP-серверам (файловая система, GitHub, Slack) вместо создания собственного, см. [Подключение MCP-серверов](/ru/agent-sdk/mcp).
826* Чтобы контролировать, какие инструменты работают автоматически, а какие требуют одобрения, см. [Настройка разрешений](/ru/agent-sdk/permissions).
827
828## Связанная документация
829
830* [Справка TypeScript SDK](/ru/agent-sdk/typescript)
831* [Справка Python SDK](/ru/agent-sdk/python)
832* [Документация MCP](https://modelcontextprotocol.io)
833* [Обзор SDK](/ru/agent-sdk/overview)