Manual schema viết vất vả: ``python { "name": "read_doc", "description": "...", "input_schema": { "type": "object", "properties": {"doc_id": {"type": "string", "description": "..."}}, "required": ["doc_id"] } } ``
- Dùng @mcp.tool decorator + Pydantic Field
- Auto-generate JSON schema từ type hints
- Implement tools read/edit documents
- Handle errors qua ValueError
Setup FastMCP
FastMCP = convenience wrapper cho building MCP servers quickly.
# mcp_server.py
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("DocumentMCP", log_level="ERROR")In-memory data
Simple dict. Production: DB, filesystem, API.
docs = {
"deposition.md": "This deposition covers Angela Smith's testimony.",
"report.pdf": "The report details a 20m condenser tower inspection.",
"financials.docx": "Q4 budget breakdown and expenditures.",
"outlook.pdf": "Future performance projections.",
"plan.md": "Project implementation steps.",
"spec.txt": "Technical requirements for equipment."
}Tool 1: Read document
Claude generates schema:
@mcp.tool(
name="read_doc_contents",
description="Read contents of a document and return as string."
)
def read_document(
doc_id: str = Field(description="ID of the document to read")
) -> str:
if doc_id not in docs:
raise ValueError(f"Doc '{doc_id}' not found. Available: {list(docs.keys())}")
return docs[doc_id]Tool 1: Read document (tiếp)
Auto.
{
"name": "read_doc_contents",
"description": "Read contents of a document and return as string.",
"input_schema": {
"type": "object",
"properties": {
"doc_id": {"type": "string", "description": "ID of the document to read"}
},
"required": ["doc_id"]
}
}Tool 2: Edit document
Return confirmation string. Claude parse và relay.
@mcp.tool(
name="edit_document",
description="Edit a document by replacing old_str with new_str. old_str must match exactly."
)
def edit_document(
doc_id: str = Field(description="ID of the document to edit"),
old_str: str = Field(description="Text to replace. Must match exactly."),
new_str: str = Field(description="Replacement text.")
) -> str:
if doc_id not in docs:
raise ValueError(f"Doc '{doc_id}' not found")
if old_str not in docs[doc_id]:
raise ValueError(f"Text '{old_str}' not found in document")
docs[doc_id] = docs[doc_id].replace(old_str, new_str)
return f"Edited {doc_id}: replaced '{old_str[:30]}...' with '{new_str[:30]}...'"Run server
Start: uv run mcp_server.py (runs as stdio, waits for client).
Test via Inspector: mcp dev mcp_server.py.
# At bottom of mcp_server.py
if __name__ == "__main__":
mcp.run(transport="stdio")Pydantic Field niceties
Default value
Constraint
@mcp.tool()
def search_docs(
query: str = Field(description="Search query"),
limit: int = Field(default=10, description="Max results")
):
...Constraint
Enum
@mcp.tool()
def create_task(
title: str = Field(description="Task title", min_length=3, max_length=100),
priority: int = Field(description="1-5", ge=1, le=5)
):
...Enum
All auto-converted to JSON schema.
from typing import Literal
@mcp.tool()
def set_status(
status: Literal["pending", "active", "done"] = Field(description="Task status")
):
...Complex input types
List
Nested object (Pydantic model)
@mcp.tool()
def batch_create(
titles: list[str] = Field(description="Titles to create")
):
...Nested object (Pydantic model)
FastMCP handles nested types.
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
zip: str
@mcp.tool()
def create_customer(
name: str,
address: Address
):
...Error handling
Tool error → MCP server send isError: true automatically khi you raise:
Claude receives error message, có thể retry với corrected input.
@mcp.tool()
def buggy_tool(x: int):
if x < 0:
raise ValueError("x must be >= 0")
return f"Got {x}"Tool testing
Via Inspector
Fastest iteration.
Via pytest
Unit tests independent of MCP protocol.
# test_server.py
import pytest
from mcp_server import read_document, edit_document
def test_read():
result = read_document(doc_id="report.pdf")
assert "20m condenser" in result
def test_read_missing():
with pytest.raises(ValueError):
read_document(doc_id="nonexistent.pdf")
def test_edit():
result = edit_document(doc_id="report.pdf", old_str="report", new_str="document")
assert "Edited" in resultBest practices
1. Descriptive tool names
read_doc_contents > get_doc > doc
2. Rich descriptions
3-4 sentences. Claude learn from description.
3. Clear Field descriptions
Every param = description. Examples help:
description="""Read contents of a document stored in the system.
Use when user asks about specific file content or wants full text.
Returns plain text string. Raises error if doc not found."""3. Clear Field descriptions
4. Consistent naming
doc_id, file_id, user_id — consistent suffix.
doc_id: str = Field(description="ID like 'report.pdf' or 'plan.md'")Anti-patterns
❌ Tool names too generic
query, get, run → Claude confused which tool for what.
Fix: Specific: search_emails, get_order_status, run_sql_query.
❌ Tool does too many things
manage_documents(action, doc_id, new_content) → Claude không biết action values.
Fix: Break into read_doc, edit_doc, delete_doc.
❌ Missing type hints
Schema incomplete. Claude guess.
Fix: Always type hints: query: str.
❌ Silent fail
@mcp.tool()
def search(query): # no type
...❌ Silent fail
Fix: raise ValueError("specific message").
if doc_id not in docs:
return None # Claude confusedÁp dụng ngay
Bài tập 1: Implement 2 tools (20 phút)
Complete read_doc_contents và edit_document in mcp_server.py. Test via Inspector.
Bài tập 2: Add tool (30 phút)
Add list_documents() tool returning all doc IDs. Add search_documents(query: str) that fuzzy-matches query in doc content.
Test chaining: "List docs, then search for 'budget'".
Tóm tắt
🎯 @mcp.tool decorator + Pydantic Field = auto JSON schema.
🎯 Type hints critical — drives schema generation.
🎯 Raise ValueError cho error handling. Claude sees và retries.
🎯 FastMCP handles: stdio transport, message routing, serialization.
🎯 Descriptive names + rich descriptions = Claude picks right tool.