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

7.10 · Prompts trong client

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

Bạn đã đi qua 10 bài. Đây là mảnh ghép cuối cùng của stack MCP đầy đủ:

Sau bài này, bạn sẽ
  1. Extend MCPClient class với 2 method cuối cùng: list_prompts() và get_prompt()
  2. Hiểu cơ chế "variable interpolation" từ client args sang server prompt body
  3. Implement slash command UX trong CLI: detect /command, show autocomplete, dispatch prompt
  4. Gửi messages từ prompt template vào agentic loop đúng cách
  5. Hoàn thành workflow end-to-end: /format plan.md → prompt craft → Claude → tool call → result

Mở đầu: Mảnh ghép cuối của bức tranh MCP

┌───────────────────────────────────────────────────────────┐
│                                                           │
│    PRIMITIVE         SERVER SIDE          CLIENT SIDE     │
│                                                           │
│    Tools             ✅ Bài 7.4         ✅ Bài 7.6       │
│    Resources         ✅ Bài 7.7         ✅ Bài 7.8       │
│    Prompts           ✅ Bài 7.9        🎯 Bài 7.10       │
│                                                           │
│    Inspector         ✅ Bài 7.5                           │
│                                                           │
└───────────────────────────────────────────────────────────┘

Sau bài này, bạn sẽ có một CLI chat app đầy đủ tính năng:

  • Hỏi tự do → Claude dùng tools tự quyết
  • @mention doc → app inject resources
  • /slash-command → user trigger prompts

3 primitive × 3 trigger mode khác nhau — chính là cái Claude Desktop, Cursor, Zed có. Bạn tự build.

Client method: list_prompts()

Pattern giống list_tools() và list_resources():

async def list_prompts(self) -> list[types.Prompt]:
    """List all prompts defined on the server."""
    result = await self.session().list_prompts()
    return result.prompts

Mỗi types.Prompt object có:

  • name — slash command name
  • description — hiển thị trong autocomplete
  • arguments — list PromptArgument với name, description, required

Output test:

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}")

Ra:

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

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

Cách interpolation chạy

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]

Key: args dict keys phải match với parameter names của function server-side. Sai 1 chữ → ValidationError.

Code đầy đủ thêm vào MCPClient

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

Thêm vào mcp_client.py. Bây giờ class MCPClient đã có đầy đủ 5 method:

MethodProtocol callPrimitive
list_tools()tools/listTools
call_tool(name, input)tools/callTools
list_resources()resources/listResources
read_resource(uri)resources/readResources
list_prompts()prompts/listPrompts
get_prompt(name, args)prompts/getPrompts

(6 method thật, nhưng list_resource_templates đếm chung với list_resources về mặt logic.)

Test prompt flow trong CLI

Quick test trong mcp_client.py main harness:

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())

Output:

Available prompts: ['format']
[user] Your goal is to reformat a document to be written with markdown...

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.

Ghép slash commands vào CLI chat

Plan

  1. Pre-fetch prompts list ở startup
  2. Detect / ở đầu user input
  3. Parse /command arg1=value1 arg2=value2
  4. Gọi get_prompt() → nhận messages
  5. Convert messages sang format Anthropic API expect
  6. Append vào conversation, tiếp tục agentic loop

Code: update main.py

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


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

Chạy uv run main.py. Thử:

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 example

Magic. User gõ 1 slash command → Claude execute full workflow, gọi tools, trả kết quả. Đó là giá trị của Prompts.

Hỗ trợ autocomplete cho slash commands

Professional UX: user gõ /, thấy popup prompts available.

Với prompt_toolkit:

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)


completer = await build_completer(mcp)
session = PromptSession(completer=completer)

while True:
    user_input = await session.prompt_async("\nBạn > ")
    ...

User gõ / → popup list commands. Gõ /format → popup doc_id=. Professional UX.

Bảng so sánh: 3 activation patterns hoàn chỉnh

Với full MCP stack, user có 3 cách trigger action:

PatternUser typePrimitiveFlow
Natural"Show me plan.md"ToolsClaude tool_use loop
@mention"Tóm tắt @plan.md"ResourcesApp inject context
Slash command"/format doc_id=plan.md"PromptsServer template + tools

Khi user nào dùng gì?

  • New user → natural language (easy, Claude handles)
  • Power user → @mention (fast, explicit context)
  • Repeat workflow → slash command (one-click, craft prompt)

