رفتن به محتوا

ابزارهای سفارشی به Claude بده

ابزارهای سفارشی Agent SDK را گسترش می‌دهند، با این امکان که توابعِ خودت را تعریف کنی تا Claude بتواند در طولِ گفتگو آن‌ها را فراخوانی کند. با استفاده از سرورِ in-process MCP در SDK، می‌توانی به Claude دسترسی به دیتابیس‌ها، APIهای بیرونی، منطقِ دامنه‌محور، یا هر قابلیتِ دیگری که برنامه‌ات لازم دارد بدهی.

این راهنما پوشش می‌دهد که چطور ابزارها را با input schema و handler تعریف کنی، آن‌ها را در یک سرورِ MCP بسته‌بندی کنی، به query پاس بدهی، و کنترل کنی Claude به کدام ابزارها دسترسی داشته باشد. همچنین مدیریتِ خطا، annotationهای ابزار، و بازگرداندنِ محتوای غیرمتنی مثلِ تصویر را پوشش می‌دهد.

اگر می‌خواهی…این کار را بکن
یک ابزار تعریف کنیاز @tool (Python) یا tool() (TypeScript) با یک name، description، schema و handler استفاده کن. به ساختنِ یک ابزارِ سفارشی نگاه کن.
یک ابزار را نزدِ Claude ثبت کنیدر create_sdk_mcp_server / createSdkMcpServer بپیچ و به mcpServers در query() پاس بده. به فراخوانیِ یک ابزارِ سفارشی نگاه کن.
یک ابزار را از پیش تأیید کنیبه فهرستِ ابزارهای مجازت اضافه کن. به پیکربندیِ ابزارهای مجاز نگاه کن.
یک ابزارِ توکار را از کانتکستِ Claude حذف کنییک آرایه‌ی tools پاس بده که فقط توکارهایی را که می‌خواهی فهرست کرده باشد. به پیکربندیِ ابزارهای مجاز نگاه کن.
بگذاری Claude ابزارها را موازی فراخوانی کندروی ابزارهای بدونِ اثرِ جانبی readOnlyHint: true بگذار. به افزودنِ annotationهای ابزار نگاه کن.
خطاها را بدونِ توقفِ حلقه مدیریت کنیبه‌جای throw کردن، isError: true برگردان. به مدیریتِ خطاها نگاه کن.
تصویر یا فایل برگردانیاز بلاک‌های image یا resource در آرایه‌ی content استفاده کن. به بازگرداندنِ تصویر و منبع نگاه کن.
یک نتیجه‌ی JSONِ ماشین‌خوان برگردانیروی نتیجه structuredContent بگذار. به بازگرداندنِ داده‌ی ساختارمند نگاه کن.
به ابزارهای بسیار زیاد مقیاس بدهیاز tool search استفاده کن تا ابزارها بنا به نیاز بارگذاری شوند.

یک ابزار با چهار بخش تعریف می‌شود که به‌عنوان آرگومان به helperِ tool() در TypeScript یا دکوریتورِ @tool در Python پاس داده می‌شوند:

  • Name (نام): یک شناسه‌ی یکتا که Claude برای فراخوانیِ ابزار از آن استفاده می‌کند.
  • Description (توضیح): ابزار چه می‌کند. Claude این را می‌خواند تا تصمیم بگیرد کِی آن را فراخوانی کند.
  • Input schema (طرحِ ورودی): آرگومان‌هایی که Claude باید فراهم کند. در TypeScript این همیشه یک Zod schema است، و argsِ handler به‌طور خودکار از روی آن تایپ می‌شود. در Python این یک دیکشنری است که نام‌ها را به نوع‌ها نگاشت می‌کند، مثلِ {"latitude": float}، که SDK برایت به JSON Schema تبدیل می‌کند. دکوریتورِ Python وقتی به enum، بازه، فیلدِ اختیاری، یا شیءِ تودرتو نیاز داری، یک دیکشنریِ کاملِ JSON Schema را هم مستقیماً می‌پذیرد.
  • Handler: تابعِ async که وقتی Claude ابزار را فراخوانی می‌کند اجرا می‌شود. آرگومان‌های اعتبارسنجی‌شده را می‌گیرد و باید شیئی برگرداند با:
    • content (الزامی): آرایه‌ای از بلاک‌های نتیجه، که هرکدام یک type از "text", "image", "audio", "resource", یا "resource_link" دارد. برای بلاک‌های غیرمتنی به بازگرداندنِ تصویر و منبع نگاه کن.
    • structuredContent (اختیاری): یک شیءِ JSON که نتیجه را به‌صورتِ داده‌ی ماشین‌خوان نگه می‌دارد و در کنارِ content برگردانده می‌شود. به بازگرداندنِ داده‌ی ساختارمند نگاه کن.
    • isError (اختیاری): روی true بگذار تا یک شکستِ ابزار را علامت بزنی تا Claude بتواند به آن واکنش نشان دهد. به مدیریتِ خطاها نگاه کن.

