완성 (Completions)

완성 (Completions)

여러분의 서버 위에 UI를 만드는 클라이언트는 사용자가 타이핑할 때 인자 값을 자동완성하고 싶어 해요: 언어 이름, 저장소 이름, 파일 경로.

**Completions(완성)**는 서버가 그 제안들을 공급하는 방법이에요.

완성할 가치가 있는 것

Completions는 정확히 두 가지에 적용돼요: 프롬프트의 인자와 리소스 템플릿의 파라미터. 그러니 각각 하나씩 가진 서버로 시작해요:

# docs_src/completions/tutorial001.py
from mcp.server import MCPServer

mcp = MCPServer("GitHub Explorer")


@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
    """A GitHub repository."""
    return f"Repository: {owner}/{repo}"


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

여기엔 아직 completions에 관한 게 없어요.

  • review_codelanguage를 받아요. 사용자가 여러분이 받아들이는 철자를 추측하게 해선 안 돼요.
  • github_repoownerrepo를 받아요. 둘 다 자유 텍스트 박스는 나쁜 폼이에요.

완성 핸들러

@mcp.completion()으로 데코레이트된 함수 하나를 추가해요:

# docs_src/completions/tutorial002.py
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference

mcp = MCPServer("GitHub Explorer")

LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]


@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
    """A GitHub repository."""
    return f"Repository: {owner}/{repo}"


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


@mcp.completion()
async def handle_completion(
    ref: PromptReference | ResourceTemplateReference,
    argument: CompletionArgument,
    context: CompletionContext | None,
) -> Completion | None:
    if isinstance(ref, PromptReference) and argument.name == "language":
        return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
    return None
  • 서버당 핸들러 하나예요. 모든 완성 요청이 여기 오고, 뭘 완성하느냐에 따라 분기해요.
  • async def여야 해요: SDK가 await 해요.
  • 인자 세 개를 받아요:
    • ref: 어느 프롬프트나 리소스 템플릿인지, PromptReference 또는 ResourceTemplateReference로. isinstance로 구분해요.
    • argument: argument.name은 완성 중인 인자, argument.value는 사용자가 지금까지 타이핑한 것.
    • context: 이미 해석된 인자들. 지금은 무시해요.
  • Completion(values=[...]), 또는 줄 게 없으면 None을 반환해요.

!!! tip argument.value는 사용자가 타이핑한 접두사예요. SDK는 여러분 대신 필터링하지 않아요: values에 넣는 것이 UI가 보여주는 것이에요. startswith는 여러분이 쓰는 거예요.

실행해 보기

**Testing**의 인메모리 Client로 구동해요. ref=PromptReference(name="review_code"), argument={"name": "language", "value": "py"}client.complete()을 호출해요:

result.completion.values  # ['python']
  • ref는 핸들러가 받는 것과 같은 참조 타입이에요.
  • argument는 정확히 namevalue 두 키를 가진 평범한 dict예요.

value를 보내면 전체 목록을 돌려받아요. lang.startswith("")는 모든 언어에 대해 참이니까요:

result.completion.values  # ['go', 'javascript', 'python', 'rust', 'typescript']

code(핸들러가 인식하지 못하는 인자)에 대해 물으면 None을 반환하고, SDK가 빈 목록으로 바꿔요:

result.completion.values  # []

None은 *"제안 없음"*을 뜻하지 에러가 아니에요. UI는 평범한 텍스트 박스로 폴백해요.

선언한 적 없는 기능

핸들러 등록이 곧 선언이에요. 클라이언트를 연결하고 보세요:

client.server_capabilities.completions  # CompletionsCapability()

completions를 어디에도 나열하지 않았어요. SDK가 핸들러를 보고 기능을 선언해줬어요. 모든 선택적 기능이 이렇게 동작해요: 핸들러가 선언이에요. (세 프리미티브는 선택적이 아니에요. MCPServer는 핸들러가 있든 없든 항상 그것들을 선언해요.)

!!! check 핸들러가 없는 첫 번째 server.py로 돌아가 어쨌든 요청해 보세요. 호출은 JSON-RPC 에러로 실패해요:

```text
Method not found
```

그리고 `client.server_capabilities.completions`는 `None`이에요. 그게 기능의 요점이에요: 잘 작동하는 클라이언트는 그것을 확인하고 답할 수 없는 요청을 절대 보내지 않아요.

의존 인자

github://repos/{owner}/{repo}에는 파라미터가 둘이고, repo의 유용한 값은 먼저 어떤 owner를 골랐느냐에 달려 있어요.

그게 context의 용도예요. 사용자가 이미 해석한 인자를 담아요:

# docs_src/completions/tutorial003.py
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference

mcp = MCPServer("GitHub Explorer")

LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]

REPOS_BY_OWNER = {
    "modelcontextprotocol": ["python-sdk", "typescript-sdk", "inspector"],
    "pydantic": ["pydantic", "pydantic-ai", "logfire"],
}


@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
    """A GitHub repository."""
    return f"Repository: {owner}/{repo}"


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


@mcp.completion()
async def handle_completion(
    ref: PromptReference | ResourceTemplateReference,
    argument: CompletionArgument,
    context: CompletionContext | None,
) -> Completion | None:
    if isinstance(ref, PromptReference) and argument.name == "language":
        return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
    if isinstance(ref, ResourceTemplateReference) and argument.name == "repo":
        if context is None or context.arguments is None:
            return None
        repos = REPOS_BY_OWNER.get(context.arguments.get("owner", ""), [])
        return Completion(values=[repo for repo in repos if repo.startswith(argument.value)])
    return None
  • 새 분기는 템플릿의 repo 파라미터에 반응해요.
  • context.arguments는 지금까지 고른 값들(여기선 owner)의 dict[str, str] | None이에요.
  • 아직 owner가 없으면 말이 되는 제안이 없으니 핸들러는 None을 반환해요.

클라이언트는 그 해석된 값들을 context_arguments=로 보내요. 이번엔 refResourceTemplateReference(uri="github://repos/{owner}/{repo}")예요. 빈 valuerepo를 요청하고 context_arguments={"owner": "modelcontextprotocol"}을 넘겨요:

result.completion.values  # ['python-sdk', 'typescript-sdk', 'inspector']

context_arguments=를 빼면 같은 호출이 []를 반환해요. 핸들러는 owner를 알기 전엔 어떤 repo를 제안할지 알 수 없어요.

!!! info Completiontotal=has_more=도 받아요. values가 더 긴 목록의 조각일 때 그걸 세팅해서 UI가 *"and 200 more"*를 보여주게 해요. 대부분의 핸들러는 그걸 필요로 하지 않아요.

요약

  • Completions는 프롬프트 인자리소스 템플릿 파라미터에 대한 제안이에요. 그 외엔 없어요.
  • @mcp.completion()이 그 하나의 핸들러를 등록해요. async def (ref, argument, context) -> Completion | None이에요.
  • isinstance(ref, ...)argument.name으로 분기해요. argument.value로는 직접 필터링해요.
  • None은 빈 목록이 돼요. 절대 에러가 아니에요.
  • context.arguments는 이미 해석된 값을 담고, 클라이언트는 context_arguments=로 공급해요.
  • completions 기능은 핸들러를 등록하는 순간 나타나요. 없으면 요청은 Method not found예요.

제안은 사용자가 프롬프트나 템플릿을 아직 채우는 중일 때 도와줘요. 도구 호출 중간에 질문을 하려면 **Elicitation**을 보고, 도구가 텍스트 외에 반환할 수 있는 모든 것은 **Media**예요.