ให้ Claude มี Custom Tools
กำหนด custom tools ด้วย in-process MCP server ของ Claude Agent SDK เพื่อให้ Claude เรียกใช้ฟังก์ชันของคุณ, เชื่อมต่อกับ API, และทำงานเฉพาะทางตามโดเมนได้
Custom tools ขยายขีดความสามารถของ Agent SDK โดยให้คุณกำหนดฟังก์ชันของตัวเองที่ Claude สามารถเรียกใช้ได้ระหว่างการสนทนา ด้วย in-process MCP server ของ SDK คุณสามารถให้ Claude เข้าถึงฐานข้อมูล, external API, logic เฉพาะโดเมน, หรือความสามารถอื่นๆ ที่แอปพลิเคชันของคุณต้องการ
คู่มือนี้ครอบคลุมวิธีกำหนด tools ด้วย input schemas และ handlers, รวม tools เข้าใน MCP server, ส่งต่อให้ query, และควบคุมว่า Claude สามารถเข้าถึง tools ไหนได้บ้าง รวมถึงการจัดการข้อผิดพลาด, tool annotations, และการส่งคืนเนื้อหาที่ไม่ใช่ข้อความ เช่น รูปภาพ
Quick reference
| ต้องการทำ... | ทำสิ่งนี้ |
|---|---|
| กำหนด tool | ใช้ @tool (Python) หรือ tool() (TypeScript) พร้อม name, description, schema, และ handler ดู Create a custom tool |
| ลงทะเบียน tool กับ Claude | ห่อใน create_sdk_mcp_server / createSdkMcpServer และส่งไปยัง mcpServers ใน query() ดู Call a custom tool |
| อนุมัติ tool ล่วงหน้า | เพิ่มใน allowed tools ดู Configure allowed tools |
| ลบ built-in tool ออกจาก context ของ Claude | ส่ง array tools ที่ระบุเฉพาะ built-ins ที่ต้องการ ดู Configure allowed tools |
| ให้ Claude เรียก tools แบบ parallel | ตั้ง readOnlyHint: true สำหรับ tools ที่ไม่มี side effects ดู Add tool annotations |
| จัดการข้อผิดพลาดโดยไม่หยุด loop | Return isError: true แทนการ throw ดู Handle errors |
| Return รูปภาพหรือไฟล์ | ใช้ image หรือ resource blocks ใน content array ดู Return images and resources |
| Return ผลลัพธ์ JSON ที่อ่านได้โดยเครื่อง | ตั้ง structuredContent ใน result ดู Return structured data |
| รองรับ tools จำนวนมาก | ใช้ tool search เพื่อโหลด tools ตามต้องการ |
Create a custom tool
Tool ถูกกำหนดด้วยสี่ส่วนที่ส่งเป็น arguments ให้ helper tool() ใน TypeScript หรือ decorator @tool ใน Python:
- Name: identifier เฉพาะที่ Claude ใช้เรียก tool
- Description: สิ่งที่ tool ทำ Claude อ่านสิ่งนี้เพื่อตัดสินใจว่าจะเรียกมันเมื่อไหร่
- Input schema: arguments ที่ Claude ต้องระบุ ใน TypeScript จะเป็น Zod schema เสมอ และ
argsของ handler จะถูก type จากมันโดยอัตโนมัติ ใน Python จะเป็น dict ที่ map ชื่อไปยัง types เช่น{"latitude": float}ซึ่ง SDK จะแปลงเป็น JSON Schema ให้ Python decorator ยังยอมรับ dict JSON Schema แบบเต็มโดยตรงเมื่อต้องการ enums, ranges, optional fields, หรือ nested objects - Handler: async function ที่รันเมื่อ Claude เรียก tool รับ validated arguments และต้อง return object ที่มี:
content(required): array ของ result blocks แต่ละ block มีtypeเป็น"text","image","audio","resource", หรือ"resource_link"ดู Return images and resources สำหรับ non-text blocksstructuredContent(optional): JSON object ที่เก็บผลลัพธ์เป็น machine-readable data ส่งคืนพร้อมกับcontentดู Return structured dataisError(optional): ตั้งเป็นtrueเพื่อส่งสัญญาณ tool failure ให้ Claude สามารถตอบสนองได้ ดู Handle errors
หลังจากกำหนด tool แล้ว ห่อไว้ใน server ด้วย createSdkMcpServer (TypeScript) หรือ create_sdk_mcp_server (Python) server รันใน process ภายในแอปพลิเคชันของคุณ ไม่ใช่เป็น process แยกต่างหาก
ตัวอย่าง Weather tool
ตัวอย่างนี้กำหนด tool get_temperature และห่อไว้ใน MCP server ตั้งค่า tool เท่านั้น สำหรับการส่งไปยัง query และรัน ดู Call a custom tool ด้านล่าง
from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server
# กำหนด 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 content array - Claude เห็นสิ่งนี้เป็นผลลัพธ์ของ tool
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}
# ห่อ tool ไว้ใน 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";
// กำหนด tool: name, description, input schema, handler
const getTemperature = tool(
"get_temperature",
"Get the current temperature at a location",
{
latitude: z.number().describe("Latitude coordinate"),
longitude: z.number().describe("Longitude coordinate")
},
async (args) => {
// args ถูก type จาก 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 content array - Claude เห็นสิ่งนี้เป็นผลลัพธ์ของ tool
return {
content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
};
}
);
// ห่อ tool ไว้ใน in-process MCP server
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature]
});
Call a custom tool
ส่ง MCP server ที่สร้างไปยัง query ผ่าน option mcpServers key ใน mcpServers กลายเป็น segment {server_name} ในชื่อ fully qualified ของแต่ละ tool: mcp__{server_name}__{tool_name} ระบุชื่อนั้นใน allowedTools เพื่อให้ tool รันโดยไม่ต้องขอ permission
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 เป็น message สุดท้ายหลังจาก tool calls ทั้งหมดเสร็จสิ้น
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" เป็น message สุดท้ายหลังจาก tool calls ทั้งหมดเสร็จสิ้น
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
เพิ่ม tools เพิ่มเติม
Server หนึ่งสามารถมี tools ได้มากเท่าที่ระบุใน array tools เมื่อมีมากกว่าหนึ่ง tool บน server คุณสามารถระบุแต่ละอันใน allowedTools ทีละตัว หรือใช้ wildcard mcp__weather__* เพื่อครอบคลุมทุก tool ที่ server เปิดเผย
# กำหนด tool ที่สองสำหรับ 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 = 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))}%",
}
]
}
# สร้าง server ใหม่พร้อม tools ทั้งสองใน array
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature, get_precipitation_chance],
)
// กำหนด tool ที่สองสำหรับ 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)
.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("%, ")}%` }]
};
}
);
// สร้าง server ใหม่พร้อม tools ทั้งสองใน array
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature, getPrecipitationChance]
});
Add tool annotations
Tool annotations คือ metadata เสริมที่อธิบายพฤติกรรมของ tool ส่งเป็น argument ที่ห้าให้ helper tool() ใน TypeScript หรือผ่าน keyword argument annotations สำหรับ decorator @tool ใน Python field hint ทั้งหมดเป็น Boolean
| Field | Default | ความหมาย |
|---|---|---|
readOnlyHint | false | Tool ไม่แก้ไข environment ของมัน ควบคุมว่า tool สามารถเรียกแบบ parallel กับ read-only tools อื่นได้หรือไม่ |
destructiveHint | true | Tool อาจทำ destructive updates (ให้ข้อมูลเท่านั้น) |
idempotentHint | false | การเรียกซ้ำด้วย arguments เดียวกันไม่มีผลเพิ่มเติม (ให้ข้อมูลเท่านั้น) |
openWorldHint | true | Tool เข้าถึงระบบนอก process ของคุณ (ให้ข้อมูลเท่านั้น) |
Annotations เป็น metadata ไม่ใช่การบังคับ tool ที่ mark readOnlyHint: true ยังสามารถเขียนลงดิสก์ได้หากนั่นคือสิ่งที่ handler ทำ รักษา annotation ให้ถูกต้องกับ handler
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
), # ให้ Claude batch นี้กับ 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 } } // ให้ Claude batch นี้กับ read-only calls อื่น
);
ควบคุมการเข้าถึง tool
รูปแบบชื่อ tool
เมื่อ MCP tools ถูกเปิดเผยให้ Claude ชื่อของพวกมันใช้รูปแบบเฉพาะ:
- รูปแบบ:
mcp__{server_name}__{tool_name} - ตัวอย่าง: tool ชื่อ
get_temperatureใน serverweatherกลายเป็นmcp__weather__get_temperature
Configure allowed tools
Option tools และ allowed/disallowed lists ส่งผลต่อสองชั้น: availability ที่ควบคุมว่า tool ปรากฏใน context ของ Claude หรือไม่ และ permission ที่ควบคุมว่า call ได้รับการอนุมัติเมื่อ Claude พยายามทำหรือไม่
| Option | ชั้น | ผลลัพธ์ |
|---|---|---|
tools: ["Read", "Grep"] | Availability | เฉพาะ built-ins ที่ระบุเท่านั้นที่อยู่ใน context ของ Claude built-ins ที่ไม่ได้ระบุจะถูกลบออก MCP tools ไม่ได้รับผลกระทบ |
tools: [] | Availability | built-ins ทั้งหมดถูกลบออก Claude สามารถใช้ได้เฉพาะ MCP tools ของคุณ |
| allowed tools | Permission | Tools ที่ระบุรันโดยไม่ต้องขอ permission tools ที่ไม่ได้ระบุยังมีอยู่ calls ผ่าน permission flow |
| disallowed tools | ทั้งสอง | ชื่อ tool เปล่าเช่น "Bash" ลบ tool ออกจาก context ของ Claude rule แบบ scoped เช่น "Bash(rm *)" ปล่อย tool ไว้ใน context และปฏิเสธเฉพาะ calls ที่ตรงกัน |
Handle errors
วิธีที่ handler รายงานข้อผิดพลาดกำหนดว่า agent loop จะดำเนินต่อหรือหยุด:
| สิ่งที่เกิดขึ้น | ผลลัพธ์ |
|---|---|
| Handler throw exception ที่ไม่ได้ catch | Agent loop หยุด Claude ไม่เห็นข้อผิดพลาด และ query call ล้มเหลว |
Handler catch ข้อผิดพลาดและ return isError: true (TS) / "is_error": True (Python) | Agent loop ดำเนินต่อ Claude เห็นข้อผิดพลาดเป็นข้อมูลและสามารถลองใหม่, ลอง tool อื่น, หรืออธิบายความล้มเหลว |
import json
import httpx
from typing import Any
@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str},
)
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 {
"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:
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 {
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) {
return {
content: [
{
type: "text",
text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
Return images and resources
Array content ใน tool result ยอมรับ blocks ประเภท text, image, audio, resource, และ resource_link คุณสามารถผสมพวกมันใน response เดียวกันได้
Images
Image block นำ image bytes ไว้ inline เข้ารหัสเป็น base64 ไม่มี URL field เพื่อ return รูปภาพที่อยู่ใน URL ให้ดึงมันใน handler อ่าน response bytes และ encode เป็น base64 ก่อน return
| Field | Type | หมายเหตุ |
|---|---|---|
type | "image" | |
data | string | bytes ที่ encode เป็น Base64 เฉพาะ base64 ดิบ ไม่มี data:image/...;base64, prefix |
mimeType | string | Required เช่น image/png, image/jpeg, image/webp, image/gif |
import base64
import httpx
@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:
response = await client.get(args["url"])
return {
"content": [
{
"type": "image",
"data": base64.b64encode(response.content).decode("ascii"),
"mimeType": response.headers.get("content-type", "image/png"),
}
]
}
Resources
Resource block ฝัง content ที่ระบุด้วย URI ไว้ URI เป็น label สำหรับ Claude ใช้อ้างอิง เนื้อหาจริงอยู่ใน field text หรือ blob ของ block ใช้สิ่งนี้เมื่อ tool ผลิตบางอย่างที่สมเหตุสมผลที่จะระบุชื่อภายหลัง เช่น ไฟล์ที่สร้างขึ้นหรือ record จากระบบภายนอก
return {
content: [
{
type: "resource",
resource: {
uri: "file:///tmp/report.md",
mimeType: "text/markdown",
text: "# Report\n..."
}
}
]
};
return {
"content": [
{
"type": "resource",
"resource": {
"uri": "file:///tmp/report.md",
"mimeType": "text/markdown",
"text": "# Report\n...",
},
}
]
}
Return structured data
structuredContent เป็น JSON object เสริมใน result แยกจาก array content ใช้มันเพื่อ return ค่า raw ที่ Claude สามารถอ่านเป็น exact fields แทนที่จะ parse จาก text string หรือรูปภาพ
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]
}
};
Decorator @tool ของ Python ส่งต่อเฉพาะ content และ is_error จาก return dict ของ handler เท่านั้น ในการ return structuredContent จาก Python ให้รัน standalone MCP server แทน in-process SDK server
ขั้นตอนถัดไป
Custom tools ห่อ async functions ในอินเตอร์เฟซมาตรฐาน คุณสามารถผสมรูปแบบในหน้านี้ใน server เดียวกันได้
- หาก server ของคุณมี tools หลายสิบตัว ดู tool search เพื่อ defer การโหลดจนกว่า Claude จะต้องการ
- หากต้องการเชื่อมต่อกับ external MCP servers (filesystem, GitHub, Slack) แทนการสร้างของตัวเอง ดู Connect MCP servers
- หากต้องการควบคุมว่า tools ไหนรันอัตโนมัติเทียบกับต้องการการอนุมัติ ดู Configure permissions