1. Công cụ như một hợp đồng: chính xác chúng ta đang mô tả điều gì
Khi bạn đăng ký công cụ trong MCP‑server, bạn mô tả nó bằng một đối tượng nhỏ. Cấu trúc giản lược cho TypeScript‑SDK trông như sau:
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description: "Gợi ý quà tặng theo hồ sơ người nhận.",
inputSchema: {
type: "object",
// chúng ta sẽ đi sâu vào phần này
},
},
async ({ input }) => {
// mã của bạn
}
);
Mô hình không biết bên trong handler async ({ input }) => { ... } có gì. Với nó chỉ có ba thứ:
- name/title — tên của công cụ.
- description — khi nào nên sử dụng.
- inputSchema — cần truyền những tham số nào và ở định dạng nào.
Mọi thứ chúng ta làm trong bài này liên quan tới mục 3 (và một chút về metadata _meta/annotations, sẽ nói sau).
Điều quan trọng cần hiểu: JSON Schema trong bối cảnh ChatGPT App không phải chỉ là validator nhàm chán, mà là một phần của prompt cho mô hình. Mô hình thực sự đọc description của các trường, hiểu enum là gì, chú ý tới minItems, format, v.v.
Tức là bạn không chỉ bảo vệ backend khỏi dữ liệu lỗi, bạn còn đang giải thích cho mô hình AI cách gọi hàm của bạn một cách đúng đắn.
2. JSON Schema cơ bản cho công cụ suggest_gifts
Bắt đầu từ ví dụ đơn giản. Giả sử ta có kịch bản sau:
Người dùng viết:
“Chọn quà cho anh trai 25 tuổi, ngân sách 50–70 đô la, thích trò chơi điện tử và board game”.
Công cụ suggest_gifts cần nhận các tham số xấp xỉ như sau:
- tuổi của người nhận;
- kiểu quan hệ (anh/chị/em, đồng nghiệp, người yêu, v.v.);
- ngân sách tối thiểu và tối đa;
- danh sách sở thích.
Mô tả điều này bằng JSON Schema “thẳng thắn”, không dùng Zod, bằng đối tượng thuần:
const suggestGiftsInputSchema = {
type: "object",
properties: {
age: {
type: "integer",
minimum: 0,
maximum: 120,
description: "Tuổi của người nhận quà (tính theo năm).",
},
relationship: {
type: "string",
enum: ["friend", "partner", "sibling", "colleague", "parent"],
description:
"Kiểu quan hệ với người nhận: friend, partner, sibling (anh/chị/em), colleague, parent.",
},
minBudget: {
type: "number",
minimum: 0,
description: "Ngân sách tối thiểu theo đơn vị tiền tệ của người dùng.",
},
maxBudget: {
type: "number",
minimum: 0,
description: "Ngân sách tối đa theo đơn vị tiền tệ của người dùng.",
},
interests: {
type: "array",
items: {
type: "string",
description:
"Tên ngắn của mối quan tâm, ví dụ: videogames, boardgames, books.",
},
minItems: 1,
description: "Danh sách sở thích của người nhận.",
},
},
required: ["relationship", "maxBudget"],
};
Vài điểm quan trọng đáng nói ngay tại đây.
Thứ nhất, description của các trường. Trong API thông thường bạn có thể không cần viết — lập trình viên frontend đọc Swagger là hiểu. Nhưng ở đây “khách hàng” là mô hình, nó cố gắng suy luận ý nghĩa từ tên và mô tả. Càng nói rõ: “tuổi tính theo năm”, “ngân sách theo tiền tệ của người dùng”, “enum với các giá trị cố định” thì càng ít đối số kỳ quặc bạn gặp ở runtime.
Thứ hai, enum là một trong những công cụ mạnh nhất để điều khiển mô hình. Nếu bạn cho phép mô hình nhập bất kỳ chuỗi nào vào relationship, bạn sẽ nhận được “bro”, “girlfriend”, “bestie”, “teammate” và còn sáng tạo hơn thế nữa. Nếu bạn đặt enum, mô hình rất có khả năng chỉ chọn trong tập giá trị đó. Đây là cách trực tiếp để giảm “ảo tưởng” (hallucinations) trong đối số.
Thứ ba, không nhất thiết phải đánh dấu mọi thứ là required. Ví dụ, age có thể không bắt buộc: nếu người dùng không nêu, mô hình sẽ không bịa “độ tuổi xấp xỉ” (nếu bạn mô tả như vậy). Đây là nghệ thuật cân bằng giữa linh hoạt và nghiêm ngặt.
Bây giờ dùng schema này khi đăng ký công cụ:
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description:
"Gợi ý ý tưởng quà tặng dựa trên ngân sách, kiểu quan hệ và sở thích của người nhận.",
inputSchema: suggestGiftsInputSchema,
},
async ({ input }) => {
// tại đây input đã gần tương ứng với schema
// ...
}
);
Đối tượng “viết tay” như vậy phù hợp cho các thử nghiệm nhanh. Nhưng khi ứng dụng lớn dần, nó biến thành một thế giới riêng rất dễ lệch khỏi các kiểu TypeScript của bạn. Chúng ta sẽ quay lại vấn đề này và xem cách giải quyết bằng Zod và sinh JSON Schema từ kiểu.
3. JSON Schema như prompt: viết description thế nào để mô hình không khổ sở
Về hình thức, JSON Schema là để xác thực. Nhưng trong thế giới LLM, nó còn là prompt có cấu trúc. Vài quy tắc thực tế:
- Trường description phải trả lời “điền gì vào đây và theo định dạng nào”.
Cách viết “Ngày” thì không giúp gì. Còn “Ngày theo chuẩn ISO 8601 ở định dạng YYYY-MM-DD, ví dụ "2025-02-14"” — lại giúp rất nhiều. - Nếu trường liên quan tới tiền — hãy nêu rõ đơn vị.
Tốt hơn là viết rõ “Số tiền theo đơn vị tiền tệ của người dùng” hoặc “Số tiền tính bằng USD”. Nếu không, mô hình có thể điền 50 và bạn sẽ phải đoán đó là 50 yên hay 50 euro. - Các “phân loại” dạng chuỗi gần như luôn nên dùng enum.
Nếu trường là chuỗi “phân loại”, hãy dùng enum và mô tả từng giá trị trong description của công cụ. Ví dụ, với relationship bạn có thể viết trong mô tả công cụ: “relationship: một trong friend (bạn), partner (người yêu), sibling (anh/chị/em), colleague (đồng nghiệp), parent (cha/mẹ). Đừng bịa thêm giá trị khác.” - Với mảng, hữu ích khi đặt minItems và giải thích đó là danh sách gì.
Nếu trường là mảng, hãy nêu minItems và mô tả ngắn gọn bản chất danh sách đó. Ví dụ, interests không phải “mô tả tự do về con người”, mà là “tập các thẻ ngắn”.
Nghe có vẻ hơi cứng nhắc, nhưng trên thực tế khác biệt giữa “có mô tả” và “không có mô tả” chính là khác biệt giữa ứng dụng ổn định và trò xổ số “hôm nay mô hình gửi gì”.
Insight
Các công cụ MCP có giới hạn kích thước nghiêm ngặt — và đó thường là nguyên nhân gây ra các lỗi “huyền bí”, hành vi lạ và việc trợ lý đột nhiên không còn thấy tools của bạn.
Quy tắc cốt lõi đơn giản: công cụ phải gói gọn trong ~4 KB JSON. Không chỉ là văn bản description, mà là toàn bộ cấu trúc:
- mô tả công cụ,
- schema của tham số (inputSchema),
- các đối tượng lồng nhau và enum,
- _meta và annotations.
Nếu công cụ của bạn phình to, nền tảng sẽ hành xử khó đoán: xuất hiện lỗi như "Tool description is too long", "Schema validation failed", "Manifest exceeds size limits", và đôi khi ChatGPT đơn giản là không tải công cụ hoặc “quên” sự tồn tại của nó.
Khuyến nghị: giữ description trong khoảng 1000–2000 ký tự, và toàn bộ công cụ trong khoảng “an toàn” ~4 KB. Nếu phần mô tả trở nên quá dài, đó gần như luôn là dấu hiệu công cụ đang làm quá nhiều việc. Tốt nhất tách công cụ thành các chức năng hẹp và thật rõ ràng — mô hình sẽ hiểu ranh giới tốt hơn và ít sai hơn ở đầu vào.
4. TypeScript và Zod: một nguồn sự thật duy nhất thay vì hai
Viết JSON Schema thủ công — là nỗi đau với dev TypeScript. Phải duy trì hai thế giới song song:
- các kiểu trong mã TS;
- JSON Schema cho mô hình.
Khi ứng dụng lớn dần, chúng bắt đầu lệch nhau. Hôm nay bạn đổi một trường trong kiểu TypeScript, ngày mai quên cập nhật schema — và tuần sau dính lỗi ở production.
Cách tiếp cận gần như tiêu chuẩn trong thế giới TS — dùng Zod và chuyển đổi Zod -> JSON Schema.
Cài đặt phụ thuộc (nếu chưa có):
npm install zod zod-to-json-schema
Mô tả schema đầu vào cho suggest_gifts bằng Zod:
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const SuggestGiftsInputZod = z.object({
age: z
.number()
.int()
.min(0)
.max(120)
.describe("Tuổi của người nhận quà (tính theo năm)."),
relationship: z
.enum(["friend", "partner", "sibling", "colleague", "parent"])
.describe(
"Kiểu quan hệ: friend (bạn), partner (người yêu), sibling (anh/chị/em), colleague (đồng nghiệp), parent (cha/mẹ)."
),
minBudget: z
.number()
.min(0)
.optional()
.describe("Ngân sách tối thiểu theo đơn vị tiền tệ của người dùng."),
maxBudget: z
.number()
.min(0)
.describe("Ngân sách tối đa theo đơn vị tiền tệ của người dùng."),
interests: z
.array(
z
.string()
.min(1)
.describe(
"Thẻ sở thích ngắn gọn, ví dụ: videogames, boardgames, books."
)
)
.min(1)
.describe("Danh sách sở thích của người nhận."),
});
Bây giờ bạn có:
- Xác thực runtime: SuggestGiftsInputZod.parse(input);
- Kiểu TypeScript: type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;
- JSON Schema cho mô hình: zodToJsonSchema(SuggestGiftsInputZod).
Dùng nó khi đăng ký công cụ:
type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;
const suggestGiftsInputSchemaJson = zodToJsonSchema(
SuggestGiftsInputZod,
"SuggestGiftsInput"
);
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description:
"Gợi ý ý tưởng quà tặng dựa trên ngân sách, kiểu quan hệ và sở thích của người nhận.",
inputSchema: suggestGiftsInputSchemaJson,
},
async ({ input }) => {
// tại đây có thể kiểm tra input bằng Zod:
const args = SuggestGiftsInputZod.parse(input) as SuggestGiftsInput;
// sau đó làm việc với args đã có kiểu
}
);
Cách này mang lại đúng single source of truth — một nguồn sự thật duy nhất: bạn mô tả schema một lần, còn kiểu TypeScript và JSON Schema được sinh tự động.
Trong thực tế bạn còn thêm test để đảm bảo zodToJsonSchema xuất ra cấu trúc mong đợi, nhưng đó là chủ đề của mô-đun về testing.
Insight: ChatGPT xử lý chưa tốt các tham số tùy chọn
Một vấn đề đau đầu trong production: ngay khi bạn dùng nhiều trường optional trong schema công cụ, chất lượng tool-call giảm thấy rõ. Về lý thuyết mô hình “hiểu” tham số không bắt buộc, nhưng thực tế nó thường không gửi chúng — ngay cả khi theo logic nghiệp vụ bạn rất cần.
Response API giải quyết gọn: họ bỏ luôn các trường optional — mọi tham số của công cụ phải được khai báo là required. Nhưng vấn đề gốc vẫn còn: ý tưởng “mình đánh dấu nửa số trường là không bắt buộc và để mô hình tự quyết điền gì” va vào thực tế: thường thì nó không gửi gì.
5. Ranh giới giữa “schema” và “thiết kế giao diện”
Đến giờ ta nói về inputSchema — tức những tham số mô hình phải sinh ra để chạy công cụ. Nhưng sau khi gọi công cụ, còn phải hiển thị kết quả lên UI.
Ở đây nên tách hai tầng:
- Schema cho công cụ mô tả các đối số đầu vào mà mô hình phải sinh. Đây luôn là JSON thuộc không gian MCP / tool‑call.
- Thành phần UI (widget) đọc toolOutput.structuredContent và dựng giao diện từ đó. Định dạng structuredContent cũng do bạn thiết kế, nhưng đó không phải JSON Schema cho mô hình (dù bạn có thể tự chuẩn hóa thêm).
Đôi khi dev cố “một mũi tên trúng hai đích” bằng một JSON duy nhất — vừa làm đầu vào cho mô hình, vừa là dữ liệu cho UI. Hiếm khi ổn. Dễ hơn khi tách:
- inputSchema — dành cho những gì mô hình cần để chạy công cụ;
- structuredContent — dành cho những gì UI cần để vẽ kết quả.
Ví dụ, inputSchema cho suggest_gifts không chứa bất kỳ id quà tặng nào. Còn structuredContent thì ngược lại — chứa danh sách thẻ với id, title, price, link mua hàng, v.v.
6. Annotations và _meta: tác động đến UX và bảo mật
Ngoài schema tham số và cấu trúc phản hồi còn có một lớp nữa — cách nền tảng đối xử với công cụ và hiển thị nó cho người dùng. Phần này do metadata và annotations đảm nhiệm.
Ngoài các trường chuẩn title, description, inputSchema, công cụ có thể có metadata và annotations bổ sung. Trong Apps SDK và MCP, một phần nằm trong _meta (ví dụ, securitySchemes), một phần khác — trong các trường gợi ý đặc thù OpenAI như readOnlyHint và destructiveHint.
Điều quan trọng: các annotation này không thay đổi JSON Schema, nhưng ảnh hưởng đến cách ChatGPT hiển thị công cụ cho người dùng và cách nó cân nhắc khi gọi.
Ví dụ: readOnlyHint và destructiveHint
Giả sử bạn có hai công cụ:
- list_gifts — chỉ lấy danh sách quà tặng (an toàn);
- create_order — tạo đơn hàng (tiềm ẩn rủi ro: tiền bạc, địa chỉ, v.v.).
Bạn có thể đánh dấu chúng như sau (giả mã):
server.registerTool(
"list_gifts",
{
title: "List gift suggestions",
description: "Lấy danh sách quà tặng sẵn có theo các bộ lọc đã cho.",
inputSchema: listGiftsInputSchema,
_meta: {
readOnlyHint: true,
},
},
async ({ input }) => { /* ... */ }
);
server.registerTool(
"create_order",
{
title: "Create gift order",
description:
"Tạo đơn hàng cho một món quà cụ thể thay mặt người dùng. Chỉ dùng sau khi có xác nhận rõ ràng.",
inputSchema: createOrderInputSchema,
_meta: {
destructiveHint: true,
},
},
async ({ input }) => { /* ... */ }
);
Ngữ nghĩa như sau. readOnlyHint báo cho ChatGPT rằng công cụ không thay đổi gì và an toàn; mô hình và UI có thể gọi tự do hơn. destructiveHint cho biết công cụ thực hiện hành động không thể đảo ngược hoặc quan trọng, vì vậy UI sẽ yêu cầu xác nhận thường xuyên hơn và mô hình sẽ thận trọng hơn.
Trong ứng dụng quà tặng của bạn, suggest_gifts rõ ràng là read‑only, còn các công cụ tạo đơn hàng, trừ tiền và sửa dữ liệu người dùng nên được đánh dấu là có thể destructive.
openWorldHint và các trường tương tự
Trong vài trường hợp, bạn muốn gợi ý cho mô hình rằng công cụ hoạt động trong “thế giới mở”, tức kết quả không đầy đủ. Ví dụ, search_products sẽ không bao giờ trả về mọi sản phẩm tồn tại, chỉ những sản phẩm liên quan.
Những annotation như vậy giúp mô hình không đưa ra kết luận mạnh kiểu “nếu không thấy sản phẩm trong search_products, nghĩa là nó không tồn tại”. Đây là chi tiết UX tinh tế, nhưng trong ứng dụng production khác biệt rất rõ.
_meta liên quan đến hiển thị UI
Khi công cụ trả về kết quả, bạn có thể chỉ ra thêm trong _meta các thiết lập ảnh hưởng đến widget. Ví dụ: dùng HTML template nào làm output‑template, có cần viền khung không, hiển thị dòng chữ nào trong lúc gọi, v.v.
Chẳng hạn, trong ví dụ chính thức, server đăng ký HTML của widget như một MCP‑resource và tham chiếu tới nó qua _meta["openai/outputTemplate"].
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description: "Gợi ý ý tưởng quà tặng.",
inputSchema: suggestGiftsInputSchemaJson,
_meta: {
"openai/outputTemplate": "ui://widget/gifts.html", // Đây là id của MCP-resource: server.registerResource(...)
"openai/toolInvocation/invoking": "Đang gợi ý quà tặng…", // Hiển thị trong quá trình tìm kiếm
"openai/toolInvocation/invoked": "Đã tìm được các phương án quà tặng", // Hiển thị khi tìm kiếm xong
},
},
async ({ input }) => {
// ...
return {
content: [],
structuredContent: { items: gifts },
};
}
);
Như vậy, bạn mô tả tại một nơi:
- hình thức dữ liệu đầu vào cho mô hình (inputSchema);
- cách công cụ hiển thị và hành xử trong UI (_meta).
7. Thiết kế schema: nên yêu cầu mô hình điều gì, và không nên điều gì
Một chiếc bẫy điển hình — cố giao phó mọi việc cho mô hình. Ví dụ, bạn mô tả trong inputSchema trường giftId, và trong description viết: “UUID của quà tặng trong cơ sở dữ liệu của chúng tôi”. Mô hình sẽ cố sinh một UUID kiểu "0f21b5f0-5a3a-4d1b-8f0b-9f1a6e3c1234", nhưng vấn đề là món quà đó rất có thể không tồn tại trong hệ thống của bạn.
Quy tắc tốt: đừng yêu cầu mô hình sinh ra các định danh kỹ thuật và dữ liệu gắn với thế giới nội bộ của bạn.
Nên làm kịch bản nhiều bước:
- suggest_gifts trả về danh sách quà tặng với id, title, price, v.v.;
- UI/mô hình cho phép người dùng chọn một phương án được đề xuất;
- create_order nhận giftId từ tập đã tồn tại.
Xét theo khía cạnh schema, điều đó có nghĩa:
- inputSchema của các công cụ hướng “ra ngoài” (về phía người dùng) chỉ mô tả những gì con người có thể nhập hợp lý: tham số tìm kiếm, bộ lọc, tiêu chí;
- inputSchema của các công cụ thao tác thực thể nội bộ dựa vào các id đã biết, thay vì yêu cầu mô hình tự bịa.
Với ứng dụng quà tặng của bạn, điều đó nghĩa là trong suggest_gifts bạn không yêu cầu mô hình “nghĩ ra mã SKU”, mà chỉ các tham số truy vấn. SKU sẽ được gắn ở phía backend, và UI sẽ hiển thị cho người dùng.
Lưu ý: SKU — mã định danh sản phẩm. Ví dụ "GFT-CHC-500-BS".
8. Khối thực hành nhỏ: lắp ghép tất cả với nhau
Hãy gom tất cả những gì đã nói: schema Zod, sinh JSON Schema, đăng ký công cụ với _meta và dùng schema trong logic nghiệp vụ. Ta sẽ xây một ví dụ tối thiểu nhưng trọn vẹn cho ứng dụng quà tặng.
Đầu tiên là schema Zod và kiểu:
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
const SuggestGiftsInputZod = z.object({
relationship: z
.enum(["friend", "partner", "sibling", "colleague", "parent"])
.describe("Kiểu quan hệ với người nhận quà."),
maxBudget: z
.number()
.min(0)
.describe("Ngân sách tối đa theo đơn vị tiền tệ của người dùng."),
interests: z
.array(
z
.string()
.min(1)
.describe("Thẻ sở thích ngắn gọn, ví dụ: videogames.")
)
.min(1)
.describe("Danh sách sở thích của người nhận."),
});
type SuggestGiftsInput = z.infer<typeof SuggestGiftsInputZod>;
const suggestGiftsInputSchemaJson = zodToJsonSchema(
SuggestGiftsInputZod,
"SuggestGiftsInput"
);
Tiếp theo — đăng ký công cụ với _meta cho UI:
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description:
"Dùng khi cần gợi ý ý tưởng quà tặng dựa trên ngân sách, quan hệ và sở thích.",
inputSchema: suggestGiftsInputSchemaJson,
_meta: {
"openai/outputTemplate": "ui://widget/gifts.html",
"openai/toolInvocation/invoking": "Đang gợi ý quà tặng…",
"openai/toolInvocation/invoked": "Đã tìm được các phương án quà tặng",
readOnlyHint: true,
},
},
async ({ input }) => {
const args = SuggestGiftsInputZod.parse(input) as SuggestGiftsInput;
const gifts = await findGifts(args); // logic nghiệp vụ của bạn
return {
content: [],
structuredContent: {
items: gifts,
},
};
}
);
Ở đâu đó gần đó bạn sẽ có hàm nghiệp vụ đã được gõ kiểu:
async function findGifts(input: SuggestGiftsInput) {
// tại đây có thể dùng input.relationship, input.maxBudget, input.interests
// và trả về một mảng đối tượng kiểu Gift
return [
{
id: "gift-1",
title: "Board game về trò chơi điện tử",
price: 45,
currency: "USD",
},
];
}
Ở phía widget, bạn sẽ lấy window.openai.toolOutput.structuredContent.items và vẽ các thẻ, nhưng chi tiết hơn sẽ nói sau vài bài nữa.
9. Lỗi thường gặp khi mô tả công cụ
Lỗi số 1: Mô tả trường quá chung chung hoặc vô nghĩa.
Nếu bạn viết description: "Ngày" hoặc description: "Tham số lọc", mô hình nhận gần như bằng không thông tin hữu ích. Nó giống kiểu tài liệu “phương thức làm điều gì đó quan trọng”. Hãy dùng mô tả trả lời “điền gì vào đây” và “theo định dạng nào”. Ví dụ: “Ngày theo chuẩn ISO 8601 ở định dạng YYYY-MM-DD, ví dụ "2025-02-14"” hoặc “Số tiền theo đơn vị tiền tệ của người dùng, ví dụ: 49.99”.
Lỗi số 2: Thiếu enum ở nơi hợp lý.
Nhiều dev lười biến chuỗi thành enum và giữ type: "string". Kết quả là mô hình bịa ra giá trị, backend bất ngờ, UI vỡ. Nếu bạn có tập giá trị cố định (relationship, loại trạng thái, cách sắp xếp) — gần như luôn nên dùng enum và liệt kê các giá trị có thể. Điều này tăng mạnh tính dự đoán của tool‑call.
Lỗi số 3: Hai nguồn sự thật cho schema và kiểu.
Kinh điển: trong TypeScript bạn đổi maxBudget thành priceMax, nhưng quên trong JSON Schema. Mô hình vẫn gửi maxBudget, còn code chờ priceMax, tất cả sụp đổ. Thường các lỗi này chỉ lộ ở production. Vì vậy tốt nhất ngay từ đầu dùng Zod hoặc công cụ tương tự để sinh cả kiểu và JSON Schema từ một khai báo duy nhất.
Lỗi số 4: Yêu cầu mô hình sinh định danh nội bộ.
Các trường như userId, giftId, orderId, nếu bạn mô tả là “UUID của người dùng trong hệ thống của chúng tôi”, sẽ tất yếu được mô hình điền bằng giá trị bịa. Ngay cả khi bạn thêm pattern cho UUID, mô hình sẽ chỉ bắt đầu sinh UUID “trông đúng” nhưng chẳng khớp với gì cả. Các trường như vậy tốt hơn nên được điền ở backend theo ngữ cảnh (xác thực, tool‑call trước), thay vì nhờ mô hình.
Lỗi số 5: Những schema “thần thánh” khổng lồ cho mọi trường hợp.
Đôi lúc muốn làm một công cụ do_everything với đối tượng khổng lồ, nửa nullable, nửa optional. Mô hình sẽ “đuối”. Tốt hơn là chia chức năng thành vài công cụ với schema hẹp và dễ hiểu: một cái tìm quà, cái khác lấy chi tiết quà, cái thứ ba tạo đơn hàng.
Lỗi số 6: Bỏ qua _meta và annotations.
Nhiều dev chỉ dùng name, description và inputSchema, bỏ qua các trường _meta như openai/outputTemplate và các gợi ý như destructiveHint. Thế là các công cụ “âm thầm” làm hành động nguy hiểm không có gợi ý hay xác nhận trong UI. Điều này làm giảm sự tin cậy của người dùng và tạo rủi ro thao tác ngoài ý muốn. Hãy dùng annotations để đánh dấu rõ công cụ read‑only và công cụ nguy hiểm, cũng như đặt các trạng thái thực thi thân thiện.
Lỗi số 7: Không xác thực đầu vào ở server.
Ngay cả khi JSON Schema và Zod đã mô tả mọi thứ, chỉ dựa vào mô hình là rủi ro. Đôi khi mô hình trả về dữ liệu chỉ “gần đúng” hợp lệ hoặc bạn đổi schema mà quên các ràng buộc nghiệp vụ. Bọc handler trong try { parse } catch { ... } với thông báo lỗi thân thiện sẽ cho mô hình cơ hội chỉnh lại đối số, và cho bạn cơ hội không làm sập cả dịch vụ vì một tool‑call lỗi.
GO TO FULL VERSION