بعد از تعریفِ یک ابزار، آن را با createSdkMcpServer (TypeScript) یا create_sdk_mcp_server (Python) در یک سرور بپیچ. این سرور به‌صورتِ in-process درونِ برنامه‌ات اجرا می‌شود، نه به‌عنوانِ یک فرایندِ جداگانه.

این مثال یک ابزارِ get_temperature تعریف می‌کند و آن را در یک سرورِ MCP می‌پیچد. فقط ابزار را راه می‌اندازد؛ برای پاس‌دادنش به query و اجرایش، به فراخوانیِ یک ابزارِ سفارشی در پایین نگاه کن.

from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server
# Define a tool: name, description, 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()
# Return a content array - Claude sees this as the tool result
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}
# Wrap the tool in an in-process MCP server
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";
// Define a tool: name, description, input schema, handler
const getTemperature = tool(
"get_temperature",
"Get the current temperature at a location",
{
latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
longitude: z.number().describe("Longitude coordinate")
},
async (args) => {
// args is typed from the 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();
// Return a content array - Claude sees this as the tool result
return {
content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
};
}
);
// Wrap the tool in an in-process MCP server
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature]
});

برای جزئیاتِ کاملِ پارامترها — شاملِ فرمت‌های ورودیِ JSON Schema و ساختارِ مقدارِ بازگشتی — به مرجعِ TypeScriptِ tool() یا مرجعِ Pythonِ @tool نگاه کن.

یک ابزارِ سفارشی را فراخوانی کن

Section titled “یک ابزارِ سفارشی را فراخوانی کن”

سروری که ساخته‌ای را از طریقِ گزینه‌ی mcpServers به query پاس بده. کلید در mcpServers به بخشِ {server_name} در نامِ کاملِ هر ابزار تبدیل می‌شود: mcp__{server_name}__{tool_name}. آن نام را در allowedTools فهرست کن تا ابزار بدونِ درخواستِ مجوز اجرا شود.

این قطعه‌ها همان weatherServer را از مثالِ بالا دوباره به کار می‌برند تا از Claude بپرسند هوا در یک مکانِ مشخص چطور است.

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 is the final message after all tool calls complete
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" is the final message after all tool calls complete
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}

ابزارهای بیشتری اضافه کن

Section titled “ابزارهای بیشتری اضافه کن”

یک سرور به همان تعداد ابزار که در آرایه‌ی toolsاش فهرست کنی نگه می‌دارد. با بیش از یک ابزار روی یک سرور، می‌توانی هرکدام را تک‌تک در allowedTools فهرست کنی یا از وایلدکاردِ mcp__weather__* استفاده کنی تا همه‌ی ابزارهایی را که سرور در معرض می‌گذارد پوشش دهد.

مثالِ زیر یک ابزارِ دوم، get_precipitation_chance، را به weatherServer از مثالِ ابزارِ هواشناسی اضافه می‌کند و آن را با هر دو ابزار در آرایه بازسازی می‌کند.

