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
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
| 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) |
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]. Schemedocs://do bạn tự quyết (không phải chuẩn internet). Pathdocumentschỉ resource cụ thể.mime_type="application/json"— Hint cho client parse response. JSON → client tự gọijson.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ơndata://invoice://tốt hơnitem://
2. Nhất quán trong cùng server
- Không mix
docs://documents/xvớidoc:x/file.md - Chọn 1 convention, dùng xuyên suốt
3. Hierarchical — parent trước child
repo://anthropic/claude/issues/42tốt hơnissue://42-in-anthropic-claude- Giống filesystem:
/folder/subfolder/file
4. Parameter tên rõ ràng
docs://documents/{doc_id}tốt hơndocs://documents/{id}repo://{owner}/{repo_name}tốt hơnrepo://{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/listtext/plain→ raw stringtext/markdown→ có thể render hoặc parse markdownimage/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? | 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) |
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 filefile:///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 contentgithub://repo/{owner}/{name}/pr/{number}— PR detailsgithub://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 issuelinear://cycle/{id}— Cycle summarylinear://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 documentationcontext7://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 aggregatepatient://{id}/labs— Lab resultspatient://{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)+ resourcedocs://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ọistr(...)decode UTF-8 sai.- Cách đúng
- Return
bytesvà để 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://documentskhô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 dungdocs://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
- 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_typequan trọng — Client dùng nó để parse:application/jsonautojson.loads,text/plainraw, binary cầnbytesreturn.- 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
- MCP spec — Resources
- RFC 6570 URI Templates
- MIME types list (IANA)
- "Defining resources in MCP" — Anthropic Academy video