Learn > Claude > MCP trong Claude API — dựng server FastMCP, client, resources & prompts

MCP trong Claude API — dựng server FastMCP, client, resources & prompts

Phần 6/7 ghi chú Building with the Claude API: Model Context Protocol — communication flow client ↔ server, dựng MCP server bằng FastMCP, test bằng MCP Inspector, viết MCP client, expose resources và prompts.

  • MCP chuẩn hoá cách app nối tool/data: server expose tools · resources · prompts; client làm cầu nối app ↔ server.
  • Dựng server bằng FastMCP SDK (decorator + type hints), test độc lập bằng MCP Inspector trước khi cắm vào app.
  • Resources = expose DATA (giống GET handler, đọc theo MIME); prompts = instruction pha sẵn chất lượng cao (list_prompts / get_prompt).

TL;DR — MCP end-to-end trong khoá: flow client ↔ server, dựng server bằng FastMCP, test bằng Inspector, viết client 2 method, rồi mở rộng sang resourcesprompts.

Phần 6/7 của ghi chú khoá Building with the Claude API — đề thi thử 589 câu (chấm điểm + giải thích) nằm ở tab "Đề thi thử" trên trang tổng quan.

MCP — Model Context Protocol (mở màn)

TL;DR mini. MCP (Model Context Protocol) = communication layer cung cấp cho Claude context + tools mà anh không phải viết đống integration code tẻ nhạt. ⭐ Cốt lõi: shift the burden of tool definitions and execution — thay vì server CỦA ANH tự viết & chạy mọi tool, việc đó chuyển sang MCP servers chuyên biệt. Kiến trúc: MCP Client (server của anh) kết nối các MCP Servers chứa tools · prompts · resources; mỗi MCP server = interface tới một outside service (GitHub, AWS…). ⚠️ Misconception kinh điển: "MCP chỉ là tool use" — SAI; hai khái niệm complementary nhưng khác nhau: tool use là cơ chế, còn MCP trả lời câu hỏi AI là người tạo & maintain tools (với MCP: người khác đã viết sẵn, đóng gói trong server).

MCP là gì. MCP = communication layer (tầng giao tiếp) đưa context và tools đến Claude mà không cần anh viết integration code. Cách nghĩ chuẩn: đây là cách đẩy gánh nặng tool definitions + execution ra khỏi server của anh, sang các MCP server chuyên biệt.

Kiến trúc cơ bản. Sơ đồ anh sẽ gặp khắp nơi:

  • MCP Client = server của anh (app đang gọi Claude API).
  • MCP Servers = các server chứa tools · prompts · resources.
  • Mỗi MCP server đóng vai interface tới một outside service nào đó.

Ví dụ thực tế — GitHub chatbot. Anh xây chat interface cho user hỏi Claude về dữ liệu GitHub của họ: "What open pull requests are there across all my repositories?" → Claude cần tools để chạm vào GitHub API.

Vấn đề nếu KHÔNG có MCP — the tool function problem. GitHub có chức năng khổng lồ: repositories · pull requests · issues · projects… Muốn chatbot phủ hết, anh phải tự viết số lượng tool khủng khiếp — mỗi tool = schema definition + function implementation (đúng pipeline 5 bước đã học ở module tool use). Toàn bộ đống code đó anh phải viết → test → maintain.

MCP giải quyết thế nào. Thay vì anh viết, các tool GitHub được authored & executed BÊN TRONG một MCP server chuyên trách. MCP server đóng vai wrapper quanh chức năng GitHub, cung cấp pre-built tools — anh dùng luôn, không implement lại. Nói cách khác: MCP server đóng gói integration phức tạp thành reusable components mà app nào cũng cắm vào được.

3 câu hỏi thường gặp (thi hay xoáy):

  1. Ai viết MCP server?Bất kỳ ai cũng viết được. Thường thì chính service provider ra bản official (vd AWS ra MCP server chính chủ cho các dịch vụ AWS).
  2. MCP khác gì gọi API trực tiếp? — MCP cho anh tool schemas + functions đã định nghĩa sẵn. Gọi API trực tiếp = anh tự chịu trách nhiệm viết toàn bộ tool definitions. MCP tiết kiệm đúng phần công đó.
  3. ⚠️ "MCP chẳng qua là tool use"? — Misconception phổ biến. MCP và tool use bổ trợ nhau nhưng khác nhau: MCP nói về chuyện AI LÀ NGƯỜI làm công việc tạo & maintain tools. Với MCP, người khác đã viết sẵn tool functions + schemas — chúng nằm gói trong MCP server.