# Define a second tool for the same 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' isn't in the schema - read it with .get() to make it optional
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))}%",
}
]
}
# Rebuild the server with both tools in the array
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature, get_precipitation_chance],
)
// Define a second tool for the same 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() makes the parameter optional
.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("%, ")}%` }]
};
}
);
// Rebuild the server with both tools in the array
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature, getPrecipitationChance]
});

هر ابزار در این آرایه در هر نوبت فضای پنجره‌ی کانتکست مصرف می‌کند. اگر داری ده‌ها ابزار تعریف می‌کنی، به‌جایش به tool search نگاه کن تا آن‌ها را بنا به نیاز بارگذاری کنی.

annotationهای ابزار را اضافه کن

Section titled “annotationهای ابزار را اضافه کن”

annotationهای ابزار متادیتای اختیاری‌اند که توصیف می‌کنند یک ابزار چطور رفتار می‌کند. آن‌ها را در TypeScript به‌عنوانِ آرگومانِ پنجمِ helperِ tool() پاس بده یا در Python از طریقِ آرگومانِ کلمه‌کلیدیِ annotations برای دکوریتورِ @tool. همه‌ی فیلدهای hint از نوعِ Boolean هستند.

فیلدپیش‌فرضمعنا
readOnlyHintfalseابزار محیطش را تغییر نمی‌دهد. کنترل می‌کند که آیا ابزار می‌تواند به‌صورتِ موازی با دیگر ابزارهای فقط‌خواندنی فراخوانی شود.
destructiveHinttrueابزار ممکن است به‌روزرسانیِ مخرب انجام دهد. فقط اطلاع‌رسانی.
idempotentHintfalseفراخوانیِ مکرر با همان آرگومان‌ها اثرِ افزوده‌ای ندارد. فقط اطلاع‌رسانی.
openWorldHinttrueابزار به سیستم‌های بیرونِ فرایندت دسترسی می‌گیرد. فقط اطلاع‌رسانی.

annotationها متادیتا هستند، نه اجبار. ابزاری که با readOnlyHint: true علامت خورده هنوز می‌تواند روی دیسک بنویسد اگر handler همان کار را بکند. annotation را با کاری که handler می‌کند هماهنگ نگه دار.

این مثال readOnlyHint را به ابزارِ get_temperature از مثالِ ابزارِ هواشناسی اضافه می‌کند.

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
), # Lets Claude batch this with other read-only calls
)
async def get_temperature(args):
return {"content": [{"type": "text", "text": "..."}]}
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 } } // Lets Claude batch this with other read-only calls
);

به ToolAnnotations در مرجعِ TypeScript یا Python نگاه کن.

دسترسی به ابزار را کنترل کن

Section titled “دسترسی به ابزار را کنترل کن”

مثالِ ابزارِ هواشناسی یک سرور را ثبت کرد و ابزارها را در allowedTools فهرست کرد. این بخش پوشش می‌دهد که نام‌های ابزار چطور ساخته می‌شوند و چطور وقتی چند ابزار داری یا می‌خواهی توکارها را محدود کنی، دسترسی را scope کنی.

وقتی ابزارهای MCP در معرضِ Claude گذاشته می‌شوند، نام‌هایشان از یک فرمتِ مشخص پیروی می‌کنند:

  • الگو: mcp__{server_name}__{tool_name}
  • مثال: ابزاری به نامِ get_temperature در سرورِ weather به mcp__weather__get_temperature تبدیل می‌شود

پیکربندیِ ابزارهای مجاز

Section titled “پیکربندیِ ابزارهای مجاز”

گزینه‌ی tools و فهرست‌های مجاز/غیرمجاز روی دو لایه اثر می‌گذارند: availability (در دسترس بودن) که کنترل می‌کند آیا یک ابزار در کانتکستِ Claude ظاهر می‌شود، و permission (مجوز) که کنترل می‌کند آیا یک فراخوان وقتی Claude اقدام می‌کند تأیید می‌شود. tools و ورودی‌های نام‌خالیِ disallowedTools availability را تغییر می‌دهند. allowedTools و قواعدِ scope‌شده‌ی disallowedTools فقط permission را تغییر می‌دهند.

گزینهلایهاثر
tools: ["Read", "Grep"]Availabilityفقط توکارهای فهرست‌شده در کانتکستِ Claude هستند. توکارهای فهرست‌نشده حذف می‌شوند. ابزارهای MCP بی‌تأثیر می‌مانند.
tools: []Availabilityهمه‌ی توکارها حذف می‌شوند. Claude فقط می‌تواند از ابزارهای MCPِ تو استفاده کند.
ابزارهای مجازPermissionابزارهای فهرست‌شده بدونِ درخواستِ مجوز اجرا می‌شوند. ابزارهای فهرست‌نشده در دسترس می‌مانند؛ فراخوان‌ها از جریانِ مجوز عبور می‌کنند.
ابزارهای غیرمجازهر دویک نامِ خالیِ ابزار مثلِ "Bash" ابزار را از کانتکستِ Claude حذف می‌کند، مثلِ حذفش از tools. یک قاعده‌ی scope‌شده مثلِ "Bash(rm *)" ابزار را در کانتکست نگه می‌دارد و فقط فراخوان‌های منطبق را رد می‌کند.

برای حذفِ کاملِ یک توکار، آن را از tools بیرون بگذار یا نامِ خالی‌اش را در disallowedTools (در Python: disallowed_tools) فهرست کن؛ هر دو ابزار را بیرونِ کانتکست نگه می‌دارند تا Claude هرگز برایش اقدام نکند. یک قاعده‌ی scope‌شده‌ی disallowedTools فراخوان‌های منطبق را مسدود می‌کند ولی ابزار را قابل‌رؤیت نگه می‌دارد، پس Claude ممکن است یک نوبت را با تلاش برایش هدر دهد. برای ترتیبِ کاملِ ارزیابی به پیکربندیِ دسترسی‌ها نگاه کن.

اینکه handler چطور خطاها را گزارش می‌کند تعیین می‌کند که حلقه‌ی ایجنت ادامه پیدا کند یا متوقف شود:

چه اتفاقی می‌افتدنتیجه
handler یک exceptionِ گرفته‌نشده throw می‌کندحلقه‌ی ایجنت متوقف می‌شود. Claude هرگز خطا را نمی‌بیند، و فراخوانِ query شکست می‌خورد.
handler خطا را می‌گیرد و isError: true (TS) / "is_error": True (Python) برمی‌گرداندحلقه‌ی ایجنت ادامه می‌یابد. Claude خطا را به‌عنوانِ داده می‌بیند و می‌تواند دوباره تلاش کند، ابزارِ دیگری را امتحان کند، یا شکست را توضیح دهد.

مثالِ زیر دو نوع شکست را درونِ handler می‌گیرد به‌جای آنکه بگذارد throw شوند. یک statusِ HTTPِ غیرِ ۲۰۰ از پاسخ گرفته می‌شود و به‌عنوانِ نتیجه‌ی خطا برگردانده می‌شود. یک خطای شبکه یا JSONِ نامعتبر توسطِ try/exceptِ احاطه‌کننده (Python) یا try/catch (TypeScript) گرفته می‌شود و آن هم به‌عنوانِ نتیجه‌ی خطا برگردانده می‌شود. در هر دو حالت handler به‌طور عادی برمی‌گردد و حلقه‌ی ایجنت ادامه می‌یابد.

import json
import httpx
from typing import Any
@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str}, # Simple schema
)
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:
# Return the failure as a tool result so Claude can react to it.
# is_error marks this as a failed call rather than odd-looking data.
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:
# Catching here keeps the agent loop alive. An uncaught exception
# would end the whole query() call.
return {
"content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
"is_error": True,
}
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) {
// Return the failure as a tool result so Claude can react to it.
// isError marks this as a failed call rather than odd-looking data.
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) {
// Catching here keeps the agent loop alive. An uncaught throw
// would end the whole query() call.
return {
content: [
{
type: "text",
text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);

آرایه‌ی content در نتیجه‌ی یک ابزار بلاک‌های text, image, audio, resource, و resource_link را می‌پذیرد. می‌توانی آن‌ها را در یک پاسخ ترکیب کنی. بلاک‌های audio روی دیسک ذخیره می‌شوند و Claude یک بلاکِ text با مسیرِ فایلِ ذخیره‌شده دریافت می‌کند. بلاک‌های resource link به یک بلاکِ text که نام، URI و توضیحِ لینک را دربردارد تبدیل می‌شوند.

یک بلاکِ image بایت‌های تصویر را به‌صورتِ inline و کدشده با base64 حمل می‌کند. هیچ فیلدِ URLی نیست. برای بازگرداندنِ تصویری که روی یک URL قرار دارد، در handler آن را fetch کن، بایت‌های پاسخ را بخوان، و پیش از بازگرداندن آن‌ها را base64-encode کن. نتیجه به‌عنوانِ ورودیِ بصری پردازش می‌شود.

فیلدنوعیادداشت
type"image"
datastringبایت‌های کدشده با base64. فقط base64ِ خام، بدونِ پیشوندِ data:image/...;base64,
mimeTypestringالزامی. مثلاً image/png, image/jpeg, image/webp, image/gif
import base64
import httpx
# Define a tool that fetches an image from a URL and returns it to 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 the image bytes
response = await client.get(args["url"])
return {
"content": [
{
"type": "image",
"data": base64.b64encode(response.content).decode(
"ascii"
), # Base64-encode the raw bytes
"mimeType": response.headers.get(
"content-type", "image/png"
), # Read MIME type from the response
}
]
}
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 the image bytes
const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
const mimeType = response.headers.get("content-type") ?? "image/png";
return {
content: [
{
type: "image",
data: buffer.toString("base64"), // Base64-encode the raw bytes
mimeType
}
]
};
}
);

یک بلاکِ resource قطعه‌ای از محتوا را که با یک URI شناسایی می‌شود جاسازی می‌کند. URI یک برچسب برای ارجاعِ Claude است؛ محتوای واقعی در فیلدِ text یا blobِ بلاک سوار است. این را وقتی استفاده کن که ابزارت چیزی تولید می‌کند که منطقی است بعداً با نام آدرس‌دهی شود، مثلِ یک فایلِ تولیدشده یا یک رکورد از یک سیستمِ بیرونی.

فیلدنوعیادداشت
type"resource"
resource.uristringشناسه‌ی محتوا. هر طرحِ URI
resource.textstringمحتوا، اگر متن باشد. این یا blob را فراهم کن، نه هر دو
resource.blobstringمحتوای کدشده با base64، اگر باینری باشد
resource.mimeTypestringاختیاری

این مثال یک بلاکِ resource را نشان می‌دهد که از درونِ یک handlerِ ابزار برگردانده شده. URIِ file:///tmp/report.md یک برچسب است که Claude می‌تواند بعداً به آن ارجاع دهد؛ SDK از آن مسیر چیزی نمی‌خواند.

return {
content: [
{
type: "resource",
resource: {
uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
mimeType: "text/markdown",
text: "# Report\n..." // The actual content, inline
}
}
]
};
return {
"content": [
{
"type": "resource",
"resource": {
"uri": "file:///tmp/report.md", # Label for Claude to reference, not a path the SDK reads
"mimeType": "text/markdown",
"text": "# Report\n...", # The actual content, inline
},
}
]
}

این شکل‌های بلاک از نوعِ CallToolResult در MCP می‌آیند. برای تعریفِ کامل به مشخصاتِ MCP نگاه کن.

داده‌ی ساختارمند برگردان

Section titled “داده‌ی ساختارمند برگردان”

structuredContent یک شیءِ JSONِ اختیاری روی نتیجه است، جدا از آرایه‌ی content. از آن استفاده کن تا مقادیرِ خامی برگردانی که Claude بتواند آن‌ها را به‌عنوانِ فیلدهای دقیق بخواند، به‌جای استخراجشان از یک رشته‌ی متنی یا تصویر.

وقتی structuredContent تنظیم شده، Claude علاوه بر JSON، هر بلاکِ image یا resourceی را از content دریافت می‌کند. بلاک‌های text در content فوروارد نمی‌شوند، چون فرض می‌شود داده‌ی ساختارمند را تکرار می‌کنند. مثالِ زیر یک نمودار را به‌عنوانِ بلاکِ image رندر می‌کند و نقاطِ داده‌ی پشتِ آن را در structuredContent از همان handler برمی‌گرداند.

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]
}
};

این ابزار مقادیر را میانِ واحدهای طول، دما و وزن تبدیل می‌کند. کاربر می‌تواند بپرسد «۱۰۰ کیلومتر را به مایل تبدیل کن» یا «۷۲°F چقدر سلسیوس است»، و Claude نوعِ واحد و واحدهای درست را از درخواست انتخاب می‌کند.

دو الگو را نشان می‌دهد:

  • طرح‌های enum: unit_type به مجموعه‌ی ثابتی از مقادیر محدود شده. در TypeScript از z.enum() استفاده کن. در Python، طرحِ دیکشنری از enum پشتیبانی نمی‌کند، پس دیکشنریِ کاملِ JSON Schema لازم است.

  • مدیریتِ ورودیِ پشتیبانی‌نشده: وقتی یک جفتِ تبدیل پیدا نشود، handler isError: true برمی‌گرداند تا Claude بتواند به کاربر بگوید چه اشتباه شد، به‌جای آنکه یک شکست را به‌عنوانِ نتیجه‌ی عادی تلقی کند.

    from typing import Any
    from claude_agent_sdk import tool, create_sdk_mcp_server
    # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
    # The dict schema has no equivalent, so full JSON Schema is required.
    @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]
    });

وقتی سرور تعریف شد، آن را همان‌طور که در مثالِ هواشناسی بود به query پاس بده. این مثال سه پرامپتِ متفاوت را در یک حلقه می‌فرستد تا نشان دهد همان ابزار نوع‌های مختلفِ واحد را مدیریت می‌کند. برای هر پاسخ، اشیاءِ AssistantMessage را بررسی می‌کند (که فراخوان‌های ابزاری را که Claude در آن نوبت انجام داده دربردارند) و هر ToolUseBlock را پیش از چاپِ متنِ نهاییِ ResultMessage چاپ می‌کند. این می‌گذارد ببینی کِی Claude از ابزار استفاده می‌کند در برابرِ پاسخ‌دادن از دانشِ خودش.

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:
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")
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) {
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`);
}
}
}

ابزارهای سفارشی توابعِ async را در یک رابطِ استاندارد می‌پیچند. می‌توانی الگوهای این صفحه را در همان سرور ترکیب کنی: یک سرورِ واحد می‌تواند یک ابزارِ دیتابیس، یک ابزارِ دروازه‌ی API، و یک رندرکننده‌ی تصویر را کنارِ هم نگه دارد.

از اینجا:

  • اگر سرورت به ده‌ها ابزار رشد کرد، به tool search نگاه کن تا بارگذاریِ آن‌ها را تا وقتی Claude نیاز دارد به تعویق بیندازی.
  • برای اتصال به سرورهای بیرونیِ MCP (filesystem, GitHub, Slack) به‌جای ساختنِ سرورِ خودت، به اتصالِ سرورهای MCP نگاه کن.
  • برای کنترلِ اینکه کدام ابزارها خودکار اجرا شوند در برابرِ نیاز به تأیید، به پیکربندیِ دسترسی‌ها نگاه کن.