ابزارهای سفارشی به Claude بده
ابزارهای سفارشی Agent SDK را گسترش میدهند، با این امکان که توابعِ خودت را تعریف کنی تا Claude بتواند در طولِ گفتگو آنها را فراخوانی کند. با استفاده از سرورِ in-process MCP در SDK، میتوانی به Claude دسترسی به دیتابیسها، APIهای بیرونی، منطقِ دامنهمحور، یا هر قابلیتِ دیگری که برنامهات لازم دارد بدهی.
این راهنما پوشش میدهد که چطور ابزارها را با input schema و handler تعریف کنی، آنها را در یک سرورِ MCP بستهبندی کنی، به query پاس بدهی، و کنترل کنی Claude به کدام ابزارها دسترسی داشته باشد. همچنین مدیریتِ خطا، annotationهای ابزار، و بازگرداندنِ محتوای غیرمتنی مثلِ تصویر را پوشش میدهد.
مرجعِ سریع
Section titled “مرجعِ سریع”| اگر میخواهی… | این کار را بکن |
|---|---|
| یک ابزار تعریف کنی | از @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 استفاده کن تا ابزارها بنا به نیاز بارگذاری شوند. |
یک ابزارِ سفارشی بساز
Section titled “یک ابزارِ سفارشی بساز”یک ابزار با چهار بخش تعریف میشود که بهعنوان آرگومان به 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 درونِ برنامهات اجرا میشود، نه بهعنوانِ یک فرایندِ جداگانه.
مثالِ ابزارِ هواشناسی
Section titled “مثالِ ابزارِ هواشناسی”این مثال یک ابزارِ get_temperature تعریف میکند و آن را در یک سرورِ MCP میپیچد. فقط ابزار را راه میاندازد؛ برای پاسدادنش به query و اجرایش، به فراخوانیِ یک ابزارِ سفارشی در پایین نگاه کن.
from typing import Anyimport httpxfrom 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 serverweather_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, handlerconst 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}¤t=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 serverconst 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 asynciofrom 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 arrayweather_server = create_sdk_mcp_server( name="weather", version="1.0.0", tools=[get_temperature, get_precipitation_chance],)// Define a second tool for the same serverconst 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 arrayconst 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 هستند.
| فیلد | پیشفرض | معنا |
|---|---|---|
readOnlyHint | false | ابزار محیطش را تغییر نمیدهد. کنترل میکند که آیا ابزار میتواند بهصورتِ موازی با دیگر ابزارهای فقطخواندنی فراخوانی شود. |
destructiveHint | true | ابزار ممکن است بهروزرسانیِ مخرب انجام دهد. فقط اطلاعرسانی. |
idempotentHint | false | فراخوانیِ مکرر با همان آرگومانها اثرِ افزودهای ندارد. فقط اطلاعرسانی. |
openWorldHint | true | ابزار به سیستمهای بیرونِ فرایندت دسترسی میگیرد. فقط اطلاعرسانی. |
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 کنی.
فرمتِ نامِ ابزار
Section titled “فرمتِ نامِ ابزار”وقتی ابزارهای 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 ممکن است یک نوبت را با تلاش برایش هدر دهد. برای ترتیبِ کاملِ ارزیابی به پیکربندیِ دسترسیها نگاه کن.
خطاها را مدیریت کن
Section titled “خطاها را مدیریت کن”اینکه 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 jsonimport httpxfrom 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 }; } });تصویر و منبع برگردان
Section titled “تصویر و منبع برگردان”آرایهی content در نتیجهی یک ابزار بلاکهای text, image, audio, resource, و resource_link را میپذیرد. میتوانی آنها را در یک پاسخ ترکیب کنی. بلاکهای audio روی دیسک ذخیره میشوند و Claude یک بلاکِ text با مسیرِ فایلِ ذخیرهشده دریافت میکند. بلاکهای resource link به یک بلاکِ text که نام، URI و توضیحِ لینک را دربردارد تبدیل میشوند.
تصاویر
Section titled “تصاویر”یک بلاکِ image بایتهای تصویر را بهصورتِ inline و کدشده با base64 حمل میکند. هیچ فیلدِ URLی نیست. برای بازگرداندنِ تصویری که روی یک URL قرار دارد، در handler آن را fetch کن، بایتهای پاسخ را بخوان، و پیش از بازگرداندن آنها را base64-encode کن. نتیجه بهعنوانِ ورودیِ بصری پردازش میشود.
| فیلد | نوع | یادداشت |
|---|---|---|
type | "image" | |
data | string | بایتهای کدشده با base64. فقط base64ِ خام، بدونِ پیشوندِ data:image/...;base64, |
mimeType | string | الزامی. مثلاً image/png, image/jpeg, image/webp, image/gif |
import base64import 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 } ] }; });منبعها
Section titled “منبعها”یک بلاکِ resource قطعهای از محتوا را که با یک URI شناسایی میشود جاسازی میکند. URI یک برچسب برای ارجاعِ Claude است؛ محتوای واقعی در فیلدِ text یا blobِ بلاک سوار است. این را وقتی استفاده کن که ابزارت چیزی تولید میکند که منطقی است بعداً با نام آدرسدهی شود، مثلِ یک فایلِ تولیدشده یا یک رکورد از یک سیستمِ بیرونی.
| فیلد | نوع | یادداشت |
|---|---|---|
type | "resource" | |
resource.uri | string | شناسهی محتوا. هر طرحِ URI |
resource.text | string | محتوا، اگر متن باشد. این یا blob را فراهم کن، نه هر دو |
resource.blob | string | محتوای کدشده با base64، اگر باینری باشد |
resource.mimeType | string | اختیاری |
این مثال یک بلاکِ 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] }};مثال: مبدلِ واحد
Section titled “مثال: مبدلِ واحد”این ابزار مقادیر را میانِ واحدهای طول، دما و وزن تبدیل میکند. کاربر میتواند بپرسد «۱۰۰ کیلومتر را به مایل تبدیل کن» یا «۷۲°F چقدر سلسیوس است»، و Claude نوعِ واحد و واحدهای درست را از درخواست انتخاب میکند.
دو الگو را نشان میدهد:
-
طرحهای enum:
unit_typeبه مجموعهی ثابتی از مقادیر محدود شده. در TypeScript ازz.enum()استفاده کن. در Python، طرحِ دیکشنری از enum پشتیبانی نمیکند، پس دیکشنریِ کاملِ JSON Schema لازم است. -
مدیریتِ ورودیِ پشتیبانینشده: وقتی یک جفتِ تبدیل پیدا نشود، handler
isError: trueبرمیگرداند تا Claude بتواند به کاربر بگوید چه اشتباه شد، بهجای آنکه یک شکست را بهعنوانِ نتیجهی عادی تلقی کند.from typing import Anyfrom 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 asynciofrom 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`); } }}گامهای بعدی
Section titled “گامهای بعدی”ابزارهای سفارشی توابعِ async را در یک رابطِ استاندارد میپیچند. میتوانی الگوهای این صفحه را در همان سرور ترکیب کنی: یک سرورِ واحد میتواند یک ابزارِ دیتابیس، یک ابزارِ دروازهی API، و یک رندرکنندهی تصویر را کنارِ هم نگه دارد.
از اینجا:
- اگر سرورت به دهها ابزار رشد کرد، به tool search نگاه کن تا بارگذاریِ آنها را تا وقتی Claude نیاز دارد به تعویق بیندازی.
- برای اتصال به سرورهای بیرونیِ MCP (filesystem, GitHub, Slack) بهجای ساختنِ سرورِ خودت، به اتصالِ سرورهای MCP نگاه کن.
- برای کنترلِ اینکه کدام ابزارها خودکار اجرا شوند در برابرِ نیاز به تأیید، به پیکربندیِ دسترسیها نگاه کن.