Good app provide cả 3. Great app make them discoverable (hint @, hint / khi user "khựng").

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:

  1. Review overnight trades
  2. Summarize market moves
  3. Draft client email

Trước MCP prompts

  • Mở dashboard → copy numbers
  • Open news feed → read, summarize mentally
  • Open Gmail → write from scratch
  • Time: ~45 phút

Với MCP prompts setup

Bạn tạo 3 prompts trên MCP server internal:

/trade-review date=today
/market-summary region=asia
/client-email account=<X> context=<Y>

Sáng thứ 2, gõ 3 command:

  1. /trade-review date=today → Claude query DB, summary trades, flag anomaly
  2. /market-summary region=asia → fetch news feed, summarize
  3. /client-email account=abc context=weekly-update → draft email dùng 2 output trên

Time: ~5 phút, output chất lượng cao vì prompt đã craft bởi senior analyst.

Ví dụ theo ngành — Slash command ecosystems

DevOps / SRE

Commands:

  • /incident-summary {channel} {timeframe} — summarize Slack incident thread
  • /deploy-check {service} {version} — pre-deploy checklist
  • /runbook {alert} — pull matching runbook

Pattern: On-call engineer quick context. No more "which runbook was it?"

Content marketing

Commands:

  • /blog-outline {topic} {tone} — generate outline
  • /seo-check {url} — audit SEO issues
  • /social-variants {post} — X/LinkedIn/Threads versions

Pattern: Writer spawn variants quickly. Consistency across channels.

Finance

Commands:

  • /variance-report {entity} {period} — budget vs actual
  • /cashflow-forecast {horizon} — rolling forecast
  • /expense-category {receipt_id} — categorize

Pattern: Monthly close accelerate. FP&A team productivity up.

Teaching / Training

Commands:

  • /quiz-generate {topic} {difficulty} — make quiz
  • /rubric {assignment} — grading rubric
  • /feedback {student_work} — structured feedback

Pattern: Teacher save 60%+ time on repetitive content gen.

Internal ops

Commands:

  • /onboard {role} {start_date} — onboarding kit
  • /review-prep {employee} — perf review context
  • /policy-lookup {question} — find policy answer

Pattern: HR / manager self-serve. IT overhead giảm.

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

Ignore PromptArgument.required

Sai lầm:

messages = await mcp.get_prompt("format", {})
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:
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

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:
if args.get('debug') == 'true':
    print(f"[Prompt body]\n{msg.content.text[:500]}...")

Arg parsing sơ sài

Sai lầm:

args = user_input.split()[1:]
Tại sao là sai
Value có space (/format style="code block") break.
Cách đúng
Dùng shlex hoặc proper parser:
import shlex
tokens = shlex.split(user_input[1:])

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:

messages.append({"content": msg.content.text})  # missing role
Tại sao là sai
Anthropic API strict về shape.
Cách đúng
Always include role:
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:

if user_input.strip() == '/':
    for p in prompts:
        print(f"  /{p.name} - {p.description}")
    continue

Mẹo 2: Prompt với default args

Nếu user không cung cấp arg optional, client tự fill default:

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 config

Mẹo 3: Server-side argument completion

MCP spec hỗ trợ completion cho prompt args:

completions = await mcp.session().complete(
    {"type": "ref/prompt", "name": "format"},
    {"name": "doc_id", "value": "re"}
)

Client dùng list này show autocomplete realtime.

Mẹo 4: Combine prompt + resource

Pattern powerful: pre-fetch context qua resource, pass vào prompt template.

Á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:
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ước 3: Run uv run mcp_client.py. Ghi lại:

  • Số prompts list ra: ___________
  • Messages có bao nhiêu item: ___________
  • Type của messages[0].content: ___________

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:
  • /format doc_id=plan.md
  • /format (no args — error message graceful?)
  • /unknown (command không tồn tại — fallback natural?)

Bước 4: Ghi lại:

  • Claude execute đúng tool chain không? ___________
  • UX khi command sai thế nào? ___________
  • Bạn có thêm validation arg không? ___________

Bài tập 3 (thử thách): Autocomplete slash (~15 phút)

Cài prompt_toolkit:

uv add prompt_toolkit

Implement autocomplete như mẹo ở trên. Verify:

  • Gõ / → thấy dropdown
  • Gõ /f → filter to /format
  • Gõ /format (với space) → thấy arg hints

Ghi lại:

  • UX có native feel không? ___________
  • Latency tạo autocomplete? ___________

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

Những điều cốt lõi
  • 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.

Tài liệu tham khảo

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