프롬프트

프롬프트 (Prompts)

프롬프트(prompt) 는 사용자가 고르는 메시지 템플릿입니다.

도구는 모델을 위한 것입니다. 프롬프트는 그 반대예요. 사용자가 클라이언트의 메뉴(슬래시 명령, 버튼)에서 하나를 고르고 인자를 채우면, 렌더링된 메시지가 마치 직접 타이핑한 것처럼 대화에 들어갑니다.

텍스트를 반환하는 함수에 @mcp.prompt()를 붙이면 선언됩니다.

첫 번째 프롬프트

from mcp.server import MCPServer

mcp = MCPServer("Code Helper")


@mcp.prompt()
def review_code(code: str) -> str:
    """Review a piece of code."""
    return f"Please review this code:\n\n{code}"

SDK는 도구에서 읽는 것과 같은 세 가지를 읽습니다.

  • 이름은 함수 이름입니다: review_code.
  • 클라이언트가 보여주는 설명은 docstring입니다: Review a piece of code.
  • 인자는 파라미터에서 옵니다. code는 기본값이 없으므로 필수입니다.

그게 클라이언트가 prompts/list에서 돌려받는 것입니다.

{
  "name": "review_code",
  "description": "Review a piece of code.",
  "arguments": [
    {"name": "code", "required": true}
  ]
}

여기엔 JSON Schema가 없습니다. 프롬프트 인자는 이름 붙은 문자열 값의 평평한 리스트입니다. 모델이 구성하는 페이로드가 아니라 사람이 채우는 폼이죠.

렌더링하기

클라이언트는 인자를 넘겨 prompts/get으로 템플릿을 렌더링합니다. 함수가 실행되고 반환한 str사용자 메시지 하나가 됩니다.

{
  "description": "Review a piece of code.",
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please review this code:\n\ndef add(a, b): return a + b"
      }
    }
  ],
  "resultType": "complete"
}

그게 프롬프트의 전체 생애입니다. 이름으로 나열되고, 요청 시 렌더링되고, 채팅에 들어가죠.

!!! check required는 함수가 실행되기 전에 강제됩니다. code 없이 review_code를 렌더링하면 요청 자체가 JSON-RPC 오류(코드 -32603)로 실패합니다.

```text
mcp.shared.exceptions.MCPError: Internal server error
```

모델에게 돌려줄 도구 스타일의 오류 결과는 없습니다. 루프에 모델이 없으니까요. 호출이 그냥 raise합니다. 이유(`Missing required arguments: {'code'}`)는 여러분 서버의 로그에 남습니다.

직접 해 보기

MCP Inspector로 서버를 실행합니다.

uv run mcp dev server.py

Prompts 탭을 열고 review_code를 선택합니다. Inspector는 필수 code 필드가 하나 있는 폼을 그립니다. 채우고 렌더링하면 정확히 위의 사용자 메시지를 돌려받습니다.

메시지가 하나보다 많을 때

코드 리뷰는 메시지 하나입니다. 디버깅 세션은 대화이고, 프롬프트가 그 전체의 씨앗이 될 수 있어요.

str 대신 메시지 리스트를 반환하세요.

from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage

mcp = MCPServer("Code Helper")


@mcp.prompt()
def review_code(code: str) -> str:
    """Review a piece of code."""
    return f"Please review this code:\n\n{code}"


@mcp.prompt()
def debug_error(error: str) -> list[Message]:
    """Start a debugging conversation."""
    return [
        UserMessage("I'm seeing this error:"),
        UserMessage(error),
        AssistantMessage("I'll help debug that. What have you tried so far?"),
    ]
  • UserMessageAssistantMessagemcp.server.mcpserver.prompts.base에서 옵니다. str을 넘기면 TextContent로 감싸 줍니다. 역할(role)은 클래스 이름이죠.
  • Message는 그 공통 베이스입니다. 반환 어노테이션으로 쓰세요.

debug_error를 렌더링하면 이제 메시지 세 개가 순서대로 나옵니다.

