Bỏ qua để đến nội dung

Tạo tool tùy chỉnh cho Claude

Bài viết được dịch tự động từ bài viết gốc, chưa được kiểm tra lại bởi con người. Chỉ những bài viết có dấu tick xanh cạnh tiêu đề là đã được kiểm tra.

Tool tùy chỉnh mở rộng Agent SDK bằng cách cho phép bạn định nghĩa các hàm riêng mà Claude có thể gọi trong quá trình hội thoại. Sử dụng MCP server chạy trong cùng process (in-process) của SDK, bạn có thể cho Claude quyền truy cập vào cơ sở dữ liệu, API bên ngoài, logic nghiệp vụ đặc thù, hoặc bất kỳ khả năng nào khác mà ứng dụng của bạn cần.

Trang này hướng dẫn cách định nghĩa tool với input schema và handler, đóng gói chúng vào một MCP server, truyền vào query, và kiểm soát tool nào Claude được phép truy cập. Trang cũng đề cập đến xử lý lỗi, tool annotation, và cách trả về nội dung không phải văn bản như hình ảnh.

Nếu bạn muốn…Hãy làm thế này
Định nghĩa một toolDùng @tool (Python) hoặc tool() (TypeScript) với tên, mô tả, schema, và handler. Xem Tạo tool tùy chỉnh.
Đăng ký tool với ClaudeBọc trong create_sdk_mcp_server / createSdkMcpServer và truyền vào mcpServers trong query(). Xem Gọi tool tùy chỉnh.
Duyệt trước (pre-approve) một toolThêm vào danh sách tool được phép. Xem Cấu hình tool được phép.
Loại bỏ một built-in tool khỏi context của ClaudeTruyền một mảng tools chỉ liệt kê những built-in tool bạn muốn giữ lại. Xem Cấu hình tool được phép.
Cho Claude gọi tool song songĐặt readOnlyHint: true cho các tool không có side effect. Xem Thêm tool annotation.
Kiểm soát thông báo lỗi Claude đọc đượcTrả về isError: true để tự soạn thông báo thay vì để lộ exception gốc. Xem Xử lý lỗi.
Trả về hình ảnh hoặc fileDùng block image hoặc resource trong mảng content. Xem Trả về hình ảnh và resource.
Trả về kết quả dạng JSON máy đọc đượcĐặt structuredContent trong kết quả. Xem Trả về dữ liệu có cấu trúc.
Mở rộng lên nhiều toolDùng tool search để nạp tool theo yêu cầu.

Một tool được định nghĩa bởi bốn thành phần, truyền vào helper tool() trong TypeScript hoặc decorator @tool trong Python:

  • Tên (Name): định danh duy nhất mà Claude dùng để gọi tool.
  • Mô tả (Description): tool làm gì. Claude đọc phần này để quyết định khi nào nên gọi tool.
  • Input schema: các tham số Claude phải cung cấp. Trong TypeScript, đây luôn là một Zod schema, và args của handler được suy ra kiểu tự động từ schema đó. Trong Python, đây là một dict ánh xạ tên tham số sang kiểu dữ liệu, ví dụ {"latitude": float}, mà SDK sẽ chuyển thành JSON Schema giúp bạn. Decorator Python cũng chấp nhận trực tiếp một dict JSON Schema đầy đủ khi bạn cần enum, khoảng giá trị, trường tùy chọn, hoặc object lồng nhau.
  • Handler: hàm bất đồng bộ chạy khi Claude gọi tool. Handler nhận các tham số đã được xác thực và phải trả về một object gồm:
    • content (bắt buộc): mảng các block kết quả, mỗi block có type"text", "image", "audio", "resource", hoặc "resource_link". Xem Trả về hình ảnh và resource cho các block không phải văn bản.
    • structuredContent (tùy chọn): một object JSON chứa kết quả dưới dạng dữ liệu máy đọc được, trả về kèm theo content. Xem Trả về dữ liệu có cấu trúc.
    • isError (tùy chọn): đặt true để báo hiệu tool thất bại, giúp Claude phản ứng phù hợp. Xem Xử lý lỗi.

Sau khi định nghĩa tool, bọc nó trong một server bằng createSdkMcpServer (TypeScript) hoặc create_sdk_mcp_server (Python). Server này chạy trong cùng process với ứng dụng của bạn, không phải là một process riêng biệt.

