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"
Mở đầu: Hai dòng code đổi mọi thứ
{
"type": "function",
"function": {
"name": "read_document",
"description": "Read document contents",
"parameters": {
"type": "object",
"properties": {
"doc_id": {
"type": "string",
"description": "Id of the document to read"
}
},
"required": ["doc_id"]
}
}
}
Rồi viết handler Python phân tích payload khi Claude gọi tool. Rồi khi bạn thêm field mới, phải cập nhật cả hai chỗ. Rồi test từng edge case. Rồi khi bạn có 10 tool, code của bạn trông như một file tools.json khổng lồ và một dispatcher.py dài vô tận.
MCP Python SDK — đặc biệt là thành phần FastMCP — thay đổi câu chuyện này chỉ với decorator pattern kiểu Flask/FastAPI:
@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")):
return docs[doc_id]
Đó là tất cả. Không JSON schema. Không dispatcher. Không boilerplate. SDK tự sinh schema từ type hints, đăng ký tool, route incoming request tới hàm của bạn, serialize return value theo đúng format MCP. Bạn viết Python, SDK lo phần còn lại.
Nhưng đây là điều ít người nhận ra: decorator chỉ là 20% câu chuyện. 80% chất lượng của MCP server phụ thuộc vào cách bạn viết description — và đó là kỹ năng prompt engineering, không phải kỹ năng coding.
Bài này sẽ dạy cả hai.
Setup FastMCP — 3 dòng đầu tiên
Mọi MCP server Python bắt đầu với 3 dòng này:
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("DocumentMCP", log_level="ERROR")
Giải thích:
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.Fieldtừ Pydantic — thêm description cho tham số. Đây là cách SDK biến Python type hints thành JSON schema MCP.
Sau dòng này, bạn đã có mcp object sẵn sàng nhận decorator tool/resource/prompt.
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:
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"
}
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.
Tool đầu tiên: Read Document
@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]
Bóc tách từng phần
Decorator @mcp.tool(...)
name— Tên tool Claude sẽ thấy. Dùng snake_case, descriptive. Không đặttool1,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.
Function signature
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ì.
Body function
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ànhCallToolResult.
Flow khi Claude gọi tool
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
Tool thứ hai: Edit Document
@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)
Đ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_strcho find-and-replace.
- Description tham số
old_strcó 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
docsdict. Trong production, thay bằng DB commit.
- Không return value tường minh — Python return
None. MCP client nhậnCallToolResultvới content rỗng. Claude hiểu tool đã chạy thành công (không có error).
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:
MCP servers and tools are really a discipline of prompting.
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Ệ:
@mcp.tool(name="gen_image", description="Generates an image")
def generate_image(prompt: str): ...
Vấn đề: Model không biế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?
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:
@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"
)): ...
Tại sao TỐT hơn:
- 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 đó
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".
Bảng so sánh: Thinking REST vs Thinking for LLM
| 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 ý |
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.
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)
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)
@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)
@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)
if __name__ == "__main__":
mcp.run()
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).
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:
@mcp.tool(name="search", description="Search things")
- 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="get_doc")
@mcp.tool(name="read_doc")
@mcp.tool(name="fetch_doc")
@mcp.tool(name="retrieve_doc")
- 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_previewvsget_doc_full).
Type hint thiếu hoặc dùng Any
Sai lầm:
def search(query, limit): # no type
...
def create_user(data: Any): # Any defeats the purpose
...
- 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:
@mcp.tool(...)
def generate_report(topic: str):
# Block 60 giây
time.sleep(60)
return "done"
- Tại sao là sai
- Block event loop, timeout client.
- Cách đúng
- Dùng
async def:
@mcp.tool(...)
async def generate_report(topic: str):
await asyncio.sleep(60)
return "done"
Trả data nhạy cảm trong tool result
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.
- 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(...)
def delete_item(item_id: str):
try:
db.delete(item_id)
except:
pass # swallow error
return "ok"
- 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:
docs = {}
@mcp.tool(...)
def add_doc(doc_id, content):
docs[doc_id] = content # race condition
- 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.
Mẹ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:
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
MCP spec hỗ trợ annotation đánh dấu tool nào mutates state:
@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):
...
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="search_v2", ...) # old: search_v1
def search(query: str, filters: dict): ...
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:
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)
SDK tự serialize thành JSON. Claude parse dễ hơn text raw.
Á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:
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("MyFirstMCP", log_level="ERROR")
if __name__ == "__main__":
mcp.run()
- Bước 3
- Copy
docs = {...}từ bài. - Bước 4
- Viết
read_documenttool với description đầy đủ (ít nhất 3 câu). - Bước 5
- Viết
edit_documenttool với 3 tham số. - Bước 6
- Ghi lạ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? ___________
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 ý:
- 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?)
Ghi lại:
- 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?
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:
- 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.)
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ớiisError=True.
Tài liệu tham khảo
- FastMCP documentation
- MCP spec — Tools
- Pydantic Field docs
- "Building with MCP and the Claude API" talk, 10/2025