{
  "description": "Start a debugging conversation.",
  "messages": [
    {"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
    {"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
    {
      "role": "assistant",
      "content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
    }
  ],
  "resultType": "complete"
}

마지막 것을 주목하세요. assistant 턴을 미리 채우는 것은 사용자가 직접 타이핑하지 않아도 모델의 다음 답변을 유도하는 방법입니다.

타이틀과 인자 설명

review_code는 함수 이름이지 라벨이 아닙니다. 클라이언트가 버튼에 넣을 더 나은 것을 주고, 각 인자를 설명해서 폼이 스스로 설명하게 하세요.

from typing import Annotated

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Code Helper")


@mcp.prompt(title="Code review")
def review_code(
    code: Annotated[str, Field(description="The code to review.")],
    language: Annotated[str, Field(description="The language the code is written in.")] = "python",
) -> str:
    """Review a piece of code."""
    return f"Please review this {language} code:\n\n{code}"
  • title="Code review"는 도구의 title과 정확히 같은, 사람이 읽을 수 있는 이름입니다.
  • Annotated[str, Field(description=...)]Tools 가 도구 파라미터를 설명하는 데 쓰는 것과 같은 패턴입니다. 여기선 설명이 스키마가 아니라 인자에 붙지요.
  • language는 기본값이 있어서 필수에서 빠집니다.

prompts/list 항목은 이제 클라이언트가 좋은 폼을 그리는 데 필요한 전부를 담고 있습니다.

{
  "name": "review_code",
  "title": "Code review",
  "description": "Review a piece of code.",
  "arguments": [
    {"name": "code", "description": "The code to review.", "required": true},
    {"name": "language", "description": "The language the code is written in.", "required": false}
  ]
}

!!! info Tools 를 읽었다면 이 시점까지는 이미 전부 아는 것입니다. 같은 데코레이터, 같은 docstring-as-description, 같은 Annotated/Field. 바뀌는 건 무엇이 트리거하느냐(사용자)와 결과가 어디로 가느냐(대화 속으로)뿐입니다.

텍스트보다 더

UserMessageAssistantMessagestr을 받는 자리에 콘텐츠 블록이나 Image/Audio 헬퍼도 받습니다. 프롬프트에서 자주 나오는 두 경우는 문서 붙이기와 그림 붙이기입니다.

파일 임베딩하기

from pathlib import Path

from mcp.server import MCPServer
from mcp.server.mcpserver import Message, UserMessage
from mcp.types import EmbeddedResource, TextResourceContents

mcp = MCPServer("Code Helper")

STYLE_GUIDE_FILE = Path(__file__).parent / "style-guide.md"  # or the path to your file on disk


@mcp.resource("style://python", mime_type="text/markdown")
def style_guide() -> str:
    """The team's Python style guide."""
    return STYLE_GUIDE_FILE.read_text(encoding="utf-8")


@mcp.prompt()
def review_code(code: str) -> list[Message]:
    """Review a piece of code against the team style guide."""
    guide = TextResourceContents(uri="style://python", mime_type="text/markdown", text=style_guide())
    return [
        UserMessage(EmbeddedResource(resource=guide)),
        UserMessage(f"Review this code against the style guide above:\n\n{code}"),
    ]
  • 스타일 가이드는 style://python의 리소스입니다(그런 것은 Resources 가 다룹니다). server.py 옆의 style-guide.md에서 읽어요. 아무 Markdown 파일이나 거기 두면 됩니다.
  • EmbeddedResource(resource=TextResourceContents(...)) — 둘 다 mcp.types에서 — 파일을 URI와 MIME 타입과 함께 첫 번째 메시지로 나릅니다. 그것을 가리키는 요청은 평문으로 뒤따르죠.
  • 가이드를 f-string에 붙여 넣는 대신 임베딩하면, 클라이언트가 첨부로 표시하고 나중에 style://python을 다시 열 수 있으며, 모델은 파일을 그대로 받습니다. 바이너리 파일은 base64 blob과 함께 BlobResourceContents를 쓰세요.

렌더링되면 첫 메시지의 contentresource 블록입니다.

{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}}

이미지 붙이기

from pathlib import Path

from mcp.server import MCPServer
from mcp.server.mcpserver import Image, Message, UserMessage

mcp = MCPServer("Code Helper")

DIAGRAM_FILE = Path(__file__).parent / "architecture.png"  # or the path to your file on disk


@mcp.prompt()
def explain_component(component: str) -> list[Message]:
    """Explain one component using the architecture diagram."""
    return [
        UserMessage(Image(path=DIAGRAM_FILE)),
        UserMessage(f"Where does {component} sit in this architecture, and what does it talk to?"),
    ]
  • ImageImages, audio & icons 의 헬퍼입니다. UserMessage는 프롬프트가 렌더링될 때 그걸 ImageContent 블록으로 변환합니다(파일은 base64, MIME 타입은 .png에서 추측). Audio는 같은 방식으로 AudioContent가 돼요.
  • server.py 옆에 architecture.png라는 아무 PNG나 두세요. 프롬프트 인자는 문자열이라 그림은 항상 서버에서 오고, component는 단어만 공급합니다.
{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"}

런타임에 목록 바꾸기

클라이언트가 연결된 동안 프롬프트를 추가할 수 있습니다. 예를 들어 사용자가 지시문을 자신만의 메뉴 항목으로 저장하게 하는 경우죠. 프롬프트를 등록한 다음 알리면 됩니다.

from contextlib import suppress

from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts import Prompt

mcp = MCPServer("Code Helper")


@mcp.prompt()
def review_code(code: str) -> str:
    """Review a piece of code."""
    return f"Please review this code:\n\n{code}"


@mcp.tool()
async def save_template(name: str, instruction: str, ctx: Context) -> str:
    """Save an instruction as a prompt the user can pick from the menu."""

    def template(code: str) -> str:
        return f"{instruction}\n\n{code}"

    with suppress(ValueError):  # replace an existing entry of the same name
        mcp.remove_prompt(name)
    mcp.add_prompt(Prompt.from_function(template, name=name, description=instruction))
    await ctx.notify_prompts_changed()
    await ctx.session.send_prompt_list_changed()
    return f"Saved '{name}' to the prompt menu."
  • mcp.add_prompt(Prompt.from_function(fn, name=..., description=...))@mcp.prompt()가 그랬을 것처럼 정확히 함수를 등록하고, mcp.remove_prompt(name)은 그 반대입니다. add_prompt는 같은 이름의 기존 항목을 덮어쓰지 않고 유지하므로, 저장을 "교체"로 만들려면 도구가 먼저 옛것을 제거합니다. prompts/list는 즉시 그 변경을 반영합니다.
  • await ctx.notify_prompts_changed()subscriptions/listen 스트림을 듣고 있는 모든 2026-07-28 클라이언트에게 notifications/prompts/list_changed를 보냅니다(Subscriptions). await ctx.session.send_prompt_list_changed()는 그 클라이언트가 2026 이전일 때 호출 클라이언트에 보냅니다(Serving legacy clients). 둘 다 호출하세요. 알릴 상대가 없으면 각각 아무것도 하지 않아요.
  • 알림을 받은 클라이언트는 prompts/list를 다시 호출합니다. Python Client에서는 async with client.listen(prompts_list_changed=True) as sub:PromptsListChanged 이벤트를 yield합니다.

정리(Recap)

  • 함수에 @mcp.prompt()를 붙이면 프롬프트가 됩니다. 이름은 함수에서, 설명은 docstring에서.
  • 프롬프트는 사용자가 제어합니다. 클라이언트가 나열하고, 사용자가 고르고 인자를 채우죠.
  • 인자는 이름 붙은 문자열의 평평한 리스트입니다(스키마 없음). 기본값이 있는 파라미터는 선택적입니다.
  • str을 반환하면 사용자 메시지 하나가 됩니다. UserMessage/AssistantMessage 리스트를 반환하면 다중 턴 대화의 씨앗이 됩니다.
  • title=Field(description=...)가 클라이언트가 UI에 넣는 것들입니다.
  • 필수 인자가 없으면 요청 전체가 실패합니다. 프롬프트별 오류 결과는 없어요.
  • UserMessageEmbeddedResourceImage를 감싸면 문서나 그림을 붙일 수 있습니다.
  • 런타임에 mcp.add_prompt(...)/mcp.remove_prompt(...)로 프롬프트를 추가·제거한 다음 await ctx.notify_prompts_changed()await ctx.session.send_prompt_list_changed()를 호출하세요.

프롬프트(또는 리소스 템플릿) 인자의 서버 측 자동완성은 Completions 입니다.

출처: Python SDK — Prompts

더 알아보기 (Learn more)