Lộ trình học›MCP›Introduction to MCP Mở bản đồ

7.7 · Định nghĩa Resources trên server

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

Hãy tưởng tượng bạn đang xây một chat app giống Claude Desktop, và bạn muốn tính năng "@mention document" — user gõ @ rồi chọn document từ autocomplete, nội dung document tự động được nhét vào prompt.

Sau bài này, bạn sẽ
  1. Giải thích Resources là gì và khác gì với Tools (model-controlled vs app-controlled)
  2. Phân biệt Direct Resources (URI tĩnh) vs Templated Resources (URI có parameter)
  3. Thiết kế URI scheme phù hợp cho resource của bạn (docs://, file://, repo://, etc.)
  4. Sử dụng mime_type để cho client biết cách parse response
  5. Implement tool vs resource đúng chỗ — tránh confuse 2 primitive
  6. Test resource qua MCP Inspector với UI riêng cho Resources và Resource Templates

Mở đầu: Hai cách cho Claude biết về documents

Có 2 cách implement:

Cách 1: Dùng Tool

Vấn đề: Claude phải quyết định gọi tool. Mỗi lần user @mention, Claude sẽ gọi lại tool. Overhead token × latency × cost. Tệ nhất: Claude có thể quên gọi tool nếu prompt dài.

Cách 2: Dùng Resource

Ưu: Không tốn tool call. Content có sẵn trong prompt. Claude không phải "quyết định" — app quyết cho Claude.

Đây là khác biệt cốt lõi giữa Tool và Resource:

  • Tool = Claude quyết định khi nào lấy data (model-controlled)
  • Resource = App quyết định khi nào lấy data (app-controlled)

Bài này sẽ dạy bạn định nghĩa Resources, phần "app đọc resource và inject" sẽ nằm ở Bài 7.8.

Resources là gì — Định nghĩa chính xác

Resources là một primitive MCP cho phép server expose dữ liệu (không phải action) qua URI. Giống REST GET endpoint.

┌────────────────────────────────────────────────────────┐
│                                                        │
│    YOUR APP                                            │
│     │                                                  │
│     │ read_resource("docs://documents/report.pdf")     │
│     ▼                                                  │
│    MCP CLIENT                                          │
│     │                                                  │
│     │ ReadResourceRequest                              │
│     │   { uri: "docs://documents/report.pdf" }         │
│     ▼                                                  │
│    MCP SERVER                                          │
│     │ parse URI → route tới fetch_doc(doc_id=...)      │
│     │ return content                                   │
│     ▼                                                  │
│    ReadResourceResult                                  │
│     │   { contents: [{ mimeType: "text/plain",         │
│     │                   text: "The report..." }] }     │
│     ▼                                                  │
│    App nhận content, inject vào prompt                 │
│                                                        │
└────────────────────────────────────────────────────────┘

Phân biệt 3 primitive

PrimitiveControlled byUse case điển hình
ToolModelClaude quyết gọi (get_data, send_email, create_file)
ResourceAppApp pre-fetch và inject (autocomplete, context injection)
PromptUserUser trigger (slash command, workflow button)

Bài 7.11 sẽ đào sâu decision tree này. Bây giờ chỉ cần biết: nếu app bạn quyết khi nào lấy data, đó là resource.

Direct Resources — URI tĩnh

Direct Resource có URI không đổi. Phù hợp cho "list" hoặc "latest" — những thứ không cần parameter.

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

Bóc tách:

  • "docs://documents" — URI, format [scheme]://[path]. Scheme docs:// do bạn tự quyết (không phải chuẩn internet). Path documents chỉ resource cụ thể.
  • mime_type="application/json" — Hint cho client parse response. JSON → client tự gọi json.loads().
  • Function return — Data bất kỳ Python serialize được. SDK tự JSON-encode.

Khi nào dùng Direct Resource

✅ Phù hợp:

  • Danh sách toàn bộ documents / items
  • Config app hiện tại
  • Metadata (version, stats, config)
  • "Latest X" không cần param (docs://recent-5, feeds://latest)

❌ Không phù hợp:

  • Cần param (dùng Templated Resource)
  • Action/mutation (dùng Tool)
  • Data lớn (>10MB — dùng chunking pattern khác)

Templated Resources — URI có parameter

Templated Resource embed parameter trong URI. MCP SDK tự parse và pass vào function.

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

Bóc tách:

  • "docs://documents/{doc_id}" — URI template kiểu RFC 6570. Tên trong {...} thành keyword argument.
  • doc_id: str — Param name phải match với template. Type hint như tool.
  • Body — Giống tool, throw exception khi error.

Client gọi như thế nào?

Client sẽ ReadResourceRequest với URI đã điền:

docs://documents/report.pdf

SDK parse doc_id = "report.pdf", gọi fetch_doc(doc_id="report.pdf").

Khi nào dùng Templated Resource

✅ Phù hợp:

  • Fetch theo ID (user, document, repo)
  • Filter theo path segment (logs://server/{date}/{hour})
  • Namespaced resource (team://{team_id}/tasks)

❌ Không phù hợp:

  • Query phức tạp (nhiều filter, sort) → dùng Tool search_docs(query, filters) thay vì nhét hết vào URI

URI Scheme design — Quy ước nào tốt?

MCP không ép bạn dùng scheme nào. Nhưng quy ước tốt giúp client/user hiểu rõ:

docs://documents               ← list all docs
docs://documents/{doc_id}      ← get doc theo id

repo://{owner}/{name}          ← get repo
repo://{owner}/{name}/files    ← list files

user://profile                 ← current user (implicit)
user://{user_id}/preferences   ← other user preferences

file:///path/to/file           ← local file (giống standard file URI)

5 nguyên tắc chọn scheme

1. Scheme tự tài liệu hóa

  • docs:// tốt hơn data://
  • invoice:// tốt hơn item://

2. Nhất quán trong cùng server

  • Không mix docs://documents/x với doc:x/file.md
  • Chọn 1 convention, dùng xuyên suốt

3. Hierarchical — parent trước child

  • repo://anthropic/claude/issues/42 tốt hơn issue://42-in-anthropic-claude
  • Giống filesystem: /folder/subfolder/file

4. Parameter tên rõ ràng

  • docs://documents/{doc_id} tốt hơn docs://documents/{id}
  • repo://{owner}/{repo_name} tốt hơn repo://{a}/{b}

5. Standard scheme khi có sẵn

  • Dùng file:// cho local files (spec chuẩn)
  • Dùng https:// cho web resources
  • Tự-define cho domain-specific

mime_type — Hint cho client parse

@mcp.resource(..., mime_type="application/json")
@mcp.resource(..., mime_type="text/plain")
@mcp.resource(..., mime_type="text/markdown")
@mcp.resource(..., mime_type="application/pdf")
@mcp.resource(..., mime_type="image/png")

Client dùng mime_type để:

  • application/json → json.loads(content) → Python dict/list
  • text/plain → raw string
  • text/markdown → có thể render hoặc parse markdown
  • image/png → binary data (base64 encoded trong protocol)

Nếu không set, default text/plain.

Mẹo: Đừng set sai mime_type. Nếu return JSON nhưng mime_type là text/plain, client không auto-parse, bạn phải json.loads() thủ công.

Implementation chi tiết — Document server mở rộng

Thêm 2 resource vào mcp_server.py từ Bài 7.4:


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


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

Save. Restart MCP Inspector (uv run mcp dev mcp_server.py). Bây giờ bạn thấy tab Resources có 2 section:

  • Resources — liệt kê Direct Resources (docs://documents)
  • Resource Templates — liệt kê Templated Resources (docs://documents/{doc_id})

Click vào docs://documents → Read Resource → thấy JSON list tên docs.

Click vào docs://documents/{doc_id} → form yêu cầu nhập doc_id → Read Resource → text plain content.

Bảng so sánh: Tool vs Resource với cùng logic

Xem xét use case: "User muốn nội dung một document".

Tiêu chíTool read_doc_contents(doc_id)Resource docs://documents/{doc_id}
Ai quyết gọi?ClaudeApp (code của bạn)
Khi nào gọi?Khi Claude thấy cần (uncertain)Khi app biết chắc cần (deterministic)
Tốn tool call?Có (1 turn extra)Không
Xuất hiện trong tool list của Claude?CóKhông
Dùng cho autocomplete UI?Không phù hợpPhù hợp
User thấy được "tool call" in UI?CóKhông (silent fetch)

Kết luận thực tế: Có thể define cả hai cho cùng data. Tool cho Claude gọi proactive, Resource cho app fetch khi biết user đã chọn. Không xung đột.

Ví dụ thực chiến: @mention document feature

Tình huống

Bạn muốn CLI chat app support @document-name để include content trực tiếp trong prompt thay vì để Claude tool-call.

Bước 1: Define resources (đã xong ở trên)

Bước 2: App detect @ trong input

def extract_mentions(text: str) -> list[str]:
    """Extract @mentions from user input."""
    import re
    return re.findall(r'@([\w.]+)', text)

Bước 3: Fetch resource cho mỗi mention

(Code cụ thể ở Bài 7.8 — đây là flow overview)

mentions = extract_mentions(user_input)
context_parts = []

for doc_id in mentions:
    content = await mcp.read_resource(f"docs://documents/{doc_id}")
    context_parts.append(f"<document name='{doc_id}'>\n{content}\n</document>")

augmented_prompt = "\n\n".join(context_parts) + "\n\n" + user_input

Bước 4: Send augmented prompt

messages.append({"role": "user", "content": augmented_prompt})
response = claude.messages.create(...)

Kết quả

User gõ: "Tóm tắt @report.pdf"

Prompt thực sự gửi Claude:

<document name='report.pdf'>
The report details the state of a 20m condenser tower.
</document>

Tóm tắt @report.pdf

Claude nhận full context ngay, không cần tool_use. Tiết kiệm 1 turn + ~200 token overhead.

Người dùng thấy: phản hồi nhanh hơn, Claude không "ngập ngừng" với việc call tool.

Ví dụ theo ngành — Resource patterns thực tế

Filesystem MCP — local file access

Resources:

  • file:///absolute/path — Read file
  • file:///absolute/path/ (trailing slash) — List dir

Sử dụng: Claude Desktop expose filesystem MCP để user @mention file từ máy.

GitHub MCP — repo context

Resources:

  • github://repo/{owner}/{name}/file/{path} — File content
  • github://repo/{owner}/{name}/pr/{number} — PR details
  • github://issue/{owner}/{name}/{number} — Issue

Sử dụng: Cursor/Zed @mention PR/issue → IDE inject context trực tiếp.

Linear MCP — task management

Resources:

  • linear://issue/{id} — Single issue
  • linear://cycle/{id} — Cycle summary
  • linear://team/{key}/active-cycle — Current cycle

Sử dụng: Michael Cohen weekly update: app fetch cycle resource → prompt Claude summarize.

Context7 — library docs

Resources:

  • context7://lib/{package}/docs — Full documentation
  • context7://lib/{package}/api/{symbol} — Specific symbol

Sử dụng: IDE extension @mention library → get fresh docs injected (bypass model knowledge cutoff).

Medical records MCP

Resources:

  • patient://{id}/timeline — Chronological aggregate
  • patient://{id}/labs — Lab results
  • patient://{id}/imaging — Scan list

Sử dụng (tham khảo AbbVie case): Clinical researcher @mention patient ID → full context aggregated vào prompt.

Anti-patterns — Những sai lầm cần tránh

Dùng Resource cho mutation

Sai lầm:

@mcp.resource("docs://delete/{doc_id}")
def delete_doc(doc_id: str):
    del docs[doc_id]
    return "deleted"
Tại sao là sai
Resources là GET/read — semantic read-only. Client/app có thể cache, retry, pre-fetch — mutation sẽ gây bug.
Cách đúng
Dùng Tool cho mutation. Resource cho read-only.

URI không có namespace

Sai lầm:

@mcp.resource("documents")
@mcp.resource("users")
@mcp.resource("files")
Tại sao là sai
Khi 2 MCP server cùng dùng documents, conflict. Không tự-tài-liệu-hóa.
Cách đúng
Luôn prefix scheme: docs://documents, users://list.

Mix tool và resource logic

Sai lầm
Define tool get_doc(id) + resource docs://documents/{id} cùng content, không có lý do rõ ràng.
Tại sao là sai
Confuse Claude (nên gọi tool hay...?), tăng surface area maintenance.
Cách đúng
Quyết định rõ:
  • Content dùng cho @mention/pre-fetch? → chỉ resource
  • Content Claude cần tự lấy tuỳ context? → chỉ tool
  • Cả hai? → define cả hai, tool có description rõ ràng "prefer @mention when possible"

mime_type sai

Sai lầm:

@mcp.resource("...", mime_type="text/plain")
def get_json_config() -> dict:
    return {"key": "value"}

Tại sao là sai: Return dict (Python JSON-ish) nhưng mime_type nói "text/plain" → client không auto-parse.

Cách đúng:

@mcp.resource("...", mime_type="application/json")
def get_json_config() -> dict:
    return {"key": "value"}

Resource trả quá lớn

Sai lầm:

@mcp.resource("logs://all")
def all_logs() -> str:
    return open("/var/log/app.log").read()  # 500MB
Tại sao là sai
Consume memory server, payload HTTP giant, client crash.
Cách đúng
Pagination / chunking:
@mcp.resource("logs://page/{page}")
def logs_page(page: int) -> list[str]:
    return get_logs_paginated(page, size=100)

URI không stable

Sai lầm:

@mcp.resource("docs://documents/{random_id}")

Và random_id đổi mỗi restart.

Tại sao là sai
Client cache URI → URI stale → lỗi not found.
Cách đúng
URI stable. Dùng business ID (doc_name, user_email_hash) thay vì UUID tạm.

Không handle MIME cho binary

Sai lầm:

@mcp.resource("images://photo/{id}", mime_type="image/png")
def get_photo(id: str) -> str:
    return open(f"photos/{id}.png").read()  # binary → crashes on decode
Tại sao là sai
read() return bytes, nhưng SDK gọi str(...) decode UTF-8 sai.
Cách đúng
Return bytes và để SDK handle base64:
def get_photo(id: str) -> bytes:
    return Path(f"photos/{id}.png").read_bytes()

Mẹo nâng cao

Mẹo 1: Subscription để auto-refresh

MCP spec hỗ trợ resources/subscribe — client đăng ký, server push update khi resource đổi. Hữu ích cho:

  • Live logs
  • Collaborative editing
  • Real-time dashboard

Python SDK: expose ResourceUpdatedNotification. Dùng cho app cần reactive data.

Mẹo 2: Resource annotation

@mcp.resource(
    "docs://documents/{doc_id}",
    mime_type="text/plain",
    annotations={
        "audience": ["user", "assistant"],
        "priority": 0.8,  # model relevance
    }
)

Annotations giúp client prioritize khi inject context. High-priority resource inject trước.

Mẹo 3: List resources dynamically

Nếu danh sách resource đổi theo state (new docs được tạo), implement list_resources handler:

@mcp.list_resources()
async def list_resources() -> list[types.Resource]:
    return [
        types.Resource(
            uri=f"docs://documents/{doc_id}",
            name=doc_id,
            mimeType="text/plain",
        )
        for doc_id in docs.keys()
    ]

Client sẽ thấy resource xuất hiện/biến mất theo realtime.

Mẹo 4: Combine tool + resource

Pattern mạnh: tool write tạo resource mới, resource read serve content.

@mcp.tool(name="create_note")
def create_note(title: str, content: str) -> str:
    notes[title] = content
    return f"notes://notes/{title}"  # return URI

@mcp.resource("notes://notes/{title}")
def get_note(title: str) -> str:
    return notes[title]

Claude gọi tool tạo note, nhận URI. User sau này @mention note, app fetch resource. Tool + Resource work cùng nhau.

Áp dụng ngay

Bài tập 1: Thêm resources (~15 phút)

Bước 1: Mở mcp_server.py. Thêm direct resource list tất cả doc_id:

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

Bước 2: Thêm templated resource fetch doc theo ID:

@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc(doc_id: str):
    if doc_id not in docs:
        raise ValueError(f"Doc {doc_id} not found")
    return docs[doc_id]
Bước 3
Save, restart uv run mcp dev mcp_server.py.
Bước 4
Trong Inspector:
  • Tab Resources → List → thấy docs://documents không?
  • Tab Resource Templates → thấy docs://documents/{doc_id} không?
  • Test cả hai, ghi lại output.

Bài tập 2: Thiết kế URI scheme (~10 phút)

Bạn đang build MCP server cho một CRM internal. Data có:

  • 1000+ contacts, mỗi contact có email, phone, company
  • 500+ deals, mỗi deal có stage, value, owner
  • 50+ companies

Thiết kế URI scheme:

  • Resource list all contacts: _______________________
  • Resource get contact theo ID: _______________________
  • Resource list deals của 1 company: _______________________
  • Resource get deal chi tiết: _______________________

Câu hỏi: Nên dùng resource hay tool cho "search contacts theo tên"? Giải thích.

Bài tập 3 (thử thách): JSON vs text resource (~15 phút)

Thêm 2 resource variant cho cùng doc:

  • docs://documents/{doc_id} (text/plain) — chỉ nội dung
  • docs://documents/{doc_id}/meta (application/json) — metadata JSON {"id", "size_bytes", "preview"}

Verify trong Inspector:

  • Variant text/plain return raw content?
  • Variant JSON return dict serialize?
  • Inspector UI render khác nhau giữa 2 mime type?

Suy ngẫm: App của bạn nên chọn variant nào khi user @mention? Lý do?

Tóm tắt bài học

Những điều cốt lõi
  • Resource = read-only data exposed qua URI — Semantic "GET request", khác Tool là "action". App quyết khi fetch, không phải Claude.
  • Direct vs Templated — Direct URI tĩnh (docs://documents), Templated có param (docs://documents/{doc_id}). SDK parse param tự động thành kwargs.
  • URI scheme tự tài liệu hóa — docs://, repo://, patient:// tốt hơn generic. 5 nguyên tắc: tự-document, nhất quán, hierarchical, param rõ, standard khi có.
  • mime_type quan trọng — Client dùng nó để parse: application/json auto json.loads, text/plain raw, binary cần bytes return.
  • Tool vs Resource khác biệt cốt lõi — Claude-controlled vs App-controlled. Không xung đột, có thể define cả hai cho cùng data nếu use case khác nhau.

Tài liệu tham khảo

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.

    Khóa học cùng lộ trình