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> Claude Agent SDK의 인프로세스 MCP 서버로 사용자 정의 도구를 정의하여 Claude가 함수를 호출하고, API를 사용하며, 도메인별 작업을 수행할 수 있도록 합니다.
8
9사용자 정의 도구는 Claude가 대화 중에 호출할 수 있는 자신의 함수를 정의하도록 하여 Agent SDK를 확장합니다. SDK의 인프로세스 MCP 서버를 사용하면 Claude에 데이터베이스, 외부 API, 도메인별 로직 또는 애플리케이션에 필요한 다른 기능에 대한 액세스 권한을 부여할 수 있습니다.
10
11이 가이드에서는 입력 스키마 및 핸들러를 사용하여 도구를 정의하고, 이를 MCP 서버로 번들링하고, `query`에 전달하며, Claude가 액세스할 수 있는 도구를 제어하는 방법을 다룹니다. 또한 오류 처리, 도구 주석, 이미지와 같은 비텍스트 콘텐츠 반환에 대해서도 다룹니다.
12
13## 빠른 참조
14
15| 원하는 작업 | 수행 방법 |
16| :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17| 도구 정의 | 이름, 설명, 스키마 및 핸들러를 사용하여 [`@tool`](/ko/agent-sdk/python#tool) (Python) 또는 [`tool()`](/ko/agent-sdk/typescript#tool) (TypeScript)을 사용합니다. [사용자 정의 도구 만들기](#create-a-custom-tool)를 참조하세요. |
18| Claude에 도구 등록 | `create_sdk_mcp_server` / `createSdkMcpServer`로 래핑하고 `query()`의 `mcpServers`에 전달합니다. [사용자 정의 도구 호출](#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| 많은 도구로 확장 | [도구 검색](/ko/agent-sdk/tool-search)을 사용하여 필요에 따라 도구를 로드합니다. |
26
27## 사용자 정의 도구 만들기
28
29도구는 TypeScript의 [`tool()`](/ko/agent-sdk/typescript#tool) 헬퍼 또는 Python의 [`@tool`](/ko/agent-sdk/python#tool) 데코레이터에 인수로 전달되는 네 부분으로 정의됩니다:
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"`의 `type`을 가진 결과 블록의 배열입니다. 비텍스트 블록은 [이미지 및 리소스 반환](#return-images-and-resources)을 참조하세요.
36 * `structuredContent` (선택사항): 머신 판독 가능한 데이터로 결과를 보유하는 JSON 객체이며, `content`와 함께 반환됩니다. [구조화된 데이터 반환](#return-structured-data)을 참조하세요.
37 * `isError` (선택사항): Claude가 반응할 수 있도록 도구 실패를 신호하려면 `true`로 설정합니다. [오류 처리](#handle-errors)를 참조하세요.
38
39도구를 정의한 후 [`createSdkMcpServer`](/ko/agent-sdk/typescript#createsdkmcpserver) (TypeScript) 또는 [`create_sdk_mcp_server`](/ko/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 # 도구 정의: 이름, 설명, 입력 스키마, 핸들러
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 # 콘텐츠 배열 반환 - Claude는 이를 도구 결과로 봅니다
72 return {
73 "content": [
74 {
75 "type": "text",
76 "text": f"Temperature: {data['current']['temperature_2m']}°F",
77 }
78 ]
79 }
80
81
82 # 도구를 인프로세스 MCP 서버로 래핑합니다
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 // 도구 정의: 이름, 설명, 입력 스키마, 핸들러
95 const getTemperature = tool(
96 "get_temperature",
97 "Get the current temperature at a location",
98 {
99 latitude: z.number().describe("Latitude coordinate"), // .describe()는 Claude가 보는 필드 설명을 추가합니다
100 longitude: z.number().describe("Longitude coordinate")
101 },
102 async (args) => {
103 // args는 스키마에서 입력됩니다: { 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 // 콘텐츠 배열 반환 - Claude는 이를 도구 결과로 봅니다
110 return {
111 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
112 };
113 }
114 );
115
116 // 도구를 인프로세스 MCP 서버로 래핑합니다
117 const weatherServer = createSdkMcpServer({
118 name: "weather",
119 version: "1.0.0",
120 tools: [getTemperature]
121 });
122 ```
123</CodeGroup>
124
125전체 매개변수 세부 정보, JSON Schema 입력 형식 및 반환 값 구조를 포함하여 [`tool()`](/ko/agent-sdk/typescript#tool) TypeScript 참조 또는 [`@tool`](/ko/agent-sdk/python#tool) Python 참조를 참조하세요.
126
127<Tip>
128 매개변수를 선택사항으로 만들려면: TypeScript에서 Zod 필드에 `.default()`를 추가합니다. Python에서 딕셔너리 스키마는 모든 키를 필수로 취급하므로 스키마에서 매개변수를 생략하고, 설명 문자열에서 언급하고, 핸들러에서 `args.get()`으로 읽습니다. 아래의 [`get_precipitation_chance` 도구](#add-more-tools)는 두 패턴을 모두 보여줍니다.
129</Tip>
130
131### 사용자 정의 도구 호출
132
133`mcpServers` 옵션을 통해 생성한 MCP 서버를 `query`에 전달합니다. `mcpServers`의 키는 각 도구의 정규화된 이름에서 `{server_name}` 세그먼트가 됩니다: `mcp__{server_name}__{tool_name}`. 도구가 권한 프롬프트 없이 실행되도록 `allowedTools`에 해당 이름을 나열합니다.
134
135이 스니펫은 [위의 예제](#weather-tool-example)의 `weatherServer`를 재사용하여 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는 모든 도구 호출이 완료된 후의 최종 메시지입니다
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"는 모든 도구 호출이 완료된 후의 최종 메시지입니다
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아래 예제는 [날씨 도구 예제](#weather-tool-example)의 `weatherServer`에 두 번째 도구인 `get_precipitation_chance`를 추가하고 배열의 두 도구로 다시 빌드합니다.
184
185<CodeGroup>
186 ```python Python theme={null}
187 # 동일한 서버에 대한 두 번째 도구 정의
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'는 스키마에 없습니다 - .get()으로 읽어서 선택사항으로 만듭니다
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 # 배열의 두 도구로 서버를 다시 빌드합니다
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 // 동일한 서버에 대한 두 번째 도구 정의
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()는 매개변수를 선택사항으로 만듭니다
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 // 배열의 두 도구로 서버를 다시 빌드합니다
258 const weatherServer = createSdkMcpServer({
259 name: "weather",
260 version: "1.0.0",
261 tools: [getTemperature, getPrecipitationChance]
262 });
263 ```
264</CodeGroup>
265
266이 배열의 모든 도구는 매 턴마다 컨텍스트 윈도우 공간을 소비합니다. 수십 개의 도구를 정의하는 경우 [도구 검색](/ko/agent-sdk/tool-search)을 참조하여 필요할 때 로드합니다.
267
268### 도구 주석 추가
269
270[도구 주석](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations)은 도구의 동작을 설명하는 선택적 메타데이터입니다. TypeScript의 `tool()` 헬퍼에 다섯 번째 인수로 전달하거나 Python의 `@tool` 데코레이터에 대해 `annotations` 키워드 인수를 통해 전달합니다. 모든 힌트 필드는 부울입니다.
271
272| 필드 | 기본값 | 의미 |
273| :---------------- | :------ | :----------------------------------------------------------- |
274| `readOnlyHint` | `false` | 도구는 환경을 수정하지 않습니다. 도구를 다른 읽기 전용 도구와 병렬로 호출할 수 있는지 여부를 제어합니다. |
275| `destructiveHint` | `true` | 도구는 파괴적인 업데이트를 수행할 수 있습니다. 정보 제공용입니다. |
276| `idempotentHint` | `false` | 동일한 인수로 반복 호출해도 추가 효과가 없습니다. 정보 제공용입니다. |
277| `openWorldHint` | `true` | 도구는 프로세스 외부의 시스템에 도달합니다. 정보 제공용입니다. |
278
279주석은 메타데이터이지 강제 사항이 아닙니다. `readOnlyHint: true`로 표시된 도구도 핸들러가 수행하는 경우 디스크에 쓸 수 있습니다. 주석을 핸들러에 정확하게 유지합니다.
280
281이 예제는 [날씨 도구 예제](#weather-tool-example)의 `get_temperature` 도구에 `readOnlyHint`를 추가합니다.
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 ), # Claude가 이를 다른 읽기 전용 호출과 일괄 처리하도록 합니다
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 } } // Claude가 이를 다른 읽기 전용 호출과 일괄 처리하도록 합니다
307 );
308 ```
309</CodeGroup>
310
311[TypeScript](/ko/agent-sdk/typescript#toolannotations) 또는 [Python](/ko/agent-sdk/python#toolannotations) 참조에서 `ToolAnnotations`를 참조하세요.
312
313## 도구 액세스 제어
314
315[날씨 도구 예제](#weather-tool-example)는 서버를 등록하고 `allowedTools`에 도구를 나열했습니다. 이 섹션에서는 도구 이름이 구성되는 방식과 여러 도구가 있거나 기본 제공 도구를 제한하려는 경우 액세스 범위를 지정하는 방법을 다룹니다.
316
317### 도구 이름 형식
318
319MCP 도구가 Claude에 노출될 때 이름은 특정 형식을 따릅니다:
320
321* 패턴: `mcp__{server_name}__{tool_name}`
322* 예제: `weather` 서버의 `get_temperature`라는 도구는 `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| 허용된 도구 | 권한 | 나열된 도구는 권한 프롬프트 없이 실행됩니다. 나열되지 않은 도구는 계속 사용 가능합니다. 호출은 [권한 흐름](/ko/agent-sdk/permissions)을 거칩니다. |
333| 거부된 도구 | 권한 | 나열된 도구에 대한 모든 호출이 거부됩니다. 도구는 Claude의 컨텍스트에 남아 있으므로 Claude는 호출이 거부되기 전에 시도할 수 있습니다. |
334
335Claude가 사용할 수 있는 기본 제공 도구를 제한하려면 거부된 도구보다 `tools`를 선호합니다. `tools`에서 도구를 생략하면 컨텍스트에서 제거되므로 Claude는 시도하지 않습니다. `disallowedTools`에 나열하면 호출을 차단하지만 도구를 표시하므로 Claude는 시도하는 데 턴을 낭비할 수 있습니다. 전체 평가 순서는 [권한 구성](/ko/agent-sdk/permissions)을 참조하세요.
336
337## 오류 처리
338
339핸들러가 오류를 보고하는 방식에 따라 에이전트 루프가 계속되는지 중지되는지가 결정됩니다:
340
341| 발생하는 상황 | 결과 |
342| :---------------------------------------------------------------------- | :------------------------------------------------------------------------ |
343| 핸들러가 포착되지 않은 예외를 발생시킵니다 | 에이전트 루프가 중지됩니다. Claude는 오류를 보지 못하고 `query` 호출이 실패합니다. |
344| 핸들러가 오류를 포착하고 `isError: true` (TS) / `"is_error": True` (Python)를 반환합니다 | 에이전트 루프가 계속됩니다. Claude는 오류를 데이터로 보고 재시도하거나, 다른 도구를 시도하거나, 실패를 설명할 수 있습니다. |
345
346아래 예제는 핸들러 내에서 두 가지 종류의 실패를 포착합니다. 0이 아닌 HTTP 상태는 응답에서 포착되어 오류 결과로 반환됩니다. 네트워크 오류 또는 잘못된 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}, # 간단한 스키마
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 # Claude가 반응할 수 있도록 실패를 도구 결과로 반환합니다.
366 # is_error는 이를 실패한 호출로 표시하지 않으면 이상한 데이터로 표시합니다.
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 # 여기서 포착하면 에이전트 루프가 살아있습니다. 포착되지 않은 예외는
381 # 전체 query() 호출을 종료합니다.
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 // Claude가 반응할 수 있도록 실패를 도구 결과로 반환합니다.
401 // isError는 이를 실패한 호출로 표시하지 않으면 이상한 데이터로 표시합니다.
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 // 여기서 포착하면 에이전트 루프가 살아있습니다. 포착되지 않은 throw는
424 // 전체 query() 호출을 종료합니다.
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로 인코딩된 바이트입니다. `data:image/...;base64,` 접두사 없이 원본 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 # URL에서 이미지를 가져와 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: # 이미지 바이트를 가져옵니다
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로 인코딩합니다
473 "mimeType": response.headers.get(
474 "content-type", "image/png"
475 ), # 응답에서 MIME 유형을 읽습니다
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); // 이미지 바이트를 가져옵니다
490 const buffer = Buffer.from(await response.arrayBuffer()); // base64 인코딩을 위해 버퍼로 읽습니다
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로 인코딩합니다
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", // Claude가 참조할 레이블이지 SDK가 읽는 경로가 아닙니다
529 mimeType: "text/markdown",
530 text: "# Report\n..." // 실제 콘텐츠, 인라인
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", # Claude가 참조할 레이블이지 SDK가 읽는 경로가 아닙니다
544 "mimeType": "text/markdown",
545 "text": "# Report\n...", # 실제 콘텐츠, 인라인
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`는 `content` 배열과 별개인 결과의 선택적 JSON 객체입니다. 이를 사용하여 텍스트 문자열이나 이미지에서 구문 분석하는 대신 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`만 전달합니다. Python에서 `structuredContent`를 반환하려면 인프로세스 SDK 서버 대신 [독립형 MCP 서버](/ko/agent-sdk/mcp)를 실행합니다.
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 # TypeScript의 z.enum()은 JSON Schema의 "enum" 제약이 됩니다.
598 # 딕셔너리 스키마는 동등한 것이 없으므로 전체 JSON Schema가 필요합니다.
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* 서버가 수십 개의 도구로 증가하면 [도구 검색](/ko/agent-sdk/tool-search)을 참조하여 Claude가 필요할 때까지 로드를 연기합니다.
825* 자신의 도구를 빌드하는 대신 외부 MCP 서버(파일 시스템, GitHub, Slack)에 연결하려면 [MCP 서버 연결](/ko/agent-sdk/mcp)을 참조하세요.
826* 어떤 도구가 자동으로 실행되는지 대 승인이 필요한지 제어하려면 [권한 구성](/ko/agent-sdk/permissions)을 참조하세요.
827
828## 관련 문서
829
830* [TypeScript SDK 참조](/ko/agent-sdk/typescript)
831* [Python SDK 참조](/ko/agent-sdk/python)
832* [MCP 문서](https://modelcontextprotocol.io)
833* [SDK 개요](/ko/agent-sdk/overview)