Prompts trong client

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

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

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

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

Code đầ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.)

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

Test 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 example

Hỗ 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)
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

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 role

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

Mẹ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 config

Mẹ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_toolkit

Tó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.

Tài liệu tham khảo
  • MCP spec — Prompts
  • prompt_toolkit docs
  • "Prompts in the client" — Anthropic Academy video
Nội dung này có hữu ích không?