첫 단계
첫 단계 (First steps)
**시작 페이지**는 빠르게 흘러갑니다. 서버를 쓰고, 돌리고, 도구를 호출하죠.
이 페이지는 천천히, 서버가 노출할 수 있는 세 가지 전부를, 그리고 그 과정의 모든 것에 이름을 붙여가며 진행합니다.
호스트, 클라이언트, 서버
이제부터 모든 페이지에서 보게 될 세 단어입니다.
- 호스트(host) 는 LLM 애플리케이션입니다. Claude, IDE, 에이전트 런타임이죠. 사용자가 대화하고 있는 바로 그 대상입니다.
- 클라이언트(client) 는 호스트 안에 살면서 MCP를 말합니다. 호스트는 자신이 연결한 서버마다 클라이언트를 하나씩 돌립니다.
- 서버(server) 는 여러분이 이 SDK로 만드는 것입니다. 서버는 클라이언트에게 뭔가를 노출하지요. 모델과 직접 대화하지는 않습니다.
여러분이 서버를 씁니다. 호스트는 다른 사람의 제품이에요. SDK는 여러분에게 Client라는 클래스도 주는데, 이건 호스트가 서버에 URL로 연결하거나 서브프로세스로 실행할 때 쓰는 것과 같은 클래스입니다. 이 페이지 뒤쪽에 나오고, 여러분이 서버를 테스트할 때도 이걸 씁니다.
세 가지 기본 요소(프리미티브)
서버는 정확히 세 종류의 것을 노출합니다. 무엇이 이들을 구분하느냐, 바로 누가 쓰기로 결정하느냐입니다.
| 프리미티브 | 제어 주체 | 무엇인가 | 예시 |
|---|---|---|---|
| 도구(Tools) | 모델 | 모델이 행동하기 위해 호출하는 함수 | API 호출, 데이터베이스 쓰기 |
| 리소스(Resources) | 애플리케이션 | 호스트가 모델의 컨텍스트로 불러들이는 데이터 | 파일 내용, API 응답 |
| 프롬프트(Prompts) | 사용자 | 사용자가 이름으로 호출하는 재사용 메시지 템플릿 | 슬래시 명령, 메뉴 항목 |
"제어 주체"가 바로 이 구분의 핵심입니다. 도구는 모델이 호출하기로 결정했기에 실행됩니다. 리소스는 애플리케이션이 모델에게 필요하다고 결정해서 붙습니다. 프롬프트는 사용자가 골라서 실행되지요.
!!! 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가 함수 자체에서 읽습니다. 함수 이름, docstring, 타입 힌트에서요. 그 어떤 것도 따로 선언하지 않았습니다.
!!! tip
SDK의 두 반쪽은 import 경로가 둘입니다. from mcp import Client와 from mcp.server import MCPServer. from mcp import MCPServer는 존재하지 않아요.
직접 해 보기
MCP Inspector로 실행합니다.
uv run mcp dev server.py
출력된 URL을 엽니다. Inspector에는 프리미티브마다 탭이 하나씩 있어요. 순서대로 훑어보세요.
도구(Tools). 항목 하나: add, *Add two numbers.*라고 설명되어 있죠. 폼에 필수 정수 필드 a와 b가 있습니다. 채워서 호출하면 결과는 3. Inspector는 그 폼을 a: int, b: int에서 만들었어요. 다른 모든 클라이언트도 그렇게 합니다.
리소스(Resources). Resources 목록은 비어 있습니다. greeting은 Resource Templates 아래 있는데, greeting://{name}에는 파라미터가 있어서 누군가 name을 채우기 전까지는 나열할 단일 리소스가 없기 때문이에요. World를 주고 읽어 봅니다.
Hello, World!
프롬프트(Prompts). 항목 하나: summarize, 필수 text 인자 하나를 가집니다. 텍스트를 넣고 가져오면 role: user인 메시지 하나에 렌더링된 문자열이 콘텐츠로 담겨 옵니다. 프롬프트란 그게 전부예요. 메시지를 만드는 함수입니다.
Inspector는 여러분의 서버를 stdio로 실행했습니다. MCP 서버가 말할 수 있는 전송 중 하나죠. 아직 하나를 고를 필요는 없습니다. 그건 Running your server 페이지에서 다룹니다.
캐퍼빌리티(Capabilities)
Inspector에서 탭 세 개를 봤죠. 세 개인 걸 어떻게 알았을까요?
클라이언트가 연결하면 서버는 자신의 캐퍼빌리티를 선언합니다. 어떤 요청 계열에 응답할지를요. 클라이언트는 그 선언을 보고 무엇을 요청할지 결정합니다. 여러분은 이걸 쓴 적이 없어요. MCPServer가 대신 선언해 줍니다.
직접 확인해 보세요. 한 터미널에서 server.py를 HTTP로 띄워 두고,
uv run mcp run server.py --transport streamable-http
다른 터미널에서 클라이언트를 그쪽에 붙입니다.
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
print(client.server_capabilities.model_dump(exclude_none=True))
if __name__ == "__main__":
anyio.run(main)
python client.py
{'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(리소스 템플릿·프롬프트 인자 자동완성)는 여러분이 써야 하는 핸들러가 필요한데, 이 서버엔 그게 없으므로 캐퍼빌리티도 없고, 예의 바른 클라이언트는 요청하지 않습니다. 선택적인 모든 것의 규칙이죠. 등록하면 캐퍼빌리티가 나타납니다. **Completions**가 그걸 증명해요.
!!! info
그 client.py는 완전한 MCP 클라이언트이고, **The Client**가 그 페이지입니다. 테스트에서는 터미널과 포트를 건너뛰고 서버 객체 자체를 넘깁니다. Client(mcp)처럼요. 그것도 한 페이지를 통째로 차지합니다. Testing.
여러분이 쓰지 않은 것
이 페이지를 다시 훑어보세요. 여러분은 작은 파이썬 함수 세 개를 썼습니다. 이런 것들은 쓰지 않았죠.
- JSON Schema.
a: int, b: int그 자체가add의 스키마입니다. - 요청 핸들러.
tools/list,resources/read,prompts/get: 전부 여러분 대신 서빙됩니다. - 캐퍼빌리티 선언.
MCPServer가 만들어 줬어요. - 프로토콜 한 줄. 버전 협상, JSON-RPC 프레이밍, 캐퍼빌리티 교환 — 그 모든 게
mcp dev와client.py안에서 일어났고 여러분은 본 적이 없습니다.
그 비율이 바로 SDK의 핵심입니다.
정리(Recap)
- 호스트는 LLM 앱, 클라이언트는 그 MCP를 말하는 절반, 서버는 여러분이 만드는 것입니다.
- 도구는 모델이 제어하고, 리소스는 애플리케이션이, 프롬프트는 사용자가 제어합니다.
- 프리미티브마다 데코레이터 하나:
@mcp.tool(),@mcp.resource(uri),@mcp.prompt(). 이름·설명·스키마는 함수에서 나옵니다. {param}이 있는 URI는 리소스 템플릿이 되어, 구체 리소스와는 별도로 나열됩니다.- 서버의 캐퍼빌리티는 여러분 대신 선언되고, 클라이언트는 서버가 선언한 것만 요청합니다.
Client("http://localhost:8000/mcp")는 실행 중인 서버와 대화합니다. 대신 서버 객체를 넘기면(Client(mcp)), 첫날부터 테스트 하네스가 됩니다.
다음은 **Connect to a real host**입니다. 이 서버를 진짜로 Claude Desktop이나 IDE 안에 넣는 거죠. 그다음 Testing — 페이지 하나, 인메모리 클라이언트 하나로 동작 여부를 절대 추측하지 않게 됩니다. 그 뒤엔 프리미티브마다 페이지가 하나씩 있는데, 모델이 구동하는 그것부터 시작합니다. Tools.