Góc Delivery Manager. (1) Đây là bài toán build-vs-buy của integrations — không MCP = anh nuôi cả codebase tool GitHub (viết, test, maintain, chạy theo API changes của họ); có MCP = gánh maintain chuyển về phía người viết server (thường là chính chủ service). Với DM, đây là khác biệt giữa "own một liability" và "consume một service". (2) MCP ≈ chuẩn USB-C của AI tools — trước kia mỗi app tự hàn dây riêng vào từng service; MCP chuẩn hoá cổng cắm để một server viết một lần, mọi app dùng lại. Đó chính là lý do hệ sinh thái MCP bùng nổ. (3) Anh đang dùng MCP hằng ngày mà có thể không để ý — Claude Code cắm MCP servers (Cloudflare, GitHub…) chính là mô hình này: em (client) không biết code bên trong tool, chỉ thấy schema và gọi; server lo phần thực thi.

MCP client & communication flow — ai chuyển tin cho ai

TL;DR mini. MCP client = communication bridge giữa server của anhMCP servers — là access point tới mọi tool mà MCP server cung cấp; nó lo hết message passing + protocol details, anh chỉ tập trung logic app. ⭐ MCP transport agnostic (không phụ thuộc cách truyền): phổ biến nhất là client & server cùng máy, nói qua standard input/output (stdio), nhưng cũng chạy được qua HTTP · WebSockets · các network protocol khác. Client ↔ server trao đổi bằng message types định trong MCP spec, 2 cặp chính: ListToolsRequest / ListToolsResult (hỏi "server có tool gì?") và CallToolRequest / CallToolResult (bảo "chạy tool X với args Y" → nhận kết quả).

MCP client làm gì. MCP client là cầu giao tiếp giữa server của anh và các MCP server. Cứ coi nó là access point duy nhất tới toàn bộ tool của MCP server. Khi cần dùng external tool/service, client xử lý hết phần message passing và chi tiết protocol thay anh.

Transport agnostic. Một điểm mạnh của MCP: client và server giao tiếp qua nhiều phương thức khác nhau đều được (thuật ngữ = transport agnostic).

  • Setup phổ biến nhất: client + server cùng một máy, nói qua standard input/output.
  • Ngoài ra chạy được qua: HTTP · WebSockets · các network protocol khác.

Message types — 2 cặp phải thuộc. Sau khi kết nối, client & server trao đổi các message type định nghĩa trong MCP specification:

  • ListToolsRequest / ListToolsResult — client hỏi server "anh có những tool nào?" → nhận về danh sách tool khả dụng.
  • CallToolRequest / CallToolResult — client bảo server "chạy tool này với các đối số này" → nhận về kết quả.

Full flow — "What repositories do I have?" Trọn vòng khi user hỏi một câu cần tool:

  1. User gửi query tới server của anh.
  2. Server nhận ra cần đưa cho Claude danh sách tool trước khi hỏi.
  3. Server nhờ MCP client → client gửi ListToolsRequest tới MCP server → nhận ListToolsResult.
  4. Giờ server có đủ: câu hỏi của user + danh sách toolgọi Claude (initial request).
  5. Claude soi tool, quyết định cần gọi một tool → trả về tool use request.
  6. Server nhờ MCP client chạy tool Claude xin → client gửi CallToolRequest tới MCP server → MCP server gọi thật GitHub API.
  7. GitHub trả data → chảy ngược: MCP server gói thành CallToolResult → về MCP client → về server của anh.
  8. Server gửi tool result về Claude trong follow-up message.
  9. Claude có đủ thông tin → trả lời hoàn chỉnh → server đưa lại cho user.

⭐ Nhìn kỹ: đây đúng cái agent loop tool use anh đã build (initial request → tool use → run tool → tool result → final answer), chỉ khác bước "run tool" giờ đi qua MCP client → MCP server → outside service thay vì anh tự chạy hàm. Nhiều bước nhưng mỗi component một trách nhiệm rõ ràng; MCP client abstract away phần phức tạp của giao tiếp server để anh chỉ lo application logic.

Góc Delivery Manager. (1) "Transport agnostic" = tách interface khỏi deployment — cùng một MCP server chạy local qua stdio lúc dev, rồi đổi sang HTTP/WebSocket khi lên production mà không đụng application logic. Với DM, đây là điểm bán hàng: một integration, nhiều môi trường triển khai. (2) Chỉ có 2 cặp message cần nhớListTools* (khám phá) và CallTool* (thực thi). Toàn bộ sự phức tạp của "gọi GitHub" bị nén vào một cặp request/result chuẩn hoá; đó là lý do một client nói chuyện được với mọi MCP server. (3) Vòng lặp không mới, chỉ dời chỗ thực thi — anh đã hiểu agent loop; MCP chỉ chèn client + server vào đúng bước "run tool". Hiểu vậy thì khỏi sợ sơ đồ "9 bước" trông rối.

