Định nghĩa tools qua MCP

8 — MCPTrung cấp20 phút

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"] } } ``

Bạn sẽ học được
  • 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 result

Best 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.

Nội dung này có hữu ích không?