Skip to main content

ให้ 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
จัดการข้อผิดพลาดโดยไม่หยุด loopReturn 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 blocks
    • structuredContent (optional): JSON object ที่เก็บผลลัพธ์เป็น machine-readable data ส่งคืนพร้อมกับ content ดู Return structured data
    • isError (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}&current=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

FieldDefaultความหมาย
readOnlyHintfalseTool ไม่แก้ไข environment ของมัน ควบคุมว่า tool สามารถเรียกแบบ parallel กับ read-only tools อื่นได้หรือไม่
destructiveHinttrueTool อาจทำ destructive updates (ให้ข้อมูลเท่านั้น)
idempotentHintfalseการเรียกซ้ำด้วย arguments เดียวกันไม่มีผลเพิ่มเติม (ให้ข้อมูลเท่านั้น)
openWorldHinttrueTool เข้าถึงระบบนอก 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 ใน server weather กลายเป็น 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: []Availabilitybuilt-ins ทั้งหมดถูกลบออก Claude สามารถใช้ได้เฉพาะ MCP tools ของคุณ
allowed toolsPermissionTools ที่ระบุรันโดยไม่ต้องขอ 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 ที่ไม่ได้ catchAgent 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

FieldTypeหมายเหตุ
type"image"
datastringbytes ที่ encode เป็น Base64 เฉพาะ base64 ดิบ ไม่มี data:image/...;base64, prefix
mimeTypestringRequired เช่น 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]
}
};
note

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

เอกสารที่เกี่ยวข้อง