Bạn đã đi qua 10 bài. Đây là mảnh ghép cuối cùng của stack MCP đầy đủ:
- Extend MCPClient class với 2 method cuối cùng: list_prompts() và get_prompt()
- Hiểu cơ chế "variable interpolation" từ client args sang server prompt body
- Implement slash command UX trong CLI: detect /command, show autocomplete, dispatch prompt
- Gửi messages từ prompt template vào agentic loop đúng cách
- Hoàn thành workflow end-to-end: /format plan.md → prompt craft → Claude → tool call → result
Client method: list_prompts()
Pattern giống list_tools() và list_resources():
Mỗi types.Prompt object có:
Output test:
- name — slash command name
- description — hiển thị trong autocomplete
- arguments — list PromptArgument với name, description, required
async def list_prompts(self) -> list[types.Prompt]:
"""List all prompts defined on the server."""
result = await self.session().list_prompts()
return result.promptsClient method: list_prompts() (tiếp)
Ra:
prompts = await client.list_prompts()
for p in prompts:
args_str = ", ".join(a.name for a in (p.arguments or []))
print(f"/{p.name}({args_str}) - {p.description}")Client method: list_prompts() (tiếp)
/format(doc_id) - Rewrites the contents of the document in Markdown format.
/summarize(doc_id, length) - Summarize document with configurable length.Client method: get_prompt()
Đây là nơi variable interpolation xảy ra. User click /format, nhập doc_id=plan.md, client gọi:
Cách interpolation chạy
Key: args dict keys phải match với parameter names của function server-side. Sai 1 chữ → ValidationError.
User input: /format doc_id=plan.md
│
▼
Client: client.get_prompt("format", {"doc_id": "plan.md"})
│
▼
Server route to: format_document(doc_id="plan.md")
│ (SDK chuyển args dict → function kwargs)
▼
Function body: prompt = f"""Reformat {doc_id}..."""
│ f-string interpolate → "Reformat plan.md..."
▼
Return: [UserMessage("Reformat plan.md...")]
│
▼
Client receive: list[PromptMessage]async def get_prompt(
self, prompt_name: str, args: dict[str, str]
) -> list[types.PromptMessage]:
"""Get a prompt with arguments interpolated."""
result = await self.session().get_prompt(prompt_name, args)
return result.messagesCode đầy đủ thêm vào MCPClient
Thêm vào mcp_client.py. Bây giờ class MCPClient đã có đầy đủ 5 method:
(6 method thật, nhưng list_resource_templates đếm chung với list_resources về mặt logic.)
| Method | Protocol call | Primitive |
|---|---|---|
| list_tools() | tools/list | Tools |
| call_tool(name, input) | tools/call | Tools |
| list_resources() | resources/list | Resources |
| read_resource(uri) | resources/read | Resources |
| list_prompts() | prompts/list | Prompts |
| get_prompt(name, args) | prompts/get | Prompts |
async def list_prompts(self) -> list[types.Prompt]:
result = await self.session().list_prompts()
return result.prompts
async def get_prompt(
self, prompt_name: str, args: dict[str, str]
) -> list[types.PromptMessage]:
result = await self.session().get_prompt(prompt_name, args)
return result.messagesTest prompt flow trong CLI
Quick test trong mcp_client.py main harness:
Output:
async def main():
async with MCPClient(
command="uv",
args=["run", "mcp_server.py"],
) as client:
prompts = await client.list_prompts()
print(f"Available prompts: {[p.name for p in prompts]}")
messages = await client.get_prompt(
"format",
{"doc_id": "plan.md"}
)
for msg in messages:
# content có thể là TextContent object, không phải plain string
content = msg.content
text = content.text if hasattr(content, 'text') else str(content)
print(f"[{msg.role}] {text[:80]}...")
if __name__ == "__main__":
asyncio.run(main())Test prompt flow trong CLI (tiếp)
Notice: msg.content là một TextContent object (giống như content từ Claude API), không phải plain string. Khi ghép vào main chat, bạn cần extract .text — xem hàm prompt_messages_to_claude() dưới đây cho pattern chuẩn.
Available prompts: ['format']
[user] Your goal is to reformat a document to be written with markdown...Ghép slash commands vào CLI chat
Plan
Code: update main.py
Test end-to-end
Chạy uv run main.py. Thử:
- Pre-fetch prompts list ở startup
- Detect / ở đầu user input
- Parse /command arg1=value1 arg2=value2
- Gọi get_prompt() → nhận messages
- Convert messages sang format Anthropic API expect
- Append vào conversation, tiếp tục agentic loop
import re
def parse_slash_command(user_input: str) -> tuple[str, dict] | None:
"""Parse '/command arg1=val1 arg2=val2' format."""
if not user_input.startswith('/'):
return None
parts = user_input[1:].split()
if not parts:
return None
command = parts[0]
args = {}
for arg in parts[1:]:
if '=' in arg:
key, value = arg.split('=', 1)
args[key] = value
return command, args
def prompt_messages_to_claude(messages):
"""Convert MCP PromptMessage list to Anthropic messages format."""
out = []
for msg in messages:
content = msg.content
# content is TextContent or ImageContent
if hasattr(content, 'text'):
out.append({
"role": msg.role,
"content": content.text,
})
elif isinstance(content, list):
# Multi-part content
out.append({
"role": msg.role,
"content": [
{"type": "text", "text": c.text}
for c in content if hasattr(c, 'text')
],
})
return out
# In chat_loop:
prompts = await mcp.list_prompts()
prompt_names = {p.name for p in prompts}
while True:
raw_input = input("\nBạn > ").strip()
if raw_input.lower() in {"quit", "exit"}:
break
# Check slash command
parsed = parse_slash_command(raw_input)
if parsed and parsed[0] in prompt_names:
cmd_name, cmd_args = parsed
print(f"[Running prompt: /{cmd_name}]")
prompt_msgs = await mcp.get_prompt(cmd_name, cmd_args)
claude_msgs = prompt_messages_to_claude(prompt_msgs)
messages.extend(claude_msgs)
else:
# Normal input (+ @mention handling từ Bài 7.8)
augmented = await build_augmented_prompt(raw_input, mcp)
messages.append({"role": "user", "content": augmented})
# Agentic loop (same as Bài 7.6)
while True:
response = claude.messages.create(...)
...Test end-to-end
Magic. User gõ 1 slash command → Claude execute full workflow, gọi tools, trả kết quả. Đó là giá trị của Prompts.
Bạn > /format doc_id=plan.md
[Running prompt: /format]
[Tool call: read_doc_contents({'doc_id': 'plan.md'})]
[Tool call: edit_document({'doc_id': 'plan.md', 'old_str': '...', 'new_str': '...'})]
Claude > I've reformatted plan.md to Markdown. Changes:
- Added H2 header for main section
- Converted inline list to bullets
- Added code block for exampleHỗ trợ autocomplete cho slash commands
Professional UX: user gõ /, thấy popup prompts available.
Với prompt_toolkit:
User gõ / → popup list commands. Gõ /format → popup doc_id=. Professional UX.
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import NestedCompleter
async def build_completer(mcp: MCPClient):
prompts = await mcp.list_prompts()
slash_commands = {}
for p in prompts:
args = [a.name for a in (p.arguments or [])]
slash_commands[f"/{p.name}"] = {
f"{a}=": None for a in args
}
return NestedCompleter.from_nested_dict(slash_commands)
# Trong chat:
completer = await build_completer(mcp)
session = PromptSession(completer=completer)
while True:
user_input = await session.prompt_async("\nBạn > ")
...Bảng so sánh: 3 activation patterns hoàn chỉnh
Với full MCP stack, user có 3 cách trigger action:
Khi user nào dùng gì?
Good app provide cả 3. Great app make them discoverable (hint @, hint / khi user "khựng").
- New user → natural language (easy, Claude handles)
- Power user → @mention (fast, explicit context)
- Repeat workflow → slash command (one-click, craft prompt)
| Pattern | User type | Primitive | Flow |
|---|---|---|---|
| Natural | "Show me plan.md" | Tools | Claude tool_use loop |
| @mention | "Tóm tắt @plan.md" | Resources | App inject context |
| Slash command | "/format doc_id=plan.md" | Prompts | Server template + tools |
Ví dụ thực chiến: Daily workflow với slash commands
Tình huống
Bạn là analyst tại công ty fintech. Mỗi sáng cần:
Trước MCP prompts
Với MCP prompts setup
Bạn tạo 3 prompts trên MCP server internal:
Sáng thứ 2, gõ 3 command:
Time: ~5 phút, output chất lượng cao vì prompt đã craft bởi senior analyst.
- Review overnight trades
- Summarize market moves
- Draft client email
- Mở dashboard → copy numbers
- Open news feed → read, summarize mentally
- Open Gmail → write from scratch
- Time: ~45 phút
- /trade-review date=today → Claude query DB, summary trades, flag anomaly
- /market-summary region=asia → fetch news feed, summarize
- /client-email account=abc context=weekly-update → draft email dùng 2 output trên
/trade-review date=today
/market-summary region=asia
/client-email account=<X> context=<Y>Ví dụ theo ngành — Slash command ecosystems
🛠️ DevOps / SRE
Commands:
Pattern: On-call engineer quick context. No more "which runbook was it?"
📝 Content marketing
Commands:
Pattern: Writer spawn variants quickly. Consistency across channels.
💰 Finance
Commands:
Pattern: Monthly close accelerate. FP&A team productivity up.
🎓 Teaching / Training
Commands:
Pattern: Teacher save 60%+ time on repetitive content gen.
🏢 Internal ops
Commands:
Pattern: HR / manager self-serve. IT overhead giảm.
- /incident-summary {channel} {timeframe} — summarize Slack incident thread
- /deploy-check {service} {version} — pre-deploy checklist
- /runbook {alert} — pull matching runbook
- /blog-outline {topic} {tone} — generate outline
- /seo-check {url} — audit SEO issues
- /social-variants {post} — X/LinkedIn/Threads versions
- /variance-report {entity} {period} — budget vs actual
- /cashflow-forecast {horizon} — rolling forecast
- /expense-category {receipt_id} — categorize
- /quiz-generate {topic} {difficulty} — make quiz
- /rubric {assignment} — grading rubric
- /feedback {student_work} — structured feedback
- /onboard {role} {start_date} — onboarding kit
- /review-prep {employee} — perf review context
- /policy-lookup {question} — find policy answer
Anti-patterns — Những sai lầm cần tránh
❌ Ignore PromptArgument.required
Sai lầm:
Tại sao là sai: Server throw error "required arg doc_id missing". Ugly crash UX.
Cách đúng: Validate args trước khi gọi:
# User gõ /format (no args)
messages = await mcp.get_prompt("format", {})❌ Ignore PromptArgument.required
❌ Shadow existing commands
Sai lầm: App có built-in /help, add MCP server cũng có /help.
Tại sao là sai: Xung đột. User confused.
Cách đúng: Namespace MCP prompts: /<server_name>/<command>. Hoặc disable built-in khi có clash.
❌ Không display prompt messages cho user
Sai lầm: Slash command chạy silent. User không biết prompt gửi gì cho Claude.
Tại sao là sai: Debugging khó. User không verify được.
Cách đúng: Option --verbose hoặc debug mode show prompt body:
prompt_def = next(p for p in prompts if p.name == cmd_name)
required = [a.name for a in prompt_def.arguments if a.required]
missing = set(required) - set(args.keys())
if missing:
print(f"Missing required args: {missing}")
continue❌ Không display prompt messages cho user
❌ Arg parsing sơ sài
Sai lầm:
if args.get('debug') == 'true':
print(f"[Prompt body]\n{msg.content.text[:500]}...")❌ Arg parsing sơ sài
Tại sao là sai: Value có space (/format style="code block") break.
Cách đúng: Dùng shlex hoặc proper parser:
# chỉ split on space
args = user_input.split()[1:]Anti-patterns — Những sai lầm cần tránh (tiếp)
❌ Chạy prompt message như tool output
Sai lầm: Nhét prompt messages vào tool_result block.
Tại sao là sai: Sai semantic. Claude confused.
Cách đúng: Prompt messages là user/assistant messages. Append vào messages[] như user input thường.
❌ Quên role field
Sai lầm:
import shlex
tokens = shlex.split(user_input[1:])❌ Quên role field
Tại sao là sai: Anthropic API strict về shape.
Cách đúng: Always include role:
messages.append({"content": msg.content.text}) # missing roleAnti-patterns — Những sai lầm cần tránh (tiếp)
messages.append({"role": msg.role, "content": msg.content.text})Mẹo nâng cao
Mẹo 1: Prompt discovery trong UI
Show all prompts khi user lost:
Mẹo 2: Prompt với default args
Nếu user không cung cấp arg optional, client tự fill default:
if user_input.strip() == '/':
for p in prompts:
print(f" /{p.name} - {p.description}")
continueMẹo 2: Prompt với default args
Mẹo 3: Server-side argument completion
MCP spec hỗ trợ completion cho prompt args:
prompt_def = next(p for p in prompts if p.name == cmd_name)
for arg in (prompt_def.arguments or []):
if not arg.required and arg.name not in args:
# Default logic
args[arg.name] = "medium" # or fetch from configMẹo 3: Server-side argument completion
Client dùng list này show autocomplete realtime.
Mẹo 4: Combine prompt + resource
# Client side
completions = await mcp.session().complete(
{"type": "ref/prompt", "name": "format"},
{"name": "doc_id", "value": "re"}
)
# Returns: ["report.pdf", "request.md"]Mẹo 4: Combine prompt + resource
Pattern powerful: pre-fetch context qua resource, pass vào prompt template.
# User: /summarize @report.pdf length=brief
# Parse: slash with mention + args
# Fetch resource first → inject into prompt argsÁp dụng ngay
Bài tập 1: Add prompt methods (~10 phút)
Bước 1: Mở mcp_client.py. Add list_prompts() và get_prompt() như bài.
Bước 2: Update test harness:
Bước 3: Run uv run mcp_client.py. Ghi lại:
Bài tập 2: Slash command trong chat (~20 phút)
Bước 1: Mở main.py. Add parse_slash_command và prompt_messages_to_claude.
Bước 2: Trong chat loop, check slash command trước natural input.
Bước 3: Test:
Bước 4: Ghi lại:
Bài tập 3 (thử thách): Autocomplete slash (~15 phút)
Cài prompt_toolkit:
- Số prompts list ra: ___________
- Messages có bao nhiêu item: ___________
- Type của messages[0].content: ___________
- /format doc_id=plan.md
- /format (no args — error message graceful?)
- /unknown (command không tồn tại — fallback natural?)
- Claude execute đúng tool chain không? ___________
- UX khi command sai thế nào? ___________
- Bạn có thêm validation arg không? ___________
prompts = await client.list_prompts()
print(f"Prompts: {[p.name for p in prompts]}")
messages = await client.get_prompt("format", {"doc_id": "plan.md"})
print(f"Messages: {messages}")Bài tập 3 (thử thách): Autocomplete slash (~15 phút)
Implement autocomplete như mẹo ở trên. Verify:
Ghi lại:
- Gõ / → thấy dropdown
- Gõ /f → filter to /format
- Gõ /format (với space) → thấy arg hints
- UX có native feel không? ___________
- Latency tạo autocomplete? ___________
uv add prompt_toolkitTóm tắt bài học
🎯 2 method đơn giản hoàn thành client — list_prompts() + get_prompt(name, args). Pattern giống tools/resources.
🎯 Variable interpolation tại server — Client gửi args dict, server function nhận qua kwargs, f-string interpolate. Một flow rõ ràng.
🎯 Slash command = prompt trigger — /command arg=value → parse → get_prompt → inject messages → agentic loop.
🎯 3 activation patterns hoàn chỉnh — Natural (tools), @mention (resources), / (prompts). Mỗi cái có chỗ tốt nhất.
🎯 MCP stack đã đầy đủ — Bạn đã build client + server với đủ 3 primitive. Giờ có thể phát triển thành production app.
- MCP spec — Prompts
- prompt_toolkit docs
- "Prompts in the client" — Anthropic Academy video