Trước năm 2024, nếu bạn muốn cho Claude sử dụng tool, bạn phải viết một JSON schema như thế này:
- Khởi tạo một MCP server bằng FastMCP chỉ với 2 dòng code
- Định nghĩa tool qua decorator @mcp.tool() với type hints + Field descriptions
- Viết mô tả tool chất lượng — thứ quyết định 80% chất lượng tool của bạn
- Xử lý lỗi và validation đúng cách trong tool implementation
- Viết tool đầu tay: read_doc_contents và edit_document
- Hiểu vì sao "designing tools for LLM" khác với "designing REST API"
Setup FastMCP — 3 dòng đầu tiên
Mọi MCP server Python bắt đầu với 3 dòng này:
Giải thích:
Sau dòng này, bạn đã có mcp object sẵn sàng nhận decorator tool/resource/prompt.
- FastMCP — Class abstraction chính. Tên "Fast" là ode to FastAPI, không phải về speed.
- "DocumentMCP" — Tên server. Xuất hiện trong handshake với client. Chọn tên có ý nghĩa, không generic như "Server".
- log_level="ERROR" — Giảm log spam. Dùng "DEBUG" khi troubleshoot.
- Field từ Pydantic — thêm description cho tham số. Đây là cách SDK biến Python type hints thành JSON schema MCP.
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("DocumentMCP", log_level="ERROR")Dữ liệu mẫu: Document storage trong memory
Để không phân tâm vào database/filesystem, chúng ta sẽ lưu docs trong một dict đơn giản:
Key = document ID, value = content. Trong thực tế bạn có thể thay bằng SQLite, S3, hay REST API — pattern vẫn đúng.
docs = {
"deposition.md": "This deposition covers the testimony of Angela Smith, P.E.",
"report.pdf": "The report details the state of a 20m condenser tower.",
"financials.docx": "These financials outline the project's budget and expenditures",
"outlook.pdf": "This document presents the projected future performance of the system",
"plan.md": "The plan outlines the steps for the project's implementation.",
"spec.txt": "These specifications define the technical requirements for the equipment"
}Tool đầu tiên: Read Document
Bóc tách từng phần
Decorator @mcp.tool(...)
Function signature
Body function
Flow khi Claude gọi tool
- name — Tên tool Claude sẽ thấy. Dùng snake_case, descriptive. Không đặt tool1, read, hay tên quá generic.
- description — Cực kỳ quan trọng. Đây là system prompt của tool. Model đọc description để quyết định khi nào gọi tool này.
- doc_id: str — Type hint bắt buộc. SDK dùng type để generate JSON schema. Không type → không work.
- = Field(description=...) — Mô tả tham số. Claude đọc cái này để biết truyền giá trị gì.
- if doc_id not in docs: raise ValueError(...) — Error handling tự nhiên qua exception Python.
- return docs[doc_id] — Return value được SDK serialize thành CallToolResult.
User: "What's in deposition.md?"
│
▼
Claude API (với tool schema đã gen từ decorator):
"I'll call read_doc_contents with doc_id='deposition.md'"
│
▼
MCP client gửi CallToolRequest:
{ method: "tools/call",
params: { name: "read_doc_contents",
arguments: { doc_id: "deposition.md" }}}
│
▼
Server dispatch → gọi read_document(doc_id="deposition.md")
│
▼
Return: "This deposition covers the testimony of Angela Smith, P.E."
│
▼
MCP server wrap thành CallToolResult → client → Claude → User@mcp.tool(
name="read_doc_contents",
description="Read the contents of a document and return it as a string."
)
def read_document(
doc_id: str = Field(description="Id of the document to read")
):
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
return docs[doc_id]Tool thứ hai: Edit Document
Điểm khác biệt đáng chú ý
- 3 tham số, mỗi cái có description riêng — Đây là pattern điển hình: doc_id + old_str + new_str cho find-and-replace.
- Description tham số old_str có nhắc "Must match exactly, including whitespace" — Đây là prompt engineering. Không có dòng này, Claude có thể truyền text đã strip whitespace, khiến replace fail.
- Side effect — Tool này thay đổi docs dict. Trong production, thay bằng DB commit.
- Không return value tường minh — Python return None. MCP client nhận CallToolResult với content rỗng. Claude hiểu tool đã chạy thành công (không có error).
@mcp.tool(
name="edit_document",
description="Edit a document by replacing a string in the documents content with a new string."
)
def edit_document(
doc_id: str = Field(description="Id of the document that will be edited"),
old_str: str = Field(description="The text to replace. Must match exactly, including whitespace."),
new_str: str = Field(description="The new text to insert in place of the old text.")
):
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
docs[doc_id] = docs[doc_id].replace(old_str, new_str)description là system prompt của tool — Cách viết tốt
Đây là phần quan trọng nhất bài này. Michael Cohen (Anthropic) nói thẳng:
Một tool với description tệ sẽ bị Claude bỏ qua hoặc gọi sai cách. Một tool với description tốt như có một senior engineer giải thích cách dùng.
Pattern TỆ vs TỐT
❌ TỆ:
Vấn đề: Model không biết:
Kết quả (case thật từ John Welsh): Claude truyền prompt "cute puppy" và bị Dall-E trả ảnh sai style.
✅ TỐT:
- Dùng diffusion model nào? (SDXL? DALL-E? Midjourney?)
- Prompt theo style nào? (tags comma-separated? natural sentence?)
- Resolution? Aspect ratio?
- Return URL hay binary?
@mcp.tool(name="gen_image", description="Generates an image")
def generate_image(prompt: str): ...Pattern TỆ vs TỐT
Tại sao TỐT hơn:
6 nguyên tắc viết description tốt
1. Nói về "when to use", không chỉ "what it does"
❌ "Get user by ID." ✅ "Get full user profile when you need email, name, role, or preferences. Don't use this for just checking if a user exists—use user_exists() instead for that."
2. Example bên trong description
❌ "Format a date string" ✅ "Format a date string. Input ISO 8601 like '2026-04-19'. Output human-readable like 'April 19, 2026'."
3. Nói về edge case
❌ "Find products matching query" ✅ "Find products matching query. Returns empty list if no match—don't treat as error. Max 100 results."
4. Làm rõ parameter semantics
❌ limit: int = Field(description="Limit") ✅ limit: int = Field(description="Max number of items to return, 1-100. Defaults to 20 if not specified.")
5. Mention cách chaining với tool khác
❌ "Delete a file" ✅ "Delete a file. Call list_files() first to confirm the file exists. Returns confirmation string or raises FileNotFoundError."
6. Ngôn ngữ nhất quán
Nếu tool dùng "document", các tool khác liên quan cũng dùng "document" — không mix với "file", "doc", "record".
- Model biết engine cụ thể → không đoán
- Ví dụ cụ thể giúp model bắt chước format
- Anti-examples giúp tránh failure mode
- Return format rõ ràng → Claude biết xử lý thế nào sau đó
@mcp.tool(
name="gen_image",
description="""Generate an image using SDXL 1.0.
Prompts should be comma-separated phrases describing subject,
style, lighting, composition. Use photographic terms when realistic,
art movement names when artistic.
Examples:
- "mountain landscape at dusk, oil painting, impressionist style,
warm tones, rule of thirds"
- "portrait of a cat wearing glasses, photorealistic, studio
lighting, 85mm lens"
Avoid: full sentences, multiple unrelated subjects, celebrity names.
Returns: URL valid for 24 hours. High-res 1024×1024 by default."""
)
def generate_image(prompt: str = Field(
description="SDXL prompt, comma-separated phrases, <200 tokens"
)): ...Bảng so sánh: Thinking REST vs Thinking for LLM
John Welsh (Anthropic) nhấn mạnh: "Tools cho LLM nên giống natural language API hơn là REST API." Nếu người chưa biết API của bạn đọc description và hiểu cách dùng → model cũng hiểu.
| Tiêu chí | REST API thinking | LLM tool thinking |
|---|---|---|
| Granularity | Nhiều endpoint nhỏ, 1:1 với resource | Ít tool broader, cover nhiều intent |
| Naming | Noun-heavy (/users/:id) | Verb-heavy (get_user_info) |
| Description | Docstring ngắn, link docs | Prompt-style, có example + anti-example |
| Parameters | Flags cứng nhắc, enum | Field với semantic description |
| Error | HTTP status code | Exception Python + clear message |
| Ai dùng? | Frontend dev biết chính xác endpoint cần | LLM đoán từ description |
| Fuzzy input? | Trả 400 | Tool handle gracefully + gợi ý |
Ví dụ thực chiến: Build document server đầy đủ
Tình huống
Bạn đang build MCP server cho legal team xử lý tài liệu. Cần: read, edit, list, search.
Bước 1: Khung server (~1 phút)
Bước 2: Tool read_doc_contents (~2 phút)
# mcp_server.py
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("DocumentMCP", log_level="ERROR")
docs = {
"deposition.md": "Angela Smith testimony...",
"contract.pdf": "Service agreement between...",
# ...
}Bước 2: Tool read_doc_contents (~2 phút)
Bước 3: Tool edit_document (~2 phút)
@mcp.tool(
name="read_doc_contents",
description="""Read the full text contents of a document by its ID.
Use this when the user asks about what's in a specific document or
wants you to analyze, summarize, or quote from it.
The doc_id must match exactly—call list_docs() first if unsure.
Returns the raw document text as a string."""
)
def read_document(
doc_id: str = Field(description="Exact document ID, e.g. 'deposition.md'")
):
if doc_id not in docs:
raise ValueError(f"Doc with id '{doc_id}' not found. Call list_docs() to see available IDs.")
return docs[doc_id]Bước 3: Tool edit_document (~2 phút)
Bước 4: Entry point (~30 giây)
@mcp.tool(
name="edit_document",
description="""Edit a document by replacing exact text with new text.
Use this when the user asks to change, update, correct, or replace
specific text in a document.
Important: old_str must match EXACTLY including whitespace and case.
For safer edits, read the document first to get the exact text."""
)
def edit_document(
doc_id: str = Field(description="Exact document ID to edit"),
old_str: str = Field(description="Text to find and replace. Must match exactly, case-sensitive."),
new_str: str = Field(description="Replacement text. Can be empty string to delete old_str.")
):
if doc_id not in docs:
raise ValueError(f"Doc with id '{doc_id}' not found")
if old_str not in docs[doc_id]:
raise ValueError(f"Text not found in document. Check for exact match including whitespace.")
docs[doc_id] = docs[doc_id].replace(old_str, new_str)Bước 4: Entry point (~30 giây)
Kết quả
File mcp_server.py ~50 dòng Python, 2 tool production-grade. So với tool use thuần (không MCP), tiết kiệm ~30 dòng JSON schema + ~20 dòng dispatcher code. Và tool này chạy ngay được với bất kỳ MCP client nào (Claude Desktop, Cursor, custom).
if __name__ == "__main__":
mcp.run()Ví dụ theo ngành — Tools thực tế đang ship
⚖️ Legal — Document review
Tool: read_contract(contract_id), flag_unusual_clauses(contract_id), compare_versions(v1, v2)
Description lý tưởng: "Flag clauses that deviate from standard templates. Focus on indemnity, termination, and liability sections. Return list of {clause_section, concern, severity}."
Kết quả thực tế: Legal team Anthropic (nguồn: claude__20251208) dùng pattern này tự động hóa contract review ban đầu, chỉ escalate những contract có clause bất thường.
💼 Sales — CRM integration
Tool: get_account(domain), log_call(account_id, notes), suggest_next_action(account_id)
Description lý tưởng: "Get full account context including recent interactions, open deals, and last touchpoint. Use before any outreach to avoid duplicate follow-up."
Kết quả thực tế: Sales rep prep cho call trong 5 phút thay vì 30 phút lục CRM.
📊 Data — Analytics query
Tool: run_sql(query, database), describe_table(table_name), sample_data(table_name, n)
Description lý tưởng: "Run read-only SQL. Writes are blocked. Use describe_table first if unsure about schema. Queries over 30s will timeout."
Kết quả thực tế: Non-engineer (product manager, CS lead) tự query database qua chat.
🏥 Healthcare — Patient records
Tool: get_patient_timeline(patient_id), flag_drug_interactions(medications), search_trials(criteria)
Description lý tưởng: "Aggregate labs, imaging, notes for a single patient chronologically. De-identified by default. Only flags drug interactions ≥ moderate severity."
Kết quả thực tế (nguồn: AbbVie case study, claude__20251020): Clinical team aggregate patient data 5 phút thay vì 30 phút.
🏢 Real Estate — Listing analysis
Tool: get_comps(address, radius_miles), market_trend(zip_code), generate_listing_desc(property)
Description lý tưởng: "Return comparable sold properties in past 6 months. Filter by similar sqft ±15% and beds/baths count."
Kết quả thực tế: Agent tạo listing presentation cá nhân hóa trong 30 phút thay vì 3-4 giờ.
Anti-patterns — Những sai lầm cần tránh
❌ Description lười một dòng
Sai lầm:
Tại sao là sai: Claude không biết tìm gì, không biết input format, không biết output shape.
Cách đúng: Tối thiểu 2-3 câu description, có example, có edge case.
❌ Nhiều tool overlap
Sai lầm:
@mcp.tool(name="search", description="Search things")❌ Nhiều tool overlap
Tại sao là sai: Model bối rối chọn cái nào. David Soria Parra: "If you put three issue-checker MCP servers next to each other, of course the model can get confused."
Cách đúng: 1 tool rõ ràng. Nếu 2 tool khác nhau đủ, đặt tên phản ánh sự khác biệt (get_doc_preview vs get_doc_full).
❌ Type hint thiếu hoặc dùng Any
Sai lầm:
@mcp.tool(name="get_doc")
@mcp.tool(name="read_doc")
@mcp.tool(name="fetch_doc")
@mcp.tool(name="retrieve_doc")
# 4 tool cùng làm 1 việc❌ Type hint thiếu hoặc dùng Any
Tại sao là sai: SDK không gen đúng schema. Claude không biết truyền kiểu gì.
Cách đúng: Type hint cụ thể cho MỌI tham số. Dùng Optional[str] thay vì Any.
❌ Tool thực thi lâu không async
Sai lầm:
def search(query, limit): # no type
...
def create_user(data: Any): # Any defeats the purpose
...❌ Tool thực thi lâu không async
Tại sao là sai: Block event loop, timeout client.
Cách đúng: Dùng async def:
@mcp.tool(...)
def generate_report(topic: str):
# Block 60 giây
time.sleep(60)
return "done"Anti-patterns — Những sai lầm cần tránh (tiếp)
❌ Trả data nhạy cảm trong tool result
Sai lầm:
@mcp.tool(...)
async def generate_report(topic: str):
await asyncio.sleep(60)
return "done"❌ Trả data nhạy cảm trong tool result
Tại sao là sai: Data leak. Claude có thể echo lại trong response.
Cách đúng: Filter field nhạy cảm server-side TRƯỚC khi return.
❌ Silent failure
Sai lầm:
@mcp.tool(name="get_user")
def get_user(user_id: str):
return db.query(f"SELECT * FROM users WHERE id={user_id}")
# Trả cả password hash, SSN, v.v.❌ Silent failure
Tại sao là sai: Claude nghĩ đã xóa thành công. User mất data thầm lặng.
Cách đúng: raise ValueError("Item not found") với message rõ. MCP client nhận error, Claude báo user.
❌ Mutable state không bảo vệ concurrent
Sai lầm:
@mcp.tool(...)
def delete_item(item_id: str):
try:
db.delete(item_id)
except:
pass # swallow error
return "ok"❌ Mutable state không bảo vệ concurrent
Tại sao là sai: Nhiều client gọi cùng lúc → lost writes.
Cách đúng: Dùng lock (asyncio.Lock()) hoặc persistent store (SQLite, Redis) cho production.
docs = {}
@mcp.tool(...)
def add_doc(doc_id, content):
docs[doc_id] = content # race conditionMẹo nâng cao
Mẹo 1: Few-shot examples trong description
Khi tool có pattern input phức tạp, nhét 2-3 ví dụ ngay trong description:
Mẹo 2: Annotations cho destructive tool
MCP spec hỗ trợ annotation đánh dấu tool nào mutates state:
description="""Parse a date range from natural language.
Examples:
- "last week" → {"start": "2026-04-07", "end": "2026-04-13"}
- "Q1 2026" → {"start": "2026-01-01", "end": "2026-03-31"}
- "since jan" → {"start": "2026-01-01", "end": "today"}"""Mẹo 2: Annotations cho destructive tool
Client (VD: Claude Desktop) có thể gate những tool này qua UI confirm, tăng safety.
Mẹo 3: Versioning tool
Khi schema thay đổi breaking:
@mcp.tool(
name="delete_all_docs",
description="Permanently delete ALL documents. Requires confirmation.",
annotations={"destructive": True, "requires_confirmation": True}
)
def delete_all_docs(confirm_token: str):
...Mẹo 3: Versioning tool
Giữ cả search_v1 và search_v2 cho clients legacy, deprecate dần.
Mẹo 4: Structured output with Pydantic
Return Pydantic model để output có shape rõ ràng:
@mcp.tool(name="search_v2", ...) # old: search_v1
def search(query: str, filters: dict): ...Mẹo 4: Structured output with Pydantic
SDK tự serialize thành JSON. Claude parse dễ hơn text raw.
from pydantic import BaseModel
class SearchResult(BaseModel):
items: list[dict]
total_count: int
query_time_ms: int
@mcp.tool(...)
def search(query: str) -> SearchResult:
...
return SearchResult(items=[...], total_count=42, query_time_ms=120)Áp dụng ngay
Bài tập 1: Viết server đầu tay (~20 phút)
Bước 1: Trong project đã setup ở Bài 7.3, tạo file mcp_server.py.
Bước 2: Copy skeleton:
Bước 3: Copy docs = {...} từ bài.
Bước 4: Viết read_document tool với description đầy đủ (ít nhất 3 câu).
Bước 5: Viết edit_document tool với 3 tham số.
Bước 6: Ghi lại:
Bài tập 2 (thử thách): Tool thứ 3 (~15 phút)
Thêm tool list_documents trả về danh sách doc_id + preview 50 ký tự đầu mỗi doc.
Gợi ý:
Ghi lại:
Bài tập 3 (suy ngẫm): Review description
Mở mã nguồn của 1 MCP server open-source bất kỳ (VD: filesystem, git, sqlite). Đọc 5 tool descriptions đầu tiên.
Tự hỏi:
- Thời gian hoàn thành: ___________
- Tool description dài bao nhiêu câu: ___________
- Bạn có thêm validation nào không có trong code mẫu? ___________
- Return kiểu gì? list[dict]? Pydantic model?
- Description nên mention: khi nào nên gọi list trước khi read/edit
- Có cần param filter không? (optional prefix: str = None?)
- Description bạn viết: ___________
- Bạn có dùng Pydantic model không? ___________
- Tool này khác read_doc_contents ở đâu về semantic?
- Description nào rõ ràng nhất? Tại sao?
- Description nào bạn thấy có thể cải thiện? Viết version tốt hơn.
- Pattern nào lặp lại giữa nhiều tool? (VD: examples inline, error case spelled out, etc.)
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("MyFirstMCP", log_level="ERROR")
# TODO: thêm data store
# TODO: thêm read tool
# TODO: thêm edit tool
if __name__ == "__main__":
mcp.run()Tóm tắt bài học
🎯 3 dòng khởi tạo FastMCP — from mcp.server.fastmcp import FastMCP; mcp = FastMCP("Name"). Decorator pattern y Flask.
🎯 Tool = function + decorator — @mcp.tool(name, description) + type hints + Field(description=...). Không JSON schema. Không dispatcher.
🎯 Description là 80% chất lượng — Michael Cohen: "MCP servers and tools are really a discipline of prompting." Đầu tư viết description = đầu tư vào chất lượng.
🎯 Design cho LLM, không cho REST — Broader tool với fuzzy description > 40 endpoint granular. Model handle fuzziness, REST thì không.
🎯 Error handling tự nhiên qua exception — raise ValueError("...") với message rõ ràng. SDK wrap thành CallToolResult với isError=True.
- FastMCP documentation
- MCP spec — Tools
- Pydantic Field docs
- "Building with MCP and the Claude API" talk, 10/2025