MCP project setup — dựng CLI chatbot (client + server)

TL;DR mini. Dự án hands-on: xây một CLI-based chatbot cho user tương tác với một collection of documents qua command line. Hệ gồm 2 phần: (1) MCP client — xử lý user interaction; (2) custom MCP server — quản lý document operations, cấp 2 tool: read nội dung + update nội dung. Documents lưu in-memory (không cần database, cho gọn). ⚠️ Note kiến trúc quan trọng: đời thực thường chỉ làm MỘT phía (server để expose service cho dev khác, HOẶC client để nối tới MCP server có sẵn); dự án này làm cả hai thuần cho mục đích học — để thấy chúng giao tiếp với nhau ra sao.

Xây cái gì. Một chatbot dòng lệnh, cho phép hỏi/sửa một bộ tài liệu. Hai component:

  • MCP client — nhận và xử lý tương tác của user.
  • Custom MCP server — quản lý thao tác tài liệu, cung cấp 2 tool thiết yếu: một để đọc nội dung document, một để cập nhật chúng.
  • Documents lưu in-memory — cho đơn giản, không cần DB.

⚠️ Vì sao làm cả hai phía. Dự án thật anh thường chỉ làm một trong hai:

  • Viết MCP server để expose service của mình cho dev khác dùng, HOẶC
  • Viết MCP client để nối tới các MCP server có sẵn.

Ở đây làm cả client lẫn server hoàn toàn vì giáo dục — để hiểu hai bên communicate & phối hợp thế nào.

Các bước setup (theo README trong cli_project.zip):

  1. Thêm Anthropic API key vào file .env.
  2. Cài dependencies bằng UV (khuyến nghị) hoặc pip.
  3. Chạy app khởi động để verify.

Chạy app. Trong thư mục dự án có các file chính: main.py · mcp_client.py · mcp_server.py. Khởi động bằng:

# UV (khuyến nghị)
uv run main.py

# hoặc Python chuẩn
python main.py

App chạy được → hiện chat prompt. Test câu đơn giản "what's 1+1?" → phải có đáp nhanh từ Claude là ổn.

Góc Delivery Manager. (1) ⭐ "Đời thực chỉ làm một phía" là câu chốt cần nhớ — nó định hình quyết định kiến trúc: mình có service muốn cho người ngoài xài → viết server; mình có app muốn dùng đồ người khác → viết client. Làm cả hai chỉ có lý do học. (2) In-memory documents = cắt nhiễu — bài học về MCP, không phải về database; giữ storage tối giản để tập trung đúng cơ chế giao tiếp, đây là tư duy scope tốt cho mọi POC. (3) Tách 3 file main / mcp_client / mcp_server phản chiếu đúng sơ đồ (Our Server / MCP Client / MCP Server) — đọc code sẽ thấy từng nhân vật trong diagram hiện ra thành một file.

MCP server — dựng bằng FastMCP SDK (decorator + type hints)

TL;DR mini. Xây MCP server đơn giản hẳn khi dùng official Python MCP SDK: thay vì tự viết JSON schema phức tạp cho tool, SDK tự lo bằng decorator + type hints. Khởi tạo server chỉ 1 dòng: mcp = FastMCP("DocumentMCP", log_level="ERROR"). Documents lưu in-memory trong một Python dict (key = doc_id, value = nội dung). Định nghĩa tool bằng @mcp.tool(name=..., description=...) đặt trên một hàm Python thường; type hints của tham số + Field (Pydantic, mô tả từng tham số) → SDK tự sinh JSON schema cho Claude. 2 tool: read_document (đọc theo doc_id) và edit_document (find-and-replace bằng str.replace). Lỗi doc_id không tồn tại → raise ValueError kèm message rõ để Claude đọc & xử lý.

Khởi tạo server — 1 dòng. SDK biến việc dựng server thành cực gọn:

from mcp.server.fastmcp import FastMCP

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

Storage in-memory — một dict. doc_id làm key, nội dung làm value:

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 expenditure",
    "outlook.pdf": "This document presents the projected future performance of the",
    "plan.md": "The plan outlines the steps for the project's implementation.",
    "spec.txt": "These specifications define the technical requirements for the equipment"
}

Tool = decorator + type hints, KHÔNG viết JSON schema tay. ⭐ Đây là điểm ăn tiền của SDK. Ở module tool use trước anh phải tự tay viết cả JSON schema (name · description · input_schema…). Giờ chỉ cần gắn decorator @mcp.tool lên một hàm Python thường — SDK đọc type hints tự sinh schema đó.

