첫걸음
이 페이지는 기계 번역본입니다
이 문서의 번역은 영어 페이지를 바탕으로 자동 생성되며, 기준이 되는 것은 이 페이지의 영어 원문입니다.
번역 문제를 발견했다면 번역이 만들어지는 방식과 문제를 신고하는 방법을 참고하세요.
시작 페이지는 빠르게 지나갑니다. 서버를 작성하고, 실행하고, 도구를 호출합니다.
이 페이지는 서버가 노출할 수 있는 세 가지를 모두 다루면서, 중간에 등장하는 모든 것에 이름을 붙여 가며 천천히 진행합니다.
호스트, 클라이언트, 서버
여기서부터 모든 페이지에 등장하는 세 단어입니다.
- 호스트는 LLM 애플리케이션입니다. Claude, IDE, 에이전트 런타임이 여기에 해당하며, 사용자가 대화하는 상대가 바로 호스트입니다.
- 클라이언트는 호스트 안에서 MCP를 말하는 쪽입니다. 호스트는 연결된 서버마다 클라이언트를 하나씩 실행합니다.
- 서버는 이 SDK로 만드는 것입니다. 클라이언트에 무언가를 노출하며, 모델과 직접 대화하지는 않습니다.
작성하는 것은 서버입니다. 호스트는 다른 사람의 제품입니다. SDK는 Client도 함께 제공합니다. 서버를 테스트할 때 사용하며, 이 페이지 뒤쪽에서 다시 등장합니다.
세 가지 프리미티브
서버가 노출하는 것은 정확히 세 종류입니다. 이들을 가르는 기준은 누가 사용을 결정하는가입니다.
| 프리미티브 | 제어 주체 | 설명 | 예시 |
|---|---|---|---|
| 도구 | 모델 | 모델이 동작을 수행하려고 호출하는 함수 | API 호출, 데이터베이스 쓰기 |
| 리소스 | 애플리케이션 | 호스트가 모델의 컨텍스트에 불러오는 데이터 | 파일 내용, API 응답 |
| 프롬프트 | 사용자 | 사용자가 이름으로 호출하는 재사용 가능한 메시지 템플릿 | 슬래시 명령, 메뉴 항목 |
"제어 주체"가 이 구분의 핵심입니다. 도구가 실행되는 이유는 모델이 호출하기로 결정했기 때문입니다. 리소스가 붙는 이유는 애플리케이션이 모델에 필요하다고 판단했기 때문입니다. 프롬프트가 실행되는 이유는 사용자가 그것을 골랐기 때문입니다.
Info
웹 API를 만들어 본 적이 있다면 감은 이미 잡혀 있습니다. 리소스는 GET이고(데이터를
불러올 뿐 아무것도 바꾸지 않습니다), 도구는 POST입니다(작업을 수행하며 부수 효과가
있을 수 있습니다). 프롬프트에 대응하는 HTTP 개념은 없습니다. 사용자가 이름으로
실행하는 저장된 쿼리에 더 가깝습니다.
하나의 서버, 세 가지 모두
from mcp.server import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@mcp.prompt()
def summarize(text: str) -> str:
"""Summarize a piece of text in one sentence."""
return f"Summarize the following text in one sentence:\n\n{text}"
평범한 함수 세 개와 데코레이터 세 개입니다. 데코레이터 하나가 등록의 전부입니다.
@mcp.tool()은add를 도구로 만듭니다.@mcp.resource("greeting://{name}")는greeting을 리소스 템플릿으로 만듭니다. URI의{name}이 함수의 매개변수입니다.@mcp.prompt()는summarize를 프롬프트로 만듭니다. 반환하는 문자열이 사용자 메시지가 됩니다.
나머지, 즉 이름과 설명과 인자 스키마는 SDK가 함수 자체에서 읽어 냅니다. 함수 이름, 독스트링, 타입 힌트가 그 출처입니다. 따로 선언한 것은 하나도 없습니다.
Tip
SDK의 두 축은 임포트 경로가 서로 다릅니다. from mcp import Client와
from mcp.server import MCPServer입니다. from mcp import MCPServer는 존재하지 않습니다.
직접 해보기
MCP Inspector로 실행해 보세요.
uv run mcp dev server.py
출력된 URL을 여세요. Inspector에는 프리미티브마다 탭이 하나씩 있으니 순서대로 살펴보세요.
도구. 항목은 add 하나이고, 설명은 Add two numbers.입니다. 폼에는 a를 위한 필수 정수 필드와 b를 위한 필드가 있습니다. 값을 채우고 호출하면 결과는 3입니다. Inspector는 a: int, b: int만 보고 그 폼을 만들었습니다. 다른 클라이언트도 모두 마찬가지입니다.
리소스. Resources 목록은 비어 있습니다. greeting은 Resource Templates 아래에 있는데, greeting://{name}에 매개변수가 있기 때문입니다. 누군가 name을 제공하기 전까지는 목록에 올릴 구체적인 리소스가 없습니다. World를 넣고 읽어 보세요.
Hello, World!
프롬프트. 항목은 하나, 필수 인자 text 하나를 받는 summarize입니다. 적당한 텍스트와 함께 가져오면 role: user이고 렌더링된 문자열이 내용인 메시지 하나가 돌아옵니다. 프롬프트는 이게 전부입니다. 메시지를 만드는 함수일 뿐입니다.
Inspector는 서버를 MCP 서버가 사용할 수 있는 트랜스포트 중 하나인 stdio로 실행했습니다. 아직 무엇을 쓸지 고를 필요는 없습니다. 그 이야기는 서버 실행하기에서 다룹니다.
기능
Inspector에서 탭 세 개를 봤습니다. 탭이 세 개라는 것을 어떻게 알았을까요?
클라이언트가 연결하면 서버는 자신의 기능을 선언합니다. 어떤 계열의 요청에 응답할지를 밝히는 것입니다. 클라이언트는 그 선언을 보고 애초에 무엇을 요청할지 결정합니다. 이 선언을 직접 작성한 적은 없습니다. MCPServer가 대신 선언해 줍니다.
직접 확인해 보세요. SDK의 Client는 서버 객체를 그대로 받아 인메모리로 연결합니다(하위 프로세스도, 포트도 없습니다).
import asyncio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.model_dump(exclude_none=True))
asyncio.run(main())
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
이 딕셔너리가 서버가 선언한 기능입니다. 연결하는 모든 클라이언트가 가장 먼저 알게 되는 정보입니다.
| 기능 | 클라이언트가 이제 호출할 수 있는 것 |
|---|---|
tools |
tools/list, tools/call |
resources |
resources/list, resources/templates/list, resources/read |
prompts |
prompts/list, prompts/get |
MCPServer는 세 가지 프리미티브를 모두 제공하므로, 셋 다 항상 선언됩니다.
없는 것에도 주목하세요. completions(리소스 템플릿과 프롬프트의 인자 자동 완성)는 직접 작성한 핸들러가 필요한데 이 서버에는 없습니다. 그래서 해당 기능은 선언되지 않고, 제대로 동작하는 클라이언트라면 요청하지 않습니다. 선택적인 요소는 모두 이 규칙을 따릅니다. 해당 항목을 등록하면 기능이 나타납니다. 자동 완성이 이를 보여 줍니다.
Info
Client(mcp)는 이 문서의 모든 예제를 테스트하는 데 쓰는 바로 그 인메모리 클라이언트이며,
직접 만든 서버도 이렇게 테스트하게 됩니다. 이 주제만 다루는 페이지가 따로 있습니다.
테스트입니다.
작성하지 않은 것
이 페이지를 다시 훑어보세요. 작성한 것은 작은 Python 함수 세 개뿐입니다. 작성하지 않은 것은 다음과 같습니다.
- JSON Schema.
a: int, b: int가 곧add의 스키마입니다. - 요청 핸들러.
tools/list,resources/read,prompts/get모두 알아서 처리됩니다. - 기능 선언.
MCPServer가 대신 만들었습니다. - 프로토콜 코드 한 줄. 버전 협상, JSON-RPC 프레이밍, 기능 교환은 모두
mcp dev와Client(mcp)안에서 일어났고, 겉으로 드러나지 않았습니다.
이 비율이야말로 SDK의 존재 이유입니다.
정리
- 호스트는 LLM 앱이고, 클라이언트는 그중 MCP를 말하는 절반이며, 서버는 직접 만드는 것입니다.
- 도구는 모델이, 리소스는 애플리케이션이, 프롬프트는 사용자가 제어합니다.
- 프리미티브마다 데코레이터 하나입니다.
@mcp.tool(),@mcp.resource(uri),@mcp.prompt(). 이름과 설명, 스키마는 함수에서 나옵니다. {param}이 들어간 URI는 리소스 템플릿이 되며, 구체적인 리소스와는 따로 목록에 표시됩니다.- 서버의 기능은 자동으로 선언되고, 클라이언트는 서버가 선언한 것만 요청합니다.
Client(mcp)는 서버 객체에 인메모리로 연결합니다. 첫날부터 쓸 수 있는 테스트 장치입니다.
다음은 실제 호스트에 연결하기입니다. 이 서버를 Claude Desktop이나 IDE 안에서 실제로 돌려 봅니다. 그다음은 테스트입니다. 페이지 하나와 인메모리 클라이언트 하나면, 동작 여부를 짐작할 일이 없습니다. 이후에는 프리미티브마다 페이지가 하나씩 이어지며, 모델이 주도하는 것부터 시작합니다. 도구입니다.