Định nghĩa Resources trên server

Xây dựng client & mở rộngTrung cấp35 phút

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.

Bạn sẽ học được
  • 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.

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)
┌────────────────────────────────────────────────────────┐
│                                                        │
│    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.pdf

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õ:

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?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?Không
Dùng cho autocomplete UI?Không phù hợpPhù hợp
User thấy được "tool call" in UI?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_input

Bướ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.pdf

Ví 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()  # 500MB

Anti-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 decode

Anti-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.

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
Nội dung này có hữu ích không?