Lộ trình học›Claude API›Building with the Claude API Mở bản đồ

6.63 · Định nghĩa tools qua MCP

Bài viếtTrung cấp20 phút đọc

Manual schema viết vất vả:

Sau bài này, bạn sẽ
  1. Dùng @mcp.tool decorator + Pydantic Field
  2. Auto-generate JSON schema từ type hints
  3. Implement tools read/edit documents
  4. Handle errors qua ValueError

Mở đầu: SDK làm hết việc nặng

{
    "name": "read_doc",
    "description": "...",
    "input_schema": {
        "type": "object",
        "properties": {"doc_id": {"type": "string", "description": "..."}},
        "required": ["doc_id"]
    }
}

FastMCP SDK:

@mcp.tool(name="read_doc", description="...")
def read_document(doc_id: str = Field(description="...")):
    ...

SDK auto-gen schema từ type hints + Field.

Setup FastMCP

from mcp.server.fastmcp import FastMCP
from pydantic import Field

mcp = FastMCP("DocumentMCP", log_level="ERROR")

FastMCP = convenience wrapper cho building MCP servers quickly.

In-memory data

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."
}

Simple dict. Production: DB, filesystem, API.

Tool 1: Read document

@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]

Claude generates schema:

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

Auto.

Tool 2: Edit document

@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]}...'"

Return confirmation string. Claude parse và relay.

Run server

if __name__ == "__main__":
    mcp.run(transport="stdio")

Start: uv run mcp_server.py (runs as stdio, waits for client).

Test via Inspector: mcp dev mcp_server.py.

Pydantic Field niceties

Default value

@mcp.tool()
def search_docs(
    query: str = Field(description="Search query"),
    limit: int = Field(default=10, description="Max results")
):
    ...

Constraint

@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

from typing import Literal

@mcp.tool()
def set_status(
    status: Literal["pending", "active", "done"] = Field(description="Task status")
):
    ...

All auto-converted to JSON schema.

Complex input types

List

@mcp.tool()
def batch_create(
    titles: list[str] = Field(description="Titles to create")
):
    ...

Nested object (Pydantic model)

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip: str

@mcp.tool()
def create_customer(
    name: str,
    address: Address
):
    ...

FastMCP handles nested types.

Error handling

Tool error → MCP server send isError: true automatically khi you raise:

@mcp.tool()
def buggy_tool(x: int):
    if x < 0:
        raise ValueError("x must be >= 0")
    return f"Got {x}"

Claude receives error message, có thể retry với corrected input.

Tool testing

Via Inspector

Fastest iteration.

Via pytest

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

Unit tests independent of MCP protocol.

Best practices

1. Descriptive tool names

read_doc_contents > get_doc > doc

2. Rich descriptions

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-4 sentences. Claude learn from description.

3. Clear Field descriptions

Every param = description. Examples help:

doc_id: str = Field(description="ID like 'report.pdf' or 'plan.md'")

4. Consistent naming

doc_id, file_id, user_id — consistent suffix.

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

@mcp.tool()
def search(query):  # no type
    ...

Schema incomplete. Claude guess.

Fix: Always type hints: query: str.

Silent fail

if doc_id not in docs:
    return None  # Claude confused

Fix: raise ValueError("specific message").

Á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 bài học

Những điều cốt lõi
  • @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.
Kiểm tra nhanh2 câu hỏi · chọn đáp án để xem giải thích
    Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.