Tool 1 — đọc 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]
  • @mcp.tool tự sinh JSON schema Claude cần.
  • Field (từ Pydantic) đưa mô tả tham số giúp Claude hiểu mỗi đối số mong đợi gì (chính là phần description trong input_schema mà trước đây phải viết tay).

Tool 2 — sửa document (find-and-replace):

@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)
  • 3 tham số: doc_id · old_str (text cần tìm) · new_str (text thay vào).
  • Dùng thẳng str.replace() cho gọn.

Error handling. Cả 2 tool đều check doc_id: không tồn tại → raise ValueError(...) với message mô tả. ⭐ Nối lại bài học cũ: Claude ĐỌC được error message và có thể gọi lại tool với tham số đã sửa (Claude learns from errors) — nên message phải rõ nghĩa, không phải lỗi vô hồn.

Lợi ích của cách dùng SDK:

  • Auto JSON schema sinh từ Python type hints.
  • Code sạch, dễ đọc, dễ maintain.
  • Pydantic validation sẵn cho tham số.
  • Ít boilerplate hơn viết schema tay.
  • Type safety + IDE hỗ trợ.

Góc Delivery Manager. (1) ⭐ Đây là "trước/sau" rõ nhất của MCP — nhớ module tool use anh cày cả pipeline 5 bước chỉ để có một tool schema đúng? FastMCP nén nó còn 1 decorator + type hints. Đó chính là nghĩa đen của "shift the burden": SDK gánh phần schema/protocol, anh chỉ viết business logic. (2) Type hints giờ là tài liệu SỐNGdoc_id: str = Field(description=...) vừa validate vừa là mô tả Claude đọc; một chỗ khai báo, ba tác dụng (type-check · schema · hướng dẫn Claude). Với team, đây là kiểu code tự-tài-liệu-hoá đáng nhân rộng. (3) edit_document cố ý ngây thơ (str.replace khớp tuyệt đối cả whitespace) — giống hệt cơ chế old_str/new_str của text editor tool. Đơn giản để học cơ chế; production sẽ cần lo khớp nhiều lần, không tìm thấy, escape… nhưng đúng tinh thần simplicity first của khoá.

MCP Inspector — test server độc lập (không cần Claude/app)

TL;DR mini. Khi xây MCP server, anh cần cách test tool mà không phải nối vào app đầy đủ. Python MCP SDK có sẵn một browser-based inspector để debug & test real-time. Chạy mcp dev mcp_server.py → khởi động dev server ở port 6277 + cho một local URL mở trên trình duyệt. Bấm "Connect" (bên trái) để chạy server → hiện thanh nav Resources · Prompts · Tools… Test tool: vào Tools"List Tools" → chọn tool → điền tham số"Run Tool" xem kết quả. ⭐ Chain được thao tác (edit xong read lại để xác nhận đổi đúng). Giá trị: eliminate việc phải wire server vào Claude/app chỉ để test chức năng cơ bản → dev nhanh & tập trung hơn.

Khởi động inspector. Đảm bảo Python environment đã activate (xem README lấy lệnh chính xác), rồi:

mcp dev mcp_server.py

→ chạy dev server ở port 6277, cho một local URL để mở trong browser → load MCP Inspector dashboard.

⚠️ Lưu ý giao diện. MCP inspector đang được phát triển tích cực nên UI có thể khác screenshot hiện tại — nhưng chức năng lõi (test tools/resources/prompts) vẫn tương tự.

Connect & test tool. Bấm "Connect" bên trái để start server → hiện nav bar Resources · Prompts · Tools… Các bước test:

  1. Vào mục Tools.
  2. Bấm "List Tools" → thấy mọi tool khả dụng.
  3. Chọn một tool → mở giao diện test của nó.
  4. Điền tham số cần thiết.
  5. Bấm "Run Tool" → chạy & xem kết quả.

Ví dụ test document. Muốn test tool đọc: nhập doc_id (vd "deposition.md") → Run → inspector hiện kết quả (nội dung trả về hoặc message thành công). ⭐ Chain thao tác để verify: sau khi edit_document thay text, chạy ngay read_document lại để xác nhận thay đổi đã áp đúng.

Development workflow. Inspector tạo một dev loop hiệu quả:

  1. Sửa code MCP server.
  2. Test từng tool qua inspector.
  3. Verify kết quả — không cần dựng app đầy đủ.
  4. Debug vấn đề cô lập (in isolation).