Ví dụ này định nghĩa tool get_temperature và bọc nó vào một MCP server. Nó chỉ thiết lập tool; để truyền vào query và chạy, xem Gọi tool tùy chỉnh bên dưới.

from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server
# Định nghĩa một tool: tên, mô tả, input schema, handler
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
)
async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"current": "temperature_2m",
"temperature_unit": "fahrenheit",
},
)
data = response.json()
# Trả về một content array - Claude sẽ thấy đây là kết quả của tool
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}
# Bọc tool trong một MCP server chạy cùng process
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature],
)
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
// Định nghĩa một tool: tên, mô tả, input schema, handler
const getTemperature = tool(
"get_temperature",
"Get the current temperature at a location",
{
latitude: z.number().describe("Latitude coordinate"), // .describe() thêm mô tả trường mà Claude sẽ thấy
longitude: z.number().describe("Longitude coordinate")
},
async (args) => {
// args được suy ra kiểu từ schema: { latitude: number; longitude: number }
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m&temperature_unit=fahrenheit`
);
const data: any = await response.json();
// Trả về một content array - Claude sẽ thấy đây là kết quả của tool
return {
content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
};
}
);
// Bọc tool trong một MCP server chạy cùng process
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature]
});

Xem tham chiếu TypeScript tool() hoặc tham chiếu Python @tool để biết chi tiết đầy đủ về tham số, bao gồm các định dạng input JSON Schema và cấu trúc giá trị trả về.

Truyền MCP server bạn đã tạo vào query qua tùy chọn mcpServers. Key trong mcpServers sẽ trở thành đoạn {server_name} trong tên đầy đủ của mỗi tool: mcp__{server_name}__{tool_name}. Liệt kê tên này trong allowedTools để tool chạy mà không cần prompt xin quyền.

Các đoạn code dưới đây tái sử dụng weatherServer từ ví dụ trên để hỏi Claude thời tiết ở một địa điểm cụ thể.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
options = ClaudeAgentOptions(
mcp_servers={"weather": weather_server},
allowed_tools=["mcp__weather__get_temperature"],
)
async for message in query(
prompt="What's the temperature in San Francisco?",
options=options,
):
# ResultMessage là message cuối cùng sau khi mọi tool call hoàn tất
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "What's the temperature in San Francisco?",
options: {
mcpServers: { weather: weatherServer },
allowedTools: ["mcp__weather__get_temperature"]
}
})) {
// "result" là message cuối cùng sau khi mọi tool call hoàn tất
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}

Kết hợp đoạn code này với phần định nghĩa tool và server từ ví dụ tool thời tiết vào một file, rồi chạy bằng python weather.py (Python) hoặc npx tsx weather.ts (TypeScript). Claude sẽ gọi get_temperature và script in ra một dòng trả lời với nhiệt độ hiện tại tại San Francisco.

Một server chứa bao nhiêu tool tùy bạn liệt kê trong mảng tools. Với nhiều hơn một tool trên cùng server, bạn có thể liệt kê từng tool riêng trong allowedTools hoặc dùng wildcard mcp__weather__* để bao phủ mọi tool mà server đó cung cấp.

Ví dụ dưới đây định nghĩa thêm tool thứ hai, get_precipitation_chance, và thay thế định nghĩa weatherServer từ ví dụ tool thời tiết bằng một phiên bản liệt kê cả hai tool trong mảng.

# Định nghĩa tool thứ hai cho cùng server
@tool(
"get_precipitation_chance",
"Get the hourly precipitation probability for a location. "
"Optionally pass 'hours' (1-24) to control how many hours to return.",
{"latitude": float, "longitude": float},
)
async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
# 'hours' không có trong schema - đọc bằng .get() để nó trở thành tùy chọn
hours = args.get("hours", 12)
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"hourly": "precipitation_probability",
"forecast_days": 1,
},
)
data = response.json()
chances = data["hourly"]["precipitation_probability"][:hours]
return {
"content": [
{
"type": "text",
"text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
}
]
}
# Xây lại server với cả hai tool trong mảng
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature, get_precipitation_chance],
)
// Định nghĩa tool thứ hai cho cùng server
const getPrecipitationChance = tool(
"get_precipitation_chance",
"Get the hourly precipitation probability for a location",
{
latitude: z.number(),
longitude: z.number(),
hours: z
.number()
.int()
.min(1)
.max(24)
.default(12) // .default() làm cho tham số trở thành tùy chọn
.describe("How many hours of forecast to return")
},
async (args) => {
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
);
const data: any = await response.json();
const chances = data.hourly.precipitation_probability.slice(0, args.hours);
return {
content: [{ type: "text", text: `Next ${args.hours} hours: ${chances.join("%, ")}%` }]
};
}
);
// Xây lại server với cả hai tool trong mảng
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature, getPrecipitationChance]
});

Tool search được bật mặc định và trì hoãn nạp SDK MCP tool: Claude thấy tên của mỗi tool trong một danh sách gọn nhẹ và nạp đầy đủ schema khi cần. Khi tắt tool search, mỗi tool trong mảng này sẽ chiếm không gian context window ở mọi lượt hội thoại. Trong TypeScript, truyền alwaysLoad: true trong tham số extras của tool() hoặc trong options của createSdkMcpServer() để giữ schema đầy đủ của một tool ngay trong prompt ban đầu.

Tool annotation là metadata tùy chọn mô tả cách tool hoạt động. Truyền chúng dưới dạng tham số thứ năm cho helper tool() trong TypeScript, hoặc qua từ khóa annotations cho decorator @tool trong Python. Tất cả các trường hint đều là kiểu Boolean.

TrườngMặc địnhÝ nghĩa
readOnlyHintfalseTool không thay đổi môi trường. Kiểm soát xem tool có được phép gọi song song với các tool chỉ đọc khác không.
destructiveHinttrueTool có thể thực hiện các cập nhật mang tính phá hủy. Chỉ mang tính thông tin.
idempotentHintfalseGọi lặp lại cùng tham số không gây thêm hiệu ứng. Chỉ mang tính thông tin.
openWorldHinttrueTool tương tác với hệ thống bên ngoài process của bạn. Chỉ mang tính thông tin.

Annotation chỉ là metadata, không phải cơ chế thực thi. Một tool đánh dấu readOnlyHint: true vẫn có thể ghi vào đĩa nếu đó là điều handler thực sự làm. Hãy giữ annotation phản ánh đúng hành vi thực tế của handler.

Ví dụ dưới đây thêm readOnlyHint cho tool get_temperature từ ví dụ tool thời tiết.

from claude_agent_sdk import tool, ToolAnnotations
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
annotations=ToolAnnotations(
readOnlyHint=True
), # Cho phép Claude gộp lệnh này với các lệnh chỉ đọc khác
)
async def get_temperature(args):
return {"content": [{"type": "text", "text": "..."}]}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"get_temperature",
"Get the current temperature at a location",
{ latitude: z.number(), longitude: z.number() },
async (args) => ({ content: [{ type: "text", text: `...` }] }),
{ annotations: { readOnlyHint: true } } // Cho phép Claude gộp lệnh này với các lệnh chỉ đọc khác
);

Xem ToolAnnotations trong tham chiếu TypeScript hoặc Python.

Ví dụ tool thời tiết ở trên đã đăng ký một server và liệt kê tool trong allowedTools. Phần này giải thích cách tên tool được xây dựng và cách giới hạn quyền truy cập khi bạn có nhiều tool hoặc muốn hạn chế built-in tool.

Khi các MCP tool được đưa vào context của Claude, tên của chúng theo một định dạng cụ thể:

  • Mẫu: mcp__{server_name}__{tool_name}
  • Ví dụ: tool tên get_temperature trong server weather sẽ trở thành mcp__weather__get_temperature

Tùy chọn tools cùng với danh sách allowed/disallowed ảnh hưởng đến hai lớp: khả năng hiển thị (availability), quyết định tool có xuất hiện trong context của Claude hay không, và quyền hạn (permission), quyết định một lệnh gọi có được duyệt khi Claude thực hiện hay không. tools và các mục disallowedTools không kèm tham số phạm vi thay đổi khả năng hiển thị. allowedTools và các quy tắc disallowedTools có phạm vi chỉ thay đổi quyền hạn.

Tùy chọnLớpHiệu ứng
tools: ["Read", "Grep"]Khả năng hiển thịChỉ những built-in tool được liệt kê mới có trong context của Claude. Các built-in tool khác bị loại bỏ. MCP tool không bị ảnh hưởng.
tools: []Khả năng hiển thịMọi built-in tool bị loại bỏ. Claude chỉ có thể dùng MCP tool của bạn.
allowed toolsQuyền hạnTool được liệt kê chạy mà không cần prompt xin quyền. Tool chưa liệt kê vẫn hiển thị; lệnh gọi sẽ đi qua luồng cấp quyền.
disallowed toolsCả haiTên tool trần như "Bash" loại tool đó khỏi context của Claude, giống như khi bỏ nó khỏi tools. Quy tắc có phạm vi như "Bash(rm *)" giữ tool trong context nhưng chỉ từ chối những lệnh gọi khớp mẫu.

Để loại bỏ hoàn toàn một built-in tool, bỏ nó khỏi tools hoặc liệt kê tên trần trong disallowedTools (Python: disallowed_tools); cả hai cách đều giữ tool ngoài context nên Claude không bao giờ thử gọi nó. Một quy tắc disallowedTools có phạm vi sẽ chặn các lệnh gọi khớp mẫu nhưng vẫn để tool hiển thị, nên Claude có thể tốn một lượt thử gọi nó. Xem Cấu hình quyền hạn để biết thứ tự đánh giá đầy đủ.

Lỗi trong handler không làm dừng agent loop. MCP server chạy cùng process của SDK bắt các exception chưa được xử lý và trả về dưới dạng kết quả lỗi, nên cách bạn báo lỗi quyết định Claude đọc được gì, chứ không quyết định query có thất bại hay không:

Điều gì xảy raKết quả
Handler ném ra một exception chưa được xử lýMCP server chuyển nó thành kết quả lỗi mang theo thông báo exception gốc. Claude thấy thông báo đó, và agent loop tiếp tục.
Handler tự bắt lỗi và trả về isError: true (TS) / "is_error": True (Python)Claude thấy thông báo do bạn soạn. Bạn có thể thêm ngữ cảnh mà exception gốc thiếu, như request nào thất bại hoặc nên thử gì tiếp theo.

Trong cả hai trường hợp, Claude có thể thử lại, thử một tool khác, hoặc giải thích lỗi cho người dùng. Hãy tự bắt lỗi khi thông báo exception gốc không đủ để Claude hành động dựa trên đó.

Ví dụ dưới đây bắt hai loại lỗi trong handler và tự soạn thông báo lỗi mà Claude đọc được. Mã trạng thái HTTP khác 200 được bắt từ response và trả về dưới dạng kết quả lỗi. Lỗi mạng hoặc JSON không hợp lệ được bắt bởi try/except (Python) hoặc try/catch (TypeScript) bao quanh và cũng trả về kết quả lỗi. Trong cả hai trường hợp, Claude nhận được thông báo mô tả sự cố thay vì một chuỗi exception trần trụi.

import json
import httpx
from typing import Any
from claude_agent_sdk import tool
@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str}, # Schema đơn giản
)
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(args["endpoint"])
if response.status_code != 200:
# Trả lỗi dưới dạng tool result để Claude có thể phản ứng lại
# is_error đánh dấu đây là một lệnh gọi thất bại, chứ không phải dữ liệu bất thường
return {
"content": [
{
"type": "text",
"text": f"API error: {response.status_code} {response.reason_phrase}",
}
],
"is_error": True,
}
data = response.json()
return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
except Exception as e:
# Soạn thông báo Claude đọc được. Một exception chưa xử lý sẽ đến Claude
# dưới dạng str(e) trần trụi, không có ngữ cảnh
return {
"content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
"is_error": True,
}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"fetch_data",
"Fetch data from an API",
{
endpoint: z.string().url().describe("API endpoint URL")
},
async (args) => {
try {
const response = await fetch(args.endpoint);
if (!response.ok) {
// Trả lỗi dưới dạng tool result để Claude có thể phản ứng lại
// isError đánh dấu đây là một lệnh gọi thất bại, chứ không phải dữ liệu bất thường
return {
content: [
{
type: "text",
text: `API error: ${response.status} ${response.statusText}`
}
],
isError: true
};
}
const data = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2)
}
]
};
} catch (error) {
// Soạn thông báo Claude đọc được. Một throw chưa xử lý sẽ đến Claude
// dưới dạng thông báo lỗi trần trụi, không có ngữ cảnh
return {
content: [
{
type: "text",
text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);

Mảng content trong kết quả của tool chấp nhận các block text, image, audio, resource, và resource_link. Bạn có thể trộn nhiều loại trong cùng một response. Trong TypeScript, SDK lưu block audio xuống đĩa và Claude nhận được một block text chứa đường dẫn file đã lưu; trong Python, SDK loại bỏ block audio khỏi kết quả tool và ghi log cảnh báo. SDK chuyển các block resource link thành một block text chứa tên, URI, và mô tả của link.

Một block image mang theo bytes hình ảnh được mã hóa base64 ngay trong nội dung. Không có trường URL. Để trả về một hình ảnh nằm ở một URL, hãy fetch nó trong handler, đọc bytes của response, và mã hóa base64 trước khi trả về. Kết quả được xử lý như input hình ảnh.

TrườngKiểuGhi chú
type"image"
datastringBytes mã hóa base64. Chỉ base64 thuần, không có tiền tố data:image/...;base64,
mimeTypestringBắt buộc. Ví dụ image/png, image/jpeg, image/webp, image/gif
import base64
import httpx
from claude_agent_sdk import tool
# Định nghĩa một tool fetch hình ảnh từ URL và trả về cho Claude
@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
async def fetch_image(args):
async with httpx.AsyncClient() as client: # Fetch bytes hình ảnh
response = await client.get(args["url"])
return {
"content": [
{
"type": "image",
"data": base64.b64encode(response.content).decode(
"ascii"
), # Mã hóa base64 bytes thô
"mimeType": response.headers.get(
"content-type", "image/png"
), # Đọc MIME type từ response
}
]
}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"fetch_image",
"Fetch an image from a URL and return it to Claude",
{
url: z.string().url()
},
async (args) => {
const response = await fetch(args.url); // Fetch bytes hình ảnh
const buffer = Buffer.from(await response.arrayBuffer()); // Đọc vào Buffer để mã hóa base64
const mimeType = response.headers.get("content-type") ?? "image/png";
return {
content: [
{
type: "image",
data: buffer.toString("base64"), // Mã hóa base64 bytes thô
mimeType
}
]
};
}
);

Một block resource nhúng một mẩu nội dung được định danh bởi URI. URI là một nhãn để Claude tham chiếu; nội dung thực nằm trong trường text hoặc blob của block. Dùng khi tool của bạn tạo ra thứ gì đó nên được gọi bằng tên sau này, như một file đã sinh ra hoặc một bản ghi từ hệ thống bên ngoài.

TrườngKiểuGhi chú
type"resource"
resource.uristringĐịnh danh cho nội dung. Bất kỳ scheme URI nào
resource.textstringNội dung, nếu là văn bản. Cung cấp trường này hoặc blob, không cả hai
resource.blobstringNội dung mã hóa base64, nếu là nhị phân. Chỉ TypeScript: Python SDK loại bỏ resource nhị phân khỏi kết quả tool và ghi log cảnh báo
resource.mimeTypestringTùy chọn

Ví dụ dưới đây minh họa một block resource được trả về từ trong handler của tool. URI file:///tmp/report.md là một nhãn Claude có thể tham chiếu sau này; SDK không đọc từ đường dẫn đó.

return {
content: [
{
type: "resource",
resource: {
uri: "file:///tmp/report.md", // Nhãn để Claude tham chiếu, không phải đường dẫn SDK đọc
mimeType: "text/markdown",
text: "# Report\n..." // Nội dung thực, đặt trực tiếp
}
}
]
};
return {
"content": [
{
"type": "resource",
"resource": {
"uri": "file:///tmp/report.md", # Nhãn để Claude tham chiếu, không phải đường dẫn SDK đọc
"mimeType": "text/markdown",
"text": "# Report\n...", # Nội dung thực, đặt trực tiếp
},
}
]
}

Các dạng block này bắt nguồn từ kiểu CallToolResult của MCP. Xem đặc tả MCP để biết định nghĩa đầy đủ.

structuredContent là một object JSON tùy chọn trong kết quả, tách biệt với mảng content. Dùng để trả về các giá trị thô mà Claude có thể đọc như các trường chính xác thay vì phải phân tích chúng từ một chuỗi văn bản hay hình ảnh.

Khi structuredContent được đặt, Claude nhận được JSON đó cộng với bất kỳ block image hoặc resource nào trong content. Block text trong content không được chuyển tiếp, vì được giả định là trùng lặp với dữ liệu có cấu trúc. Ví dụ dưới đây render một biểu đồ dưới dạng block image và trả về các điểm dữ liệu đằng sau nó trong structuredContent từ cùng một handler. Trong đoạn code, chartPngBuffer là một Buffer chứa bytes PNG đã render.

return {
content: [
{
type: "image",
data: chartPngBuffer.toString("base64"),
mimeType: "image/png"
}
],
structuredContent: {
series: "temperature_2m",
unit: "fahrenheit",
points: [62.1, 63.4, 65.0, 64.2]
}
};

Tool này chuyển đổi giá trị giữa các đơn vị đo chiều dài, nhiệt độ, và trọng lượng. Người dùng có thể hỏi “convert 100 kilometers to miles” hoặc “what is 72°F in Celsius,” và Claude sẽ chọn đúng loại đơn vị và đơn vị cụ thể từ yêu cầu.

Nó minh họa hai mẫu:

  • Enum schema: unit_type bị giới hạn trong một tập giá trị cố định. Trong TypeScript, dùng z.enum(). Trong Python, dict schema không hỗ trợ enum, nên cần dùng dict JSON Schema đầy đủ.
  • Xử lý input không được hỗ trợ: khi không tìm thấy cặp chuyển đổi, handler trả về isError: true để Claude có thể báo cho người dùng biết điều gì đã sai, thay vì coi lỗi như một kết quả bình thường.
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server
# z.enum() trong TypeScript trở thành ràng buộc "enum" trong JSON Schema.
# Dict schema không có tương đương, nên cần JSON Schema đầy đủ.
@tool(
"convert_units",
"Convert a value from one unit to another",
{
"type": "object",
"properties": {
"unit_type": {
"type": "string",
"enum": ["length", "temperature", "weight"],
"description": "Category of unit",
},
"from_unit": {
"type": "string",
"description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
},
"to_unit": {"type": "string", "description": "Unit to convert to"},
"value": {"type": "number", "description": "Value to convert"},
},
"required": ["unit_type", "from_unit", "to_unit", "value"],
},
)
async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
conversions = {
"length": {
"kilometers_to_miles": lambda v: v * 0.621371,
"miles_to_kilometers": lambda v: v * 1.60934,
"meters_to_feet": lambda v: v * 3.28084,
"feet_to_meters": lambda v: v * 0.3048,
},
"temperature": {
"celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
"fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
"celsius_to_kelvin": lambda v: v + 273.15,
"kelvin_to_celsius": lambda v: v - 273.15,
},
"weight": {
"kilograms_to_pounds": lambda v: v * 2.20462,
"pounds_to_kilograms": lambda v: v * 0.453592,
"grams_to_ounces": lambda v: v * 0.035274,
"ounces_to_grams": lambda v: v * 28.3495,
},
}
key = f"{args['from_unit']}_to_{args['to_unit']}"
fn = conversions.get(args["unit_type"], {}).get(key)
if not fn:
return {
"content": [
{
"type": "text",
"text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
}
],
"is_error": True,
}
result = fn(args["value"])
return {
"content": [
{
"type": "text",
"text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
}
]
}
converter_server = create_sdk_mcp_server(
name="converter",
version="1.0.0",
tools=[convert_units],
)
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const convert = tool(
"convert_units",
"Convert a value from one unit to another",
{
unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
from_unit: z
.string()
.describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
to_unit: z.string().describe("Unit to convert to"),
value: z.number().describe("Value to convert")
},
async (args) => {
type Conversions = Record<string, Record<string, (v: number) => number>>;
const conversions: Conversions = {
length: {
kilometers_to_miles: (v) => v * 0.621371,
miles_to_kilometers: (v) => v * 1.60934,
meters_to_feet: (v) => v * 3.28084,
feet_to_meters: (v) => v * 0.3048
},
temperature: {
celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
celsius_to_kelvin: (v) => v + 273.15,
kelvin_to_celsius: (v) => v - 273.15
},
weight: {
kilograms_to_pounds: (v) => v * 2.20462,
pounds_to_kilograms: (v) => v * 0.453592,
grams_to_ounces: (v) => v * 0.035274,
ounces_to_grams: (v) => v * 28.3495
}
};
const key = `${args.from_unit}_to_${args.to_unit}`;
const fn = conversions[args.unit_type]?.[key];
if (!fn) {
return {
content: [
{
type: "text",
text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
}
],
isError: true
};
}
const result = fn(args.value);
return {
content: [
{
type: "text",
text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
}
]
};
}
);
const converterServer = createSdkMcpServer({
name: "converter",
version: "1.0.0",
tools: [convert]
});

Sau khi định nghĩa server, truyền nó vào query giống như ví dụ thời tiết. Ví dụ này gửi ba prompt khác nhau trong một vòng lặp để cho thấy cùng một tool xử lý các loại đơn vị khác nhau. Với mỗi response, nó kiểm tra các object AssistantMessage (chứa các tool call Claude thực hiện trong lượt đó) và in ra từng ToolUseBlock trước khi in văn bản ResultMessage cuối cùng. Cách này cho phép bạn thấy khi nào Claude đang dùng tool so với khi nào nó trả lời từ kiến thức riêng.

tool search được bật mặc định, kết quả có thể bao gồm cả một lệnh gọi ToolSearch khi Claude nạp schema của tool đã bị trì hoãn.

import asyncio
from claude_agent_sdk import (
query,
ClaudeAgentOptions,
ResultMessage,
AssistantMessage,
ToolUseBlock,
)
async def main():
options = ClaudeAgentOptions(
mcp_servers={"converter": converter_server},
allowed_tools=["mcp__converter__convert_units"],
)
prompts = [
"Convert 100 kilometers to miles.",
"What is 72°F in Celsius?",
"How many pounds is 5 kilograms?",
]
for prompt in prompts:
try:
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, ToolUseBlock):
print(f"[tool call] {block.name}({block.input})")
elif isinstance(message, ResultMessage) and message.subtype == "success":
print(f"Q: {prompt}\nA: {message.result}\n")
except Exception as error:
# query() một lượt sẽ raise sau khi trả về một kết quả lỗi. Chỉ các kết quả
# success mới được in ở trên, nên xử lý lỗi ở đây và tiếp tục với prompt tiếp theo.
print(f"Call failed: {error}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
const prompts = [
"Convert 100 kilometers to miles.",
"What is 72°F in Celsius?",
"How many pounds is 5 kilograms?"
];
for (const prompt of prompts) {
try {
for await (const message of query({
prompt,
options: {
mcpServers: { converter: converterServer },
allowedTools: ["mcp__converter__convert_units"]
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "tool_use") {
console.log(`[tool call] ${block.name}`, block.input);
}
}
} else if (message.type === "result" && message.subtype === "success") {
console.log(`Q: ${prompt}\nA: ${message.result}\n`);
}
}
} catch (error) {
// query() một lượt sẽ throw sau khi trả về một kết quả lỗi. Chỉ các kết quả
// success mới được log ở trên, nên xử lý lỗi ở đây và tiếp tục với prompt tiếp theo.
console.error(`Call failed: ${error}`);
}
}

Tool tùy chỉnh bọc các hàm bất đồng bộ trong một giao diện chuẩn. Bạn có thể trộn các mẫu trong trang này trong cùng một server: một server duy nhất có thể chứa một tool cơ sở dữ liệu, một tool gateway API, và một tool render hình ảnh cạnh nhau.

Từ đây, bạn có thể:

  • Nếu server của bạn phát triển lên hàng chục tool, xem tool search để trì hoãn việc nạp chúng cho đến khi Claude cần.
  • Để kết nối tới các MCP server bên ngoài (filesystem, GitHub, Slack) thay vì tự xây dựng, xem Kết nối MCP server.
  • Để kiểm soát tool nào chạy tự động so với tool cần duyệt, xem Cấu hình quyền hạn.