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.
- Giải thích Resources là gì và khác gì với Tools (model-controlled vs app-controlled)
- Phân biệt Direct Resources (URI tĩnh) vs Templated Resources (URI có parameter)
- Thiết kế URI scheme phù hợp cho resource của bạn (docs://, file://, repo://, etc.)
- Sử dụng mime_type để cho client biết cách parse response
- Implement tool vs resource đúng chỗ — tránh confuse 2 primitive
- Test resource qua MCP Inspector với UI riêng cho Resources và Resource Templates
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.
Phân biệt 3 primitive
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.
| Primitive | Controlled by | Use case điển hình |
|---|---|---|
| Tool | Model | Claude quyết gọi (get_data, send_email, create_file) |
| Resource | App | App pre-fetch và inject (autocomplete, context injection) |
| Prompt | User | User trigger (slash command, workflow button) |
┌────────────────────────────────────────────────────────┐
│ │
│ 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 │
│ │
└────────────────────────────────────────────────────────┘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.
Bóc tách:
Khi nào dùng Direct Resource
✅ Phù hợp:
❌ Không phù hợp:
- "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.
- 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)
- Cần param (dùng Templated Resource)
- Action/mutation (dùng Tool)
- Data lớn (>10MB — dùng chunking pattern khác)
@mcp.resource(
"docs://documents",
mime_type="application/json"
)
def list_docs() -> list[str]:
return list(docs.keys())Templated Resources — URI có parameter
Templated Resource embed parameter trong URI. MCP SDK tự parse và pass vào function.
Bóc tách:
Client gọi như thế nào?
Client sẽ ReadResourceRequest với URI đã điền:
- "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.
@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]Client gọi như thế nào?
SDK parse doc_id = "report.pdf", gọi fetch_doc(doc_id="report.pdf").
Khi nào dùng Templated Resource
✅ Phù hợp:
❌ Không phù hợp:
- Fetch theo ID (user, document, repo)
- Filter theo path segment (logs://server/{date}/{hour})
- Namespaced resource (team://{team_id}/tasks)
- 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
docs://documents/report.pdfURI 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õ:
5 nguyên tắc chọn scheme
1. Scheme tự tài liệu hóa
2. Nhất quán trong cùng server
3. Hierarchical — parent trước child
4. Parameter tên rõ ràng
5. Standard scheme khi có sẵn
- docs:// tốt hơn data://
- invoice:// tốt hơn item://
- Không mix docs://documents/x với doc:x/file.md
- Chọn 1 convention, dùng xuyên suốt
- repo://anthropic/claude/issues/42 tốt hơn issue://42-in-anthropic-claude
- Giống filesystem: /folder/subfolder/file
- docs://documents/{doc_id} tốt hơn docs://documents/{id}
- repo://{owner}/{repo_name} tốt hơn repo://{a}/{b}
- Dùng file:// cho local files (spec chuẩn)
- Dùng https:// cho web resources
- Tự-define cho domain-specific
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)mime_type — Hint cho client parse
Client dùng mime_type để:
Nếu không set, default text/plain.
- 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)
@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")Implementation chi tiết — Document server mở rộng
Thêm 2 resource vào mcp_server.py từ Bài 7.4:
Save. Restart MCP Inspector (uv run mcp dev mcp_server.py). Bây giờ bạn thấy tab Resources có 2 section:
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.
- Resources — liệt kê Direct Resources (docs://documents)
- Resource Templates — liệt kê Templated Resources (docs://documents/{doc_id})
# Sau phần tool definitions
@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]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".
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.
| Tiêu chí | Tool read_doc_contents(doc_id) | Resource docs://documents/{doc_id} |
|---|---|---|
| Ai quyết gọi? | Claude | App (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ợp | Phù hợp |
| User thấy được "tool call" in UI? | Có | Không (silent fetch) |
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
Bước 3: Fetch resource cho mỗi mention
(Code cụ thể ở Bài 7.8 — đây là flow overview)
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
Bước 4: Send augmented prompt
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_inputBước 4: Send augmented prompt
Kết quả
User gõ: "Tóm tắt @report.pdf"
Prompt thực sự gửi Claude:
messages.append({"role": "user", "content": augmented_prompt})
response = claude.messages.create(...)Kết quả
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.
<document name='report.pdf'>
The report details the state of a 20m condenser tower.
</document>
Tóm tắt @report.pdfVí dụ theo ngành — Resource patterns thực tế
📁 Filesystem MCP — local file access
Resources:
Sử dụng: Claude Desktop expose filesystem MCP để user @mention file từ máy.
🛠️ GitHub MCP — repo context
Resources:
Sử dụng: Cursor/Zed @mention PR/issue → IDE inject context trực tiếp.
📊 Linear MCP — task management
Resources:
Sử dụng: Michael Cohen weekly update: app fetch cycle resource → prompt Claude summarize.
📚 Context7 — library docs
Resources:
Sử dụng: IDE extension @mention library → get fresh docs injected (bypass model knowledge cutoff).
🏥 Medical records MCP
Resources:
Sử dụng (tham khảo AbbVie case): Clinical researcher @mention patient ID → full context aggregated vào prompt.
- file:///absolute/path — Read file
- file:///absolute/path/ (trailing slash) — List dir
- github://repo/{owner}/{name}/file/{path} — File content
- github://repo/{owner}/{name}/pr/{number} — PR details
- github://issue/{owner}/{name}/{number} — Issue
- linear://issue/{id} — Single issue
- linear://cycle/{id} — Cycle summary
- linear://team/{key}/active-cycle — Current cycle
- context7://lib/{package}/docs — Full documentation
- context7://lib/{package}/api/{symbol} — Specific symbol
- patient://{id}/timeline — Chronological aggregate
- patient://{id}/labs — Lab results
- patient://{id}/imaging — Scan list
Anti-patterns — Những sai lầm cần tránh
❌ Dùng Resource cho mutation
Sai lầm:
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("docs://delete/{doc_id}")
def delete_doc(doc_id: str):
del docs[doc_id]
return "deleted"❌ URI không có namespace
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õ:
❌ mime_type sai
Sai lầm:
- 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"
@mcp.resource("documents")
@mcp.resource("users")
@mcp.resource("files")❌ mime_type sai
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="text/plain")
def get_json_config() -> dict:
return {"key": "value"}Anti-patterns — Những sai lầm cần tránh (tiếp)
❌ Resource trả quá lớn
Sai lầm:
@mcp.resource("...", mime_type="application/json")
def get_json_config() -> dict:
return {"key": "value"}❌ Resource trả quá lớn
Tại sao là sai: Consume memory server, payload HTTP giant, client crash.
Cách đúng: Pagination / chunking:
@mcp.resource("logs://all")
def all_logs() -> str:
return open("/var/log/app.log").read() # 500MBAnti-patterns — Những sai lầm cần tránh (tiếp)
❌ URI không stable
Sai lầm:
@mcp.resource("logs://page/{page}")
def logs_page(page: int) -> list[str]:
return get_logs_paginated(page, size=100)❌ URI không stable
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("docs://documents/{random_id}")❌ Không handle MIME cho binary
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:
@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 decodeAnti-patterns — Những sai lầm cần tránh (tiếp)
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:
Python SDK: expose ResourceUpdatedNotification. Dùng cho app cần reactive data.
Mẹo 2: Resource annotation
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:
- Live logs
- Collaborative editing
- Real-time dashboard
@mcp.resource(
"docs://documents/{doc_id}",
mime_type="text/plain",
annotations={
"audience": ["user", "assistant"],
"priority": 0.8, # model relevance
}
)Mẹo 3: List resources dynamically
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.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()
]Mẹo 4: Combine tool + resource
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.
@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]Á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:
Bước 2: Thêm templated resource fetch doc theo ID:
@mcp.resource("docs://documents", mime_type="application/json")
def list_docs():
return list(docs.keys())Bài tập 1: Thêm resources (~15 phút)
Bước 3: Save, restart uv run mcp dev mcp_server.py.
Bước 4: Trong Inspector:
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ó:
Thiết kế URI scheme:
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:
Verify trong Inspector:
Suy ngẫm: App của bạn nên chọn variant nào khi user @mention? Lý do?
- 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.
- 1000+ contacts, mỗi contact có email, phone, company
- 500+ deals, mỗi deal có stage, value, owner
- 50+ companies
- Resource list all contacts: _______________________
- Resource get contact theo ID: _______________________
- Resource list deals của 1 company: _______________________
- Resource get deal chi tiết: _______________________
- docs://documents/{doc_id} (text/plain) — chỉ nội dung
- docs://documents/{doc_id}/meta (application/json) — metadata JSON {"id", "size_bytes", "preview"}
- Variant text/plain return raw content?
- Variant JSON return dict serialize?
- Inspector UI render khác nhau giữa 2 mime type?
@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]Tóm tắt bài học
🎯 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.
- MCP spec — Resources
- RFC 6570 URI Templates
- MIME types list (IANA)
- "Defining resources in MCP" — Anthropic Academy video