Góc Delivery Manager. (1) ⭐ Đây là "unit test bằng tay" cho tool — tách khâu "tool có chạy đúng không" khỏi khâu "Claude gọi tool có đúng không". Bug ở tầng nào sửa tầng đó, khỏi mò cả stack. Với DM, đây là kỷ luật test in isolation kinh điển, chỉ khác vỏ. (2) mcp dev = giống npm run dev của thế giới MCP — một dev server + URL trình duyệt để soi live; anh làm web nên mental model này quen tay ngay. (3) Cảnh báo "UI khác screenshot" là bài học đọc tài liệu tool đang tiến hoá: bám luồng thao tác (Connect → List Tools → Run) chứ đừng bám hình — nút có thể đổi chỗ nhưng khái niệm thì bền.

MCP client (code) — cầu nối app ↔ server bằng 2 method

TL;DR mini. Xong server → tới lượt client, thứ cho app giao tiếp với MCP server. Client gồm 2 phần: (1) MCP Clientclass tự viết bọc lại cho dễ dùng + tự lo cleanup; (2) Client Sessionkết nối thật tới server, thuộc MCP Python SDK. Vì session cần cleanup tài nguyên đúng cách nên mình bọc nó trong class MCP Client để cleanup tự động. App cần client làm 2 việc: lấy danh sách tool gửi cho Claude + chạy tool khi Claude yêu cầu → cài 2 method: list_tools()call_tool() (đều async).

Kiến trúc client — 2 phần. Đừng lẫn:

  • MCP Client = class anh tự tạo để dùng session cho tiện + auto cleanup.
  • Client Session = kết nối thật tới server, là của MCP Python SDK (không phải anh viết).

⭐ Lý do bọc: Client Session đòi cleanup tài nguyên khi dùng xong → gói trong class MCP Client để lo hết chuyện đó tự động (dùng async with).

Client làm 2 việc cho app (nhớ lại flow): (1) lấy list tool để gửi Claude · (2) execute tool khi Claude xin. Hai việc này lộ ra qua 2 method gọn.

Method list_tools() — lấy tool từ server:

async def list_tools(self) -> list[types.Tool]:
    result = await self.session().list_tools()
    return result.tools

→ truy cập session (kết nối tới server) → gọi hàm built-in list_tools() → trả result.tools.

Method call_tool() — chạy một tool trên server:

async def call_tool(
    self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
    return await self.session().call_tool(tool_name, tool_input)

→ truyền tool_name + tool_input (do Claude cung cấp) xuống server → trả CallToolResult.

Test client độc lập (harness). Chạy thẳng client, không cần Claude:

async with MCPClient(
    command="uv", args=["run", "mcp_server.py"]
) as client:
    result = await client.list_tools()
    print(result)

→ in ra định nghĩa tool, gồm read_doc_contentsedit_document đã tạo ở server. ⭐ async with = khớp với chuyện auto-cleanup: hết block thì session tự đóng.

Ghép full flow — hỏi Claude về một document:

  1. Code dùng client lấy available tools.
  2. Gửi tools kèm câu hỏi user cho Claude.
  3. Claude quyết định dùng tool read_doc_contents.
  4. Code dùng client để execute tool đó.
  5. Kết quả gửi về Claude → Claude trả lời user.

VD hỏi "What is the contents of the report.pdf document?" → Claude gọi tool đọc → trả về info document "20m condenser tower" đã set trong server. ⭐ Client = cây cầu giữa application logicMCP server, che hết chi tiết kết nối bên dưới.

Góc Delivery Manager. (1) ⭐ Cả sơ đồ MCP rút gọn còn đúng 2 methodlist_tools() (= cặp ListTools*) và call_tool() (= cặp CallTool*). Mấy message type nghe hàn lâm ở bài trước, giờ hiện nguyên hình thành 2 lời gọi hàm một dòng. Đó là bằng chứng "client abstracts away complexity" không phải lời nói suông. (2) Tách MCP Client (tiện dụng) khỏi Client Session (kết nối thật) là mẫu wrapper/RAII kinh điển — quấn tài nguyên-cần-dọn vào một object tự-dọn (async with), khỏi rò kết nối. Team nào từng quên đóng connection pool sẽ thấm. (3) tool_input do CLAUDE cấp — nối lại điểm cũ: Claude quyết định tên tool + tham số, client chỉ thi hành. Ranh giới "ai nghĩ / ai làm" giữ nguyên từ đầu module tới giờ.

MCP resources — expose DATA (giống GET handler)

TL;DR mini. Resources = cách MCP server expose DATA cho client, giống GET request handler của một HTTP server. ⭐ Ranh giới vàng: resources expose data · tools perform actions — resource để FETCH thông tin, không phải để làm gì. Theo request-response: client gửi ReadResourceRequest kèm một URI (địa chỉ của resource) → server trả data. 2 loại: Direct Resource (URI tĩnh, vd docs://documents) và Templated Resource (URI có tham số, vd docs://documents/{doc_id} — SDK tự parse param từ URI thành keyword argument cho hàm). Định nghĩa bằng decorator @mcp.resource(uri, mime_type=...); mime_type = gợi ý kiểu data (application/json · text/plain…); SDK tự serialize (khỏi tự đổi JSON).

Vì sao cần resources — ví dụ document mention. Muốn làm tính năng gõ @document_name để nhắc file, anh cần 2 thao tác:

  • Lấy danh sách mọi document (cho autocomplete khi user gõ @).
  • Lấy nội dung một document cụ thể (khi được mention → inject vào prompt gửi Claude).

Cách hoạt động. Request-response: client gửi ReadResourceRequest + URI → server trả data. URI đóng vai địa chỉ của resource muốn lấy.

2 loại resource:

  • Direct Resource — URI tĩnh, không đổi: docs://documents.
  • Templated Resource — URI có tham số: docs://documents/{doc_id}. SDK tự parse tham số từ URI và truyền vào hàm dạng keyword argument.

Code — Direct (list documents):

@mcp.resource(
    "docs://documents",
    mime_type="application/json"
)
def list_docs() -> list[str]:
    return list(docs.keys())

Code — Templated (fetch một document):

@mcp.resource(
    "docs://documents/{doc_id}",
    mime_type="text/plain"
)
def fetch_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

Tên tham số trong URI template ({doc_id}) → thành đối số hàm (doc_id).

MIME types. Resource trả bất kỳ kiểu data nào (string · JSON · binary…); mime_type cho client gợi ý đang nhận kiểu gì:

  • application/json — data JSON có cấu trúc.
  • text/plain — text thuần.
  • MIME type hợp lệ khác cho định dạng khác.

⭐ SDK tự serialize return valuekhông cần tự convert sang JSON string.

Test bằng Inspector. Chạy mcp dev mcp_server.py → trong inspector thấy tách 2 mục: Resources (direct/static) và Resource Templates (có tham số). Click để test & xem đúng cấu trúc response client sẽ nhận.

Key points:

  • Resources expose data · tools perform actions (ranh giới cốt lõi).
  • Direct cho static data · Templated cho parameterized query.
  • MIME types giúp client hiểu format.
  • SDK lo serialization tự động.
  • Tên tham số trong URI template = tên đối số hàm.

Góc Delivery Manager. (1) ⭐ "Resources = GET, tools = POST/action" là mental model chuẩn REST anh có sẵn — resource để đọc (idempotent, không side-effect), tool để làm (đổi trạng thái). Phân vai đúng ngay từ đầu giúp API sạch: đừng nhét việc "sửa doc" vào resource, cũng đừng bắt tool gánh việc "liệt kê doc". (2) URI có template là routingdocs://documents/{doc_id} y hệt route /documents/:id bên web; SDK parse param hộ, anh chỉ khai tên. Anh làm web nên khỏi học lại khái niệm, chỉ đổi vỏ. (3) Feature @mention là ví dụ rất "đời": autocomplete = direct resource (list), inject nội dung = templated resource (fetch) — một tính năng UX quen thuộc lắp từ đúng 2 mảnh resource, thấy ngay resource dùng để làm gì thật.

MCP read_resource (client-side) — fetch & parse theo MIME

TL;DR mini. ⭐ Điểm bán hàng của resource: cho phép nhét data THẲNG vào prompt, khỏi cần tool call để lấy info → tương tác nhanh & hiệu quả hơn. Client cần một method read_resource(uri): gọi await self.session().read_resource(AnyUrl(uri)) → nhận resultcontents (một list; thường chỉ cần contents[0], kèm metadata mimeType). Parse theo MIME: nếu isinstance(resource, types.TextResourceContents)mimeType == "application/json"json.loads(resource.text); ngược lại trả resource.text. Cần import json + from pydantic import AnyUrl.

Vì sao resource "ăn tiền". Resource cho server expose data để nhét thẳng vào prompt thay vì phải tool call mới lấy được info. ⭐ Claude nhận nội dung document ngay trong promptbỏ được vòng tool call → nhanh hơn, ít bước hơn (nhớ: mỗi tool call = thêm ≥1 lượt gọi API).

Client là cầu nối. Khi server đã khai resource, client cần cách request & dùng chúng — client lo giao tiếp + parse data tự động. Flow: user gõ @report.pdf → app dùng client fetch resource đó từ server → include nội dung vào prompt gửi Claude.

Code — read_resource trong client:

async def read_resource(self, uri: str) -> Any:
    result = await self.session().read_resource(AnyUrl(uri))
    resource = result.contents[0]

    if isinstance(resource, types.TextResourceContents):
        if resource.mimeType == "application/json":
            return json.loads(resource.text)

        return resource.text
  • Response server có contentslist — thường chỉ cần phần tử đầu (contents[0]), chứa data thật + metadata (mimeType).
  • Parse theo MIME: JSON → json.loads ra Python object; text thuần → trả resource.text (string). ⭐ mimeType là gợi ý để chọn cách parse.

Import cần thêm:

import json
from pydantic import AnyUrl

Test qua CLI."What's in the @report.pdf document?" → app: hiện autocomplete resource khả dụng → chọn → fetch content tự độnginclude vào prompt cho Claude. ⭐ Claude nhận nội dung trực tiếp trong prompt, không cần tool call → nhanh & hiệu quả.

Tách bạch trách nhiệm. read_resourcebuilding block cho các phần khác của app gọi (fetch nội dung · list resource · nhét data vào prompt). Client lo giao tiếp với server; application logic lo dùng data thế nào.

Góc Delivery Manager. (1) ⭐ Resource vs Tool là 2 đường lấy data, chọn theo tình huống — nếu app đã biết cần document nào (user chỉ đích danh @report.pdf), dùng resource nhét thẳng vào prompt, rẻ hơn vì bỏ được vòng tool call. Nếu để Claude tự quyết cần đọc gì thì mới cần tool. Đây là quyết định cost/latency thật, không phải chuyện style. (2) Parse theo mimeType = defensive coding — cùng một read_resource xử được nhiều kiểu (JSON → object, text → string); server đổi kiểu trả về, client vẫn đỡ. (3) read_resource là "building block" — nối lại điểm client = bridge: viết một lần, nhiều chỗ trong app tái dùng; ranh giới "client giao tiếp · app logic quyết định dùng" giữ code sạch, đúng bài separation of concerns.

MCP prompts — khuôn thứ 3: instruction pha sẵn, chất lượng cao

TL;DR mini. Prompts = khuôn thứ 3 của MCP server (sau tools = làm, resources = đọc). Đây là pre-built, high-quality instructions do tác giả MCP server soạn & test kỹ, cho client dùng thay vì tự viết prompt từ đầu. ⭐ Insight: user vẫn tự làm được việc đó, nhưng prompt đã tinh chỉnh cho kết quả nhất quán & chất lượng hơn. Cơ chế: prompt định nghĩa một loạt user/assistant messages; client request prompt → server trả về list messages gửi thẳng cho Claude. Định nghĩa bằng @mcp.prompt(name=..., description=...); from mcp.server.fastmcp import base; hàm trả list[base.Message] (vd [base.UserMessage(prompt)]).

Vì sao dùng prompt. VD muốn Claude reformat document sang markdown. User gõ "convert report.pdf to markdown" cũng chạy — nhưng kết quả tốt hơn nhiều với một prompt đã test kỹ, có chỉ dẫn cụ thể về formatting · structure · output requirements. ⭐ Giá trị: prompt là chuyên môn của tác giả server đóng gói sẵn — thứ user khó tự có.

Cách hoạt động. Prompt định nghĩa một set user/assistant messages mà client dùng trực tiếp. Client request một prompt → server trả list messages → gửi thẳng cho Claude. Cấu trúc cơ bản:

  • Định nghĩa bằng decorator @mcp.prompt().
  • Thêm name + description cho mỗi prompt.
  • Trả về list messages hợp thành prompt hoàn chỉnh.
  • Prompt nên chất lượng cao · test kỹ · liên quan mục đích server.

Code — lệnh format document:

from mcp.server.fastmcp import base

@mcp.prompt(
    name="format",
    description="Rewrites the contents of the document in Markdown format."
)
def format_document(
    doc_id: str = Field(description="Id of the document to format")
) -> list[base.Message]:
    prompt = f"""
Your goal is to reformat a document to be written with markdown syntax.

The id of the document you need to reformat is:

{doc_id}

Add in headers, bullet points, tables, etc as necessary. Feel free to add in extra formatting.
Use the 'edit_document' tool to edit the document. After the document has been reformatted...
"""

    return [
        base.UserMessage(prompt)
    ]

⭐ Để ý: prompt này bảo Claude dùng edit_document tool — tức prompt phối hợp với tool của server; doc_id được nội suy vào text.

🐛 Gotcha import (đã verify trên mcp 1.8.0): dòng from mcp.server.fastmcp import base trong tài liệu gốc ImportError. Đường đúng là from mcp.server.fastmcp.prompts import base (chứa UserMessage · AssistantMessage · Message). Đây là lỗi thứ 2 của tài liệu (sau "Model Control Protocol"). Đề thi có thể vẫn hỏi theo dòng gốc, nhưng khi chạy thật phải sửa import.

Test qua Inspector. Vào mục Prompts → chọn prompt → điền tham số → inspector hiện messages sinh ra sẽ gửi cho Claude → verify biến nội suy đúng + cấu trúc message đúng trước khi dùng thật.

Best practices:

  • Tập trung task trung tâm mục đích server.
  • Viết instruction chi tiết, cụ thể (không mơ hồ).
  • Test kỹ với nhiều input.
  • Description rõ để user hiểu prompt làm gì.
  • Cân nhắc prompt phối hợp với tools & resources của server.
  • ⭐ Prompt nên đem lại giá trị user khó tự có — thể hiện expertise trong domain server phủ.

Góc Delivery Manager. (1) ⭐ Prompt = "SOP đóng gói" phát qua protocol — thay vì mỗi user tự mò câu lệnh, tác giả server chuẩn hoá cách hỏi tốt nhất một lần rồi phát cho mọi client (giống mình chuẩn hoá checklist/quy trình cho team). Đây là "expertise as a service", đúng tinh thần shift-the-burden xuyên suốt MCP. (2) 3 khuôn = 3 chủ thể kích hoạttool do Claude quyết gọi, resource do app fetch, prompt do user chọn (gõ /format). Nhớ trục "ai kích hoạt" này thì khỏi lẫn 3 primitive. (3) Prompt nối liền với tool/resource — ca format ra lệnh Claude dùng edit_document; một prompt tốt là chất keo điều phối cả tool lẫn resource, không đứng một mình. Đó là lý do best practice nhắc "cân nhắc phối hợp với tools/resources".

MCP prompts (client-side) — list_prompts & get_prompt

TL;DR mini. Mặt client của prompt: cài 2 method. list_prompts() = await self.session().list_prompts()result.prompts (mọi prompt khả dụng). get_prompt(prompt_name, args) = await self.session().get_prompt(prompt_name, args)result.messages (một conversation feed thẳng vào Claude). ⭐ args là dict — key phải khớp tham số hàm prompt phía server; MCP server truyền chúng dạng keyword argument, nội suy giá trị vào template. CLI: gõ / → prompt hiện như command → chọn → hệ hỏi argument (vd doc_id) → prompt nội suy gửi Claude → Claude dùng tool hoàn tất task.

list_prompts — lấy mọi prompt từ server:

async def list_prompts(self) -> list[types.Prompt]:
    result = await self.session().list_prompts()
    return result.prompts

get_prompt — lấy một prompt cụ thể (đã nội suy argument):

async def get_prompt(self, prompt_name, args: dict[str, str]):
    result = await self.session().get_prompt(prompt_name, args)
    return result.messages

→ trả result.messages = một hội thoại feed thẳng vào Claude.

Argument hoạt động thế nào. Hàm prompt phía server nhận tham số (vd def format_document(doc_id: str)). Khi client gọi get_prompt, dict args phải chứa đúng key server mong đợi; MCP server truyền vào dạng keyword argumentnội suy nội dung động vào template.

Test qua CLI./ → prompt hiện ra dạng command. Chọn một cái → hệ có thể hỏi chọn option (vd document nào) → prompt hoàn chỉnh gửi Claude. Workflow:

  1. User chọn prompt (vd format).
  2. Hệ hỏi required arguments (vd document nào để format).
  3. Prompt nội suy giá trị → gửi Claude.
  4. Claude có thể dùng tool fetch thêm data & hoàn tất task.

Prompts bắc cầu giữa predefined functionality và dynamic user needs — cho Claude structured starting point cho task phức tạp, vẫn linh hoạt nhờ parameterization.

Góc Delivery Manager. (1) ⭐ Đối xứng client↔server đã đủ bộ 3 khuôn — mỗi khuôn có cặp method client tương ứng: toollist_tools/call_tool · resourcelist_resources/read_resource · promptlist_prompts/get_prompt. Nhìn ra pattern này thì cả SDK gói gọn trong một bảng, học cái mới chỉ là điền ô còn trống. (2) args là dict khớp key — y hệt truyền props/params; sai key thì server không nội suy được. Chỗ này đúng bài "hợp đồng interface" mình hay soi khi nghiệm thu: hai bên phải thống nhất tên tham số. (3) Prompt trả messages (một hội thoại), không phải string — nó là điểm khởi đầu có cấu trúc rồi Claude tự chạy tiếp (gọi tool). Đây là lý do prompt mạnh hơn "một câu lệnh": nó dựng sẵn cả sân khấu cho Claude diễn.


Phần tiếp theo: Claude Code & agentic workflows — parallelization, chaining, routing, agents

Nguồn: Building with the Claude API (Anthropic Academy) — Copyright Anthropic. Phần đề thi thử cho khoá này nằm ở tab "Đề thi thử" trên trang tổng quan khoá.